[{"data":1,"prerenderedAt":1510},["ShallowReactive",2],{"doc:\u002Fqgis-plugin-development\u002Fbackground-tasks-and-plugin-performance\u002Fspeed-up-feature-iteration-with-qgsfeaturerequest-pyqgis":3},{"id":4,"title":5,"body":6,"description":1499,"extension":1500,"meta":1501,"navigation":236,"path":1506,"seo":1507,"stem":1508,"__hash__":1509},"docs\u002Fqgis-plugin-development\u002Fbackground-tasks-and-plugin-performance\u002Fspeed-up-feature-iteration-with-qgsfeaturerequest-pyqgis\u002Findex.md","Speed Up Feature Iteration with QgsFeatureRequest",{"type":7,"value":8,"toc":1485},"minimark",[9,13,21,27,36,174,179,199,203,311,330,334,380,393,397,404,528,557,674,678,681,789,813,817,1005,1021,1025,1028,1155,1168,1183,1247,1251,1257,1330,1334,1394,1398,1404,1408,1414,1424,1430,1436,1452,1456,1481],[10,11,5],"h1",{"id":12},"speed-up-feature-iteration-with-qgsfeaturerequest",[14,15,16,20],"p",{},[17,18,19],"code",{},"for feature in layer.getFeatures()"," is the first line of PyQGIS everyone learns and the last one they think to optimise. It is also, on a wide table, the most expensive thing in the script: every column is read, every geometry is decoded, and the loop body then uses two fields and ignores the shape entirely.",[14,22,23,26],{},[17,24,25],{},"QgsFeatureRequest"," is how you say what you actually need. It is the cheapest performance win in PyQGIS — usually a two-line change — and it frequently removes the need for the background task you were about to write.",[14,28,29,30,35],{},"This recipe belongs to ",[31,32,34],"a",{"href":33},"\u002Fqgis-plugin-development\u002Fbackground-tasks-and-plugin-performance\u002F","Background Tasks and Plugin Performance",". It covers attribute subsets, dropping geometry, expression and rectangle filters, sorting and limiting, and the traps that make a request silently return the wrong thing.",[14,37,38],{},[39,40,45,49,53,60,77,86,96,102,111,116,119,123,126,130,135,139,143,146,150,153,156,158,161,163,166,168,171],"svg",{"viewBox":41,"role":42,"ariaLabel":43,"xmlns":44},"0 0 760 288","img","What the provider does for an unqualified request compared with a narrowed one: reading all columns and decoding all geometry against reading two columns with no geometry decoding","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg",[46,47,48],"title",{},"What the provider does for each request",[50,51,52],"desc",{},"An unqualified request causes the provider to read every column of every row, decode every geometry into an object and construct a full feature. A narrowed request reads two columns, skips geometry decoding entirely and constructs a much smaller feature, so the same loop body runs against far less work.",[54,55],"rect",{"x":56,"y":56,"width":57,"height":58,"fill":59},"0","760","288","#f6f3ea",[61,62,63],"defs",{},[64,65,72],"marker",{"id":66,"viewBox":67,"refX":68,"refY":69,"markerWidth":70,"markerHeight":70,"orient":71},"frArrow","0 0 10 10","8","5","7","auto-start-reverse",[73,74],"path",{"d":75,"fill":76},"M0 0 L10 5 L0 10 z","#2f3b35",[78,79,85],"text",{"x":80,"y":81,"style":82,"fill":83,"textAnchor":84},"380","28","text-anchor:middle;font-size:14px;font-weight:bold;font-family:sans-serif","#17211d","middle","The loop body is the same — the work behind it is not",[54,87],{"x":88,"y":89,"width":90,"height":91,"rx":92,"fill":93,"stroke":94,"style":95},"16","48","356","212","10","#fdf2e2","#b45309","stroke-width:2.5",[78,97,101],{"x":98,"y":99,"style":100,"fill":94,"textAnchor":84},"194","74","text-anchor:middle;font-size:12px;font-weight:bold;font-family:sans-serif","getFeatures() — no request",[54,103],{"x":104,"y":105,"width":106,"height":107,"rx":69,"fill":108,"stroke":109,"style":110},"40","90","308","30","#fffdf7","#59645f","stroke-width:1.5",[78,112,115],{"x":98,"y":113,"style":114,"fill":76,"textAnchor":84},"110","text-anchor:middle;font-size:11px;font-family:sans-serif","read 40 columns from disk",[54,117],{"x":104,"y":118,"width":106,"height":107,"rx":69,"fill":108,"stroke":109,"style":110},"126",[78,120,122],{"x":98,"y":121,"style":114,"fill":76,"textAnchor":84},"146","decode WKB into a geometry object",[54,124],{"x":104,"y":125,"width":106,"height":107,"rx":69,"fill":108,"stroke":109,"style":110},"162",[78,127,129],{"x":98,"y":128,"style":114,"fill":76,"textAnchor":84},"182","build a 40-attribute feature",[54,131],{"x":104,"y":132,"width":106,"height":107,"rx":69,"fill":133,"stroke":134,"style":110},"198","#edf8e9","#15803d",[78,136,138],{"x":98,"y":137,"style":114,"fill":76,"textAnchor":84},"218","your loop uses 2 of them",[78,140,142],{"x":98,"y":141,"style":114,"fill":94,"textAnchor":84},"248","95% of the work is discarded",[54,144],{"x":145,"y":89,"width":90,"height":91,"rx":92,"fill":133,"stroke":134,"style":95},"388",[78,147,149],{"x":148,"y":99,"style":100,"fill":134,"textAnchor":84},"566","getFeatures(request)",[54,151],{"x":152,"y":105,"width":106,"height":107,"rx":69,"fill":108,"stroke":109,"style":110},"412",[78,154,155],{"x":148,"y":113,"style":114,"fill":76,"textAnchor":84},"read 2 columns from disk",[54,157],{"x":152,"y":118,"width":106,"height":107,"rx":69,"fill":108,"stroke":109,"style":110},[78,159,160],{"x":148,"y":121,"style":114,"fill":76,"textAnchor":84},"geometry never decoded",[54,162],{"x":152,"y":125,"width":106,"height":107,"rx":69,"fill":108,"stroke":109,"style":110},[78,164,165],{"x":148,"y":128,"style":114,"fill":76,"textAnchor":84},"build a 2-attribute feature",[54,167],{"x":152,"y":132,"width":106,"height":107,"rx":69,"fill":133,"stroke":134,"style":110},[78,169,170],{"x":148,"y":137,"style":114,"fill":76,"textAnchor":84},"your loop uses both",[78,172,173],{"x":148,"y":141,"style":114,"fill":134,"textAnchor":84},"nothing is wasted",[175,176,178],"h2",{"id":177},"prerequisites","Prerequisites",[180,181,182,190,193],"ul",{},[183,184,185,189],"li",{},[186,187,188],"strong",{},"QGIS 3.34 LTR"," (bundled Python 3.12) or newer.",[183,191,192],{},"A layer with enough rows that timing means something — a few tens of thousands upward.",[183,194,195,198],{},[17,196,197],{},"time.perf_counter()"," and a willingness to measure before and after; the numbers vary hugely by provider and table shape.",[175,200,202],{"id":201},"ask-for-fewer-attributes","Ask for fewer attributes",[204,205,210],"pre",{"className":206,"code":207,"language":208,"meta":209,"style":209},"language-python shiki shiki-themes github-dark","from qgis.core import QgsFeatureRequest\n\nrequest = QgsFeatureRequest().setSubsetOfAttributes(\n    [\"parcel_ref\", \"area_m2\"], layer.fields()\n)\n\nfor feature in layer.getFeatures(request):\n    process(feature[\"parcel_ref\"], feature[\"area_m2\"])\n","python","",[17,211,212,231,238,250,269,275,280,295],{"__ignoreMap":209},[213,214,217,221,225,228],"span",{"class":215,"line":216},"line",1,[213,218,220],{"class":219},"snl16","from",[213,222,224],{"class":223},"s95oV"," qgis.core ",[213,226,227],{"class":219},"import",[213,229,230],{"class":223}," QgsFeatureRequest\n",[213,232,234],{"class":215,"line":233},2,[213,235,237],{"emptyLinePlaceholder":236},true,"\n",[213,239,241,244,247],{"class":215,"line":240},3,[213,242,243],{"class":223},"request ",[213,245,246],{"class":219},"=",[213,248,249],{"class":223}," QgsFeatureRequest().setSubsetOfAttributes(\n",[213,251,253,256,260,263,266],{"class":215,"line":252},4,[213,254,255],{"class":223},"    [",[213,257,259],{"class":258},"sU2Wk","\"parcel_ref\"",[213,261,262],{"class":223},", ",[213,264,265],{"class":258},"\"area_m2\"",[213,267,268],{"class":223},"], layer.fields()\n",[213,270,272],{"class":215,"line":271},5,[213,273,274],{"class":223},")\n",[213,276,278],{"class":215,"line":277},6,[213,279,237],{"emptyLinePlaceholder":236},[213,281,283,286,289,292],{"class":215,"line":282},7,[213,284,285],{"class":219},"for",[213,287,288],{"class":223}," feature ",[213,290,291],{"class":219},"in",[213,293,294],{"class":223}," layer.getFeatures(request):\n",[213,296,298,301,303,306,308],{"class":215,"line":297},8,[213,299,300],{"class":223},"    process(feature[",[213,302,259],{"class":258},[213,304,305],{"class":223},"], feature[",[213,307,265],{"class":258},[213,309,310],{"class":223},"])\n",[14,312,313,316,317,321,322,325,326,329],{},[186,314,315],{},"Breakdown:"," The two-argument form takes field ",[318,319,320],"em",{},"names"," and the layer's field list, which is far less error-prone than the index-based overload — a hard-coded index breaks the moment someone adds a column. The provider then reads only those columns; on a PostGIS layer the narrowed column list even reaches the ",[17,323,324],{},"SELECT",". Accessing an attribute that was not requested returns ",[17,327,328],{},"NULL"," rather than raising, which is exactly how this optimisation turns into a silent bug, so keep the requested list and the loop body next to each other.",[175,331,333],{"id":332},"drop-geometry-when-you-do-not-need-it","Drop geometry when you do not need it",[204,335,337],{"className":206,"code":336,"language":208,"meta":209,"style":209},"request = QgsFeatureRequest().setFlags(QgsFeatureRequest.NoGeometry)\n\ntotal = sum(feature[\"area_m2\"] for feature in layer.getFeatures(request))\n",[17,338,339,348,352],{"__ignoreMap":209},[213,340,341,343,345],{"class":215,"line":216},[213,342,243],{"class":223},[213,344,246],{"class":219},[213,346,347],{"class":223}," QgsFeatureRequest().setFlags(QgsFeatureRequest.NoGeometry)\n",[213,349,350],{"class":215,"line":233},[213,351,237],{"emptyLinePlaceholder":236},[213,353,354,357,359,363,366,368,371,373,375,377],{"class":215,"line":240},[213,355,356],{"class":223},"total ",[213,358,246],{"class":219},[213,360,362],{"class":361},"sDLfK"," sum",[213,364,365],{"class":223},"(feature[",[213,367,265],{"class":258},[213,369,370],{"class":223},"] ",[213,372,285],{"class":219},[213,374,288],{"class":223},[213,376,291],{"class":219},[213,378,379],{"class":223}," layer.getFeatures(request))\n",[14,381,382,384,385,388,389,392],{},[186,383,315],{}," ",[17,386,387],{},"NoGeometry"," skips both the read and the decode of the geometry blob. For polygon layers with detailed boundaries this is often the single largest saving available, because parsing thousands of vertices per feature dwarfs reading a handful of numbers. The features still arrive; ",[17,390,391],{},"feature.geometry()"," simply returns an empty geometry, so any code path that touches the shape must be excluded — a good reason to keep attribute-only passes in their own function.",[175,394,396],{"id":395},"filter-in-the-provider-not-in-python","Filter in the provider, not in Python",[14,398,399,400,403],{},"Every ",[17,401,402],{},"if"," at the top of a loop body is a row the provider should not have sent.",[204,405,407],{"className":206,"code":406,"language":208,"meta":209,"style":209},"from qgis.core import QgsFeatureRequest, QgsRectangle\n\nby_expression = QgsFeatureRequest().setFilterExpression(\n    '\"status\" = \\'active\\' AND \"area_m2\" > 500'\n)\n\nin_view = QgsFeatureRequest().setFilterRect(\n    QgsRectangle(432000, 187000, 436000, 191000)\n)\n\nby_ids = QgsFeatureRequest().setFilterFids([12, 48, 91])\n",[17,408,409,420,424,434,450,454,458,468,493,498,503],{"__ignoreMap":209},[213,410,411,413,415,417],{"class":215,"line":216},[213,412,220],{"class":219},[213,414,224],{"class":223},[213,416,227],{"class":219},[213,418,419],{"class":223}," QgsFeatureRequest, QgsRectangle\n",[213,421,422],{"class":215,"line":233},[213,423,237],{"emptyLinePlaceholder":236},[213,425,426,429,431],{"class":215,"line":240},[213,427,428],{"class":223},"by_expression ",[213,430,246],{"class":219},[213,432,433],{"class":223}," QgsFeatureRequest().setFilterExpression(\n",[213,435,436,439,442,445,447],{"class":215,"line":252},[213,437,438],{"class":258},"    '\"status\" = ",[213,440,441],{"class":361},"\\'",[213,443,444],{"class":258},"active",[213,446,441],{"class":361},[213,448,449],{"class":258}," AND \"area_m2\" > 500'\n",[213,451,452],{"class":215,"line":271},[213,453,274],{"class":223},[213,455,456],{"class":215,"line":277},[213,457,237],{"emptyLinePlaceholder":236},[213,459,460,463,465],{"class":215,"line":282},[213,461,462],{"class":223},"in_view ",[213,464,246],{"class":219},[213,466,467],{"class":223}," QgsFeatureRequest().setFilterRect(\n",[213,469,470,473,476,478,481,483,486,488,491],{"class":215,"line":297},[213,471,472],{"class":223},"    QgsRectangle(",[213,474,475],{"class":361},"432000",[213,477,262],{"class":223},[213,479,480],{"class":361},"187000",[213,482,262],{"class":223},[213,484,485],{"class":361},"436000",[213,487,262],{"class":223},[213,489,490],{"class":361},"191000",[213,492,274],{"class":223},[213,494,496],{"class":215,"line":495},9,[213,497,274],{"class":223},[213,499,501],{"class":215,"line":500},10,[213,502,237],{"emptyLinePlaceholder":236},[213,504,506,509,511,514,517,519,521,523,526],{"class":215,"line":505},11,[213,507,508],{"class":223},"by_ids ",[213,510,246],{"class":219},[213,512,513],{"class":223}," QgsFeatureRequest().setFilterFids([",[213,515,516],{"class":361},"12",[213,518,262],{"class":223},[213,520,89],{"class":361},[213,522,262],{"class":223},[213,524,525],{"class":361},"91",[213,527,310],{"class":223},[14,529,530,384,532,535,536,539,540,543,544,547,548,551,552,556],{},[186,531,315],{},[17,533,534],{},"setFilterExpression()"," accepts any QGIS expression, and where the provider can translate it — PostGIS can translate most — it becomes a ",[17,537,538],{},"WHERE"," clause evaluated by the database. ",[17,541,542],{},"setFilterRect()"," uses the layer's spatial index, so it is orders of magnitude faster than testing each geometry's bounding box in Python; note that it selects features whose ",[318,545,546],{},"bounding box"," intersects, so an exact test is still needed for precise work. ",[17,549,550],{},"setFilterFids()"," is the right way to re-read a known set of features, and it is what a selection-driven plugin should use rather than iterating everything and comparing ids. Expression syntax is covered in ",[31,553,555],{"href":554},"\u002Fpyqgis-fundamentals-environment-setup\u002Fworking-with-qgis-expressions\u002Fselect-features-by-expression-pyqgis\u002F","Select Features by Expression in PyQGIS",".",[14,558,559],{},[39,560,563,566,569,572,579,582,587,591,599,605,608,613,616,619,621,624,629,633,635,638,642,646,649,652,659,662,666,670],{"viewBox":561,"role":42,"ariaLabel":562,"xmlns":44},"0 0 760 260","Decision tree choosing between a rectangle filter, an expression filter, a feature id filter and no filter, based on what limits the set of features you need",[46,564,565],{},"Choosing the right filter",[50,567,568],{},"If the limit is spatial, use a filter rectangle so the spatial index is used. If the limit is an attribute condition, use a filter expression so the provider evaluates it. If you already know which features you want, filter by their identifiers. Only when you genuinely need every feature should the request carry no filter at all.",[54,570],{"x":56,"y":56,"width":57,"height":571,"fill":59},"260",[61,573,574],{},[64,575,577],{"id":576,"viewBox":67,"refX":68,"refY":69,"markerWidth":70,"markerHeight":70,"orient":71},"filtArrow",[73,578],{"d":75,"fill":109},[78,580,581],{"x":80,"y":81,"style":82,"fill":83,"textAnchor":84},"What limits the features you need?",[54,583],{"x":584,"y":89,"width":585,"height":586,"rx":68,"fill":108,"stroke":83,"style":95},"290","180","44",[78,588,590],{"x":80,"y":589,"style":100,"fill":83,"textAnchor":84},"76","what limits the set?",[54,592],{"x":88,"y":593,"width":594,"height":595,"rx":68,"fill":596,"stroke":597,"style":598},"140","172","72","#eff3ff","#2563eb","stroke-width:2",[78,600,604],{"x":601,"y":602,"style":603,"fill":597,"textAnchor":84},"102","166","text-anchor:middle;font-size:11px;font-weight:bold;font-family:sans-serif","an area of the map",[78,606,542],{"x":601,"y":607,"style":114,"fill":76,"textAnchor":84},"188",[78,609,612],{"x":601,"y":610,"style":611,"fill":109,"textAnchor":84},"206","text-anchor:middle;font-size:10px;font-family:sans-serif","uses the spatial index",[54,614],{"x":615,"y":593,"width":594,"height":595,"rx":68,"fill":133,"stroke":134,"style":598},"204",[78,617,618],{"x":584,"y":602,"style":603,"fill":134,"textAnchor":84},"an attribute rule",[78,620,534],{"x":584,"y":607,"style":114,"fill":76,"textAnchor":84},[78,622,623],{"x":584,"y":610,"style":611,"fill":109,"textAnchor":84},"pushed to the database",[54,625],{"x":626,"y":593,"width":594,"height":595,"rx":68,"fill":627,"stroke":628,"style":598},"392","#eef7f4","#0f766e",[78,630,632],{"x":631,"y":602,"style":603,"fill":628,"textAnchor":84},"478","a known list",[78,634,550],{"x":631,"y":607,"style":114,"fill":76,"textAnchor":84},[78,636,637],{"x":631,"y":610,"style":611,"fill":109,"textAnchor":84},"for selections",[54,639],{"x":640,"y":593,"width":641,"height":595,"rx":68,"fill":93,"stroke":94,"style":598},"580","164",[78,643,645],{"x":644,"y":602,"style":603,"fill":94,"textAnchor":84},"662","nothing",[78,647,648],{"x":644,"y":607,"style":114,"fill":76,"textAnchor":84},"no filter",[78,650,651],{"x":644,"y":610,"style":611,"fill":109,"textAnchor":84},"still narrow the columns",[215,653],{"x1":654,"y1":655,"x2":656,"y2":657,"stroke":109,"style":658},"320","92","130","134","stroke-width:2;marker-end:url(#filtArrow)",[215,660],{"x1":90,"y1":655,"x2":661,"y2":657,"stroke":109,"style":658},"300",[215,663],{"x1":664,"y1":655,"x2":665,"y2":657,"stroke":109,"style":658},"404","462",[215,667],{"x1":668,"y1":655,"x2":669,"y2":657,"stroke":109,"style":658},"440","630",[78,671,673],{"x":80,"y":672,"style":114,"fill":109,"textAnchor":84},"244","Combine freely — a rectangle and an expression on the same request is normal",[175,675,677],{"id":676},"combine-sort-and-limit","Combine, sort and limit",[14,679,680],{},"Requests chain, because each setter returns the request.",[204,682,684],{"className":206,"code":683,"language":208,"meta":209,"style":209},"request = (\n    QgsFeatureRequest()\n    .setFilterExpression('\"status\" = \\'active\\'')\n    .setSubsetOfAttributes([\"parcel_ref\", \"area_m2\"], layer.fields())\n    .setFlags(QgsFeatureRequest.NoGeometry)\n    .addOrderBy(\"area_m2\", ascending=False)\n    .setLimit(20)\n)\n\nlargest = list(layer.getFeatures(request))\n",[17,685,686,695,700,719,733,738,758,768,772,776],{"__ignoreMap":209},[213,687,688,690,692],{"class":215,"line":216},[213,689,243],{"class":223},[213,691,246],{"class":219},[213,693,694],{"class":223}," (\n",[213,696,697],{"class":215,"line":233},[213,698,699],{"class":223},"    QgsFeatureRequest()\n",[213,701,702,705,708,710,712,714,717],{"class":215,"line":240},[213,703,704],{"class":223},"    .setFilterExpression(",[213,706,707],{"class":258},"'\"status\" = ",[213,709,441],{"class":361},[213,711,444],{"class":258},[213,713,441],{"class":361},[213,715,716],{"class":258},"'",[213,718,274],{"class":223},[213,720,721,724,726,728,730],{"class":215,"line":252},[213,722,723],{"class":223},"    .setSubsetOfAttributes([",[213,725,259],{"class":258},[213,727,262],{"class":223},[213,729,265],{"class":258},[213,731,732],{"class":223},"], layer.fields())\n",[213,734,735],{"class":215,"line":271},[213,736,737],{"class":223},"    .setFlags(QgsFeatureRequest.NoGeometry)\n",[213,739,740,743,745,747,751,753,756],{"class":215,"line":277},[213,741,742],{"class":223},"    .addOrderBy(",[213,744,265],{"class":258},[213,746,262],{"class":223},[213,748,750],{"class":749},"s9osk","ascending",[213,752,246],{"class":219},[213,754,755],{"class":361},"False",[213,757,274],{"class":223},[213,759,760,763,766],{"class":215,"line":282},[213,761,762],{"class":223},"    .setLimit(",[213,764,765],{"class":361},"20",[213,767,274],{"class":223},[213,769,770],{"class":215,"line":297},[213,771,274],{"class":223},[213,773,774],{"class":215,"line":495},[213,775,237],{"emptyLinePlaceholder":236},[213,777,778,781,783,786],{"class":215,"line":500},[213,779,780],{"class":223},"largest ",[213,782,246],{"class":219},[213,784,785],{"class":361}," list",[213,787,788],{"class":223},"(layer.getFeatures(request))\n",[14,790,791,793,794,797,798,801,802,805,806,384,809,812],{},[186,792,315],{}," This is the \"twenty largest active parcels\" question answered in one provider round trip instead of a full read plus a Python sort. ",[17,795,796],{},"addOrderBy()"," is translated to ",[17,799,800],{},"ORDER BY"," where the provider supports it and falls back to an in-QGIS sort where it does not, so it is always correct and sometimes fast. ",[17,803,804],{},"setLimit()"," caps the number returned — combined with an ordering it is a top-N query, and used alone it is the right way to sample a layer while developing. Note that ",[17,807,808],{},"setFlags()",[186,810,811],{},"replaces"," the flag set rather than adding to it, so call it once with everything you want.",[175,814,816],{"id":815},"measure-the-difference","Measure the difference",[204,818,820],{"className":206,"code":819,"language":208,"meta":209,"style":209},"import time\nfrom qgis.core import QgsFeatureRequest\n\ndef timed(label, request=None):\n    started = time.perf_counter()\n    count = sum(1 for _ in (layer.getFeatures(request) if request else layer.getFeatures()))\n    print(f\"{label}: {count} features in {time.perf_counter() - started:.2f}s\")\n\ntimed(\"everything\")\ntimed(\"narrowed\", QgsFeatureRequest()\n      .setSubsetOfAttributes([\"area_m2\"], layer.fields())\n      .setFlags(QgsFeatureRequest.NoGeometry))\n",[17,821,822,829,839,843,863,873,910,966,970,980,990,999],{"__ignoreMap":209},[213,823,824,826],{"class":215,"line":216},[213,825,227],{"class":219},[213,827,828],{"class":223}," time\n",[213,830,831,833,835,837],{"class":215,"line":233},[213,832,220],{"class":219},[213,834,224],{"class":223},[213,836,227],{"class":219},[213,838,230],{"class":223},[213,840,841],{"class":215,"line":240},[213,842,237],{"emptyLinePlaceholder":236},[213,844,845,848,852,855,857,860],{"class":215,"line":252},[213,846,847],{"class":219},"def",[213,849,851],{"class":850},"svObZ"," timed",[213,853,854],{"class":223},"(label, request",[213,856,246],{"class":219},[213,858,859],{"class":361},"None",[213,861,862],{"class":223},"):\n",[213,864,865,868,870],{"class":215,"line":271},[213,866,867],{"class":223},"    started ",[213,869,246],{"class":219},[213,871,872],{"class":223}," time.perf_counter()\n",[213,874,875,878,880,882,885,888,891,894,896,899,901,904,907],{"class":215,"line":277},[213,876,877],{"class":223},"    count ",[213,879,246],{"class":219},[213,881,362],{"class":361},[213,883,884],{"class":223},"(",[213,886,887],{"class":361},"1",[213,889,890],{"class":219}," for",[213,892,893],{"class":223}," _ ",[213,895,291],{"class":219},[213,897,898],{"class":223}," (layer.getFeatures(request) ",[213,900,402],{"class":219},[213,902,903],{"class":223}," request ",[213,905,906],{"class":219},"else",[213,908,909],{"class":223}," layer.getFeatures()))\n",[213,911,912,915,917,920,923,926,929,932,935,937,940,942,945,947,950,953,956,959,961,964],{"class":215,"line":282},[213,913,914],{"class":361},"    print",[213,916,884],{"class":223},[213,918,919],{"class":219},"f",[213,921,922],{"class":258},"\"",[213,924,925],{"class":361},"{",[213,927,928],{"class":223},"label",[213,930,931],{"class":361},"}",[213,933,934],{"class":258},": ",[213,936,925],{"class":361},[213,938,939],{"class":223},"count",[213,941,931],{"class":361},[213,943,944],{"class":258}," features in ",[213,946,925],{"class":361},[213,948,949],{"class":223},"time.perf_counter() ",[213,951,952],{"class":219},"-",[213,954,955],{"class":223}," started",[213,957,958],{"class":219},":.2f",[213,960,931],{"class":361},[213,962,963],{"class":258},"s\"",[213,965,274],{"class":223},[213,967,968],{"class":215,"line":297},[213,969,237],{"emptyLinePlaceholder":236},[213,971,972,975,978],{"class":215,"line":495},[213,973,974],{"class":223},"timed(",[213,976,977],{"class":258},"\"everything\"",[213,979,274],{"class":223},[213,981,982,984,987],{"class":215,"line":500},[213,983,974],{"class":223},[213,985,986],{"class":258},"\"narrowed\"",[213,988,989],{"class":223},", QgsFeatureRequest()\n",[213,991,992,995,997],{"class":215,"line":505},[213,993,994],{"class":223},"      .setSubsetOfAttributes([",[213,996,265],{"class":258},[213,998,732],{"class":223},[213,1000,1002],{"class":215,"line":1001},12,[213,1003,1004],{"class":223},"      .setFlags(QgsFeatureRequest.NoGeometry))\n",[14,1006,1007,384,1009,1012,1013,1016,1017,556],{},[186,1008,315],{},[17,1010,1011],{},"perf_counter()"," is monotonic and high-resolution, which matters for durations under a second. Counting with ",[17,1014,1015],{},"sum(1 for _ in …)"," avoids building a list, so the measurement reflects iteration rather than memory allocation. Run each twice and take the second number — the first pass warms the operating system's file cache, and comparing a cold read against a warm one produces a flattering result that will not survive production. The wider profiling story is in ",[31,1018,1020],{"href":1019},"\u002Fqgis-plugin-development\u002Fbackground-tasks-and-plugin-performance\u002Fprofile-slow-pyqgis-code\u002F","Profile Slow PyQGIS Code",[175,1022,1024],{"id":1023},"comparing-two-layers-without-a-nested-loop","Comparing two layers without a nested loop",[14,1026,1027],{},"A feature request narrows one layer's read. It cannot help with the other classic slow pattern: comparing every feature of one layer against every feature of another, which costs the product of the two counts and becomes unusable well before either layer is large.",[204,1029,1031],{"className":206,"code":1030,"language":208,"meta":209,"style":209},"from qgis.core import QgsSpatialIndex, QgsFeatureRequest\n\nindex = QgsSpatialIndex(wards.getFeatures(QgsFeatureRequest()))\nward_geometries = {f.id(): f.geometry() for f in wards.getFeatures()}\n\nrequest = QgsFeatureRequest().setSubsetOfAttributes([\"id\"], incidents.fields())\n\nfor incident in incidents.getFeatures(request):\n    geometry = incident.geometry()\n    for candidate_id in index.intersects(geometry.boundingBox()):\n        if ward_geometries[candidate_id].contains(geometry):\n            assign(incident, candidate_id)\n            break\n",[17,1032,1033,1044,1048,1058,1078,1082,1097,1101,1113,1123,1136,1144,1149],{"__ignoreMap":209},[213,1034,1035,1037,1039,1041],{"class":215,"line":216},[213,1036,220],{"class":219},[213,1038,224],{"class":223},[213,1040,227],{"class":219},[213,1042,1043],{"class":223}," QgsSpatialIndex, QgsFeatureRequest\n",[213,1045,1046],{"class":215,"line":233},[213,1047,237],{"emptyLinePlaceholder":236},[213,1049,1050,1053,1055],{"class":215,"line":240},[213,1051,1052],{"class":223},"index ",[213,1054,246],{"class":219},[213,1056,1057],{"class":223}," QgsSpatialIndex(wards.getFeatures(QgsFeatureRequest()))\n",[213,1059,1060,1063,1065,1068,1070,1073,1075],{"class":215,"line":252},[213,1061,1062],{"class":223},"ward_geometries ",[213,1064,246],{"class":219},[213,1066,1067],{"class":223}," {f.id(): f.geometry() ",[213,1069,285],{"class":219},[213,1071,1072],{"class":223}," f ",[213,1074,291],{"class":219},[213,1076,1077],{"class":223}," wards.getFeatures()}\n",[213,1079,1080],{"class":215,"line":271},[213,1081,237],{"emptyLinePlaceholder":236},[213,1083,1084,1086,1088,1091,1094],{"class":215,"line":277},[213,1085,243],{"class":223},[213,1087,246],{"class":219},[213,1089,1090],{"class":223}," QgsFeatureRequest().setSubsetOfAttributes([",[213,1092,1093],{"class":258},"\"id\"",[213,1095,1096],{"class":223},"], incidents.fields())\n",[213,1098,1099],{"class":215,"line":282},[213,1100,237],{"emptyLinePlaceholder":236},[213,1102,1103,1105,1108,1110],{"class":215,"line":297},[213,1104,285],{"class":219},[213,1106,1107],{"class":223}," incident ",[213,1109,291],{"class":219},[213,1111,1112],{"class":223}," incidents.getFeatures(request):\n",[213,1114,1115,1118,1120],{"class":215,"line":495},[213,1116,1117],{"class":223},"    geometry ",[213,1119,246],{"class":219},[213,1121,1122],{"class":223}," incident.geometry()\n",[213,1124,1125,1128,1131,1133],{"class":215,"line":500},[213,1126,1127],{"class":219},"    for",[213,1129,1130],{"class":223}," candidate_id ",[213,1132,291],{"class":219},[213,1134,1135],{"class":223}," index.intersects(geometry.boundingBox()):\n",[213,1137,1138,1141],{"class":215,"line":505},[213,1139,1140],{"class":219},"        if",[213,1142,1143],{"class":223}," ward_geometries[candidate_id].contains(geometry):\n",[213,1145,1146],{"class":215,"line":1001},[213,1147,1148],{"class":223},"            assign(incident, candidate_id)\n",[213,1150,1152],{"class":215,"line":1151},13,[213,1153,1154],{"class":219},"            break\n",[14,1156,1157,1159,1160,1163,1164,1167],{},[186,1158,315],{}," The index answers \"which wards could possibly contain this point\" from a bounding-box tree, turning the inner loop from every ward into typically one or two candidates. The exact ",[17,1161,1162],{},"contains()"," test still runs, because a bounding box overlap is not containment — skipping that second test is the most common way this optimisation produces wrong answers. Caching the geometries in a dictionary avoids re-reading each candidate from the provider inside the loop, which would reintroduce the cost the index just removed. ",[17,1165,1166],{},"break"," stops at the first containing ward, which is correct when the wards tile the area without overlapping.",[14,1169,1170,1171,1174,1175,1178,1179,556],{},"The cost model is worth internalising: the nested loop is proportional to ",[318,1172,1173],{},"n × m",", the indexed version to roughly ",[318,1176,1177],{},"n log m",". On ten thousand points and five hundred wards that is five million comparisons against about ninety thousand — the difference between a coffee break and a blink. When both layers live in the same database, the equivalent query with a GiST index is faster still and needs no Python at all, as shown in ",[31,1180,1182],{"href":1181},"\u002Fspatial-data-processing-automation\u002Fvector-data-manipulation\u002Fspatial-join-points-in-polygons-pyqgis\u002F","Spatial Join Points to Polygons in PyQGIS",[14,1184,1185],{},[39,1186,1189,1192,1195,1198,1201,1205,1209,1213,1217,1224,1228,1230,1233,1236,1239,1244],{"viewBox":1187,"role":42,"ariaLabel":1188,"xmlns":44},"0 0 760 240","A nested loop comparing every point against every polygon, against an indexed lookup that tests each point only against the few candidates its bounding box overlaps",[46,1190,1191],{},"Every polygon, or only the plausible ones",[50,1193,1194],{},"Without an index, each of ten thousand points is tested against all five hundred polygons, giving five million geometry comparisons. With a spatial index, each point retrieves the handful of polygons whose bounding boxes overlap it, so only about twenty thousand exact tests run.",[54,1196],{"x":56,"y":56,"width":57,"height":1197,"fill":59},"240",[78,1199,1200],{"x":80,"y":81,"style":82,"fill":83,"textAnchor":84},"The index does not answer the question — it shortens the list",[54,1202],{"x":88,"y":1203,"width":90,"height":641,"rx":92,"fill":93,"stroke":1204,"style":95},"52","#b91c1c",[78,1206,1208],{"x":98,"y":1207,"style":100,"fill":1204,"textAnchor":84},"78","nested loop",[78,1210,1212],{"x":98,"y":1211,"style":114,"fill":76,"textAnchor":84},"106","10 000 points × 500 polygons",[78,1214,1216],{"x":98,"y":1215,"style":114,"fill":76,"textAnchor":84},"132","5 000 000 geometry tests",[54,1218],{"x":89,"y":1219,"width":1220,"height":1221,"rx":1222,"fill":1204,"fillOpacity":1223,"stroke":1204,"style":110},"150","292","22","4",0.3,[78,1225,1227],{"x":98,"y":1226,"style":114,"fill":1204,"textAnchor":84},"196","grows with the product of both layers",[54,1229],{"x":145,"y":1203,"width":90,"height":641,"rx":92,"fill":133,"stroke":134,"style":95},[78,1231,1232],{"x":148,"y":1207,"style":100,"fill":134,"textAnchor":84},"spatial index",[78,1234,1235],{"x":148,"y":1211,"style":114,"fill":76,"textAnchor":84},"10 000 points × ~2 candidates",[78,1237,1238],{"x":148,"y":1215,"style":114,"fill":76,"textAnchor":84},"about 20 000 exact tests",[54,1240],{"x":1241,"y":1219,"width":1242,"height":1221,"rx":1222,"fill":134,"fillOpacity":1243,"stroke":134,"style":110},"420","14",0.4,[78,1245,1246],{"x":148,"y":1226,"style":114,"fill":134,"textAnchor":84},"still run the exact test on each candidate",[175,1248,1250],{"id":1249},"qgis-version-compatibility","QGIS version compatibility",[14,1252,1253,1254,1256],{},"The examples target ",[186,1255,188],{}," (Python 3.12).",[1258,1259,1260,1276],"table",{},[1261,1262,1263],"thead",{},[1264,1265,1266,1270,1273],"tr",{},[1267,1268,1269],"th",{},"QGIS version",[1267,1271,1272],{},"Python",[1267,1274,1275],{},"Notes",[1277,1278,1279,1291,1305,1316],"tbody",{},[1264,1280,1281,1285,1288],{},[1282,1283,1284],"td",{},"3.22 LTR",[1282,1286,1287],{},"3.9",[1282,1289,1290],{},"All methods shown are present and behave identically.",[1264,1292,1293,1296,1298],{},[1282,1294,1295],{},"3.28 LTR",[1282,1297,1287],{},[1282,1299,1300,1301,1304],{},"Adds ",[17,1302,1303],{},"setFilterExpression"," support for more provider push-down cases.",[1264,1306,1307,1310,1313],{},[1282,1308,1309],{},"3.34 LTR",[1282,1311,1312],{},"3.12",[1282,1314,1315],{},"Baseline for this page.",[1264,1317,1318,1321,1323],{},[1282,1319,1320],{},"3.40 \u002F 3.44",[1282,1322,1312],{},[1282,1324,1325,1326,1329],{},"Flag members are scoped (",[17,1327,1328],{},"QgsFeatureRequest.Flag.NoGeometry","); the unscoped spelling still resolves.",[175,1331,1333],{"id":1332},"troubleshooting","Troubleshooting",[180,1335,1336,1348,1358,1368,1374,1382],{},[183,1337,1338,1343,1344,1347],{},[186,1339,1340,1341,556],{},"Attributes are suddenly ",[17,1342,328],{}," They were excluded by ",[17,1345,1346],{},"setSubsetOfAttributes()",". Requesting a narrower set silently nulls the rest rather than raising.",[183,1349,1350,384,1355,1357],{},[186,1351,1352,1354],{},[17,1353,391],{}," is empty.",[17,1356,387],{}," is set. Remove the flag for that pass, or split the work into two passes.",[183,1359,1360,1363,1364,556],{},[186,1361,1362],{},"The rectangle filter returns features outside the rectangle."," It filters on bounding boxes. Follow with an exact predicate — see ",[31,1365,1367],{"href":1366},"\u002Fspatial-data-processing-automation\u002Fgeometry-operations-and-predicates\u002Fcheck-geometry-intersects-pyqgis\u002F","Check Whether Geometries Intersect in PyQGIS",[183,1369,1370,1373],{},[186,1371,1372],{},"The expression filter is slow on PostGIS."," It could not be translated, so QGIS is evaluating it locally on every row. Simplify the expression, or move the condition into a subset string or query layer.",[183,1375,1376,1379,1380,556],{},[186,1377,1378],{},"Ordering appears to be ignored."," Some providers cannot sort; QGIS then sorts locally, which needs the whole result in memory and is unaffected by ",[17,1381,804],{},[183,1383,1384,1390,1391,556],{},[186,1385,1386,1387,1389],{},"A second ",[17,1388,808],{}," call lost an earlier flag."," Flags replace rather than accumulate. Pass them together, combined with ",[17,1392,1393],{},"|",[175,1395,1397],{"id":1396},"conclusion","Conclusion",[14,1399,1400,1401,1403],{},"Before threading a slow loop, narrow the request: name the attributes you use, add ",[17,1402,387],{}," when you do not touch the shape, and push every filter — spatial, attribute or identifier — down to the provider. Measure both versions on warm caches. Most loops get several times faster for two lines of change, which is a better outcome than making the same slow loop asynchronous.",[175,1405,1407],{"id":1406},"frequently-asked-questions","Frequently Asked Questions",[14,1409,1410,1413],{},[186,1411,1412],{},"Does a feature request work on any layer type?","\nYes. Every vector provider accepts one; what differs is how much of it can be pushed down. A memory layer honours everything in QGIS, a PostGIS layer translates most of it into SQL.",[14,1415,1416,1423],{},[186,1417,1418,1419,1422],{},"Is ",[17,1420,1421],{},"setSubsetOfAttributes"," worth it for a narrow table?","\nRarely. The saving scales with the number and width of the columns you skip — on a five-column table it is noise, on a forty-column one it is substantial.",[14,1425,1426,1429],{},[186,1427,1428],{},"Can I reuse one request object for several iterations?","\nYes, a request is a plain value object and can be reused or copied. Build it once outside the loop rather than per iteration.",[14,1431,1432,1435],{},[186,1433,1434],{},"How does this interact with a layer's subset string?","\nThey compose: the subset string restricts the layer itself, and the request restricts that. Both end up in the provider's query where possible.",[14,1437,1438,1441,1443,1444,1447,1448,556],{},[186,1439,1440],{},"Should I still use a spatial index?",[17,1442,542],{}," uses the provider's index. ",[17,1445,1446],{},"QgsSpatialIndex"," is for comparisons between two layers in memory, which the request cannot help with — see ",[31,1449,1451],{"href":1450},"\u002Fspatial-data-processing-automation\u002Fgeometry-operations-and-predicates\u002Fbuild-spatial-index-pyqgis\u002F","Build a Spatial Index in PyQGIS",[175,1453,1455],{"id":1454},"related","Related",[180,1457,1458,1463,1467,1471,1475],{},[183,1459,1460,1462],{},[31,1461,34],{"href":33}," — the guide this recipe belongs to",[183,1464,1465],{},[31,1466,1020],{"href":1019},[183,1468,1469],{},[31,1470,1451],{"href":1450},[183,1472,1473],{},[31,1474,555],{"href":554},[183,1476,1477],{},[31,1478,1480],{"href":1479},"\u002Fspatial-data-processing-automation\u002Fattribute-tables-and-field-management\u002Fcount-features-by-attribute-pyqgis\u002F","Count Features by Attribute in PyQGIS",[1482,1483,1484],"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 .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);}html pre.shiki code .sDLfK, html code.shiki .sDLfK{--shiki-default:#79B8FF}html pre.shiki code .s9osk, html code.shiki .s9osk{--shiki-default:#FFAB70}html pre.shiki code .svObZ, html code.shiki .svObZ{--shiki-default:#B392F0}",{"title":209,"searchDepth":233,"depth":233,"links":1486},[1487,1488,1489,1490,1491,1492,1493,1494,1495,1496,1497,1498],{"id":177,"depth":233,"text":178},{"id":201,"depth":233,"text":202},{"id":332,"depth":233,"text":333},{"id":395,"depth":233,"text":396},{"id":676,"depth":233,"text":677},{"id":815,"depth":233,"text":816},{"id":1023,"depth":233,"text":1024},{"id":1249,"depth":233,"text":1250},{"id":1332,"depth":233,"text":1333},{"id":1396,"depth":233,"text":1397},{"id":1406,"depth":233,"text":1407},{"id":1454,"depth":233,"text":1455},"Make PyQGIS loops fast — request only the attributes you use, drop geometry when you do not need it, filter with an expression or a rectangle, and let the provider do the work.","md",{"slug":1502,"type":1503,"breadcrumb":1504,"datePublished":1505,"dateModified":1505},"speed-up-feature-iteration-with-qgsfeaturerequest-pyqgis","article","Faster Feature Iteration","2026-08-10","\u002Fqgis-plugin-development\u002Fbackground-tasks-and-plugin-performance\u002Fspeed-up-feature-iteration-with-qgsfeaturerequest-pyqgis",{"title":5,"description":1499},"qgis-plugin-development\u002Fbackground-tasks-and-plugin-performance\u002Fspeed-up-feature-iteration-with-qgsfeaturerequest-pyqgis\u002Findex","RNoYzoICZYqfI-4uIIEaH9giYmGgWWEEOz3QFepkhCI",1786401337553]