Skip to content
AI Agent
QGIS
Tutorial

QGIS Field Calculator: 24 Tested Expressions and the Traps

Published

To calculate a column in QGIS, open the layer's attribute table, click the Field Calculator icon (Ctrl+I), choose a new or existing field, type an expression and click OK. That part is quick. The hard part is when the expression runs without an error and writes NULLs, square degrees or the wrong class.

On 7 November 2026 I ran 24 field calculator expressions on real OpenStreetMap layers for Indiranagar and neighbouring wards in Bengaluru, India (8,709 buildings, 854 roads, 35 schools, 37 bus stops, 10 wards), and ran the common wrong version of each one beside it. Below is the cookbook with the exact output values, the silent traps measured against an ellipsoidal reference, and what happened when I handed the three hardest ones to an AI agent inside QGIS.

What is the QGIS Field Calculator?

The QGIS Field Calculator is the built-in tool that creates or updates attribute table columns using QGIS expressions. It can compute geometry values such as area, length and coordinates, rewrite text, classify rows with CASE WHEN, and pull counts or distances from other layers in your project.

QGIS actually gives you four places to run the same expressions, and picking the wrong one is the first way to lose work:

  • The Field Calculator dialog (Ctrl+I): edits the layer in place. You can create a new column or overwrite an existing one, on all features or only the selected ones. Your changes sit in the edit buffer until you save edits.
  • A virtual field (checkbox inside the dialog): computes the expression on the fly every time you open the table or draw the map. When I doubled the footprint of building fid 27 in my test layer, its normal area column stayed at 197.355 while the virtual field updated to 789.418. The catch is that a virtual field lives in your .qgs project file, not in the GeoPackage or Shapefile. Open the file in a fresh project and the column is gone.
  • The Processing "Field calculator" tool (native:fieldcalculator): writes a brand-new output layer and leaves your original layer untouched. Use this in models or when you don't want to edit in place. Watch out for @row_number, which starts at 0 here and at 1 in the dialog.
  • The quick calculation bar at the top of the attribute table: updates an existing column without opening the full dialog, good for a quick one-liner when you are already in edit mode.

How to calculate a field step by step

Open the Field Calculator

Right-click your layer and choose Open Attribute Table, then click the abacus icon in the attribute table toolbar. With the layer selected in the Layers panel, Ctrl+I opens the Field Calculator directly.

Check the selection and field settings

If any features are selected on your map, Only update N selected feature(s) is ticked by default. Untick it if you want the whole layer calculated. Choose Create a new field (or Update existing field), set the Output field name and pick the Output field type (Decimal number (real) for areas and lengths with decimals, Integer for whole counts, Text (string) for labels and classes).

Write the expression and check the Preview

Type your expression in the left-hand editor, or search the function list in the middle and double-click to insert. Always check the Preview line below the editor before clicking OK. If it shows an error, NULL, or a number that looks like 0.0000003, fix the expression before you write it to 8,000 rows.

Click OK and save your edits

Click OK. If the layer wasn't in edit mode, QGIS warns you that it will turn edit mode on automatically. When the calculation finishes, click the Save Edits floppy disk icon in the attribute table toolbar so the column is written to disk.

Here is the dialog on the roads layer, ready to write a road class:

The QGIS Field Calculator dialog on the Bengaluru roads layer, with Only update 39 selected features checked, a new Text field named road_rank, a CASE WHEN expression classifying primary and secondary roads as arterial, and the Preview showing arterial.
The Field Calculator dialog in QGIS 3.44. Notice the top-left checkbox: because 39 roads were selected on the canvas, only those 39 rows will be calculated.

When you don't know a function's name, use the search box above the middle column. It filters the function tree and opens the documentation on the right, including the parameter order and examples.

The QGIS Field Calculator expression builder on the wards layer, searching the function list for overlay_contains, with the right-hand panel showing the syntax and arguments for overlay_contains and the preview returning 18.
Searching the function list for overlay_contains. The Preview at the bottom shows 18 schools inside the selected ward before you run the calculation.

The tested expression cookbook

