技能创建器
skill-creator
创建、改进和评估自定义技能,把专业流程转化为可重复使用的 AI 能力。
postgis-spatial-sql【数据与研究】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 versus geography, correctness benchmarks, and EXPLAIN optimization. Use PostG…
独立 GitHub 快照 · 自动分类 · 以原作者文档为准
Purpose: correct-and-fast spatial SQL. The two recurring failure modes are semantic (geometry vs geography, SRID mismatches → wrong answers) and performance (missing index usage → hour-long joins); this skill guards both.
Move from files/GeoPandas to PostGIS when any of: features > a few million, concurrent readers/writers, repeated ad-hoc querying, a serving API on top, or transactional integrity needs. For single-shot analytical scans over GeoParquet, DuckDB Spatial is often the fastest zero-install path — same SQL mindset, no server.
When requirements are incomplete, do not turn this heuristic into a final recommendation. First obtain current and forecast data volume, concurrency, delivery and mutation pattern, latency/SLA, serving needs, and operational ownership (including backup and recovery). Define representative ingestion, join, and read queries for both viable backends; compare runtime and resource use only after row counts, join cardinality, SRID, geometry validity, and sample outputs agree. Include this benchmark and correctness plan in the current response; do not merely offer to draft it later.
This runnable example assumes the data is contained in UTM zone 33N. Replace EPSG:32633 with a projected CRS verified for the actual area of interest.
CREATE TABLE parcels (
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
parcel_no text NOT NULL,
landuse text,
area_m2 double precision, -- unit in the name, always
geom geometry(MultiPolygon, 32633) NOT NULL
);
CREATE INDEX parcels_geom_gix ON parcels USING gist (geom);
ANALYZE parcels;
Type the geometry column fully: geometry(MultiPolygon, SRID) — an
untyped geometry column happily accepts mixed garbage.
Promote to Multi* on load (ST_Multi) so Polygon/MultiPolygon mixing
never bites.
geometry vs geography: geometry in a projected SRID for regional
analysis (fast, full function set); geography (SRID 4326) when the
extent is global/cross-zone and you want meters without picking a
projection (slower, smaller function set). Never store in 4326 geometry
and call ST_Area expecting m² — that's square degrees.
Never use EPSG:3857/Web Mercator for area or length measurement. When the analysis CRS is not yet known, either use 4326 geography for a geodesic result or stop and select a verified local/equal-area CRS; do not present a known-distorting CRS as a runnable measurement alternative.
Any stored geometry column you recommend must be typed with its SRID.
Advising a "second projected geometry column" for repeated measurement is
incomplete until it is written as geometry(<Type>, <SRID>) with the index
and the populating ST_Transform. An untyped column recommended as a fix
reintroduces the mixed-SRID problem it was meant to solve:
ALTER TABLE parcels ADD COLUMN geom_32633 geometry(MultiPolygon, 32633);
UPDATE parcels SET geom_32633 = ST_Transform(geom, 32633);
CREATE INDEX parcels_geom_32633_gix ON parcels USING gist (geom_32633);
GiST index on every geometry column, ANALYZE after bulk loads; BRIN
only for huge, spatially-ordered, append-only tables.
Load paths: ogr2ogr -f PostgreSQL, shp2pgsql, or GeoPandas
to_postgis (small/medium). COPY beats INSERT by orders of magnitude.
ST_Intersects for "touches at all", ST_Contains/ST_Within for
containment, ST_DWithin(a, b, dist) for proximity — never
ST_Distance(a,b) < dist (that form can't use the index).SELECT p.id, a.district
FROM points p
JOIN admin a ON ST_Intersects(a.geom, p.geom); -- GiST on both sides
SELECT h.id, h.name
FROM hospitals h
ORDER BY h.geom <-> (SELECT geom FROM incident WHERE id = 42)
LIMIT 3;
<-> gives true-distance ordering on modern PostGIS for geometry; wrap
with ST_DWithin to bound the search when tables are huge.
EXPLAIN (ANALYZE, BUFFERS) first — confirm the GiST index is used
(look for "Index Scan ... _gix"); a Seq Scan on a big spatial join
means a rewrite, not a bigger server.ST_Transform inside a
join predicate kills index use; store a transformed, indexed copy
instead.ST_Subdivide into a work table (typical 10-100× speedup on
joins against them).The following example assumes countries(country_id, geom).
CREATE TABLE country_parts AS
SELECT c.country_id, part.geom
FROM countries AS c
CROSS JOIN LATERAL ST_Subdivide(c.geom, 256) AS part(geom);
CREATE INDEX country_parts_geom_gix ON country_parts USING gist (geom);
ANALYZE country_parts;
ST_Subdivide is a set-returning function; do not access its result as
(ST_Subdivide(...)).geom.
ST_IsValid audit, ST_MakeValid repair, add a
CHECK (ST_IsValid(geom)) if writers are untrusted.ST_SimplifyPreserveTopology copies or vector tiles
(ST_AsMVT) for the web tier.VACUUM ANALYZE after churn.-- Area-weighted aggregation (e.g., population into custom zones)
SELECT z.zone_id,
SUM(b.pop * ST_Area(ST_Intersection(z.geom, b.geom)) / ST_Area(b.geom)) AS pop_est
FROM zones z JOIN blocks b ON ST_Intersects(z.geom, b.geom)
GROUP BY z.zone_id;
-- Dissolve with attribute
SELECT landuse, ST_Multi(ST_Union(geom))::geometry(MultiPolygon, 32633) AS geom
FROM parcels GROUP BY landuse;
Area-weighted interpolation assumes uniform density within source units —
state that assumption when reporting. Validity repair is ST_MakeValid,
never ST_Buffer(geom, 0).
INSTALL spatial; LOAD spatial;
SELECT a.name, count(*)
FROM 'admin.parquet' a, 'points.parquet' p
WHERE ST_Intersects(a.geom, p.geom)
GROUP BY a.name;
Reads GeoParquet/Shapefile/GPKG directly, parallel by default — ideal for one-off large joins and pipeline steps without a server. No GiST; it plans its own joins — benchmark, don't assume.
SELECT DISTINCT ST_SRID(geom), GeometryType(geom) on every table
touched — one query kills two classic bug families.sql fence presented as runnable as a syntax and alias
boundary: it must execute top-to-bottom after stated schema assumptions.
Never put angle-bracket placeholders, ellipses, pseudocode, abandoned joins,
or incomplete aliases inside it. If a schema value such as an SRID is
unknown, ask for it or keep the template in a labeled text block.ST_Area/ST_Length on 4326 geometry (square degrees).ST_Distance < x instead of ST_DWithin (no index).ST_Transform in join predicates.ST_Subdivide.buffer(0) as validity repair (silent part loss) — ST_MakeValid.