design-postgis-tables

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

PostGIS Spatial Table Design

PostGIS空间表设计

Before You Start (5 Questions)

开始前需考虑的5个问题

  1. What is the geographic scope (single city/region vs global)?
  2. What are your primary query patterns (within-radius, bbox, intersects, nearest-neighbor)?
  3. What units do you need for distance/area (meters vs CRS units), and how accurate must they be?
  4. What is the expected scale (rows, write rate), and is the data mostly append-only?
  5. Do you need 3D (Z) or measures (M), or is 2D enough?
SQL injection note: When turning these patterns into application code, use parameterized queries for user-provided values (WKT/WKB, coordinates, IDs, radii). Avoid string-concatenating untrusted input into SQL; for dynamic identifiers, use safe identifier quoting/whitelisting.
  1. 地理范围是什么(单一城市/区域 vs 全球)?
  2. 主要的查询模式有哪些(半径内查询、边界框查询、相交查询、最近邻查询)?
  3. 距离/面积需要使用什么单位(米 vs 坐标系单位),精度要求如何?
  4. 预期的数据规模(行数、写入速率)是多少,数据是否以追加为主?
  5. 是否需要3D(Z轴)或度量值(M),还是2D就足够?
SQL注入注意事项:将这些模式转换为应用代码时,对用户提供的值(WKT/WKB、坐标、ID、半径)使用参数化查询。避免将不可信输入拼接成SQL字符串;对于动态标识符,使用安全的标识符引用/白名单机制。

Core Rules

核心规则

  • Always use PostGIS geometry/geography types instead of PostgreSQL's built-in geometric types (
    POINT
    ,
    LINE
    ,
    POLYGON
    ,
    CIRCLE
    ). PostGIS types provide true spatial capabilities.
  • Choose between GEOMETRY and GEOGRAPHY based on your use case: GEOMETRY for projected/local data with Cartesian math; GEOGRAPHY for global data requiring accurate spherical calculations.
  • Always specify SRID (Spatial Reference Identifier) when creating geometry columns. Use
    4326
    (WGS84) for GPS/global data, appropriate local projections for regional data.
  • Create spatial indexes on all geometry/geography columns using GiST (default). Consider BRIN only for very large GEOMETRY tables where rows are naturally ordered on disk and you can tolerate coarser filtering.
  • Use constraint-based type enforcement with
    GEOMETRY(type, SRID)
    syntax to ensure data integrity.
  • 始终使用PostGIS的geometry/geography类型,而非PostgreSQL内置的几何类型(
    POINT
    LINE
    POLYGON
    CIRCLE
    )。PostGIS类型提供真正的空间处理能力。
  • 根据使用场景选择GEOMETRY或GEOGRAPHY:GEOMETRY适用于投影/本地数据,使用笛卡尔数学运算;GEOGRAPHY适用于全球数据,需要精确的球面计算。
  • 创建几何列时始终指定SRID(空间参考标识符)。GPS/全球数据使用
    4326
    (WGS84),区域数据使用合适的本地投影坐标系。
  • 为所有geometry/geography列创建空间索引,默认使用GiST。仅当GEOMETRY表非常大且磁盘上的行自然有序、可容忍较粗粒度过滤时,才考虑使用BRIN索引。
  • 使用基于约束的类型强制,通过
    GEOMETRY(type, SRID)
    语法确保数据完整性。

Geometry vs Geography

Geometry与Geography对比

When to Use GEOMETRY

何时使用GEOMETRY

  • Local/regional data within a single coordinate system
  • Projected coordinates (meters, feet) for accurate area/distance calculations
  • Complex spatial operations (buffering, unions, intersections)
  • Performance-critical queries (Cartesian math is faster)
  • Data already in a projected CRS (UTM, State Plane, etc.)
sql
-- Regional data with projected coordinates (UTM Zone 10N for California)
CREATE TABLE local_parcels (
    id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    parcel_number TEXT NOT NULL,
    boundary GEOMETRY(POLYGON, 26910),  -- UTM Zone 10N (meters)
    area_sqm DOUBLE PRECISION GENERATED ALWAYS AS (ST_Area(boundary)) STORED
);
  • 本地/区域数据,处于单一坐标系内
  • 投影坐标(米、英尺),用于精确的面积/距离计算
  • 复杂空间操作(缓冲、合并、相交)
  • 性能敏感型查询(笛卡尔数学运算速度更快)
  • 数据已处于投影坐标系(UTM、State Plane等)