Every expression below was run in QGIS 3.44.15 on 7 November 2026 on our Bengaluru OpenStreetMap layers (stored in EPSG:4326, project ellipsoid WGS 84). If you need test data for your own area, our guide on how to download OpenStreetMap data in QGIS walks through fetching buildings, roads and points cleanly.

Sample building outputs below come from buildings fid 27, 28, 30, 60 and 62 (which have addresses and level counts) and fid 1, 2 and 3 (which have neither).

Area, length, perimeter and coordinates

What you wantExpression to pasteTested result on sampleCommon wrong version (and what happens)
Building area in m² (ellipsoidal)$area197.35, 110.86, 141.11, 219.71, 318.83area($geometry) returns 0.0 (about 3e-7 square degrees), no error
Building area in m² (projected in UTM 43N)area(transform($geometry, 'EPSG:4326', 'EPSG:32643'))197.60, 110.99, 141.29, 219.98, 319.22Transforming to 'EPSG:3857' (Web Mercator) inflates area by +5.95% here
Road length in metres (ellipsoidal)$length616.34, 7.99, 1104.32, 9.14, 72.24length($geometry) returns 0.005681, 0.000074 (degrees), no error
Road length in metres (projected in UTM 43N)length(transform($geometry, 'EPSG:4326', 'EPSG:32643'))616.72, 7.99, 1105.01, 9.14, 72.29Transforming to 'EPSG:3857' inflates road length by +3.03% median
Building perimeter in metres$perimeter56.72, 44.97, 48.60, 61.54, 72.69perimeter($geometry) returns degrees on an EPSG:4326 layer
Polygon centroid X (Easting in metres)x(centroid(transform($geometry, 'EPSG:4326', 'EPSG:32643')))786714.46, 786437.79, 786237.28$x on a polygon returns NULL with no error; x_at($geometry, 0) returns the first vertex (77.642744), not the centre
Polygon centroid Y (Northing in metres)y(centroid(transform($geometry, 'EPSG:4326', 'EPSG:32643')))1435359.45, 1435116.32, 1436699.32y(centroid($geometry)) returns degrees (12.97...) when the layer is in EPSG:4326
Row number or feature ID@row_number or $id1, 2, 3, 4 (dialog) or 27 for $id on fid 27@row_number starts at 1 in the GUI dialog, but starts at 0 in Processing native:fieldcalculator

If you write $area into an Integer output field instead of Decimal number (real) (tested with the Processing Field calculator), QGIS rounds to the nearest whole number (197.35 becomes 197, 110.86 becomes 111) rather than truncating.

Conditional classes (CASE WHEN and NULL handling)

What you wantExpression to pasteTested result on sampleCommon wrong version (and what happens)
Size class from areaCASE WHEN $area < 100 THEN 'small' WHEN $area < 250 THEN 'medium' ELSE 'large' ENDmedium, medium, medium, medium, largeNot tested
Replace NULL levels with a default integercoalesce(to_int("building:levels"), 1)4, 4, 2, 4, 3 (and 1 where levels is NULL)"building:levels" + 'a' returns '4a' because OSM tags are stored as text strings
Height class with NULLs kept separateCASE WHEN to_int("building:levels") IS NULL THEN 'unknown' WHEN to_int("building:levels") <= 2 THEN 'low' WHEN to_int("building:levels") <= 4 THEN 'mid' ELSE 'high' ENDmid, mid, low, mid, mid (and unknown for fid 1)Skipping WHEN ... IS NULL sends every empty row into ELSE, labelling 7,636 unmapped buildings as 'high'

Text cleanup, formatting and dates

What you wantExpression to pasteTested result on sampleCommon wrong version (and what happens)
Join house number and street safelyconcat("addr:housenumber", ' ', "addr:street")'594 12th Main Road', '83/4 13th Main Road' (' ' if both NULL)"addr:housenumber" || ' ' || "addr:street" returns NULL if either column is NULL
Strip suffix (Road, Main, Cross) from road nameregexp_replace("name", '\\s+(Road|Main|Cross)$', '')'Chinmaya Mission Hospital', '13th Main', '12th Main'A single backslash \s+ fails with Invalid regular expression '?+(Road|Main|Cross)$'
Zero-pad an ID to 12 digitslpad(to_string("osm_id"), 12, '0')'000313829229', '000313835091'Not tested
Format a number for displayformat_number($area, 1)'197,4', '110,9' ('3 958,7' for 3958.7 on a comma-locale Mac)The output is text that follows your OS locale (decimal comma and narrow non-breaking space), so you can't do maths on it afterwards
Days between two datesday(age(to_date('2026-11-07'), to_date('2019-03-15')))2794age(...) alone returns a QgsInterval object; to_date('07/11/2026') fails unless you pass 'dd/MM/yyyy' as the second argument

