design-postgis-tables
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChinesePostGIS Spatial Table Design
PostGIS空间表设计
Before You Start (5 Questions)
开始前需考虑的5个问题
- What is the geographic scope (single city/region vs global)?
- What are your primary query patterns (within-radius, bbox, intersects, nearest-neighbor)?
- What units do you need for distance/area (meters vs CRS units), and how accurate must they be?
- What is the expected scale (rows, write rate), and is the data mostly append-only?
- 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.
- 地理范围是什么(单一城市/区域 vs 全球)?
- 主要的查询模式有哪些(半径内查询、边界框查询、相交查询、最近邻查询)?
- 距离/面积需要使用什么单位(米 vs 坐标系单位),精度要求如何?
- 预期的数据规模(行数、写入速率)是多少,数据是否以追加为主?
- 是否需要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). PostGIS types provide true spatial capabilities.CIRCLE - 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 (WGS84) for GPS/global data, appropriate local projections for regional data.
4326 - 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 syntax to ensure data integrity.
GEOMETRY(type, SRID)
- 始终使用PostGIS的geometry/geography类型,而非PostgreSQL内置的几何类型(、
POINT、LINE、POLYGON)。PostGIS类型提供真正的空间处理能力。CIRCLE - 根据使用场景选择GEOMETRY或GEOGRAPHY:GEOMETRY适用于投影/本地数据,使用笛卡尔数学运算;GEOGRAPHY适用于全球数据,需要精确的球面计算。
- 创建几何列时始终指定SRID(空间参考标识符)。GPS/全球数据使用(WGS84),区域数据使用合适的本地投影坐标系。
4326 - 为所有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
对比表格
| Aspect | GEOMETRY | GEOGRAPHY |
|---|---|---|
| Coordinate system | Any SRID (projected or geodetic) | WGS84 (SRID 4326) only |
| Distance units | CRS units (degrees, meters, feet) | Meters (always) |
| Distance accuracy | Depends on projection | True spheroidal distance |
| Area accuracy | Accurate in projected CRS | Accurate on sphere |
| Function support | Full (300+ functions) | Limited (~40 functions) |
| Performance | Faster (Cartesian math) | Slower (spherical math) |
| Index type | GiST, BRIN, SP-GiST | GiST only |
| Best for | Regional/local data, complex analysis | Global data, GPS tracking |
| 维度 | GEOMETRY | GEOGRAPHY |
|---|---|---|
| 坐标系 | 任意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
| SRID | Name | Use Case | Units |
|---|---|---|---|
| 4326 | WGS84 | GPS, global data, web maps | Degrees |
| 3857 | Web Mercator | Web map tiles (display only) | Meters |
| 26910-26919 | UTM Zones (US) | Regional analysis | Meters |
| 32601-32660 | UTM Zones (North) | Regional analysis | Meters |
| 32701-32760 | UTM Zones (South) | Regional analysis | Meters |
| SRID | 名称 | 适用场景 | 单位 |
|---|---|---|---|
| 4326 | WGS84 | GPS、全球数据、Web地图 | 度 |
| 3857 | Web Mercator | Web地图瓦片(仅用于展示) | 米 |
| 26910-26919 | UTM区(美国) | 区域分析 | 米 |
| 32601-32660 | UTM区(北半球) | 区域分析 | 米 |
| 32701-32760 | UTM区(南半球) | 区域分析 | 米 |
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
索引选择指南
| Scenario | Index Type | Reasoning |
|---|---|---|
| General spatial queries | GiST | Most versatile, supports all operators |
| Very large, append-only | BRIN | Tiny footprint, good for time-ordered data |
| Point-only, uniform distribution | SP-GiST | Efficient for point lookups |
| Geography columns | GiST | Only supported option |
| Composite spatial + attribute | GiST + B-tree | Separate 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) - use PostGIS types insteadCIRCLE - 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)——请使用PostGIS类型替代CIRCLE - SRID 0(未定义)——始终指定正确的SRID
- 使用ST_Distance进行过滤——使用ST_DWithin以支持索引的距离查询
- 空间操作中混合SRID——必须先转换为统一SRID
- 使用GEOGRAPHY进行复杂分析——使用GEOMETRY并搭配合适的投影坐标系
- 过度精确的坐标——GPS精度约为3-5米,6位小数(0.1米)已足够
Common Pitfalls
常见陷阱
- Longitude/Latitude order: PostGIS uses =
(longitude, latitude), not(X, Y)(lat, lon) - GEOGRAPHY distance units: Always in meters, regardless of display
- Index not used: Run to verify spatial index usage
EXPLAIN ANALYZE - Transform performance: Cache transformed geometries for repeated queries
- Large geometries: Consider ST_Subdivide for very complex polygons
- SQL injection / unsafe dynamic SQL: Don't concatenate untrusted input into SQL. Parameterize values; for dynamic identifiers use safe quoting (,
quote_ident) or strict allowlists.format('%I', ...)
- 经度/纬度顺序:PostGIS使用=
(经度, 纬度),而非(X, Y)(纬度, 经度) - GEOGRAPHY距离单位:始终为米,与展示无关
- 索引未被使用:执行以验证空间索引是否被使用
EXPLAIN ANALYZE - 转换性能:为重复查询缓存转换后的几何图形
- 大型几何图形:对于非常复杂的多边形,考虑使用ST_Subdivide
- SQL注入/不安全动态SQL:请勿将不可信输入拼接成SQL。对值使用参数化;对动态标识符使用安全引用(、
quote_ident)或严格白名单。format('%I', ...)