[{"data":1,"prerenderedAt":1539},["ShallowReactive",2],{"doc:\u002Fspatial-data-processing-automation\u002Fpostgis-and-database-workflows\u002Fquery-spatialite-database-pyqgis":3},{"id":4,"title":5,"body":6,"description":1528,"extension":1529,"meta":1530,"navigation":219,"path":1535,"seo":1536,"stem":1537,"__hash__":1538},"docs\u002Fspatial-data-processing-automation\u002Fpostgis-and-database-workflows\u002Fquery-spatialite-database-pyqgis\u002Findex.md","Query a SpatiaLite Database in PyQGIS",{"type":7,"value":8,"toc":1512},"minimark",[9,13,17,26,148,153,172,176,187,311,329,333,336,485,503,507,527,649,658,725,729,732,854,872,876,879,951,978,982,985,1196,1212,1216,1219,1285,1351,1360,1364,1371,1375,1389,1393,1427,1431,1440,1444,1450,1456,1462,1475,1479,1508],[10,11,5],"h1",{"id":12},"query-a-spatialite-database-in-pyqgis",[14,15,16],"p",{},"Not every project needs a database server. SpatiaLite adds spatial types and functions to SQLite, giving a full spatial SQL database in a single file that can be emailed, versioned or carried on a laptop to the field. GeoPackage is built on the same SQLite foundation, and QGIS can run SpatiaLite's spatial SQL against GeoPackage tables too. For one-person projects, offline work and data exchange with analysis attached, a single-file database often beats both shapefiles and a server.",[14,18,19,20,25],{},"This recipe belongs to ",[21,22,24],"a",{"href":23},"\u002Fspatial-data-processing-automation\u002Fpostgis-and-database-workflows\u002F","PostGIS & Database Workflows",". It loads SpatiaLite tables, runs spatial SQL and loads query results as layers, uses spatial indexes correctly in queries, creates views and tables, and compares the single-file approach with PostGIS.",[14,27,28],{},[29,30,35,39,43,50,67,76,85,90,95,102,107,112,116,120,125,129,132,135,138,145],"svg",{"viewBox":31,"role":32,"ariaLabel":33,"xmlns":34},"0 0 760 280","img","SQLite as the base, SpatiaLite adding spatial SQL and indexes, and GeoPackage as an OGC standard on the same base, both readable by QGIS","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg",[36,37,38],"title",{},"SQLite, SpatiaLite and GeoPackage",[40,41,42],"desc",{},"SQLite is a file-based SQL database. SpatiaLite extends it with spatial geometry types, hundreds of spatial SQL functions and R-tree spatial indexes. GeoPackage is an OGC standard built on SQLite with its own geometry encoding and metadata tables. QGIS reads both, and with the SpatiaLite extension loaded can run spatial SQL against GeoPackage tables as well.",[44,45],"rect",{"x":46,"y":46,"width":47,"height":48,"fill":49},"0","760","280","#f6f3ea",[51,52,53],"defs",{},[54,55,62],"marker",{"id":56,"viewBox":57,"refX":58,"refY":59,"markerWidth":60,"markerHeight":60,"orient":61},"slFamilyArrow","0 0 10 10","8","5","7","auto-start-reverse",[63,64],"path",{"d":65,"fill":66},"M0 0 L10 5 L0 10 z","#2f3b35",[68,69,75],"text",{"x":70,"y":71,"style":72,"fill":73,"textAnchor":74},"380","28","text-anchor:middle;font-size:14px;font-family:sans-serif;font-weight:bold","#17211d","middle","One file, full spatial SQL",[44,77],{"x":78,"y":79,"width":80,"height":81,"rx":58,"fill":82,"stroke":83,"style":84},"260","200","240","56","#fffdf7","#59645f","stroke-width:2",[68,86,89],{"x":70,"y":87,"style":88,"fill":73,"textAnchor":74},"221.78","text-anchor:middle;font-size:11.5px;font-family:sans-serif;font-weight:bold","SQLite",[68,91,94],{"x":70,"y":92,"style":93,"fill":83,"textAnchor":74},"241.78","text-anchor:middle;font-size:10.5px;font-family:sans-serif","file-based SQL",[44,96],{"x":97,"y":98,"width":48,"height":99,"rx":58,"fill":100,"stroke":101,"style":84},"60","70","110","#eef7f4","#0f766e",[68,103,106],{"x":79,"y":104,"style":105,"fill":101,"textAnchor":74},"92.6","text-anchor:middle;font-size:11.0px;font-family:sans-serif;font-weight:bold","SpatiaLite",[68,108,111],{"x":79,"y":109,"style":110,"fill":66,"textAnchor":74},"116.6","text-anchor:middle;font-size:10.0px;font-family:sans-serif","spatial types + functions",[68,113,115],{"x":79,"y":114,"style":110,"fill":66,"textAnchor":74},"140.6","R-tree indexes",[68,117,119],{"x":79,"y":118,"style":110,"fill":83,"textAnchor":74},"164.6","spatialite provider",[44,121],{"x":122,"y":98,"width":48,"height":99,"rx":58,"fill":123,"stroke":124,"style":84},"420","#eff3ff","#2563eb",[68,126,128],{"x":127,"y":104,"style":105,"fill":124,"textAnchor":74},"560","GeoPackage",[68,130,131],{"x":127,"y":109,"style":110,"fill":66,"textAnchor":74},"OGC standard",[68,133,134],{"x":127,"y":114,"style":110,"fill":66,"textAnchor":74},"own geometry encoding",[68,136,137],{"x":127,"y":118,"style":110,"fill":83,"textAnchor":74},"ogr provider",[139,140],"line",{"x1":79,"y1":141,"x2":142,"y2":143,"stroke":66,"style":144},"180","300","198","stroke-width:1.8;marker-end:url(#slFamilyArrow)",[139,146],{"x1":127,"y1":141,"x2":147,"y2":143,"stroke":66,"style":144},"460",[149,150,152],"h2",{"id":151},"prerequisites","Prerequisites",[154,155,156,160],"ul",{},[157,158,159],"li",{},"QGIS 3.34 LTR or newer, or the QGIS 4 series; both include SpatiaLite support.",[157,161,162,163,167,168,171],{},"A SpatiaLite ",[164,165,166],"code",{},".sqlite"," database or a GeoPackage. QGIS can create either: GeoPackage through any \"save as\", SpatiaLite through the Browser or ",[164,169,170],{},"QgsVectorFileWriter"," with the SpatiaLite driver.",[149,173,175],{"id":174},"load-tables-from-a-spatialite-file","Load tables from a SpatiaLite file",[14,177,178,179,182,183,186],{},"SpatiaLite tables load through the ",[164,180,181],{},"spatialite"," provider with a URI naming the file, table and geometry column. ",[164,184,185],{},"QgsDataSourceUri"," builds it safely.",[188,189,194],"pre",{"className":190,"code":191,"language":192,"meta":193,"style":193},"language-python shiki shiki-themes github-dark","from qgis.core import QgsDataSourceUri, QgsVectorLayer, QgsProject\n\ndb = \"\u002Fdata\u002Ffield\u002Fsurvey_2026.sqlite\"\nuri = QgsDataSourceUri()\nuri.setDatabase(db)\nuri.setDataSource(\"\", \"sample_points\", \"geom\")\npoints = QgsVectorLayer(uri.uri(), \"sample points\", \"spatialite\")\nprint(points.isValid(), points.featureCount(), points.crs().authid())\nQgsProject.instance().addMapLayer(points)\n","python","",[164,195,196,214,221,234,245,251,274,295,305],{"__ignoreMap":193},[197,198,200,204,208,211],"span",{"class":139,"line":199},1,[197,201,203],{"class":202},"snl16","from",[197,205,207],{"class":206},"s95oV"," qgis.core ",[197,209,210],{"class":202},"import",[197,212,213],{"class":206}," QgsDataSourceUri, QgsVectorLayer, QgsProject\n",[197,215,217],{"class":139,"line":216},2,[197,218,220],{"emptyLinePlaceholder":219},true,"\n",[197,222,224,227,230],{"class":139,"line":223},3,[197,225,226],{"class":206},"db ",[197,228,229],{"class":202},"=",[197,231,233],{"class":232},"sU2Wk"," \"\u002Fdata\u002Ffield\u002Fsurvey_2026.sqlite\"\n",[197,235,237,240,242],{"class":139,"line":236},4,[197,238,239],{"class":206},"uri ",[197,241,229],{"class":202},[197,243,244],{"class":206}," QgsDataSourceUri()\n",[197,246,248],{"class":139,"line":247},5,[197,249,250],{"class":206},"uri.setDatabase(db)\n",[197,252,254,257,260,263,266,268,271],{"class":139,"line":253},6,[197,255,256],{"class":206},"uri.setDataSource(",[197,258,259],{"class":232},"\"\"",[197,261,262],{"class":206},", ",[197,264,265],{"class":232},"\"sample_points\"",[197,267,262],{"class":206},[197,269,270],{"class":232},"\"geom\"",[197,272,273],{"class":206},")\n",[197,275,277,280,282,285,288,290,293],{"class":139,"line":276},7,[197,278,279],{"class":206},"points ",[197,281,229],{"class":202},[197,283,284],{"class":206}," QgsVectorLayer(uri.uri(), ",[197,286,287],{"class":232},"\"sample points\"",[197,289,262],{"class":206},[197,291,292],{"class":232},"\"spatialite\"",[197,294,273],{"class":206},[197,296,298,302],{"class":139,"line":297},8,[197,299,301],{"class":300},"sDLfK","print",[197,303,304],{"class":206},"(points.isValid(), points.featureCount(), points.crs().authid())\n",[197,306,308],{"class":139,"line":307},9,[197,309,310],{"class":206},"QgsProject.instance().addMapLayer(points)\n",[14,312,313,317,318,321,322,325,326,328],{},[314,315,316],"strong",{},"Breakdown:"," SpatiaLite has no schemas, so the schema argument is empty. The geometry column name must match the one registered in the database's ",[164,319,320],{},"geometry_columns"," table; list it with the connections API as shown below if unsure. The same file can also be opened with the ",[164,323,324],{},"ogr"," provider, but the ",[164,327,181],{}," provider is the one that supports SQL query layers and SpatiaLite-specific features cleanly.",[149,330,332],{"id":331},"explore-the-database-with-the-connections-api","Explore the database with the connections API",[14,334,335],{},"The provider connections API works for SpatiaLite files as for PostGIS, which makes exploring an unfamiliar database quick.",[188,337,339],{"className":190,"code":338,"language":192,"meta":193,"style":193},"from qgis.core import QgsProviderRegistry\n\nmd = QgsProviderRegistry.instance().providerMetadata(\"spatialite\")\nconn = md.createConnection(db, {})\nfor t in conn.tables():\n    cols = t.geometryColumnTypes()\n    print(t.tableName(), [(c.crs.authid()) for c in cols] or \"no geometry\")\n\nversion = conn.executeSql(\"SELECT spatialite_version(), sqlite_version()\")[0]\nprint(\"SpatiaLite\", version[0], \"on SQLite\", version[1])\n",[164,340,341,352,356,370,380,394,404,430,434,455],{"__ignoreMap":193},[197,342,343,345,347,349],{"class":139,"line":199},[197,344,203],{"class":202},[197,346,207],{"class":206},[197,348,210],{"class":202},[197,350,351],{"class":206}," QgsProviderRegistry\n",[197,353,354],{"class":139,"line":216},[197,355,220],{"emptyLinePlaceholder":219},[197,357,358,361,363,366,368],{"class":139,"line":223},[197,359,360],{"class":206},"md ",[197,362,229],{"class":202},[197,364,365],{"class":206}," QgsProviderRegistry.instance().providerMetadata(",[197,367,292],{"class":232},[197,369,273],{"class":206},[197,371,372,375,377],{"class":139,"line":236},[197,373,374],{"class":206},"conn ",[197,376,229],{"class":202},[197,378,379],{"class":206}," md.createConnection(db, {})\n",[197,381,382,385,388,391],{"class":139,"line":247},[197,383,384],{"class":202},"for",[197,386,387],{"class":206}," t ",[197,389,390],{"class":202},"in",[197,392,393],{"class":206}," conn.tables():\n",[197,395,396,399,401],{"class":139,"line":253},[197,397,398],{"class":206},"    cols ",[197,400,229],{"class":202},[197,402,403],{"class":206}," t.geometryColumnTypes()\n",[197,405,406,409,412,414,417,419,422,425,428],{"class":139,"line":276},[197,407,408],{"class":300},"    print",[197,410,411],{"class":206},"(t.tableName(), [(c.crs.authid()) ",[197,413,384],{"class":202},[197,415,416],{"class":206}," c ",[197,418,390],{"class":202},[197,420,421],{"class":206}," cols] ",[197,423,424],{"class":202},"or",[197,426,427],{"class":232}," \"no geometry\"",[197,429,273],{"class":206},[197,431,432],{"class":139,"line":297},[197,433,220],{"emptyLinePlaceholder":219},[197,435,436,439,441,444,447,450,452],{"class":139,"line":307},[197,437,438],{"class":206},"version ",[197,440,229],{"class":202},[197,442,443],{"class":206}," conn.executeSql(",[197,445,446],{"class":232},"\"SELECT spatialite_version(), sqlite_version()\"",[197,448,449],{"class":206},")[",[197,451,46],{"class":300},[197,453,454],{"class":206},"]\n",[197,456,458,460,463,466,469,471,474,477,479,482],{"class":139,"line":457},10,[197,459,301],{"class":300},[197,461,462],{"class":206},"(",[197,464,465],{"class":232},"\"SpatiaLite\"",[197,467,468],{"class":206},", version[",[197,470,46],{"class":300},[197,472,473],{"class":206},"], ",[197,475,476],{"class":232},"\"on SQLite\"",[197,478,468],{"class":206},[197,480,481],{"class":300},"1",[197,483,484],{"class":206},"])\n",[14,486,487,489,490,493,494,497,498,502],{},[314,488,316],{}," ",[164,491,492],{},"createConnection"," wraps a file directly without saving it in the Browser. Listing tables shows which have geometry and in which CRS. ",[164,495,496],{},"spatialite_version()"," confirms the spatial extension is active; if it raises an error, the file is plain SQLite without SpatiaLite metadata. Everything in ",[21,499,501],{"href":500},"\u002Fspatial-data-processing-automation\u002Fpostgis-and-database-workflows\u002Fuse-database-connections-api-pyqgis\u002F","using the database connections API"," applies here too.",[149,504,506],{"id":505},"run-spatial-sql","Run spatial SQL",[14,508,509,510,262,513,262,516,262,519,522,523,526],{},"SpatiaLite provides several hundred spatial functions with names familiar from PostGIS: ",[164,511,512],{},"ST_Buffer",[164,514,515],{},"ST_Intersects",[164,517,518],{},"ST_Area",[164,520,521],{},"ST_Transform",". Queries run through ",[164,524,525],{},"executeSql"," and return rows.",[188,528,530],{"className":190,"code":529,"language":192,"meta":193,"style":193},"rows = conn.executeSql(\"\"\"\n    SELECT h.habitat, count(*) AS n, round(avg(p.species_count), 1) AS mean_species\n    FROM sample_points AS p\n    JOIN habitats AS h ON ST_Within(p.geom, h.geom)\n    WHERE p.ROWID IN (\n        SELECT ROWID FROM SpatialIndex\n        WHERE f_table_name = 'sample_points' AND search_frame = h.geom)\n    GROUP BY h.habitat ORDER BY mean_species DESC\n\"\"\")\nfor habitat, n, mean in rows:\n    print(f\"{habitat:\u003C20} {n:>4} samples, mean {mean} species\")\n",[164,531,532,544,549,554,559,564,569,574,579,586,598],{"__ignoreMap":193},[197,533,534,537,539,541],{"class":139,"line":199},[197,535,536],{"class":206},"rows ",[197,538,229],{"class":202},[197,540,443],{"class":206},[197,542,543],{"class":232},"\"\"\"\n",[197,545,546],{"class":139,"line":216},[197,547,548],{"class":232},"    SELECT h.habitat, count(*) AS n, round(avg(p.species_count), 1) AS mean_species\n",[197,550,551],{"class":139,"line":223},[197,552,553],{"class":232},"    FROM sample_points AS p\n",[197,555,556],{"class":139,"line":236},[197,557,558],{"class":232},"    JOIN habitats AS h ON ST_Within(p.geom, h.geom)\n",[197,560,561],{"class":139,"line":247},[197,562,563],{"class":232},"    WHERE p.ROWID IN (\n",[197,565,566],{"class":139,"line":253},[197,567,568],{"class":232},"        SELECT ROWID FROM SpatialIndex\n",[197,570,571],{"class":139,"line":276},[197,572,573],{"class":232},"        WHERE f_table_name = 'sample_points' AND search_frame = h.geom)\n",[197,575,576],{"class":139,"line":297},[197,577,578],{"class":232},"    GROUP BY h.habitat ORDER BY mean_species DESC\n",[197,580,581,584],{"class":139,"line":307},[197,582,583],{"class":232},"\"\"\"",[197,585,273],{"class":206},[197,587,588,590,593,595],{"class":139,"line":457},[197,589,384],{"class":202},[197,591,592],{"class":206}," habitat, n, mean ",[197,594,390],{"class":202},[197,596,597],{"class":206}," rows:\n",[197,599,601,603,605,608,611,614,617,620,623,626,629,632,634,637,639,642,644,647],{"class":139,"line":600},11,[197,602,408],{"class":300},[197,604,462],{"class":206},[197,606,607],{"class":202},"f",[197,609,610],{"class":232},"\"",[197,612,613],{"class":300},"{",[197,615,616],{"class":206},"habitat",[197,618,619],{"class":202},":\u003C20",[197,621,622],{"class":300},"}",[197,624,625],{"class":300}," {",[197,627,628],{"class":206},"n",[197,630,631],{"class":202},":>4",[197,633,622],{"class":300},[197,635,636],{"class":232}," samples, mean ",[197,638,613],{"class":300},[197,640,641],{"class":206},"mean",[197,643,622],{"class":300},[197,645,646],{"class":232}," species\"",[197,648,273],{"class":206},[14,650,651,653,654,657],{},[314,652,316],{}," The join finds each sample point within each habitat polygon. The subquery against the virtual ",[164,655,656],{},"SpatialIndex"," table is SpatiaLite's way of using its R-tree index: unlike PostGIS, SpatiaLite does not use spatial indexes automatically in joins, so without it the query compares every point with every polygon. With it, only candidates whose bounding boxes intersect are tested. This one difference explains most \"SpatiaLite is slow\" experiences.",[14,659,660],{},[29,661,664,667,670,673,680,683,690,695,699,703,708,713,716,719],{"viewBox":662,"role":32,"ariaLabel":663,"xmlns":34},"0 0 760 222","A SpatiaLite spatial join testing every pair without the index subquery, and only bounding-box candidates with it",[36,665,666],{},"Using the spatial index explicitly",[40,668,669],{},"A spatial join without the SpatialIndex subquery tests every point against every polygon, so cost grows with the product of their counts. With the subquery, SpatiaLite first retrieves only points whose bounding boxes intersect each polygon's frame, then tests those exactly. PostGIS uses its indexes automatically; SpatiaLite needs to be told.",[44,671],{"x":46,"y":46,"width":47,"height":672,"fill":49},"222",[51,674,675],{},[54,676,678],{"id":677,"viewBox":57,"refX":58,"refY":59,"markerWidth":60,"markerHeight":60,"orient":61},"slIndexArrow",[63,679],{"d":65,"fill":66},[68,681,682],{"x":70,"y":71,"style":72,"fill":73,"textAnchor":74},"In SpatiaLite, ask for the index",[44,684],{"x":685,"y":81,"width":686,"height":687,"rx":58,"fill":688,"stroke":689,"style":84},"24","340","150","#fdf2e2","#b91c1c",[68,691,694],{"x":692,"y":693,"style":88,"fill":689,"textAnchor":74},"194","106.78","no index subquery",[68,696,698],{"x":692,"y":697,"style":93,"fill":66,"textAnchor":74},"134.78","every point × every polygon",[68,700,702],{"x":692,"y":701,"style":93,"fill":83,"textAnchor":74},"162.78","minutes on modest data",[44,704],{"x":705,"y":81,"width":686,"height":687,"rx":58,"fill":706,"stroke":707,"style":84},"396","#e8efe6","#15803d",[68,709,712],{"x":710,"y":693,"style":88,"fill":711,"textAnchor":74},"566","#166534","SpatialIndex subquery",[68,714,715],{"x":710,"y":697,"style":93,"fill":66,"textAnchor":74},"bbox candidates first",[68,717,718],{"x":710,"y":701,"style":93,"fill":83,"textAnchor":74},"seconds",[139,720],{"x1":721,"y1":722,"x2":723,"y2":722,"stroke":66,"style":724},"364","131","392","stroke-width:1.8;marker-end:url(#slIndexArrow)",[149,726,728],{"id":727},"load-a-query-as-a-layer","Load a query as a layer",[14,730,731],{},"A query with a geometry column can be loaded as a layer — a live view of the result, re-run as the map is drawn — through the provider URI's SQL syntax.",[188,733,735],{"className":190,"code":734,"language":192,"meta":193,"style":193},"query = \"\"\"\n    SELECT p.ROWID AS uid, p.sample_id, p.species_count, h.habitat, p.geom\n    FROM sample_points AS p\n    JOIN habitats AS h ON ST_Within(p.geom, h.geom)\n    WHERE p.species_count >= 20\n\"\"\"\nquri = QgsDataSourceUri()\nquri.setDatabase(db)\nquri.setDataSource(\"\", f\"({query})\", \"geom\", \"\", \"uid\")\nrich = QgsVectorLayer(quri.uri(), \"species-rich samples\", \"spatialite\")\nprint(rich.isValid(), rich.featureCount())\nQgsProject.instance().addMapLayer(rich)\n",[164,736,737,747,752,756,760,765,769,778,783,822,841,848],{"__ignoreMap":193},[197,738,739,742,744],{"class":139,"line":199},[197,740,741],{"class":206},"query ",[197,743,229],{"class":202},[197,745,746],{"class":232}," \"\"\"\n",[197,748,749],{"class":139,"line":216},[197,750,751],{"class":232},"    SELECT p.ROWID AS uid, p.sample_id, p.species_count, h.habitat, p.geom\n",[197,753,754],{"class":139,"line":223},[197,755,553],{"class":232},[197,757,758],{"class":139,"line":236},[197,759,558],{"class":232},[197,761,762],{"class":139,"line":247},[197,763,764],{"class":232},"    WHERE p.species_count >= 20\n",[197,766,767],{"class":139,"line":253},[197,768,543],{"class":232},[197,770,771,774,776],{"class":139,"line":276},[197,772,773],{"class":206},"quri ",[197,775,229],{"class":202},[197,777,244],{"class":206},[197,779,780],{"class":139,"line":297},[197,781,782],{"class":206},"quri.setDatabase(db)\n",[197,784,785,788,790,792,794,797,799,802,804,807,809,811,813,815,817,820],{"class":139,"line":307},[197,786,787],{"class":206},"quri.setDataSource(",[197,789,259],{"class":232},[197,791,262],{"class":206},[197,793,607],{"class":202},[197,795,796],{"class":232},"\"(",[197,798,613],{"class":300},[197,800,801],{"class":206},"query",[197,803,622],{"class":300},[197,805,806],{"class":232},")\"",[197,808,262],{"class":206},[197,810,270],{"class":232},[197,812,262],{"class":206},[197,814,259],{"class":232},[197,816,262],{"class":206},[197,818,819],{"class":232},"\"uid\"",[197,821,273],{"class":206},[197,823,824,827,829,832,835,837,839],{"class":139,"line":457},[197,825,826],{"class":206},"rich ",[197,828,229],{"class":202},[197,830,831],{"class":206}," QgsVectorLayer(quri.uri(), ",[197,833,834],{"class":232},"\"species-rich samples\"",[197,836,262],{"class":206},[197,838,292],{"class":232},[197,840,273],{"class":206},[197,842,843,845],{"class":139,"line":600},[197,844,301],{"class":300},[197,846,847],{"class":206},"(rich.isValid(), rich.featureCount())\n",[197,849,851],{"class":139,"line":850},12,[197,852,853],{"class":206},"QgsProject.instance().addMapLayer(rich)\n",[14,855,856,858,859,862,863,866,867,871],{},[314,857,316],{}," Wrapping the query in parentheses as the \"table\" and naming a unique key column (",[164,860,861],{},"uid"," from ",[164,864,865],{},"ROWID",") lets the provider treat the result as a read-only layer. It updates whenever the data changes, which is convenient for derived layers in a field project. For large results that are drawn often, materialising the result into a table is faster than re-running the query on every pan. The equivalent technique for PostGIS is in ",[21,868,870],{"href":869},"\u002Fspatial-data-processing-automation\u002Fpostgis-and-database-workflows\u002Fload-postgis-query-layer-pyqgis\u002F","loading a PostGIS query layer",".",[149,873,875],{"id":874},"create-views-and-tables","Create views and tables",[14,877,878],{},"Persistent derived data belongs in the database. A view keeps the logic; a table stores the result. Both must be registered for SpatiaLite to recognise their geometry.",[188,880,882],{"className":190,"code":881,"language":192,"meta":193,"style":193},"conn.executeSql(\"\"\"\n    CREATE TABLE IF NOT EXISTS habitat_summary AS\n    SELECT h.habitat, h.geom, count(p.ROWID) AS samples\n    FROM habitats AS h LEFT JOIN sample_points AS p ON ST_Within(p.geom, h.geom)\n    GROUP BY h.ROWID;\n\"\"\")\nconn.executeSql(\"SELECT RecoverGeometryColumn('habitat_summary', 'geom', 25832, 'MULTIPOLYGON', 'XY')\")\nconn.executeSql(\"SELECT CreateSpatialIndex('habitat_summary', 'geom')\")\nprint([t.tableName() for t in conn.tables()])\n",[164,883,884,891,896,901,906,911,917,926,935],{"__ignoreMap":193},[197,885,886,889],{"class":139,"line":199},[197,887,888],{"class":206},"conn.executeSql(",[197,890,543],{"class":232},[197,892,893],{"class":139,"line":216},[197,894,895],{"class":232},"    CREATE TABLE IF NOT EXISTS habitat_summary AS\n",[197,897,898],{"class":139,"line":223},[197,899,900],{"class":232},"    SELECT h.habitat, h.geom, count(p.ROWID) AS samples\n",[197,902,903],{"class":139,"line":236},[197,904,905],{"class":232},"    FROM habitats AS h LEFT JOIN sample_points AS p ON ST_Within(p.geom, h.geom)\n",[197,907,908],{"class":139,"line":247},[197,909,910],{"class":232},"    GROUP BY h.ROWID;\n",[197,912,913,915],{"class":139,"line":253},[197,914,583],{"class":232},[197,916,273],{"class":206},[197,918,919,921,924],{"class":139,"line":276},[197,920,888],{"class":206},[197,922,923],{"class":232},"\"SELECT RecoverGeometryColumn('habitat_summary', 'geom', 25832, 'MULTIPOLYGON', 'XY')\"",[197,925,273],{"class":206},[197,927,928,930,933],{"class":139,"line":297},[197,929,888],{"class":206},[197,931,932],{"class":232},"\"SELECT CreateSpatialIndex('habitat_summary', 'geom')\"",[197,934,273],{"class":206},[197,936,937,939,942,944,946,948],{"class":139,"line":307},[197,938,301],{"class":300},[197,940,941],{"class":206},"([t.tableName() ",[197,943,384],{"class":202},[197,945,387],{"class":206},[197,947,390],{"class":202},[197,949,950],{"class":206}," conn.tables()])\n",[14,952,953,489,955,958,959,962,963,966,967,970,971,974,975,977],{},[314,954,316],{},[164,956,957],{},"CREATE TABLE … AS SELECT"," stores the result, but SpatiaLite does not know the new ",[164,960,961],{},"geom"," column is a geometry until ",[164,964,965],{},"RecoverGeometryColumn"," registers it with its SRID, type and dimensions; skip that and QGIS sees a table without geometry. ",[164,968,969],{},"CreateSpatialIndex"," builds the R-tree for fast drawing and joins. Views need an entry in ",[164,972,973],{},"views_geometry_columns"," instead — use QGIS's DB Manager or the ",[164,976,181],{}," command-line tool for view registration if you use them often.",[149,979,981],{"id":980},"create-a-spatialite-database-from-qgis-layers","Create a SpatiaLite database from QGIS layers",[14,983,984],{},"Starting a single-file project database from layers already loaded in QGIS takes one writer call per layer. The first call creates the file with SpatiaLite metadata; later calls add tables to it.",[188,986,988],{"className":190,"code":987,"language":192,"meta":193,"style":193},"from qgis.core import QgsVectorFileWriter, QgsCoordinateTransformContext\n\nout_db = \"\u002Fdata\u002Ffield\u002Fnew_project.sqlite\"\nlayers = [QgsProject.instance().mapLayersByName(n)[0] for n in (\"plots\", \"transects\", \"habitats\")]\nfor i, lyr in enumerate(layers):\n    opts = QgsVectorFileWriter.SaveVectorOptions()\n    opts.driverName = \"SQLite\"\n    opts.layerName = lyr.name()\n    opts.datasourceOptions = [\"SPATIALITE=YES\"]\n    opts.actionOnExistingFile = (QgsVectorFileWriter.CreateOrOverwriteFile if i == 0\n                                 else QgsVectorFileWriter.CreateOrOverwriteLayer)\n    err, msg, *_ = QgsVectorFileWriter.writeAsVectorFormatV3(\n        lyr, out_db, QgsCoordinateTransformContext(), opts)\n    print(lyr.name(), \"ok\" if err == QgsVectorFileWriter.NoError else msg)\n",[164,989,990,1001,1005,1015,1056,1071,1081,1091,1101,1116,1138,1146,1162,1168],{"__ignoreMap":193},[197,991,992,994,996,998],{"class":139,"line":199},[197,993,203],{"class":202},[197,995,207],{"class":206},[197,997,210],{"class":202},[197,999,1000],{"class":206}," QgsVectorFileWriter, QgsCoordinateTransformContext\n",[197,1002,1003],{"class":139,"line":216},[197,1004,220],{"emptyLinePlaceholder":219},[197,1006,1007,1010,1012],{"class":139,"line":223},[197,1008,1009],{"class":206},"out_db ",[197,1011,229],{"class":202},[197,1013,1014],{"class":232}," \"\u002Fdata\u002Ffield\u002Fnew_project.sqlite\"\n",[197,1016,1017,1020,1022,1025,1027,1030,1032,1035,1037,1040,1043,1045,1048,1050,1053],{"class":139,"line":236},[197,1018,1019],{"class":206},"layers ",[197,1021,229],{"class":202},[197,1023,1024],{"class":206}," [QgsProject.instance().mapLayersByName(n)[",[197,1026,46],{"class":300},[197,1028,1029],{"class":206},"] ",[197,1031,384],{"class":202},[197,1033,1034],{"class":206}," n ",[197,1036,390],{"class":202},[197,1038,1039],{"class":206}," (",[197,1041,1042],{"class":232},"\"plots\"",[197,1044,262],{"class":206},[197,1046,1047],{"class":232},"\"transects\"",[197,1049,262],{"class":206},[197,1051,1052],{"class":232},"\"habitats\"",[197,1054,1055],{"class":206},")]\n",[197,1057,1058,1060,1063,1065,1068],{"class":139,"line":247},[197,1059,384],{"class":202},[197,1061,1062],{"class":206}," i, lyr ",[197,1064,390],{"class":202},[197,1066,1067],{"class":300}," enumerate",[197,1069,1070],{"class":206},"(layers):\n",[197,1072,1073,1076,1078],{"class":139,"line":253},[197,1074,1075],{"class":206},"    opts ",[197,1077,229],{"class":202},[197,1079,1080],{"class":206}," QgsVectorFileWriter.SaveVectorOptions()\n",[197,1082,1083,1086,1088],{"class":139,"line":276},[197,1084,1085],{"class":206},"    opts.driverName ",[197,1087,229],{"class":202},[197,1089,1090],{"class":232}," \"SQLite\"\n",[197,1092,1093,1096,1098],{"class":139,"line":297},[197,1094,1095],{"class":206},"    opts.layerName ",[197,1097,229],{"class":202},[197,1099,1100],{"class":206}," lyr.name()\n",[197,1102,1103,1106,1108,1111,1114],{"class":139,"line":307},[197,1104,1105],{"class":206},"    opts.datasourceOptions ",[197,1107,229],{"class":202},[197,1109,1110],{"class":206}," [",[197,1112,1113],{"class":232},"\"SPATIALITE=YES\"",[197,1115,454],{"class":206},[197,1117,1118,1121,1123,1126,1129,1132,1135],{"class":139,"line":457},[197,1119,1120],{"class":206},"    opts.actionOnExistingFile ",[197,1122,229],{"class":202},[197,1124,1125],{"class":206}," (QgsVectorFileWriter.CreateOrOverwriteFile ",[197,1127,1128],{"class":202},"if",[197,1130,1131],{"class":206}," i ",[197,1133,1134],{"class":202},"==",[197,1136,1137],{"class":300}," 0\n",[197,1139,1140,1143],{"class":139,"line":600},[197,1141,1142],{"class":202},"                                 else",[197,1144,1145],{"class":206}," QgsVectorFileWriter.CreateOrOverwriteLayer)\n",[197,1147,1148,1151,1154,1157,1159],{"class":139,"line":850},[197,1149,1150],{"class":206},"    err, msg, ",[197,1152,1153],{"class":202},"*",[197,1155,1156],{"class":206},"_ ",[197,1158,229],{"class":202},[197,1160,1161],{"class":206}," QgsVectorFileWriter.writeAsVectorFormatV3(\n",[197,1163,1165],{"class":139,"line":1164},13,[197,1166,1167],{"class":206},"        lyr, out_db, QgsCoordinateTransformContext(), opts)\n",[197,1169,1171,1173,1176,1179,1182,1185,1187,1190,1193],{"class":139,"line":1170},14,[197,1172,408],{"class":300},[197,1174,1175],{"class":206},"(lyr.name(), ",[197,1177,1178],{"class":232},"\"ok\"",[197,1180,1181],{"class":202}," if",[197,1183,1184],{"class":206}," err ",[197,1186,1134],{"class":202},[197,1188,1189],{"class":206}," QgsVectorFileWriter.NoError ",[197,1191,1192],{"class":202},"else",[197,1194,1195],{"class":206}," msg)\n",[14,1197,1198,1200,1201,1204,1205,1207,1208,1211],{},[314,1199,316],{}," The SQLite driver with ",[164,1202,1203],{},"SPATIALITE=YES"," creates a real SpatiaLite database rather than a plain SQLite file, so the spatial metadata tables and functions are available. Creating the file on the first layer and adding layers afterwards collects everything in one file. GDAL creates a spatial index for each geometry table by default. The result opens with the ",[164,1206,181],{}," provider and works with every query technique above; for a GeoPackage instead, change the driver to ",[164,1209,1210],{},"GPKG"," and drop the datasource option.",[149,1213,1215],{"id":1214},"spatialite-sql-on-geopackage","SpatiaLite SQL on GeoPackage",[14,1217,1218],{},"GeoPackage files are SQLite databases too, and QGIS's OGR provider exposes SpatiaLite functions on them through GDAL's SQLite dialect — so the same spatial SQL works on the format most people exchange.",[14,1220,1221],{},[29,1222,1225,1228,1231,1234,1241,1244,1247,1251,1255,1260,1263,1267,1271,1275,1278,1282],{"viewBox":1223,"role":32,"ariaLabel":1224,"xmlns":34},"0 0 760 196","SQL with SpatiaLite functions running against a GeoPackage through GDAL's SQLite dialect",[36,1226,1227],{},"Spatial SQL on a GeoPackage",[40,1229,1230],{},"A GeoPackage opened through the connections API with the ogr provider accepts SQL that uses SpatiaLite functions such as ST_Area and ST_Buffer, because GDAL loads the SpatiaLite extension for its SQLite dialect. Geometry columns use GeoPackage encoding, which the functions read transparently. Results come back like any SQL result.",[44,1232],{"x":46,"y":46,"width":47,"height":1233,"fill":49},"196",[51,1235,1236],{},[54,1237,1239],{"id":1238,"viewBox":57,"refX":58,"refY":59,"markerWidth":60,"markerHeight":60,"orient":61},"slGpkgArrow",[63,1240],{"d":65,"fill":66},[68,1242,1243],{"x":70,"y":71,"style":72,"fill":73,"textAnchor":74},"GeoPackage + SpatiaLite functions via GDAL",[44,1245],{"x":685,"y":97,"width":672,"height":1246,"rx":58,"fill":123,"stroke":124,"style":84},"120",[68,1248,128],{"x":1249,"y":1250,"style":88,"fill":124,"textAnchor":74},"135","110.78",[68,1252,1254],{"x":1249,"y":1253,"style":93,"fill":83,"textAnchor":74},"136.78","gpkg geometry",[139,1256],{"x1":1257,"y1":1246,"x2":1258,"y2":1246,"stroke":66,"style":1259},"246","288","stroke-width:1.8;marker-end:url(#slGpkgArrow)",[44,1261],{"x":1262,"y":97,"width":79,"height":1246,"rx":58,"fill":100,"stroke":101,"style":84},"292",[68,1264,1266],{"x":723,"y":1265,"style":105,"fill":101,"textAnchor":74},"110.6","GDAL SQLite dialect",[68,1268,1270],{"x":723,"y":1269,"style":110,"fill":83,"textAnchor":74},"136.6","SpatiaLite loaded",[139,1272],{"x1":1273,"y1":1246,"x2":1274,"y2":1246,"stroke":66,"style":1259},"492","534",[44,1276],{"x":1277,"y":97,"width":143,"height":1246,"rx":58,"fill":706,"stroke":707,"style":84},"538",[68,1279,1281],{"x":1280,"y":1250,"style":88,"fill":711,"textAnchor":74},"637","spatial SQL",[68,1283,1284],{"x":1280,"y":1253,"style":93,"fill":83,"textAnchor":74},"ST_Area, ST_Buffer",[188,1286,1288],{"className":190,"code":1287,"language":192,"meta":193,"style":193},"gpkg = QgsProviderRegistry.instance().providerMetadata(\"ogr\").createConnection(\n    \"\u002Fdata\u002Fprojects\u002Fparcels.gpkg\", {})\nrows = gpkg.executeSql(\"\"\"\n    SELECT land_use, round(sum(ST_Area(geom)) \u002F 10000, 1) AS ha\n    FROM parcels GROUP BY land_use ORDER BY ha DESC\n\"\"\")\nprint(rows[:5])\n",[164,1289,1290,1305,1313,1324,1329,1334,1340],{"__ignoreMap":193},[197,1291,1292,1295,1297,1299,1302],{"class":139,"line":199},[197,1293,1294],{"class":206},"gpkg ",[197,1296,229],{"class":202},[197,1298,365],{"class":206},[197,1300,1301],{"class":232},"\"ogr\"",[197,1303,1304],{"class":206},").createConnection(\n",[197,1306,1307,1310],{"class":139,"line":216},[197,1308,1309],{"class":232},"    \"\u002Fdata\u002Fprojects\u002Fparcels.gpkg\"",[197,1311,1312],{"class":206},", {})\n",[197,1314,1315,1317,1319,1322],{"class":139,"line":223},[197,1316,536],{"class":206},[197,1318,229],{"class":202},[197,1320,1321],{"class":206}," gpkg.executeSql(",[197,1323,543],{"class":232},[197,1325,1326],{"class":139,"line":236},[197,1327,1328],{"class":232},"    SELECT land_use, round(sum(ST_Area(geom)) \u002F 10000, 1) AS ha\n",[197,1330,1331],{"class":139,"line":247},[197,1332,1333],{"class":232},"    FROM parcels GROUP BY land_use ORDER BY ha DESC\n",[197,1335,1336,1338],{"class":139,"line":253},[197,1337,583],{"class":232},[197,1339,273],{"class":206},[197,1341,1342,1344,1347,1349],{"class":139,"line":276},[197,1343,301],{"class":300},[197,1345,1346],{"class":206},"(rows[:",[197,1348,59],{"class":300},[197,1350,484],{"class":206},[14,1352,1353,1355,1356,1359],{},[314,1354,316],{}," The OGR connection runs the query through GDAL, which understands GeoPackage geometry and provides SpatiaLite functions when GDAL is built with SpatiaLite support — as it is in QGIS's distributions. This makes a GeoPackage a self-contained analysis database: data, styles and the SQL that summarises it travel in one file. Function availability depends on the GDAL build; test a simple ",[164,1357,1358],{},"SELECT ST_Area(geom) FROM … LIMIT 1"," first.",[149,1361,1363],{"id":1362},"spatialite-or-postgis","SpatiaLite or PostGIS?",[14,1365,1366,1367,871],{},"Single-file databases excel for one user or a small team working in turns, offline field work, data exchange and reproducible project archives. They struggle with many simultaneous editors (SQLite locks the file during writes), very large datasets with heavy concurrent querying, and fine-grained permissions. PostGIS is the answer for shared, multi-user, server-side data; a common pattern is PostGIS as the master and GeoPackage or SpatiaLite exports for field and exchange copies, as in ",[21,1368,1370],{"href":1369},"\u002Fspatial-data-processing-automation\u002Fpostgis-and-database-workflows\u002Fwrite-vector-layer-to-geopackage-pyqgis\u002F","writing a vector layer to GeoPackage",[149,1372,1374],{"id":1373},"qgis-version-compatibility","QGIS version compatibility",[14,1376,1377,1378,1380,1381,262,1383,1385,1386,1388],{},"The ",[164,1379,181],{}," provider, connections API and SQL query layers work on QGIS 3.34 LTR, 3.40 LTR and QGIS 4. Bundled SpatiaLite is version 5 on current installers, which includes ",[164,1382,965],{},[164,1384,969],{}," and the ",[164,1387,656],{}," virtual table.",[149,1390,1392],{"id":1391},"troubleshooting","Troubleshooting",[154,1394,1395,1404,1412,1418],{},[157,1396,1397,1400,1401,1403],{},[314,1398,1399],{},"Spatial joins take forever."," The ",[164,1402,656],{}," subquery is missing, or the index was never created.",[157,1405,1406,1409,1410,871],{},[314,1407,1408],{},"A new table has no geometry in QGIS."," It was not registered with ",[164,1411,965],{},[157,1413,1414,1417],{},[314,1415,1416],{},"\"database is locked\"."," Another process is writing; SQLite allows one writer at a time — close other editors.",[157,1419,1420,1423,1424,1426],{},[314,1421,1422],{},"Spatial functions are unknown on a GeoPackage."," The GDAL build lacks SpatiaLite; use the SpatiaLite provider on a ",[164,1425,166],{}," file instead.",[149,1428,1430],{"id":1429},"conclusion","Conclusion",[14,1432,1433,1434,1436,1437,1439],{},"Load SpatiaLite tables with the ",[164,1435,181],{}," provider, explore and query through the connections API, use the ",[164,1438,656],{}," virtual table explicitly in spatial joins, load queries as layers with a unique key, register new geometry tables and index them, run the same spatial SQL on GeoPackages through GDAL, and move to PostGIS when many people edit at once.",[149,1441,1443],{"id":1442},"frequently-asked-questions","Frequently Asked Questions",[14,1445,1446,1449],{},[314,1447,1448],{},"Should I choose SpatiaLite or GeoPackage for new projects?","\nGeoPackage for exchange and broad tool support; SpatiaLite when you rely heavily on its SQL functions and tooling. Both are SQLite underneath.",[14,1451,1452,1455],{},[314,1453,1454],{},"Can several people read the same file at once?","\nYes. Concurrent reads are fine; writes are serialised.",[14,1457,1458,1461],{},[314,1459,1460],{},"How big can a SpatiaLite database get?","\nMany gigabytes work well; performance depends more on indexes and queries than size.",[14,1463,1464,1467,1468,1471,1472,871],{},[314,1465,1466],{},"Can I use SpatiaLite from Python without QGIS?","\nYes — load the ",[164,1469,1470],{},"mod_spatialite"," extension into Python's ",[164,1473,1474],{},"sqlite3",[149,1476,1478],{"id":1477},"related","Related",[154,1480,1481,1486,1491,1496,1502],{},[157,1482,1483,1485],{},[21,1484,24],{"href":23}," — the guide this recipe belongs to",[157,1487,1488],{},[21,1489,1490],{"href":500},"Use the Database Connections API in PyQGIS",[157,1492,1493],{},[21,1494,1495],{"href":869},"Load a PostGIS Query Layer in PyQGIS",[157,1497,1498],{},[21,1499,1501],{"href":1500},"\u002Fspatial-data-processing-automation\u002Flayer-data-sources-and-formats\u002Fquery-layers-with-virtual-layer-sql-pyqgis\u002F","Query Layers with Virtual Layer SQL in PyQGIS",[157,1503,1504],{},[21,1505,1507],{"href":1506},"\u002Fspatial-data-processing-automation\u002Flayer-data-sources-and-formats\u002Flist-and-load-geopackage-sublayers-pyqgis\u002F","List and Load GeoPackage Sublayers in PyQGIS",[1509,1510,1511],"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":193,"searchDepth":216,"depth":216,"links":1513},[1514,1515,1516,1517,1518,1519,1520,1521,1522,1523,1524,1525,1526,1527],{"id":151,"depth":216,"text":152},{"id":174,"depth":216,"text":175},{"id":331,"depth":216,"text":332},{"id":505,"depth":216,"text":506},{"id":727,"depth":216,"text":728},{"id":874,"depth":216,"text":875},{"id":980,"depth":216,"text":981},{"id":1214,"depth":216,"text":1215},{"id":1362,"depth":216,"text":1363},{"id":1373,"depth":216,"text":1374},{"id":1391,"depth":216,"text":1392},{"id":1429,"depth":216,"text":1430},{"id":1442,"depth":216,"text":1443},{"id":1477,"depth":216,"text":1478},"Use SpatiaLite and GeoPackage files as lightweight spatial databases from PyQGIS — loading tables and SQL query layers, spatial SQL with SpatiaLite functions, using spatial indexes in queries, creating views and new tables, and knowing when PostGIS is the better choice.","md",{"slug":1531,"type":1532,"breadcrumb":1533,"datePublished":1534,"dateModified":1534},"query-spatialite-database-pyqgis","article","Query a SpatiaLite Database","2026-10-02","\u002Fspatial-data-processing-automation\u002Fpostgis-and-database-workflows\u002Fquery-spatialite-database-pyqgis",{"title":5,"description":1528},"spatial-data-processing-automation\u002Fpostgis-and-database-workflows\u002Fquery-spatialite-database-pyqgis\u002Findex","NSYqFe4Z4lhoUYkdqOFO_2CbO4rZkOV3WHpMRRYtt0U",1790966264253]