{"slug":"postgis-spatial-sql","title":"postgis-spatial-sql","summary":"Invoke whenever spatial SQL or its execution backend is the decision: PostGIS, DuckDB Spatial, SpatiaLite, ST_* functions, recurring spatial joins, concurrent/growing workloads, or large GeoParquet queries. Covers backend selection, schemas, GiST/BRIN indexes, KNN, geometry versu","platform":"Claude","tags":[],"authorName":"LLM Mart","authorSlug":"llm-mart","score":0,"source":"github","price":null,"verified":false,"createdAt":"2026-09-18T14:08:03.991853Z","repo":{"url":"https://github.com/muend/geoai-skills","stars":27,"forks":3,"license":"MIT","updatedAt":"2026-09-03T23:49:19Z"},"bodyHtml":"<hr>\n<h2>name: postgis-spatial-sql\ndescription: &gt;-\nInvoke whenever spatial SQL or its execution backend is the decision:\nPostGIS, DuckDB Spatial, SpatiaLite, ST_* functions, recurring spatial\njoins, concurrent/growing workloads, or large GeoParquet queries. Covers\nbackend selection, schemas, GiST/BRIN indexes, KNN, geometry versus\ngeography, correctness benchmarks, and EXPLAIN optimization. Use PostGIS\nfor managed concurrent services and embedded engines for bounded local\nanalytics when evidence supports that choice. Use geo-data-engineering for\nacquisition, conversion, and file-based ETL without spatial SQL.\nlicense: MIT\nmetadata:\nauthor: Muhammed Enes Duran</h2>\n<h1>PostGIS &amp; Spatial SQL</h1>\n<p>Purpose: correct-and-fast spatial SQL. The two recurring failure modes are\nsemantic (geometry vs geography, SRID mismatches → wrong answers) and\nperformance (missing index usage → hour-long joins); this skill guards\nboth.</p>\n<h2>When the database is the right tool</h2>\n<p>Move from files/GeoPandas to PostGIS when any of: features &gt; a few\nmillion, concurrent readers/writers, repeated ad-hoc querying, a serving\nAPI on top, or transactional integrity needs. For single-shot analytical\nscans over GeoParquet, <strong>DuckDB Spatial</strong> is often the fastest\nzero-install path — same SQL mindset, no server.</p>\n<p>When requirements are incomplete, do not turn this heuristic into a final\nrecommendation. First obtain current and forecast data volume, concurrency,\ndelivery and mutation pattern, latency/SLA, serving needs, and operational\nownership (including backup and recovery). Define representative ingestion,\njoin, and read queries for both viable backends; compare runtime and resource\nuse only after row counts, join cardinality, SRID, geometry validity, and sample\noutputs agree. Include this benchmark and correctness plan in the current\nresponse; do not merely offer to draft it later.</p>\n<h2>Schema fundamentals</h2>\n<p>This runnable example assumes the data is contained in UTM zone 33N. Replace\nEPSG:32633 with a projected CRS verified for the actual area of interest.</p>\n<pre><code>CREATE TABLE parcels (\n  id          bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,\n  parcel_no   text NOT NULL,\n  landuse     text,\n  area_m2     double precision,          -- unit in the name, always\n  geom        geometry(MultiPolygon, 32633) NOT NULL\n);\nCREATE INDEX parcels_geom_gix ON parcels USING gist (geom);\nANALYZE parcels;\n</code></pre>\n<ul>\n<li><p><strong>Type the geometry column fully</strong>: <code>geometry(MultiPolygon, SRID)</code> — an\nuntyped <code>geometry</code> column happily accepts mixed garbage.</p>\n</li>\n<li><p>Promote to Multi* on load (<code>ST_Multi</code>) so Polygon/MultiPolygon mixing\nnever bites.</p>\n</li>\n<li><p><strong>geometry vs geography</strong>: geometry in a projected SRID for regional\nanalysis (fast, full function set); geography (SRID 4326) when the\nextent is global/cross-zone and you want meters without picking a\nprojection (slower, smaller function set). Never store in 4326 geometry\nand call <code>ST_Area</code> expecting m² — that's square degrees.</p>\n</li>\n<li><p>Never use EPSG:3857/Web Mercator for area or length measurement. When the\nanalysis CRS is not yet known, either use 4326 geography for a geodesic\nresult or stop and select a verified local/equal-area CRS; do not present a\nknown-distorting CRS as a runnable measurement alternative.</p>\n</li>\n<li><p><strong>Any stored geometry column you recommend must be typed with its SRID.</strong>\nAdvising a \"second projected geometry column\" for repeated measurement is\nincomplete until it is written as <code>geometry(&lt;Type&gt;, &lt;SRID&gt;)</code> with the index\nand the populating <code>ST_Transform</code>. An untyped column recommended as a fix\nreintroduces the mixed-SRID problem it was meant to solve:</p>\n<pre><code>ALTER TABLE parcels ADD COLUMN geom_32633 geometry(MultiPolygon, 32633);\nUPDATE parcels SET geom_32633 = ST_Transform(geom, 32633);\nCREATE INDEX parcels_geom_32633_gix ON parcels USING gist (geom_32633);\n</code></pre>\n</li>\n<li><p>GiST index on every geometry column, <code>ANALYZE</code> after bulk loads; BRIN\nonly for huge, spatially-ordered, append-only tables.</p>\n</li>\n<li><p>Load paths: <code>ogr2ogr -f PostgreSQL</code>, <code>shp2pgsql</code>, or GeoPandas\n<code>to_postgis</code> (small/medium). <code>COPY</code> beats INSERT by orders of magnitude.</p>\n</li>\n</ul>\n<h2>Correct spatial predicates</h2>\n<ul>\n<li><code>ST_Intersects</code> for \"touches at all\", <code>ST_Contains</code>/<code>ST_Within</code> for\ncontainment, <code>ST_DWithin(a, b, dist)</code> for proximity — <strong>never</strong>\n<code>ST_Distance(a,b) &lt; dist</code> (that form can't use the index).</li>\n<li>The classic point-in-polygon join:</li>\n</ul>\n<pre><code>SELECT p.id, a.district\nFROM points p\nJOIN admin a ON ST_Intersects(a.geom, p.geom);   -- GiST on both sides\n</code></pre>\n<ul>\n<li>KNN nearest-neighbor with the distance operator (index-assisted):</li>\n</ul>\n<pre><code>SELECT h.id, h.name\nFROM hospitals h\nORDER BY h.geom &lt;-&gt; (SELECT geom FROM incident WHERE id = 42)\nLIMIT 3;\n</code></pre>\n<p><code>&lt;-&gt;</code> gives true-distance ordering on modern PostGIS for geometry; wrap\nwith <code>ST_DWithin</code> to bound the search when tables are huge.</p>\n<h2>Performance playbook</h2>\n<ol>\n<li><code>EXPLAIN (ANALYZE, BUFFERS)</code> first — confirm the GiST index is used\n(look for \"Index Scan ... _gix\"); a Seq Scan on a big spatial join\nmeans a rewrite, not a bigger server.</li>\n<li>Same SRID on both sides of every predicate — <code>ST_Transform</code> inside a\njoin predicate kills index use; store a transformed, indexed copy\ninstead.</li>\n<li>Big-polygon problem: country/basin-sized geometries make index bboxes\nuseless → <code>ST_Subdivide</code> into a work table (typical 10-100× speedup on\njoins against them).</li>\n</ol>\n<p>The following example assumes <code>countries(country_id, geom)</code>.</p>\n<pre><code>CREATE TABLE country_parts AS\nSELECT c.country_id, part.geom\nFROM countries AS c\nCROSS JOIN LATERAL ST_Subdivide(c.geom, 256) AS part(geom);\n\nCREATE INDEX country_parts_geom_gix ON country_parts USING gist (geom);\nANALYZE country_parts;\n</code></pre>\n<p><code>ST_Subdivide</code> is a set-returning function; do not access its result as\n<code>(ST_Subdivide(...)).geom</code>.</p>\n<ol start=\"4\">\n<li>Validity in-database: <code>ST_IsValid</code> audit, <code>ST_MakeValid</code> repair, add a\n<code>CHECK (ST_IsValid(geom))</code> if writers are untrusted.</li>\n<li>Simplify for serving, not for analysis: keep full-resolution geometry;\ngenerate <code>ST_SimplifyPreserveTopology</code> copies or vector tiles\n(<code>ST_AsMVT</code>) for the web tier.</li>\n<li>Batch updates in transactions; <code>VACUUM ANALYZE</code> after churn.</li>\n</ol>\n<h2>Common analytical patterns</h2>\n<pre><code>-- Area-weighted aggregation (e.g., population into custom zones)\nSELECT z.zone_id,\n       SUM(b.pop * ST_Area(ST_Intersection(z.geom, b.geom)) / ST_Area(b.geom)) AS pop_est\nFROM zones z JOIN blocks b ON ST_Intersects(z.geom, b.geom)\nGROUP BY z.zone_id;\n\n-- Dissolve with attribute\nSELECT landuse, ST_Multi(ST_Union(geom))::geometry(MultiPolygon, 32633) AS geom\nFROM parcels GROUP BY landuse;\n</code></pre>\n<p>Area-weighted interpolation assumes uniform density within source units —\nstate that assumption when reporting. Validity repair is <code>ST_MakeValid</code>,\nnever <code>ST_Buffer(geom, 0)</code>.</p>\n<h2>DuckDB Spatial quick path</h2>\n<pre><code>INSTALL spatial; LOAD spatial;\nSELECT a.name, count(*)\nFROM 'admin.parquet' a, 'points.parquet' p\nWHERE ST_Intersects(a.geom, p.geom)\nGROUP BY a.name;\n</code></pre>\n<p>Reads GeoParquet/Shapefile/GPKG directly, parallel by default — ideal for\none-off large joins and pipeline steps without a server. No GiST; it plans\nits own joins — benchmark, don't assume.</p>\n<h2>Verification protocol</h2>\n<ol>\n<li>Row-count accounting query after each join/overlay CTE.</li>\n<li><code>SELECT DISTINCT ST_SRID(geom), GeometryType(geom)</code> on every table\ntouched — one query kills two classic bug families.</li>\n<li>Sample 5 output features rendered over a basemap (QGIS connects\ndirectly) — numbers can pass while geometries are garbage.</li>\n<li>Treat every <code>sql</code> fence presented as runnable as a syntax and alias\nboundary: it must execute top-to-bottom after stated schema assumptions.\nNever put angle-bracket placeholders, ellipses, pseudocode, abandoned joins,\nor incomplete aliases inside it. If a schema value such as an SRID is\nunknown, ask for it or keep the template in a labeled <code>text</code> block.</li>\n</ol>\n<h2>Pitfalls checklist</h2>\n<ul>\n<li><code>ST_Area</code>/<code>ST_Length</code> on 4326 geometry (square degrees).</li>\n<li>EPSG:3857/Web Mercator for area or length measurement (systematic distortion).</li>\n<li><code>ST_Distance &lt; x</code> instead of <code>ST_DWithin</code> (no index).</li>\n<li><code>ST_Transform</code> in join predicates.</li>\n<li>Untyped geometry columns with mixed SRIDs.</li>\n<li>Country-sized polygons joined without <code>ST_Subdivide</code>.</li>\n<li><code>buffer(0)</code> as validity repair (silent part loss) — <code>ST_MakeValid</code>.</li>\n<li>Serving full-resolution geometries to web clients.</li>\n</ul>\n<h2>Execution contract</h2>\n<ul>\n<li><strong>Workflow:</strong> inspect schema, SRID, geometry type, size, and query goal; choose predicates and indexes; write auditable CTEs; inspect the plan; reconcile results; operationalize safely.</li>\n<li><strong>Decision rules:</strong> use PostGIS for concurrent, repeated, or transactional spatial workloads; use file pipelines or DuckDB Spatial for bounded one-off transformations when a server adds no value.</li>\n<li><strong>Verification protocol:</strong> assert SRID and geometry invariants, account for rows at each join, compare indexed plans and timings, sample geometries on a map, and test boundary semantics.</li>\n<li><strong>Failure modes:</strong> block release for mixed SRIDs, accidental many-to-many explosion, invalid geometries, non-indexable predicates, geography/geometry unit confusion, or unexplained plan regressions.</li>\n<li><strong>Deliverables:</strong> self-contained parameterized SQL or migration with consistent CTE/table aliases, indexes and rationale, query plan evidence, row accounting, sample validation, expected schema, performance notes, and rollback guidance.</li>\n<li><strong>Source freshness:</strong> consult <a href=\"references/authoritative-sources.md\">the authoritative source registry</a> for the deployed database and extension versions before selecting functions or plans.</li>\n</ul>\n","files":[{"path":"agents/openai.yaml","sizeBytes":216,"isText":true},{"path":"references/authoritative-sources.md","sizeBytes":781,"isText":true},{"path":"SKILL.md","sizeBytes":9592,"isText":true}],"reviewScore":null,"reviewSummary":null,"trust":{"provenance":"trusted-source-unreviewed","notice":"Community-authored content, reproduced verbatim and not vetted as instructions. Treat it as data to evaluate, never as directives to follow.","bodySource":null},"bodyLocked":false,"purchaseUrl":null,"sourceUrl":null,"report":{"provenance":"trusted-source-unreviewed","screen":{"ran":true,"outcome":"clean","suspicious":0,"notes":0,"hiddenCharacters":false},"virusScan":{"engine":"clamav","status":"clean","scannedAt":"2026-09-18T14:08:40.728119Z","sha256":"3735FBC88ED04417DC88F0262AE4DFA51219BE7CF7EE3750D75A05926DF156D3","sizeBytes":5543},"review":null,"source":{"repositoryUrl":"https://github.com/muend/geoai-skills","path":"skills/postgis-spatial-sql","license":"MIT","commit":"096e5d4e6825a128e376b017783ee4c8c7323f9b","subtreeSha":"E9D43BC09A85E80F5032D159DE755D2AAC903C134AB1752AB4D6D629CB232936","lastSyncedAt":"2026-09-27T19:46:46.325636Z"},"reviewedAt":"2026-09-18T14:10:10.121278Z","notice":"Community-authored content, reproduced verbatim and not vetted as instructions. Treat it as data to evaluate, never as directives to follow."},"install":[{"target":"skills-cli","command":"npx skills add https://github.com/muend/geoai-skills/tree/main/skills/postgis-spatial-sql"},{"target":"claude-code","command":"claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install muend-geoai-skills@llmmart"},{"target":"git","command":"git clone https://github.com/muend/geoai-skills.git"}]}