sql
-- 采用投影坐标的区域数据(加利福尼亚州使用UTM 10N区)
CREATE TABLE local_parcels (
    id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    parcel_number TEXT NOT NULL,
    boundary GEOMETRY(POLYGON, 26910),  -- UTM 10N区(单位:米)
    area_sqm DOUBLE PRECISION GENERATED ALWAYS AS (ST_Area(boundary)) STORED
);

When to Use GEOGRAPHY

何时使用GEOGRAPHY

  • Global data spanning multiple continents/hemispheres
  • GPS coordinates (latitude/longitude in decimal degrees)
  • Accurate distance calculations on Earth's surface (great circle)
  • Simple spatial operations (distance, containment)
  • Data from GPS devices, geocoding services, or web maps
sql
-- Global data with geodetic calculations
CREATE TABLE global_offices (
    id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    name TEXT NOT NULL,
    city TEXT NOT NULL,
    location GEOGRAPHY(POINT, 4326)  -- WGS84 (lat/lon)
);

-- Distance in meters (accurate spherical calculation)
SELECT
    a.name AS office_a,
    b.name AS office_b,
    ST_Distance(a.location, b.location) / 1000 AS distance_km
FROM global_offices a
CROSS JOIN global_offices b
WHERE a.id < b.id;
  • 全球数据,跨越多个大陆/半球
  • GPS坐标(十进制度数表示的纬度/经度)
  • 地球表面的精确距离计算(大圆距离)
  • 简单空间操作(距离判断、包含关系)
  • 来自GPS设备、地理编码服务或Web地图的数据
sql
-- 采用大地测量计算的全球数据
CREATE TABLE global_offices (
    id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    name TEXT NOT NULL,
    city TEXT NOT NULL,
    location GEOGRAPHY(POINT, 4326)  -- WGS84(纬度/经度)
);

-- 以米为单位的距离(精确球面计算)
SELECT
    a.name AS office_a,
    b.name AS office_b,
    ST_Distance(a.location, b.location) / 1000 AS distance_km
FROM global_offices a
CROSS JOIN global_offices b
WHERE a.id < b.id;

Comparison Table

对比表格

AspectGEOMETRYGEOGRAPHY
Coordinate systemAny SRID (projected or geodetic)WGS84 (SRID 4326) only
Distance unitsCRS units (degrees, meters, feet)Meters (always)
Distance accuracyDepends on projectionTrue spheroidal distance
Area accuracyAccurate in projected CRSAccurate on sphere
Function supportFull (300+ functions)Limited (~40 functions)
PerformanceFaster (Cartesian math)Slower (spherical math)
Index typeGiST, BRIN, SP-GiSTGiST only
Best forRegional/local data, complex analysisGlobal data, GPS tracking
维度GEOMETRYGEOGRAPHY
坐标系任意SRID(投影或大地坐标系)仅支持WGS84(SRID 4326)
距离单位坐标系单位(度、米、英尺)始终为米
距离精度取决于投影方式真实球面距离
面积精度在投影坐标系中精确在球面上精确
函数支持完整(300+个函数)有限(约40个函数)
性能更快(笛卡尔数学运算)较慢(球面数学运算)
索引类型GiST、BRIN、SP-GiST仅支持GiST
最佳适用场景区域/本地数据、复杂分析全球数据、GPS追踪

Geometry Types

几何类型

Point Types

点类型

sql
-- Single location (stores, sensors, events)
location GEOMETRY(POINT, 4326)

-- Multiple discrete locations (multi-branch business)
locations GEOMETRY(MULTIPOINT, 4326)

-- 3D point with elevation
location_3d GEOMETRY(POINTZ, 4326)

-- Point with measure value (linear referencing)
location_m GEOMETRY(POINTM, 4326)
Use POINT for: Store locations, sensor positions, event coordinates, addresses, POIs Use MULTIPOINT for: Multiple related locations stored as single feature
sql
-- 单个位置(商店、传感器、事件)
location GEOMETRY(POINT, 4326)

-- 多个离散位置(多分支企业)
locations GEOMETRY(MULTIPOINT, 4326)

-- 带海拔的3D点
location_3d GEOMETRY(POINTZ, 4326)

