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
fid27 in my test layer, its normal area column stayed at197.355while the virtual field updated to789.418. The catch is that a virtual field lives in your.qgsproject 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 at0here and at1in 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:

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 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 want | Expression to paste | Tested result on sample | Common wrong version (and what happens) |
|---|---|---|---|
| Building area in m² (ellipsoidal) | $area | 197.35, 110.86, 141.11, 219.71, 318.83 | area($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.22 | Transforming to 'EPSG:3857' (Web Mercator) inflates area by +5.95% here |
| Road length in metres (ellipsoidal) | $length | 616.34, 7.99, 1104.32, 9.14, 72.24 | length($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.29 | Transforming to 'EPSG:3857' inflates road length by +3.03% median |
| Building perimeter in metres | $perimeter | 56.72, 44.97, 48.60, 61.54, 72.69 | perimeter($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.32 | y(centroid($geometry)) returns degrees (12.97...) when the layer is in EPSG:4326 |
| Row number or feature ID | @row_number or $id | 1, 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 want | Expression to paste | Tested result on sample | Common wrong version (and what happens) |
|---|---|---|---|
| Size class from area | CASE WHEN $area < 100 THEN 'small' WHEN $area < 250 THEN 'medium' ELSE 'large' END | medium, medium, medium, medium, large | Not tested |
| Replace NULL levels with a default integer | coalesce(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 separate | 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 | mid, 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 want | Expression to paste | Tested result on sample | Common wrong version (and what happens) |
|---|---|---|---|
| Join house number and street safely | concat("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 name | regexp_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 digits | lpad(to_string("osm_id"), 12, '0') | '000313829229', '000313835091' | Not tested |
| Format a number for display | format_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 dates | day(age(to_date('2026-11-07'), to_date('2019-03-15'))) | 2794 | age(...) 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 want | Expression to paste | Tested result on sample | Common wrong version (and what happens) |
|---|---|---|---|
| Count buildings touching each ward | aggregate('buildings', 'count', "osm_id", filter:=intersects($geometry, geometry(@parent))) | 4, 1552, 336, 3141, 79, 402, 1179, 348, 1539, 139 | Forgetting filter:=... gives every single ward the total count of the whole layer (8709) with no error |
| Total building area in ha per ward | round(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.7 | Sums full footprints that touch the ward border, so boundary buildings are counted in both wards |
| Count schools inside each ward | array_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 stop | distance(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.61 | distance($geometry, array_first(overlay_nearest('bus_stops', $geometry))) returns 0.001142 (degrees) |
| Count bus stops within 300 m of a building | aggregate('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, 7 | array_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).
$areaand$lengthuse the project ellipsoid (set underProject > Properties > General > Measurements). When the ellipsoid isWGS 84,$areareturns accurate square metres even when your layer and project are in degrees (EPSG:4326).- When the project ellipsoid is set to
None / Planimetric,$areaon ourEPSG:4326layer 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)andlength($geometry)are always planimetric in the layer's own CRS. On anEPSG:4326layer,area($geometry)returns square degrees (around0.0000003for a house), which rounds to0.0in a standard decimal column.- Transforming to Web Mercator (
EPSG:3857) beforearea()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 ellipsoid | Max 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.

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.
| Task | Exact prompt sent | Agent time | Result 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 s | Pass. 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 s | Pass. 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 s | Pass (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.

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.

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.

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.

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

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:
- A feature was selected on the map, so Only update N selected feature(s) was ticked and left every unselected row
NULL. - You concatenated text with
||(such as"addr:housenumber" || ' ' || "addr:street"), where a singleNULLinput turns the whole resultNULL. Useconcat()instead. - You put double quotes around a text value (
"building" = "yes"), which tells QGIS to look for a column namedyes. Use single quotes for values ("building" = 'yes'). - You used
$xor$yon a polygon or line layer.$xonly works on points; usex(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.
Do it in QGIS by asking for it
AI Agent carries out the task you describe, in your own QGIS project. Free, no card.
, open the layer's attribute table, click the Field Calculator icon (