Also watch your output field length when writing text to a GeoPackage or Shapefile. When I ran concat("addr:housenumber", ' ', "addr:street") into a text field of length 40 in Processing, the entire run aborted with no output because one road (" 80 Feet Road(Sir C.V. Raman Hospital Road)") was 43 characters long.

Spatial counts, sums and nearest distance across layers

These expressions let one layer query another layer in your project without running a separate spatial join in QGIS.

What you wantExpression to pasteTested result on sampleCommon wrong version (and what happens)
Count buildings touching each wardaggregate('buildings', 'count', "osm_id", filter:=intersects($geometry, geometry(@parent)))4, 1552, 336, 3141, 79, 402, 1179, 348, 1539, 139Forgetting filter:=... gives every single ward the total count of the whole layer (8709) with no error
Total building area in ha per wardround(aggregate('buildings', 'sum', $area, filter:=intersects($geometry, geometry(@parent))) / 10000, 3)0.565, 20.292, 5.605, 59.116, 1.037, 14.094, 16.646, 7.991, 21.39, 1.7Sums full footprints that touch the ward border, so boundary buildings are counted in both wards
Count schools inside each wardarray_length(overlay_contains('schools', "osm_id"))0, 2, 3, 18, 0, 0, 7, 3, 2, 0 (sum: 35)overlay_contains('schools') with no second argument returns true or false, not a count
Distance in metres to nearest bus stopdistance(transform($geometry, 'EPSG:4326', 'EPSG:32643'), transform(array_first(overlay_nearest('bus_stops', $geometry)), 'EPSG:4326', 'EPSG:32643'))125.17, 49.39, 199.59, 158.39, 187.97, 74.67, 123.00, 226.61distance($geometry, array_first(overlay_nearest('bus_stops', $geometry))) returns 0.001142 (degrees)
Count bus stops within 300 m of a buildingaggregate('bus_stops', 'count', "osm_id", filter:=distance(transform($geometry, 'EPSG:4326', 'EPSG:32643'), transform(geometry(@parent), 'EPSG:4326', 'EPSG:32643')) <= 300)1, 1, 4, 2, 2, 6, 7, 7array_length(overlay_nearest('bus_stops', $id, max_distance:=300, limit:=-1)) returns 37 (every stop) because 300 is read as 300 degrees

Five field calculator traps that fail without an error

I read 70 help comments under QGIS tutorials on YouTube: 48 were real questions about writing an expression, and 8 of those were about an expression that is invalid, returns NULL or selects nothing. The worse case is the one nobody asks about, because the expression runs and writes plausible numbers. Here are the five traps we measured on our Bengaluru data.

1. $area vs area($geometry), and what the project ellipsoid does

$area and area($geometry) do not do the same calculation, and at least one 2026 tutorial says $area returns square degrees in a geographic CRS. It didn't, in either project setting we tested. On 7 November 2026 we measured both, plus transform(), across the first 200 buildings and 200 roads in our EPSG:4326 Bengaluru layers against an ellipsoidal WGS 84 reference (QgsDistanceArea).

  • $area and $length use the project ellipsoid (set under Project > Properties > General > Measurements). When the ellipsoid is WGS 84, $area returns accurate square metres even when your layer and project are in degrees (EPSG:4326).
  • When the project ellipsoid is set to None / Planimetric, $area on our EPSG:4326 layer still did not return square degrees. In QGIS 3.44.15 it returned square metres that were +3.24% too large across the 200 buildings.
  • area($geometry) and length($geometry) are always planimetric in the layer's own CRS. On an EPSG:4326 layer, area($geometry) returns square degrees (around 0.0000003 for a house), which rounds to 0.0 in a standard decimal column.
  • Transforming to Web Mercator (EPSG:3857) before area() inflates every building by +5.95% even at Bengaluru's latitude (13° N), and inflates road lengths by +3.03%. Transforming to the local UTM zone (EPSG:32643) stays within +0.12% of the ellipsoidal area.