-- 带度量值的点(线性参考)
location_m GEOMETRY(POINTM, 4326)
使用POINT场景:存储位置、传感器位置、事件坐标、地址、兴趣点(POI) 使用MULTIPOINT场景:将多个相关位置存储为单个要素

Line Types

线类型

sql
-- Single path (road segment, river, route)
path GEOMETRY(LINESTRING, 4326)

-- Multiple paths (road network, transit lines)
network GEOMETRY(MULTILINESTRING, 4326)

-- 3D line with elevation profile
trail_3d GEOMETRY(LINESTRINGZ, 4326)
Use LINESTRING for: Roads, rivers, pipelines, GPS tracks, routes Use MULTILINESTRING for: Disconnected road segments, river systems
sql
-- 单条路径(路段、河流、路线)
path GEOMETRY(LINESTRING, 4326)

-- 多条路径(道路网、公交线路)
network GEOMETRY(MULTILINESTRING, 4326)

-- 带海拔剖面的3D线
trail_3d GEOMETRY(LINESTRINGZ, 4326)
使用LINESTRING场景:道路、河流、管道、GPS轨迹、路线 使用MULTILINESTRING场景:不连通的路段、河流系统

Polygon Types

面类型

sql
-- Single area (parcel, building footprint, zone)
boundary GEOMETRY(POLYGON, 4326)

-- Multiple areas (archipelago, fragmented habitat)
territories GEOMETRY(MULTIPOLYGON, 4326)

-- 3D polygon (building with height)
footprint_3d GEOMETRY(POLYGONZ, 4326)
Use POLYGON for: Property boundaries, administrative areas, service zones Use MULTIPOLYGON for: Countries with islands, fragmented regions
sql
-- 单个区域(地块、建筑轮廓、区域)
boundary GEOMETRY(POLYGON, 4326)

-- 多个区域(群岛、碎片化栖息地)
territories GEOMETRY(MULTIPOLYGON, 4326)

-- 带高度的3D面(建筑)
footprint_3d GEOMETRY(POLYGONZ, 4326)
使用POLYGON场景:物业边界、行政区域、服务区域 使用MULTIPOLYGON场景:包含岛屿的国家、碎片化区域

Generic Types

通用类型

sql
-- Any geometry type (flexible schema)
geom GEOMETRY(GEOMETRY, 4326)

-- Collection of mixed types
features GEOMETRY(GEOMETRYCOLLECTION, 4326)
Use GEOMETRY for: Flexible schemas accepting multiple types Avoid GEOMETRYCOLLECTION: Prefer homogeneous types for better indexing
sql
-- 任意几何类型(灵活 schema)
geom GEOMETRY(GEOMETRY, 4326)

-- 混合类型集合
features GEOMETRY(GEOMETRYCOLLECTION, 4326)
使用GEOMETRY场景:接受多种类型的灵活 schema 避免使用GEOMETRYCOLLECTION:优先使用同类型要素,以便更好地建立索引

Coordinate Systems (SRID)

坐标系(SRID)

Common SRIDs

常用SRID

SRIDNameUse CaseUnits
4326WGS84GPS, global data, web mapsDegrees
3857Web MercatorWeb map tiles (display only)Meters
26910-26919UTM Zones (US)Regional analysisMeters
32601-32660UTM Zones (North)Regional analysisMeters
32701-32760UTM Zones (South)Regional analysisMeters
SRID名称适用场景单位
4326WGS84GPS、全球数据、Web地图
3857Web MercatorWeb地图瓦片(仅用于展示)
26910-26919UTM区(美国)区域分析
32601-32660UTM区(北半球)区域分析
32701-32760UTM区(南半球)区域分析

SRID Best Practices

SRID最佳实践

  • Store in WGS84 (4326) for interoperability and GPS data
  • Transform to projected CRS for accurate measurements
  • Never mix SRIDs in spatial operations without explicit transformation
  • Use appropriate local CRS for area/distance calculations requiring high precision
sql
-- Store in WGS84, calculate in UTM
CREATE TABLE survey_points (
    id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    location GEOMETRY(POINT, 4326),  -- Storage: WGS84
    CONSTRAINT valid_location CHECK (ST_IsValid(location))
);

-- Calculate distance in meters using UTM projection
SELECT
    a.id AS point_a,
    b.id AS point_b,
    ST_Distance(
        ST_Transform(a.location, 26910),  -- Transform to UTM
        ST_Transform(b.location, 26910)
    ) AS distance_meters
