[{"data":1,"prerenderedAt":1220},["ShallowReactive",2],{"doc:\u002Fpyqgis-fundamentals-environment-setup\u002Fworking-with-qgis-expressions\u002Fuse-aggregate-expressions-pyqgis":3},{"id":4,"title":5,"body":6,"description":1209,"extension":1210,"meta":1211,"navigation":271,"path":1216,"seo":1217,"stem":1218,"__hash__":1219},"docs\u002Fpyqgis-fundamentals-environment-setup\u002Fworking-with-qgis-expressions\u002Fuse-aggregate-expressions-pyqgis\u002Findex.md","Use Aggregate Expressions in PyQGIS",{"type":7,"value":8,"toc":1195},"minimark",[9,13,17,35,208,213,235,239,368,398,435,439,442,485,499,537,560,564,601,616,620,696,848,876,880,886,928,968,982,995,999,1002,1019,1022,1026,1040,1044,1109,1113,1122,1126,1132,1141,1149,1158,1162,1191],[10,11,5],"h1",{"id":12},"use-aggregate-expressions-in-pyqgis",[14,15,16],"p",{},"Most QGIS expressions look at one feature. The aggregate family looks at many — every feature in a layer, every feature in a group, or every related child record — and returns one number. That makes them the shortest route from \"each parcel's area\" to \"each parcel's share of its district's area\", with no join, no temporary layer and no Python loop.",[14,18,19,20,25,26,30,31,34],{},"This recipe belongs to ",[21,22,24],"a",{"href":23},"\u002Fpyqgis-fundamentals-environment-setup\u002Fworking-with-qgis-expressions\u002F","Working with QGIS Expressions",". It covers ",[27,28,29],"code",{},"aggregate"," and ",[27,32,33],{},"relation_aggregate",", grouping and filtering, evaluating them from Python with the right context, and where they become too slow to use.",[14,36,37],{},[38,39,44,48,52,59,76,85,95,101,111,116,123,127,131,136,141,146,150,171,177,182,187,192,195,203],"svg",{"viewBox":40,"role":41,"ariaLabel":42,"xmlns":43},"0 0 760 320","img","How an aggregate expression differs from an ordinary one: an ordinary expression reads one feature, an aggregate reads many and returns a single value into the current feature","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg",[45,46,47],"title",{},"One feature in, or many",[49,50,51],"desc",{},"An ordinary expression evaluates against the current feature and returns a value derived from it alone. An aggregate expression scans a whole layer, optionally restricted to features sharing a group value, and returns a single summary that is then used in the current feature's context.",[53,54],"rect",{"x":55,"y":55,"width":56,"height":57,"fill":58},"0","760","320","#f6f3ea",[60,61,62],"defs",{},[63,64,71],"marker",{"id":65,"viewBox":66,"refX":67,"refY":68,"markerWidth":69,"markerHeight":69,"orient":70},"aggArrow","0 0 10 10","8","5","7","auto-start-reverse",[72,73],"path",{"d":74,"fill":75},"M0 0 L10 5 L0 10 z","#2f3b35",[77,78,84],"text",{"x":79,"y":80,"style":81,"fill":82,"textAnchor":83},"380","26","text-anchor:middle;font-size:14px;font-weight:bold;font-family:sans-serif","#17211d","middle","The scan is the cost, and the power",[53,86],{"x":87,"y":88,"width":89,"height":90,"rx":91,"fill":92,"stroke":93,"style":94},"24","46","330","196","10","#eef7f4","#0f766e","stroke-width:2.5",[77,96,100],{"x":97,"y":98,"style":99,"fill":93,"textAnchor":83},"189","72","text-anchor:middle;font-size:11.5px;font-weight:bold;font-family:sans-serif","ordinary expression",[53,102],{"x":103,"y":104,"width":105,"height":106,"rx":107,"fill":108,"stroke":109,"style":110},"60","96","120","34","6","#e8efe6","#15803d","stroke-width:1.8",[77,112,115],{"x":105,"y":113,"style":114,"fill":75,"textAnchor":83},"118","text-anchor:middle;font-size:10px;font-family:sans-serif","this feature",[117,118],"line",{"x1":119,"y1":120,"x2":121,"y2":120,"stroke":75,"style":122},"180","113","212","stroke-width:1.8;marker-end:url(#aggArrow)",[53,124],{"x":125,"y":104,"width":126,"height":106,"rx":107,"fill":108,"stroke":109,"style":110},"218","112",[77,128,130],{"x":129,"y":113,"style":114,"fill":75,"textAnchor":83},"274","a value",[77,132,135],{"x":97,"y":133,"style":134,"fill":75,"textAnchor":83},"166","text-anchor:middle;font-size:10.5px;font-family:monospace","$area \u002F 10000",[77,137,140],{"x":97,"y":138,"style":114,"fill":139,"textAnchor":83},"200","#59645f","constant cost per feature",[53,142],{"x":143,"y":88,"width":89,"height":90,"rx":91,"fill":144,"stroke":145,"style":94},"406","#eff3ff","#2563eb",[77,147,149],{"x":148,"y":98,"style":99,"fill":145,"textAnchor":83},"571","aggregate expression",[151,152,155,162,165,168],"g",{"fill":153,"stroke":145,"style":154},"#dbeafe","stroke-width:1.6",[53,156],{"x":157,"y":158,"width":159,"height":160,"rx":161},"430","92","52","18","3",[53,163],{"x":157,"y":164,"width":159,"height":160,"rx":161},"114",[53,166],{"x":157,"y":167,"width":159,"height":160,"rx":161},"136",[53,169],{"x":157,"y":170,"width":159,"height":160,"rx":161},"158",[77,172,176],{"x":173,"y":174,"style":175,"fill":139,"textAnchor":83},"456","192","text-anchor:middle;font-size:9.5px;font-family:sans-serif","whole layer",[117,178],{"x1":179,"y1":180,"x2":181,"y2":180,"stroke":75,"style":122},"486","134","518",[53,183],{"x":184,"y":185,"width":119,"height":186,"rx":107,"fill":153,"stroke":145,"style":110},"524","116","36",[77,188,191],{"x":189,"y":190,"style":114,"fill":75,"textAnchor":83},"614","139","one summary value",[77,193,194],{"x":148,"y":121,"style":114,"fill":139,"textAnchor":83},"a scan, cached per group",[53,196],{"x":105,"y":197,"width":198,"height":199,"rx":67,"fill":200,"stroke":201,"style":202},"266","520","42","#fdf2e2","#b45309","stroke-width:2",[77,204,207],{"x":79,"y":205,"style":206,"fill":75,"textAnchor":83},"292","text-anchor:middle;font-size:10.5px;font-family:sans-serif","a filter argument re-scans per feature — a group_by argument does not",[209,210,212],"h2",{"id":211},"prerequisites","Prerequisites",[214,215,216,224],"ul",{},[217,218,219,223],"li",{},[220,221,222],"strong",{},"QGIS 3.34 LTR"," or newer.",[217,225,226,227,229,230,234],{},"A layer to summarise, and — for ",[27,228,33],{}," — a defined relation, as covered in ",[21,231,233],{"href":232},"\u002Fpyqgis-fundamentals-environment-setup\u002Fworking-with-qgis-projects\u002Fdefine-layer-relations-pyqgis\u002F","defining layer relations",".",[209,236,238],{"id":237},"the-aggregate-function","The aggregate function",[240,241,246],"pre",{"className":242,"code":243,"language":244,"meta":245,"style":245},"language-python shiki shiki-themes github-dark","from qgis.core import QgsExpression, QgsExpressionContext, QgsExpressionContextUtils\n\nlayer = QgsProject.instance().mapLayersByName(\"parcels\")[0]\n\nexpression = QgsExpression(\n    \"aggregate(layer:='parcels', aggregate:='sum', expression:=$area)\"\n)\n\ncontext = QgsExpressionContext()\ncontext.appendScopes(QgsExpressionContextUtils.globalProjectLayerScopes(layer))\n\nexpression.prepare(context)\nprint(expression.evaluate(context))\n","python","",[27,247,248,266,273,298,303,314,320,326,331,342,348,353,359],{"__ignoreMap":245},[249,250,252,256,260,263],"span",{"class":117,"line":251},1,[249,253,255],{"class":254},"snl16","from",[249,257,259],{"class":258},"s95oV"," qgis.core ",[249,261,262],{"class":254},"import",[249,264,265],{"class":258}," QgsExpression, QgsExpressionContext, QgsExpressionContextUtils\n",[249,267,269],{"class":117,"line":268},2,[249,270,272],{"emptyLinePlaceholder":271},true,"\n",[249,274,276,279,282,285,289,292,295],{"class":117,"line":275},3,[249,277,278],{"class":258},"layer ",[249,280,281],{"class":254},"=",[249,283,284],{"class":258}," QgsProject.instance().mapLayersByName(",[249,286,288],{"class":287},"sU2Wk","\"parcels\"",[249,290,291],{"class":258},")[",[249,293,55],{"class":294},"sDLfK",[249,296,297],{"class":258},"]\n",[249,299,301],{"class":117,"line":300},4,[249,302,272],{"emptyLinePlaceholder":271},[249,304,306,309,311],{"class":117,"line":305},5,[249,307,308],{"class":258},"expression ",[249,310,281],{"class":254},[249,312,313],{"class":258}," QgsExpression(\n",[249,315,317],{"class":117,"line":316},6,[249,318,319],{"class":287},"    \"aggregate(layer:='parcels', aggregate:='sum', expression:=$area)\"\n",[249,321,323],{"class":117,"line":322},7,[249,324,325],{"class":258},")\n",[249,327,329],{"class":117,"line":328},8,[249,330,272],{"emptyLinePlaceholder":271},[249,332,334,337,339],{"class":117,"line":333},9,[249,335,336],{"class":258},"context ",[249,338,281],{"class":254},[249,340,341],{"class":258}," QgsExpressionContext()\n",[249,343,345],{"class":117,"line":344},10,[249,346,347],{"class":258},"context.appendScopes(QgsExpressionContextUtils.globalProjectLayerScopes(layer))\n",[249,349,351],{"class":117,"line":350},11,[249,352,272],{"emptyLinePlaceholder":271},[249,354,356],{"class":117,"line":355},12,[249,357,358],{"class":258},"expression.prepare(context)\n",[249,360,362,365],{"class":117,"line":361},13,[249,363,364],{"class":294},"print",[249,366,367],{"class":258},"(expression.evaluate(context))\n",[14,369,370,373,374,377,378,381,382,385,386,389,390,393,394,397],{},[220,371,372],{},"Breakdown:"," The named-argument form (",[27,375,376],{},"layer:=",", ",[27,379,380],{},"aggregate:=",") is worth using even though positional works, because the argument order is easy to misremember and a silent mis-order gives a plausible wrong number. ",[27,383,384],{},"layer"," takes a layer name or id. ",[27,387,388],{},"prepare()"," is what lets the expression cache the aggregate rather than recomputing it, and skipping it is the difference between one scan and one scan per evaluation. ",[27,391,392],{},"globalProjectLayerScopes"," builds the full context — global variables, project variables and the layer's own — which is what makes ",[27,395,396],{},"@project_folder"," and layer fields resolve.",[14,399,400,401,377,404,377,407,377,410,377,413,377,416,377,419,377,422,377,425,428,429,30,432,234],{},"The available aggregates are the ones you expect — ",[27,402,403],{},"sum",[27,405,406],{},"mean",[27,408,409],{},"median",[27,411,412],{},"min",[27,414,415],{},"max",[27,417,418],{},"count",[27,420,421],{},"count_distinct",[27,423,424],{},"stdev",[27,426,427],{},"concatenate"," — plus the geometry ones, ",[27,430,431],{},"collect",[27,433,434],{},"array_agg",[209,436,438],{"id":437},"grouping-and-filtering","Grouping and filtering",[14,440,441],{},"The two optional arguments do very different things and have very different costs.",[240,443,445],{"className":242,"code":444,"language":244,"meta":245,"style":245},"\"\"\"\naggregate(\n  layer:='parcels',\n  aggregate:='sum',\n  expression:=$area,\n  group_by:=\"district\"\n)\n\"\"\"\n",[27,446,447,452,457,462,467,472,477,481],{"__ignoreMap":245},[249,448,449],{"class":117,"line":251},[249,450,451],{"class":287},"\"\"\"\n",[249,453,454],{"class":117,"line":268},[249,455,456],{"class":287},"aggregate(\n",[249,458,459],{"class":117,"line":275},[249,460,461],{"class":287},"  layer:='parcels',\n",[249,463,464],{"class":117,"line":300},[249,465,466],{"class":287},"  aggregate:='sum',\n",[249,468,469],{"class":117,"line":305},[249,470,471],{"class":287},"  expression:=$area,\n",[249,473,474],{"class":117,"line":316},[249,475,476],{"class":287},"  group_by:=\"district\"\n",[249,478,479],{"class":117,"line":322},[249,480,325],{"class":287},[249,482,483],{"class":117,"line":328},[249,484,451],{"class":287},[14,486,487,489,490,493,494,498],{},[220,488,372],{}," ",[27,491,492],{},"group_by"," partitions the layer once and caches a value per group, then hands each feature the value for ",[495,496,497],"em",{},"its"," group. That is what makes \"this parcel's district total\" cheap: one scan for the whole layer regardless of how many features evaluate it. The grouping expression is evaluated on the aggregated layer's features, so it must reference that layer's fields.",[240,500,502],{"className":242,"code":501,"language":244,"meta":245,"style":245},"\"\"\"\naggregate(\n  layer:='parcels',\n  aggregate:='sum',\n  expression:=$area,\n  filter:=\"use_class\" = attribute(@parent, 'use_class')\n)\n\"\"\"\n",[27,503,504,508,512,516,520,524,529,533],{"__ignoreMap":245},[249,505,506],{"class":117,"line":251},[249,507,451],{"class":287},[249,509,510],{"class":117,"line":268},[249,511,456],{"class":287},[249,513,514],{"class":117,"line":275},[249,515,461],{"class":287},[249,517,518],{"class":117,"line":300},[249,519,466],{"class":287},[249,521,522],{"class":117,"line":305},[249,523,471],{"class":287},[249,525,526],{"class":117,"line":316},[249,527,528],{"class":287},"  filter:=\"use_class\" = attribute(@parent, 'use_class')\n",[249,530,531],{"class":117,"line":322},[249,532,325],{"class":287},[249,534,535],{"class":117,"line":328},[249,536,451],{"class":287},[14,538,539,489,541,544,545,548,549,552,553,556,557,559],{},[220,540,372],{},[27,542,543],{},"filter"," is evaluated per calling feature, which means a fresh scan for every feature that evaluates the expression — quadratic behaviour, and the reason an expression that is instant on a hundred features takes minutes on ten thousand. ",[27,546,547],{},"@parent"," is how the filter reaches the calling feature's values, and ",[27,550,551],{},"attribute(@parent, 'use_class')"," is the safe form because a bare field name inside the filter refers to the ",[495,554,555],{},"aggregated"," layer. Where a filter is really a grouping, rewrite it as ",[27,558,492],{}," and the cost collapses.",[209,561,563],{"id":562},"aggregating-related-records","Aggregating related records",[240,565,567],{"className":242,"code":566,"language":244,"meta":245,"style":245},"\"\"\"\nrelation_aggregate(\n  relation:='inspections_parcel_fk',\n  aggregate:='max',\n  expression:=\"inspected_on\"\n)\n\"\"\"\n",[27,568,569,573,578,583,588,593,597],{"__ignoreMap":245},[249,570,571],{"class":117,"line":251},[249,572,451],{"class":287},[249,574,575],{"class":117,"line":268},[249,576,577],{"class":287},"relation_aggregate(\n",[249,579,580],{"class":117,"line":275},[249,581,582],{"class":287},"  relation:='inspections_parcel_fk',\n",[249,584,585],{"class":117,"line":300},[249,586,587],{"class":287},"  aggregate:='max',\n",[249,589,590],{"class":117,"line":305},[249,591,592],{"class":287},"  expression:=\"inspected_on\"\n",[249,594,595],{"class":117,"line":316},[249,596,325],{"class":287},[249,598,599],{"class":117,"line":322},[249,600,451],{"class":287},[14,602,603,489,605,607,608,611,612,615],{},[220,604,372],{},[27,606,33],{}," walks a defined relation from the current parent feature to its children and summarises them, which is how a parcel shows the date of its most recent inspection without a join. The relation argument is the relation's ",[495,609,610],{},"id",", not its name — the two differ, and the id is what ",[27,613,614],{},"QgsProject.instance().relationManager().relations()"," gives you. Because the relation carries the join fields, there is no filter to write and no chance of getting it wrong. This is the function that makes attribute forms genuinely useful, and it is only available where a relation exists.",[209,617,619],{"id":618},"evaluating-from-python-over-every-feature","Evaluating from Python over every feature",[14,621,622],{},[38,623,626,629,632,635,638,643,646,651,655,659,663,667,670,674,677,680,682,686,689,692],{"viewBox":624,"role":41,"ariaLabel":625,"xmlns":43},"0 0 760 306","The evaluation loop: building the context once and setting the feature per iteration, versus rebuilding the context each time",[45,627,628],{},"Build the context once",[49,630,631],{},"Rebuilding the expression context inside the loop discards the aggregate cache on every iteration, so a layer of ten thousand features performs ten thousand full scans. Building the context and preparing the expression before the loop, then only setting the current feature, performs one scan.",[53,633],{"x":55,"y":55,"width":56,"height":634,"fill":58},"306",[77,636,637],{"x":79,"y":80,"style":81,"fill":82,"textAnchor":83},"Same result; one is ten thousand times the work",[53,639],{"x":87,"y":88,"width":640,"height":90,"rx":91,"fill":641,"stroke":642,"style":94},"344","#fffdf7","#b91c1c",[77,644,645],{"x":90,"y":98,"style":99,"fill":642,"textAnchor":83},"context inside the loop",[77,647,650],{"x":88,"y":648,"style":649,"fill":75},"104","font-size:10px;font-family:monospace","for feature in layer:",[77,652,654],{"x":103,"y":653,"style":649,"fill":642},"128","context = QgsExpressionContext()",[77,656,658],{"x":103,"y":657,"style":649,"fill":642},"152","context.appendScopes(...)",[77,660,662],{"x":103,"y":661,"style":649,"fill":75},"176","expression.evaluate(context)",[77,664,666],{"x":88,"y":121,"style":665,"fill":642},"font-size:10px;font-family:sans-serif","a full layer scan per feature",[53,668],{"x":669,"y":88,"width":640,"height":90,"rx":91,"fill":641,"stroke":109,"style":94},"392",[77,671,673],{"x":672,"y":98,"style":99,"fill":109,"textAnchor":83},"564","context outside the loop",[77,675,654],{"x":676,"y":648,"style":649,"fill":109},"414",[77,678,679],{"x":676,"y":653,"style":649,"fill":109},"expression.prepare(context)",[77,681,650],{"x":676,"y":657,"style":649,"fill":75},[77,683,685],{"x":684,"y":661,"style":649,"fill":75},"428","context.setFeature(feature)",[77,687,688],{"x":676,"y":121,"style":665,"fill":109},"one scan, cached throughout",[53,690],{"x":105,"y":691,"width":198,"height":106,"rx":67,"fill":200,"stroke":201,"style":110},"262",[77,693,695],{"x":79,"y":694,"style":206,"fill":75,"textAnchor":83},"284","the cache lives on the context, not on the expression",[240,697,699],{"className":242,"code":698,"language":244,"meta":245,"style":245},"expression = QgsExpression(\n    \"$area \u002F aggregate('parcels', 'sum', $area, group_by:=\\\"district\\\") * 100\"\n)\n\ncontext = QgsExpressionContext()\ncontext.appendScopes(QgsExpressionContextUtils.globalProjectLayerScopes(layer))\nexpression.prepare(context)\n\nfor feature in layer.getFeatures():\n    context.setFeature(feature)\n    share = expression.evaluate(context)\n    if expression.hasEvalError():\n        print(\"error:\", expression.evalErrorString())\n        break\n    print(feature[\"parcel_id\"], f\"{share:.2f}% of its district\")\n",[27,700,701,709,725,729,733,741,745,749,753,767,772,782,790,804,810],{"__ignoreMap":245},[249,702,703,705,707],{"class":117,"line":251},[249,704,308],{"class":258},[249,706,281],{"class":254},[249,708,313],{"class":258},[249,710,711,714,717,720,722],{"class":117,"line":268},[249,712,713],{"class":287},"    \"$area \u002F aggregate('parcels', 'sum', $area, group_by:=",[249,715,716],{"class":294},"\\\"",[249,718,719],{"class":287},"district",[249,721,716],{"class":294},[249,723,724],{"class":287},") * 100\"\n",[249,726,727],{"class":117,"line":275},[249,728,325],{"class":258},[249,730,731],{"class":117,"line":300},[249,732,272],{"emptyLinePlaceholder":271},[249,734,735,737,739],{"class":117,"line":305},[249,736,336],{"class":258},[249,738,281],{"class":254},[249,740,341],{"class":258},[249,742,743],{"class":117,"line":316},[249,744,347],{"class":258},[249,746,747],{"class":117,"line":322},[249,748,358],{"class":258},[249,750,751],{"class":117,"line":328},[249,752,272],{"emptyLinePlaceholder":271},[249,754,755,758,761,764],{"class":117,"line":333},[249,756,757],{"class":254},"for",[249,759,760],{"class":258}," feature ",[249,762,763],{"class":254},"in",[249,765,766],{"class":258}," layer.getFeatures():\n",[249,768,769],{"class":117,"line":344},[249,770,771],{"class":258},"    context.setFeature(feature)\n",[249,773,774,777,779],{"class":117,"line":350},[249,775,776],{"class":258},"    share ",[249,778,281],{"class":254},[249,780,781],{"class":258}," expression.evaluate(context)\n",[249,783,784,787],{"class":117,"line":355},[249,785,786],{"class":254},"    if",[249,788,789],{"class":258}," expression.hasEvalError():\n",[249,791,792,795,798,801],{"class":117,"line":361},[249,793,794],{"class":294},"        print",[249,796,797],{"class":258},"(",[249,799,800],{"class":287},"\"error:\"",[249,802,803],{"class":258},", expression.evalErrorString())\n",[249,805,807],{"class":117,"line":806},14,[249,808,809],{"class":254},"        break\n",[249,811,813,816,819,822,825,828,831,834,837,840,843,846],{"class":117,"line":812},15,[249,814,815],{"class":294},"    print",[249,817,818],{"class":258},"(feature[",[249,820,821],{"class":287},"\"parcel_id\"",[249,823,824],{"class":258},"], ",[249,826,827],{"class":254},"f",[249,829,830],{"class":287},"\"",[249,832,833],{"class":294},"{",[249,835,836],{"class":258},"share",[249,838,839],{"class":254},":.2f",[249,841,842],{"class":294},"}",[249,844,845],{"class":287},"% of its district\"",[249,847,325],{"class":258},[14,849,850,852,853,856,857,860,861,864,865,867,868,871,872,234],{},[220,851,372],{}," The aggregate cache lives on the context, so building it once outside the loop is what makes this a single scan. ",[27,854,855],{},"setFeature"," is the only thing that changes per iteration. Checking ",[27,858,859],{},"hasEvalError()"," matters because expression evaluation returns ",[27,862,863],{},"None"," on failure rather than raising, and a ",[27,866,863],{}," propagating into arithmetic gives a ",[27,869,870],{},"TypeError"," several lines later that names nothing useful. The mechanics of evaluating expressions generally are covered in ",[21,873,875],{"href":874},"\u002Fpyqgis-fundamentals-environment-setup\u002Fworking-with-qgis-expressions\u002Fevaluate-qgis-expression-in-pyqgis\u002F","evaluating a QGIS expression in PyQGIS",[209,877,879],{"id":878},"overlay-functions-aggregating-by-geometry","Overlay functions: aggregating by geometry",[14,881,882,883,885],{},"There is a second family that summarises by spatial relationship rather than by attribute, and it is often what people reach for ",[27,884,29],{}," to fake.",[240,887,889],{"className":242,"code":888,"language":244,"meta":245,"style":245},"\"\"\"\narray_sum(\n  overlay_intersects(\n    layer:='buildings',\n    expression:=\"floor_area\"\n  )\n)\n\"\"\"\n",[27,890,891,895,900,905,910,915,920,924],{"__ignoreMap":245},[249,892,893],{"class":117,"line":251},[249,894,451],{"class":287},[249,896,897],{"class":117,"line":268},[249,898,899],{"class":287},"array_sum(\n",[249,901,902],{"class":117,"line":275},[249,903,904],{"class":287},"  overlay_intersects(\n",[249,906,907],{"class":117,"line":300},[249,908,909],{"class":287},"    layer:='buildings',\n",[249,911,912],{"class":117,"line":305},[249,913,914],{"class":287},"    expression:=\"floor_area\"\n",[249,916,917],{"class":117,"line":316},[249,918,919],{"class":287},"  )\n",[249,921,922],{"class":117,"line":322},[249,923,325],{"class":287},[249,925,926],{"class":117,"line":328},[249,927,451],{"class":287},[14,929,930,932,933,936,937,377,940,377,943,377,946,377,949,952,953,956,957,377,960,963,964,967],{},[220,931,372],{}," The ",[27,934,935],{},"overlay_*"," functions — ",[27,938,939],{},"overlay_intersects",[27,941,942],{},"overlay_within",[27,944,945],{},"overlay_contains",[27,947,948],{},"overlay_nearest",[27,950,951],{},"overlay_touches"," and the rest — return an ",[495,954,955],{},"array"," of values from the features of another layer that satisfy the relationship with the current feature. Wrapping that in ",[27,958,959],{},"array_sum",[27,961,962],{},"array_length"," or ",[27,965,966],{},"array_max"," turns it into a number, so \"the total floor area of buildings inside this parcel\" is one expression with no spatial join.",[14,969,970,971,973,974,976,977,981],{},"They use a spatial index, so they are far faster than the ",[27,972,543],{}," form of ",[27,975,29],{}," with a geometry test, and they are still per-feature scans of the index rather than a single pass. On a layer of a few thousand features against another few thousand they are comfortable; on hundreds of thousands they are not, and a real ",[21,978,980],{"href":979},"\u002Fspatial-data-processing-automation\u002Fvector-data-manipulation\u002Fspatial-join-points-in-polygons-pyqgis\u002F","spatial join"," is the right tool.",[14,983,984,985,987,988,30,991,994],{},"One behaviour worth knowing: ",[27,986,948],{}," takes an optional ",[27,989,990],{},"limit",[27,992,993],{},"max_distance",", and leaving the distance unbounded means every feature searches the whole index. Bounding it is usually both faster and more correct, since a \"nearest\" hundreds of kilometres away is rarely the answer anyone wanted.",[209,996,998],{"id":997},"when-to-stop-using-aggregates","When to stop using aggregates",[14,1000,1001],{},"Aggregates are expressions, which means they are re-evaluated whenever anything asks for them — on every render, on every attribute table refresh, on every export. That is fine for a virtual field on a few thousand features and untenable on a large layer being panned around.",[14,1003,1004,1005,1007,1008,1010,1011,1014,1015,234],{},"The threshold in practice is roughly: a ",[27,1006,492],{}," aggregate over tens of thousands of features is fine; a ",[27,1009,543],{}," aggregate over more than a few thousand is not. Past that, compute the value once into a real field and store it, which turns an expensive expression into a cheap column read. ",[27,1012,1013],{},"native:joinbyattribute"," after a summary, or a short Python loop writing a field, both do the job — see ",[21,1016,1018],{"href":1017},"\u002Fspatial-data-processing-automation\u002Fattribute-tables-and-field-management\u002Fcount-features-by-attribute-pyqgis\u002F","counting features by attribute",[14,1020,1021],{},"The other reason to stop is reproducibility: a stored field is in the data and travels with it, while a virtual field is in the project and does not.",[209,1023,1025],{"id":1024},"qgis-version-compatibility","QGIS version compatibility",[14,1027,1028,30,1030,1032,1033,1036,1037,1039],{},[27,1029,29],{},[27,1031,33],{}," have been present since QGIS 2.16 and unchanged through 3.44. Named arguments in expression functions arrived in 3.0. ",[27,1034,1035],{},"QgsExpression.prepare"," has existed throughout and is still the difference between a cached and an uncached aggregate. The ",[27,1038,434],{}," aggregate needs 3.0 or newer.",[209,1041,1043],{"id":1042},"troubleshooting","Troubleshooting",[214,1045,1046,1055,1067,1081,1089,1095],{},[217,1047,1048,1054],{},[220,1049,1050,1051,234],{},"The result is always ",[27,1052,1053],{},"NULL"," The layer name does not match, or the expression references a field that does not exist on the aggregated layer.",[217,1056,1057,1060,1061,1063,1064,1066],{},[220,1058,1059],{},"The expression is extremely slow."," A ",[27,1062,543],{}," argument re-scanning per feature. Rewrite as ",[27,1065,492],{}," if the filter is an equality on a shared value.",[217,1068,1069,1074,1075,963,1077,1080],{},[220,1070,1071,1073],{},[27,1072,547],{}," is not recognised."," It only exists inside an aggregate's ",[27,1076,543],{},[27,1078,1079],{},"expression"," argument.",[217,1082,1083,1088],{},[220,1084,1085,1087],{},[27,1086,33],{}," returns nothing."," The relation id is wrong, or the relation is defined in the other direction.",[217,1090,1091,1094],{},[220,1092,1093],{},"Different values from the attribute table and from a script."," The script rebuilt the context per feature and the cache was discarded, or the layer has a subset string applied in one case and not the other.",[217,1096,1097,1103,1104,1106,1107,234],{},[220,1098,1099,1100,1102],{},"A ",[27,1101,870],{}," on the returned value."," Evaluation failed and returned ",[27,1105,863],{},"; check ",[27,1108,859],{},[209,1110,1112],{"id":1111},"conclusion","Conclusion",[14,1114,1115,1116,1118,1119,1121],{},"Use ",[27,1117,492],{}," rather than ",[27,1120,543],{}," whenever the filter is really a grouping, build the context once and prepare the expression, check for evaluation errors explicitly, and materialise into a real field once the layer is large enough that the scan cost matters. Aggregates replace a surprising amount of Python with one expression, right up to the point where they should not.",[209,1123,1125],{"id":1124},"frequently-asked-questions","Frequently Asked Questions",[14,1127,1128,1131],{},[220,1129,1130],{},"Can an aggregate reference a layer that is not in the project?","\nNo. The layer argument resolves against the project's layers by name or id, so the layer must be loaded.",[14,1133,1134,1140],{},[220,1135,1136,1137,1139],{},"Does ",[27,1138,418],{}," include features filtered out by a subset string?","\nNo — the aggregate sees the layer as the provider presents it, so a subset string restricts it. That is often useful and occasionally surprising.",[14,1142,1143,1146,1148],{},[220,1144,1145],{},"How do I aggregate geometry?",[27,1147,431],{}," returns a combined geometry, which is how \"the union of this district's parcels\" is expressed. It is memory-hungry on large groups.",[14,1150,1151,1154,1155,1157],{},[220,1152,1153],{},"Can I use an aggregate in a data-defined symbol property?","\nYes, and it is a good use of ",[27,1156,492],{}," — sizing a symbol by its share of a category is one expression. Expect it to be evaluated on every render, so keep the group count modest.",[209,1159,1161],{"id":1160},"related","Related",[214,1163,1164,1170,1175,1181,1186],{},[217,1165,1166,1169],{},[21,1167,1168],{"href":23},"Working with QGIS Expressions in PyQGIS"," — the guide this recipe belongs to",[217,1171,1172],{},[21,1173,1174],{"href":874},"Evaluate a QGIS Expression in PyQGIS",[217,1176,1177],{},[21,1178,1180],{"href":1179},"\u002Fpyqgis-fundamentals-environment-setup\u002Fworking-with-qgis-expressions\u002Fregister-custom-expression-function-pyqgis\u002F","Register a Custom Expression Function in PyQGIS",[217,1182,1183],{},[21,1184,1185],{"href":232},"Define Layer Relations in PyQGIS",[217,1187,1188],{},[21,1189,1190],{"href":1017},"Count Features by Attribute in PyQGIS",[1192,1193,1194],"style",{},"html pre.shiki code .snl16, html code.shiki .snl16{--shiki-default:#F97583}html pre.shiki code .s95oV, html code.shiki .s95oV{--shiki-default:#E1E4E8}html pre.shiki code .sU2Wk, html code.shiki .sU2Wk{--shiki-default:#9ECBFF}html pre.shiki code .sDLfK, html code.shiki .sDLfK{--shiki-default:#79B8FF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}",{"title":245,"searchDepth":268,"depth":268,"links":1196},[1197,1198,1199,1200,1201,1202,1203,1204,1205,1206,1207,1208],{"id":211,"depth":268,"text":212},{"id":237,"depth":268,"text":238},{"id":437,"depth":268,"text":438},{"id":562,"depth":268,"text":563},{"id":618,"depth":268,"text":619},{"id":878,"depth":268,"text":879},{"id":997,"depth":268,"text":998},{"id":1024,"depth":268,"text":1025},{"id":1042,"depth":268,"text":1043},{"id":1111,"depth":268,"text":1112},{"id":1124,"depth":268,"text":1125},{"id":1160,"depth":268,"text":1161},"Summarise across features and across layers with the aggregate, relation_aggregate and overlay functions — grouping, filtering, evaluation context, and where they get slow.","md",{"slug":1212,"type":1213,"breadcrumb":1214,"datePublished":1215,"dateModified":1215},"use-aggregate-expressions-pyqgis","article","Aggregate Expressions","2026-09-05","\u002Fpyqgis-fundamentals-environment-setup\u002Fworking-with-qgis-expressions\u002Fuse-aggregate-expressions-pyqgis",{"title":5,"description":1209},"pyqgis-fundamentals-environment-setup\u002Fworking-with-qgis-expressions\u002Fuse-aggregate-expressions-pyqgis\u002Findex","W9fMwV3vw_lnD955_VqoX4WPRhOpIhEhnQB2QV3EvIg",1788563847752]