Expression on EPSG:4326 layers (n = 200 buildings, 200 roads)Median error vs WGS 84 ellipsoidMax error
$area (project ellipsoid WGS 84)0.00%0.00%
$area (project ellipsoid None)+3.24%+3.25%
$perimeter (project ellipsoid None)+1.72%+2.25%
$length on roads (project ellipsoid None)+1.31%+2.61%
area($geometry)-100.00% (square degrees)-100.00%
length($geometry) on roads-100.00% (degrees)-100.00%
area(transform($geometry, 'EPSG:4326', 'EPSG:32643'))+0.12%+0.12%
length(transform($geometry, 'EPSG:4326', 'EPSG:32643')) on roads+0.06%+0.06%
area(transform($geometry, 'EPSG:4326', 'EPSG:3857'))+5.95%+5.96%
length(transform($geometry, 'EPSG:4326', 'EPSG:3857')) on roads+3.03%+3.26%

If you are unsure why EPSG:4326, EPSG:3857 and UTM zones behave so differently, read our explainer on what a CRS is in QGIS.

2. overlay_nearest reads max_distance in layer units, not metres

The overlay_nearest function is fast because it uses a spatial index, and it has a handy max_distance parameter. But max_distance is always measured in the layer's coordinate units.

When our buildings and bus stops are in EPSG:4326 (degrees) and you write array_length(overlay_nearest('bus_stops', $id, max_distance:=300, limit:=-1)), QGIS looks for bus stops within 300 degrees. That covers the entire Earth, so every single building gets 37 (all 37 bus stops in the layer). Guessing max_distance:=0.003 for 300 metres doesn't work cleanly either, because a degree of longitude and a degree of latitude do not have the same length in metres: on building fid 62, 0.003 returned 8 stops instead of the true 7. Either reproject both layers to UTM first, or use the aggregate(..., filter:=distance(transform(...))) pattern from the last row of the spatial table above.

3. NULL rows fall straight into the ELSE branch of CASE WHEN

In our Bengaluru dataset, only 1,073 of the 8,709 buildings (12.3%) have a building:levels tag. The other 7,636 are NULL.

If you write CASE WHEN to_int("building:levels") <= 2 THEN 'low' WHEN to_int("building:levels") <= 4 THEN 'mid' ELSE 'high' END, both <= comparisons evaluate to unknown on a NULL row, and the row falls into ELSE. You end up classifying 7,636 unmapped buildings as high-rise towers without a single warning. Always put WHEN "field" IS NULL THEN 'unknown' (or leave out ELSE so unmatched rows stay NULL) as the first branch when your data has gaps.

4. "Only update selected features" leaves the rest of the column NULL

Whenever even one feature is selected on the map canvas, the Field Calculator ticks Only update N selected feature(s) automatically.

When I selected the 39 primary roads in Bengaluru and ran CASE WHEN "highway" IN ('primary', 'secondary') THEN 'arterial' ELSE 'local' END into a new field road_rank, QGIS wrote 'arterial' to those 39 rows and left the other 815 roads (including all 28 secondary roads that my expression explicitly named) as NULL.

The QGIS attribute table for the roads layer after running the Field Calculator with 39 primary roads selected: primary rows show road_rank as arterial, while secondary and residential rows show NULL, and row_n shows 1, 2, 3 from @row_number.
Because 39 primary roads were selected when the dialog opened, the 28 secondary roads and 787 local roads came out NULL even though the CASE expression handled them.

5. Double quotes vs single quotes (why "building" = "yes" returns NULL)

In QGIS expressions, double quotes " " mean a field name and single quotes ' ' mean a text value.

If you type "building" = "yes", QGIS looks for a column called building and compares it to another column called yes. Because there is no column called yes, the expression evaluates to NULL (and the underlying QgsExpression parser reports Field 'yes' not found). Similarly, if a column name contains a colon, such as OpenStreetMap's building:levels, leaving off the double quotes fails with syntax error, unexpected Unknown_CHARACTER. Double-clicking field names in the Fields and Values list inserts the right quotes every time.