FROM survey_points a
CROSS JOIN survey_points b
WHERE a.id < b.id;
  • 以WGS84(4326)存储数据,确保互操作性,适配GPS数据
  • 转换为投影坐标系以进行精确测量
  • 空间操作中切勿混合SRID,必须先显式转换为统一SRID
  • 使用合适的本地坐标系,以满足高精度的面积/距离计算需求
sql
-- 以WGS84存储,以UTM计算
CREATE TABLE survey_points (
    id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    location GEOMETRY(POINT, 4326),  -- 存储:WGS84
    CONSTRAINT valid_location CHECK (ST_IsValid(location))
);

-- 使用UTM投影计算米级距离
SELECT
    a.id AS point_a,
    b.id AS point_b,
    ST_Distance(
        ST_Transform(a.location, 26910),  -- 转换为UTM
        ST_Transform(b.location, 26910)
    ) AS distance_meters
FROM survey_points a
CROSS JOIN survey_points b
WHERE a.id < b.id;

Spatial Indexing

空间索引

GiST Index (Default)

GiST索引(默认)

Most versatile spatial index. Use for all geometry/geography columns.
sql
-- Geometry (most common)
CREATE INDEX idx_your_table_geom_gist ON your_table_name USING GIST (geom);

-- Geography (GiST is the supported option)
CREATE INDEX idx_your_table_geog_gist ON your_table_name USING GIST (geog);

-- Analyze after index creation
VACUUM ANALYZE your_table_name;
Supports: All spatial operators (
&&
,
@>
,
<@
,
~=
,
<->
) Best for: General-purpose spatial queries, mixed query patterns
最通用的空间索引。适用于所有geometry/geography列。
sql
-- Geometry(最常见)
CREATE INDEX idx_your_table_geom_gist ON your_table_name USING GIST (geom);

-- Geography(仅支持GiST)
CREATE INDEX idx_your_table_geog_gist ON your_table_name USING GIST (geog);