Skipping the syntax: running the same calculations with an AI agent

Writing $area is quick by hand. Writing a nested aggregate() with two transform() calls, a @parent geometry reference and a distance filter is where people end up on Stack Exchange.

We build AI Agent, a chat panel that docks inside QGIS, so this section is about our own tool. Because it runs inside your open QGIS project, it inspects your layer names, field types, CRS and unique values before it runs the Field Calculator or Processing tools for you.

On 7 November 2026 I gave AI Agent (v1.9.4, Medium effort, Balanced permissions) the three multi-step calculations from our test plan, one sentence per chat, on the exact same five Bengaluru layers in EPSG:4326.

TaskExact prompt sentAgent timeResult against our independent check
A. Proximity count"Add a field called bus_stops_300m to the buildings layer with the number of bus stops within 300 metres of each building."39 sPass. Integer field created, all 8,709 buildings filled, 0 mismatches against our footprint-to-stop count in EPSG:32643 (range 0 to 8).
B. Classification + styling"Class the buildings into low (1 or 2 levels), mid (3 or 4) and high (5 or more) from the building:levels field, store the class in a new field called height_class, and style the buildings by that class."18 sPass. 563 low, 442 mid, 68 high (all 1,073 match our rule). The 7,636 rows without levels were kept NULL and styled as a fourth grey "Unknown levels" category.
C. Per-ward school count and building area"For each ward, add the number of schools and the total building area in hectares."37 sPass (with a method difference). school_count matched 10/10 wards (total 35). building_ha matched clipped footprint areas across 10/10 wards (total 148.25 ha).

On Task A, the agent did not fall into the 300-degree overlay_nearest trap. Its tool log shows it inspected both layers, checked the CRS, ran the calculation in metric units from the building footprints (not centroids, which would have changed the answer on 667 buildings), and verified the min and max values before answering.

The AI Agent panel in QGIS after adding the bus_stops_300m field to 8,709 Bengaluru buildings, reporting counts from 0 to 8 with no missing values and noting straight-line distances from each building footprint to the 37 stops.
Task A in AI Agent: 39 seconds to compute the 300 m bus stop count across 8,709 buildings in an EPSG:4326 project without the degrees trap.

On Task B, it checked the unique values of building:levels first, saw that 7,636 of the 8,709 buildings had no level count, left those rows NULL in height_class instead of letting them fall into high, and applied a 4-class categorized style on the map.

The AI Agent panel and QGIS Layers panel showing the height_class field categorized into low, mid, high and Unknown levels, with the answer listing 563 low, 442 mid, 68 high and 7,636 unknown buildings.
Task B: the agent checked the column first, kept the 7,636 unmapped buildings out of the ELSE branch, and styled all four groups.

Looking at the rendered map makes it obvious why checking for NULL matters on OpenStreetMap data: the grey buildings are the 87.7% that have no building:levels tag recorded.

Rendered QGIS map of central Indiranagar, Bengaluru, with buildings coloured light yellow for low, orange for mid, dark red for high and grey for unknown levels, overlaid with roads and black dots for bus stops.
Central Indiranagar styled by height_class: 563 low (yellow), 442 mid (orange), 68 high (red) and 7,636 with no level tag in OSM (grey).

On Task C, the agent's building_ha numbers differed slightly from the hectares-per-ward row of our cookbook: New Thippasandra came out at 20.151 ha instead of 20.292 ha (-0.69%), Jeevan Bhimanagar -0.14% and Indiranagar -0.06%. When I checked why, the agent had actually used a stricter geometric method than my intersects() filter: it clipped the building polygons at the ward boundaries before summing their areas, so buildings straddling a border weren't double-counted in two wards, and it noted that in its reply. It did leave the wards layer in edit mode with uncommitted edits, telling me in the answer that the edits were not yet saved.

The AI Agent panel in QGIS reporting school_count and building_ha for each of the 10 Bengaluru wards, noting that footprints were clipped to ward boundaries and that edits are not yet saved.
Task C: the agent clipped border-straddling buildings to ward boundaries before summing hectares, and noted that the layer edits were still uncommitted.