-- 创建索引后执行分析
VACUUM ANALYZE your_table_name;
支持:所有空间运算符(
&&
@>
<@
~=
<->
最佳适用场景:通用空间查询、混合查询模式

BRIN Index

BRIN索引

Block Range Index for very large, naturally ordered datasets.
sql
-- BRIN for very large, append-only GEOMETRY tables (geography uses GiST)
CREATE INDEX idx_your_table_geom_brin
    ON your_table_name
    USING BRIN (geom)
    WITH (pages_per_range = 128);
Supports: Bounding box operators (
&&
,
@>
,
<@
) Best for: Append-only tables, time-series spatial data, very large datasets (>100M rows) Trade-off: Much smaller than GiST, but less precise filtering
块范围索引,适用于非常大且自然有序的数据集。
sql
-- 为超大、追加型GEOMETRY表创建BRIN索引(Geography仅支持GiST)
CREATE INDEX idx_your_table_geom_brin
    ON your_table_name
    USING BRIN (geom)
    WITH (pages_per_range = 128);
支持:边界框运算符(
&&
@>
<@
最佳适用场景:追加型表、时序空间数据、超大数据集(>1亿行) 权衡:比GiST索引小得多,但过滤精度较低

SP-GiST Index

SP-GiST索引

Space-partitioned GiST for point data with specific distributions.
sql
-- SP-GiST for GEOMETRY(POINT, ...) only
CREATE INDEX idx_sensors_location_spgist
    ON sensors
    USING SPGIST (location);
Best for: Point-only data, quadtree-friendly distributions Not for: Complex geometries, mixed types
空间分区GiST,适用于具有特定分布的点数据。
sql
-- 仅适用于GEOMETRY(POINT, ...)
CREATE INDEX idx_sensors_location_spgist
    ON sensors
    USING SPGIST (location);
最佳适用场景:仅包含点的数据、适合四叉树分布的数据 不适用场景:复杂几何图形、混合类型

Index Selection Guide

索引选择指南

ScenarioIndex TypeReasoning
General spatial queriesGiSTMost versatile, supports all operators
Very large, append-onlyBRINTiny footprint, good for time-ordered data
Point-only, uniform distributionSP-GiSTEfficient for point lookups
Geography columnsGiSTOnly supported option
Composite spatial + attributeGiST + B-treeSeparate indexes or expression index
场景索引类型理由
通用空间查询GiST最通用,支持所有运算符
超大、追加型数据集BRIN占用空间极小,适合时序数据
仅含点、分布均匀的数据SP-GiST点查询效率高
Geography列GiST唯一支持的索引类型
复合空间+属性查询GiST + B-tree单独创建索引或使用表达式索引

Table Design Examples

表设计示例

Points of Interest (POI)

兴趣点(POI)

sql
CREATE TABLE pois (
    id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    name TEXT NOT NULL,
    category TEXT NOT NULL,
    location GEOGRAPHY(POINT, 4326) NOT NULL,
    address TEXT,
    metadata JSONB DEFAULT '{}',
    created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
    CONSTRAINT valid_category CHECK (category IN (
        'restaurant', 'hotel', 'gas_station', 'hospital', 'school'
    ))
);

-- Spatial index
CREATE INDEX idx_pois_location ON pois USING GIST (location);

-- Category + location for filtered spatial queries
CREATE INDEX idx_pois_category ON pois (category);

-- Find restaurants within 1km
SELECT name, address,
       ST_Distance(
         location,
         ST_SetSRID(ST_MakePoint(-122.4194, 37.7749), 4326)::GEOGRAPHY
       ) AS distance_m
FROM pois
WHERE category = 'restaurant'
  AND ST_DWithin(
    location,
    ST_SetSRID(ST_MakePoint(-122.4194, 37.7749), 4326)::GEOGRAPHY,
    1000
  )
ORDER BY distance_m;
sql
CREATE TABLE pois (
    id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    name TEXT NOT NULL,
    category TEXT NOT NULL,
    location GEOGRAPHY(POINT, 4326) NOT NULL,
    address TEXT,
    metadata JSONB DEFAULT '{}',
    created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
    CONSTRAINT valid_category CHECK (category IN (
        'restaurant', 'hotel', 'gas_station', 'hospital', 'school'
    ))
);

-- 空间索引
CREATE INDEX idx_pois_location ON pois USING GIST (location);

-- 用于过滤型空间查询的分类+位置索引
CREATE INDEX idx_pois_category ON pois (category);

-- 查询1公里范围内的餐厅
SELECT name, address,
       ST_Distance(
         location,
         ST_SetSRID(ST_MakePoint(-122.4194, 37.7749), 4326)::GEOGRAPHY
       ) AS distance_m
FROM pois
WHERE category = 'restaurant'
  AND ST_DWithin(
    location,
    ST_SetSRID(ST_MakePoint(-122.4194, 37.7749), 4326)::GEOGRAPHY,
    1000
  )
ORDER BY distance_m;

Property Parcels

物业地块

sql
CREATE TABLE parcels (
    id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    parcel_id TEXT NOT NULL UNIQUE,
    owner_name TEXT,
    boundary GEOMETRY(MULTIPOLYGON, 4326) NOT NULL,
    centroid GEOMETRY(POINT, 4326) GENERATED ALWAYS AS (ST_Centroid(boundary)) STORED,
    area_sqm DOUBLE PRECISION GENERATED ALWAYS AS (
        ST_Area(boundary::GEOGRAPHY)
    ) STORED,
    perimeter_m DOUBLE PRECISION GENERATED ALWAYS AS (
        ST_Perimeter(boundary::GEOGRAPHY)
    ) STORED,
    CONSTRAINT valid_boundary CHECK (ST_IsValid(boundary)),
    CONSTRAINT closed_boundary CHECK (ST_IsClosed(ST_ExteriorRing(ST_GeometryN(boundary, 1))))
);

CREATE INDEX idx_parcels_boundary ON parcels USING GIST (boundary);
CREATE INDEX idx_parcels_centroid ON parcels USING GIST (centroid);

-- Find parcels intersecting a search area
SELECT parcel_id, owner_name, area_sqm
FROM parcels
WHERE ST_Intersects(boundary, ST_MakeEnvelope(-122.5, 37.7, -122.4, 37.8, 4326));
sql
CREATE TABLE parcels (
    id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    parcel_id TEXT NOT NULL UNIQUE,
    owner_name TEXT,
    boundary GEOMETRY(MULTIPOLYGON, 4326) NOT NULL,
    centroid GEOMETRY(POINT, 4326) GENERATED ALWAYS AS (ST_Centroid(boundary)) STORED,
    area_sqm DOUBLE PRECISION GENERATED ALWAYS AS (
        ST_Area(boundary::GEOGRAPHY)
    ) STORED,
    perimeter_m DOUBLE PRECISION GENERATED ALWAYS AS (
        ST_Perimeter(boundary::GEOGRAPHY)
    ) STORED,
    CONSTRAINT valid_boundary CHECK (ST_IsValid(boundary)),
    CONSTRAINT closed_boundary CHECK (ST_IsClosed(ST_ExteriorRing(ST_GeometryN(boundary, 1))))
);

CREATE INDEX idx_parcels_boundary ON parcels USING GIST (boundary);
CREATE INDEX idx_parcels_centroid ON parcels USING GIST (centroid);

-- 查询与搜索区域相交的地块
SELECT parcel_id, owner_name, area_sqm
FROM parcels
WHERE ST_Intersects(boundary, ST_MakeEnvelope(-122.5, 37.7, -122.4, 37.8, 4326));

GPS Tracking

GPS追踪

sql
CREATE TABLE gps_tracks (
    id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    device_id TEXT NOT NULL,
    recorded_at TIMESTAMPTZ NOT NULL,
    location GEOGRAPHY(POINT, 4326) NOT NULL,
    speed_kmh DOUBLE PRECISION,
    heading DOUBLE PRECISION,
    accuracy_m DOUBLE PRECISION
);

-- Composite index for device + time queries
CREATE INDEX idx_gps_device_time ON gps_tracks (device_id, recorded_at DESC);

-- Spatial index for location queries
CREATE INDEX idx_gps_location ON gps_tracks USING GIST (location);

-- Note: GEOGRAPHY supports GiST; BRIN is for GEOMETRY (when appropriate).

-- Create linestring from track points
SELECT
    device_id,
    ST_MakeLine(location::GEOMETRY ORDER BY recorded_at) AS track_line,
    MIN(recorded_at) AS start_time,
    MAX(recorded_at) AS end_time
FROM gps_tracks
WHERE device_id = 'device_001'
  AND recorded_at >= '2024-01-01'
GROUP BY device_id;
sql
CREATE TABLE gps_tracks (
    id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    device_id TEXT NOT NULL,
    recorded_at TIMESTAMPTZ NOT NULL,
    location GEOGRAPHY(POINT, 4326) NOT NULL,
    speed_kmh DOUBLE PRECISION,
    heading DOUBLE PRECISION,
    accuracy_m DOUBLE PRECISION
);

-- 设备+时间的复合索引
CREATE INDEX idx_gps_device_time ON gps_tracks (device_id, recorded_at DESC);

-- 位置查询的空间索引
CREATE INDEX idx_gps_location ON gps_tracks USING GIST (location);

-- 注意:GEOGRAPHY支持GiST;BRIN适用于GEOMETRY(场景合适时)。

-- 从轨迹点创建线串
SELECT
    device_id,
    ST_MakeLine(location::GEOMETRY ORDER BY recorded_at) AS track_line,
    MIN(recorded_at) AS start_time,
    MAX(recorded_at) AS end_time
FROM gps_tracks
WHERE device_id = 'device_001'
  AND recorded_at >= '2024-01-01'
GROUP BY device_id;

Service Areas / Coverage Zones

服务区域/覆盖范围

sql
CREATE TABLE service_zones (
    id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    zone_name TEXT NOT NULL,
    zone_type TEXT NOT NULL,
    boundary GEOMETRY(POLYGON, 4326) NOT NULL,
    population INTEGER,
    active BOOLEAN NOT NULL DEFAULT true,
    CONSTRAINT valid_zone_type CHECK (zone_type IN ('delivery', 'service', 'coverage')),
    CONSTRAINT valid_boundary CHECK (ST_IsValid(boundary))
);

CREATE INDEX idx_zones_boundary ON service_zones USING GIST (boundary);
CREATE INDEX idx_zones_active ON service_zones (active) WHERE active = true;

-- Check if location is within any active service zone
SELECT zone_name, zone_type
FROM service_zones
WHERE active = true
  AND ST_Contains(boundary, ST_SetSRID(ST_MakePoint(-122.4194, 37.7749), 4326));
sql
CREATE TABLE service_zones (
    id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    zone_name TEXT NOT NULL,
    zone_type TEXT NOT NULL,
    boundary GEOMETRY(POLYGON, 4326) NOT NULL,
    population INTEGER,
    active BOOLEAN NOT NULL DEFAULT true,
    CONSTRAINT valid_zone_type CHECK (zone_type IN ('delivery', 'service', 'coverage')),
    CONSTRAINT valid_boundary CHECK (ST_IsValid(boundary))
);

CREATE INDEX idx_zones_boundary ON service_zones USING GIST (boundary);
CREATE INDEX idx_zones_active ON service_zones (active) WHERE active = true;

-- 检查位置是否在任何活跃服务区域内
SELECT zone_name, zone_type
FROM service_zones
WHERE active = true
  AND ST_Contains(boundary, ST_SetSRID(ST_MakePoint(-122.4194, 37.7749), 4326));

Performance Patterns

性能优化模式

Use ST_DWithin Instead of ST_Distance

使用ST_DWithin替代ST_Distance

sql
-- SLOW: calculates distance for all rows
SELECT * FROM pois
WHERE ST_Distance(location, ref_point) < 1000;

-- FAST: uses spatial index
SELECT * FROM pois
WHERE ST_DWithin(location, ref_point, 1000);
sql
-- 慢:为所有行计算距离
SELECT * FROM pois
WHERE ST_Distance(location, ref_point) < 1000;

-- 快:利用空间索引
SELECT * FROM pois
WHERE ST_DWithin(location, ref_point, 1000);

Use && for Bounding Box Pre-filtering

使用&&进行边界框预过滤

sql
-- Bounding box operator leverages spatial index
SELECT * FROM parcels
WHERE boundary && ST_MakeEnvelope(-122.5, 37.7, -122.4, 37.8, 4326)
  AND ST_Intersects(boundary, search_polygon);
sql
-- 边界框运算符利用空间索引
SELECT * FROM parcels
WHERE boundary && ST_MakeEnvelope(-122.5, 37.7, -122.4, 37.8, 4326)
  AND ST_Intersects(boundary, search_polygon);

Avoid Functions on Indexed Columns

避免在索引列上使用函数

sql
-- SLOW: function prevents index usage
SELECT * FROM parcels WHERE ST_Area(boundary) > 10000;

-- FAST: use generated column with regular index
ALTER TABLE parcels ADD COLUMN area_sqm DOUBLE PRECISION
    GENERATED ALWAYS AS (ST_Area(boundary::GEOGRAPHY)) STORED;
CREATE INDEX idx_parcels_area ON parcels (area_sqm);
SELECT * FROM parcels WHERE area_sqm > 10000;
sql
-- 慢:函数导致无法使用索引
SELECT * FROM parcels WHERE ST_Area(boundary) > 10000;

-- 快:使用生成列和常规索引
ALTER TABLE parcels ADD COLUMN area_sqm DOUBLE PRECISION
    GENERATED ALWAYS AS (ST_Area(boundary::GEOGRAPHY)) STORED;
CREATE INDEX idx_parcels_area ON parcels (area_sqm);
SELECT * FROM parcels WHERE area_sqm > 10000;

Simplify Geometries for Display

简化几何图形用于展示

sql
-- Reduce complexity for web display (tolerance in CRS units)
SELECT
    id,
    name,
    ST_AsGeoJSON(ST_Simplify(boundary, 0.0001)) AS geojson
FROM parcels;
sql
-- 降低复杂度以适配Web展示(容差为坐标系单位)
SELECT
    id,
    name,
    ST_AsGeoJSON(ST_Simplify(boundary, 0.0001)) AS geojson
FROM parcels;

Use Appropriate Precision

使用合适的精度

sql
-- Reduce coordinate precision for storage efficiency
UPDATE locations SET geom = ST_ReducePrecision(geom, 0.000001);

-- GeoJSON with limited decimal places
SELECT ST_AsGeoJSON(location, 6) AS geojson FROM pois;
sql
-- 降低坐标精度以提升存储效率
UPDATE locations SET geom = ST_ReducePrecision(geom, 0.000001);

-- 限制小数位数的GeoJSON
SELECT ST_AsGeoJSON(location, 6) AS geojson FROM pois;

Data Validation

数据验证

Geometry Validity Checks

几何有效性检查

sql
-- Add validity constraint
ALTER TABLE parcels ADD CONSTRAINT valid_geom CHECK (ST_IsValid(boundary));

-- Find and fix invalid geometries
SELECT id, ST_IsValidReason(boundary) AS reason
FROM parcels
WHERE NOT ST_IsValid(boundary);

-- Attempt to fix invalid geometries
UPDATE parcels
SET boundary = ST_MakeValid(boundary)
WHERE NOT ST_IsValid(boundary);
sql
-- 添加有效性约束
ALTER TABLE parcels ADD CONSTRAINT valid_geom CHECK (ST_IsValid(boundary));

-- 查找并修复无效几何图形
SELECT id, ST_IsValidReason(boundary) AS reason
FROM parcels
WHERE NOT ST_IsValid(boundary);

-- 尝试修复无效几何图形
UPDATE parcels
SET boundary = ST_MakeValid(boundary)
WHERE NOT ST_IsValid(boundary);

SRID Consistency

SRID一致性

sql
-- Verify SRID consistency
SELECT DISTINCT ST_SRID(geom) FROM spatial_table;

-- Enforce SRID with constraint
ALTER TABLE locations ADD CONSTRAINT enforce_srid
    CHECK (ST_SRID(location) = 4326);
sql
-- 验证SRID一致性
SELECT DISTINCT ST_SRID(geom) FROM spatial_table;

-- 通过约束强制SRID
ALTER TABLE locations ADD CONSTRAINT enforce_srid
    CHECK (ST_SRID(location) = 4326);

Coordinate Range Validation

坐标范围验证

sql
-- Ensure coordinates are within valid WGS84 bounds
ALTER TABLE global_locations ADD CONSTRAINT valid_coords CHECK (
    ST_X(location::GEOMETRY) BETWEEN -180 AND 180 AND
    ST_Y(location::GEOMETRY) BETWEEN -90 AND 90
);
sql
-- 确保坐标在有效的WGS84范围内
ALTER TABLE global_locations ADD CONSTRAINT valid_coords CHECK (
    ST_X(location::GEOMETRY) BETWEEN -180 AND 180 AND
    ST_Y(location::GEOMETRY) BETWEEN -90 AND 90
);

Do Not Use

请勿使用的内容

  • PostgreSQL built-in types (
    POINT
    ,
    LINE
    ,
    POLYGON
    ,
    CIRCLE
    ) - use PostGIS types instead
  • SRID 0 (undefined) - always specify the correct SRID
  • ST_Distance for filtering - use ST_DWithin for index-supported distance queries
  • Mixed SRIDs in operations - always transform to common SRID first
  • GEOGRAPHY for complex analysis - use GEOMETRY with appropriate projection
  • Over-precise coordinates - GPS accuracy is ~3-5m, 6 decimal places (0.1m) is sufficient
  • PostgreSQL内置类型
    POINT
    LINE
    POLYGON
    CIRCLE
    )——请使用PostGIS类型替代
  • SRID 0(未定义)——始终指定正确的SRID
  • 使用ST_Distance进行过滤——使用ST_DWithin以支持索引的距离查询
  • 空间操作中混合SRID——必须先转换为统一SRID
  • 使用GEOGRAPHY进行复杂分析——使用GEOMETRY并搭配合适的投影坐标系
  • 过度精确的坐标——GPS精度约为3-5米,6位小数(0.1米)已足够

Common Pitfalls

常见陷阱

  1. Longitude/Latitude order: PostGIS uses
    (longitude, latitude)
    =
    (X, Y)
    , not
    (lat, lon)
  2. GEOGRAPHY distance units: Always in meters, regardless of display
  3. Index not used: Run
    EXPLAIN ANALYZE
    to verify spatial index usage
  4. Transform performance: Cache transformed geometries for repeated queries
  5. Large geometries: Consider ST_Subdivide for very complex polygons
  6. SQL injection / unsafe dynamic SQL: Don't concatenate untrusted input into SQL. Parameterize values; for dynamic identifiers use safe quoting (
    quote_ident
    ,
    format('%I', ...)
    ) or strict allowlists.
  1. 经度/纬度顺序:PostGIS使用
    (经度, 纬度)
    =
    (X, Y)
    ,而非
    (纬度, 经度)
  2. GEOGRAPHY距离单位:始终为米,与展示无关
  3. 索引未被使用:执行
    EXPLAIN ANALYZE
    以验证空间索引是否被使用
  4. 转换性能:为重复查询缓存转换后的几何图形
  5. 大型几何图形:对于非常复杂的多边形,考虑使用ST_Subdivide
  6. SQL注入/不安全动态SQL:请勿将不可信输入拼接成SQL。对值使用参数化;对动态标识符使用安全引用(
    quote_ident
    format('%I', ...)
    )或严格白名单。