Here is the resulting attribute table on the wards layer after committing those edits:

The QGIS attribute table for the 10 Bengaluru wards showing the new school_count integer column and building_ha decimal column alongside ward names such as Domlur, Indiranagar, New Thippasandra and Ulsoor.
The 10 wards with school_count (total 35) and building_ha (total 148.25 ha) added.

Three tasks is a small sample (n = 3), and I gave clear prompts that named the fields and class bounds, so treat this as a demonstration rather than a benchmark. For simple formulas such as $area or concat(), the Field Calculator dialog is faster than typing a chat message. Where an agent earns its place is when you need cross-layer aggregates, CRS transformations or multi-step Processing chains, or when you'd otherwise write a PyQGIS script. You can also compare other assistants on our QGIS AI hub.

How we measured this. All tests ran on 7 November 2026 in QGIS 3.44.15-Solothurn on macOS with OpenStreetMap data for Indiranagar, Bengaluru (bounding box 12.968, 77.634, 12.986, 77.656, ODbL) in project CRS EPSG:4326. Every cookbook expression was verified against an independent Python or QgsDistanceArea calculation on the same feature IDs. Area and length errors were measured across the first 200 buildings and 200 roads by fid against QgsDistanceArea with the WGS 84 ellipsoid (EPSG:7030). AI Agent 1.9.4 ran at Medium effort and Balanced permissions in a fresh chat per task. Our YouTube question counts come from 70 help comments across 59 videos filtered from a 25,276-comment corpus.

Frequently asked questions

Why is my $area calculation wrong or returning 0 in QGIS?

First check whether you typed area($geometry) instead of $area. On a layer stored in degrees (EPSG:4326), area($geometry) returns square degrees (around 0.0000003 for a house), which rounds to 0.0 in a decimal field. Use $area with your project ellipsoid set to WGS 84 under Project > Properties > General > Measurements, or reproject the geometry in the expression with area(transform($geometry, 'EPSG:4326', 'EPSG:32643')) using your local UTM zone. Avoid EPSG:3857 (Web Mercator), which inflated building areas by +5.95% even at 13° N in our test.

How do I write an IF or CASE statement in the QGIS Field Calculator?

For two outcomes, use if(condition, value_if_true, value_if_false), for example if($area < 100, 'small', 'large'). For three or more classes, use CASE WHEN ... THEN ... ELSE ... END:

CASE WHEN to_int("building:levels") IS NULL THEN 'unknown' WHEN to_int("building:levels") <= 2 THEN 'low' WHEN to_int("building:levels") <= 4 THEN 'mid' ELSE 'high' END

Put the IS NULL check first. Otherwise every row with an empty value falls into ELSE.

Why did the Field Calculator return NULL for all or most of my rows?

Four common causes:

  1. A feature was selected on the map, so Only update N selected feature(s) was ticked and left every unselected row NULL.
  2. You concatenated text with || (such as "addr:housenumber" || ' ' || "addr:street"), where a single NULL input turns the whole result NULL. Use concat() instead.
  3. You put double quotes around a text value ("building" = "yes"), which tells QGIS to look for a column named yes. Use single quotes for values ("building" = 'yes').
  4. You used $x or $y on a polygon or line layer. $x only works on points; use x(centroid($geometry)) for polygons.

How do I calculate area in hectares in the QGIS Field Calculator?

Set the Output field type to Decimal number (real), set Precision to 2 or 3, and enter $area / 10000 (assuming your project area units are square metres and your project ellipsoid is set under Project > Properties > General). To round the stored value itself to 3 decimal places, use round($area / 10000, 3).

How do I update only selected features in the Field Calculator?

Select the features on the map canvas or in the attribute table first, then open the Field Calculator (Ctrl+I). The Only update N selected feature(s) checkbox at the top left will be ticked automatically. Choose Update existing field, pick the column from the dropdown, enter your expression and click OK.

Try AI Agent for free

Do it in QGIS by asking for it

AI Agent carries out the task you describe, in your own QGIS project. Free, no card.

Try AI Agent for free