PostGIS 完全指南 / 第 5 章:空间查询
第 5 章:空间查询
5.1 空间查询概述
空间查询是 PostGIS 的核心能力,它回答了"地理要素之间存在什么空间关系"这一根本问题。空间查询分为三大类:
| 类别 | 说明 | 代表函数 |
|---|---|---|
| 空间谓词 (Predicate) | 判断两个几何是否满足某种拓扑关系 | ST_Intersects, ST_Contains |
| 空间度量 (Measurement) | 计算几何之间的距离、面积等数值 | ST_Distance, ST_Area |
| 空间操作 (Operation) | 对几何进行变换、裁剪、合并等操作 | ST_Buffer, ST_Intersection |
5.2 DE-9IM 模型
空间拓扑关系的理论基础是 DE-9IM(Dimensionally Extended 9-Intersection Model)。它通过比较两个几何的内部(Interior)、边界(Boundary)和外部(External)的交集维度来定义空间关系。
九交矩阵
Interior(B) Boundary(B) Exterior(B)
Interior(A) [0,0] [0,1] [0,2]
Boundary(A) [1,0] [1,1] [1,2]
External(A) [2,0] [2,1] [2,2]
每个元素取值为 T(true), F(false), *(any), 0, 1, 2, 其中数字表示交集维度。
DE-9IM 对应的标准关系
| 空间关系 | DE-9IM 模式 | 说明 |
|---|---|---|
| Equals | TF**FFF | 完全相等 |
| Disjoint | FFFF*** | 完全不相交 |
| Intersects | T*T****** | 相交(非 Disjoint) |
| Touches | FT*******, F**T*****, F***T**** | 边界接触 |
| Crosses | T*T****** (线/面), T*****T** (线/线) | 交叉穿过 |
| Within | T*F**F*** | 被包含于 |
| Contains | T*****FF* | 包含 |
| Overlaps | T*T***T** (同维) | 部分重叠 |
-- 查看两个几何的 DE-9IM 矩阵
SELECT ST_Relate(
ST_GeomFromText('POLYGON((0 0, 2 0, 2 2, 0 2, 0 0))'),
ST_GeomFromText('POLYGON((1 1, 3 1, 3 3, 1 3, 1 1))')
);
-- 输出: T*T***T**
-- 使用特定模式匹配
SELECT ST_Relate(
ST_GeomFromText('LINESTRING(0 0, 2 2)'),
ST_GeomFromText('LINESTRING(0 2, 2 0)'),
'T*F**FFF*' -- 测试是否相等
);
-- 输出: FALSE
5.3 ST_Intersects(相交)
ST_Intersects 是最常用的空间谓词,判断两个几何是否有公共部分(包括点接触)。
基本用法
-- 判断两条路是否相交
SELECT ST_Intersects(
ST_GeomFromText('LINESTRING(0 0, 10 10)'),
ST_GeomFromText('LINESTRING(0 10, 10 0)')
);
-- 输出: TRUE
-- 判断点是否在区域内
SELECT ST_Intersects(
ST_GeomFromText('POINT(5 5)'),
ST_GeomFromText('POLYGON((0 0, 10 0, 10 10, 0 10, 0 0))')
);
-- 输出: TRUE
业务场景:查询覆盖区域内的门店
-- 查询某个配送区域内的所有门店
SELECT s.name, s.address
FROM stores s
JOIN delivery_zones dz ON ST_Intersects(s.geom, dz.geom)
WHERE dz.zone_name = '朝阳区配送区';
-- 查询与某条地铁线 500 米缓冲区相交的门店
SELECT s.name, s.address
FROM stores s
WHERE ST_Intersects(
s.geom,
ST_Buffer(
(SELECT geom FROM metro_lines WHERE line_name = '1号线')::geography,
500
)::geometry
);
5.4 ST_Contains 和 ST_Within
ST_Contains(A, B): A 完全包含 B(B 的所有点都在 A 的内部)ST_Within(B, A): B 完全在 A 内部(与 ST_Contains 逻辑相反)
-- 判断某个点是否在某个区域内
SELECT ST_Contains(
ST_GeomFromText('POLYGON((0 0, 10 0, 10 10, 0 10, 0 0))'),
ST_GeomFromText('POINT(5 5)')
);
-- 输出: TRUE
-- 边界上的点不算包含
SELECT ST_Contains(
ST_GeomFromText('POLYGON((0 0, 10 0, 10 10, 0 10, 0 0))'),
ST_GeomFromText('POINT(0 0)')
);
-- 输出: FALSE (边界上的点不属于"内部")
-- ST_ContainsProperly: 严格在内部(不接触边界)
SELECT ST_ContainsProperly(
ST_GeomFromText('POLYGON((0 0, 10 0, 10 10, 0 10, 0 0))'),
ST_GeomFromText('POINT(0 0)')
);
-- 输出: FALSE
业务场景:POI 归属区域
-- 查找每个 POI 所属的行政区
SELECT p.name AS poi_name, d.name AS district_name
FROM pois p
JOIN districts d ON ST_Contains(d.geom, p.geom);
-- 查找包含在某个多边形内的所有设施
SELECT f.name, f.facility_type
FROM facilities f
WHERE ST_Within(f.geom, ST_GeomFromGeoJSON('{"type":"Polygon","coordinates":[...]}'));
5.5 ST_Distance(距离计算)
ST_Distance 返回两个几何之间的最短距离。
Geometry vs Geography
-- Geometry 距离(坐标单位,这里是度)
SELECT ST_Distance(
ST_GeomFromText('POINT(116.4074 39.9042)', 4326),
ST_GeomFromText('POINT(121.4737 31.2304)', 4326)
);
-- 输出: ~10.77 (度)
-- 这个数字没有直观意义!
-- Geography 距离(米,真实球面距离)
SELECT ST_Distance(
ST_SetSRID(ST_MakePoint(116.4074, 39.9042), 4326)::geography,
ST_SetSRID(ST_MakePoint(121.4737, 31.2304), 4326)::geography
) / 1000 AS distance_km;
-- 输出: ~1068 (公里)
-- 这才是真实的直线距离!
距离查询优化
-- ❌ 错误方式:先计算所有距离再过滤(全表扫描)
SELECT name, ST_Distance(geom::geography, target::geography) AS dist
FROM stores
WHERE ST_Distance(geom::geography, target::geography) < 3000;
-- 这会计算所有记录的距离,非常慢!
-- ✅ 正确方式:使用 ST_DWithin(利用空间索引)
SELECT name, ST_Distance(geom::geography, target::geography) AS dist
FROM stores
WHERE ST_DWithin(geom::geography, target::geography, 3000);
-- ST_DWithin 会利用空间索引先过滤,再精确计算
KNN 查询(K 最近邻)
-- 查找距离目标点最近的 5 个门店
SELECT name, address,
ST_Distance(
geom::geography,
ST_SetSRID(ST_MakePoint(116.4074, 39.9042), 4326)::geography
) AS distance_m
FROM stores
ORDER BY geom <-> ST_SetSRID(ST_MakePoint(116.4074, 39.9042), 4326)
LIMIT 5;
注意:
<->操作符返回的是边界框距离(度),用于索引排序。实际距离需要用ST_Distance计算。
5.6 ST_DWithin(距离范围内)
ST_DWithin 判断两个几何是否在指定距离内,是带索引加速的距离过滤。
-- 查找某点 3 公里范围内的门店
SELECT name, address,
ROUND(ST_Distance(
geom::geography,
ST_SetSRID(ST_MakePoint(116.4074, 39.9042), 4326)::geography
)) AS distance_m
FROM stores
WHERE ST_DWithin(
geom::geography,
ST_SetSRID(ST_MakePoint(116.4074, 39.9042), 4326)::geography,
3000 -- 3000 米
)
ORDER BY distance_m;
业务场景:地理围栏
-- 场景:用户进入任何门店 500 米范围内时触发推送
CREATE OR REPLACE FUNCTION check_geofence(
user_lng DOUBLE PRECISION,
user_lat DOUBLE PRECISION
) RETURNS TABLE(store_name TEXT, distance DOUBLE PRECISION) AS $$
BEGIN
RETURN QUERY
SELECT s.name::TEXT,
ST_Distance(
s.geom::geography,
ST_SetSRID(ST_MakePoint(user_lng, user_lat), 4326)::geography
)
FROM stores s
WHERE ST_DWithin(
s.geom::geography,
ST_SetSRID(ST_MakePoint(user_lng, user_lat), 4326)::geography,
500
);
END;
$$ LANGUAGE plpgsql;
-- 调用
SELECT * FROM check_geofence(116.4074, 39.9042);
5.7 ST_Touches(边界接触)
-- 两个多边形是否边界相接(不重叠)
SELECT ST_Touches(
ST_GeomFromText('POLYGON((0 0, 2 0, 2 2, 0 2, 0 0))'),
ST_GeomFromText('POLYGON((2 0, 4 0, 4 2, 2 2, 2 0))')
);
-- 输出: TRUE (共享边)
-- 业务场景:查找相邻地块
SELECT a.id AS plot_a, b.id AS plot_b
FROM land_plots a, land_plots b
WHERE a.id < b.id
AND ST_Touches(a.geom, b.geom);
5.8 ST_Crosses(交叉穿过)
-- 线穿过面
SELECT ST_Crosses(
ST_GeomFromText('LINESTRING(0 5, 10 5)'),
ST_GeomFromText('POLYGON((3 3, 7 3, 7 7, 3 7, 3 3))')
);
-- 输出: TRUE
-- 业务场景:管线穿越行政区
SELECT p.pipeline_name, d.district_name
FROM pipelines p
JOIN districts d ON ST_Crosses(p.geom, d.geom);
5.9 ST_Overlaps(重叠)
-- 两个同维度几何部分重叠
SELECT ST_Overlaps(
ST_GeomFromText('POLYGON((0 0, 4 0, 4 4, 0 4, 0 0))'),
ST_GeomFromText('POLYGON((2 2, 6 2, 6 6, 2 6, 2 2))')
);
-- 输出: TRUE
-- 业务场景:检测规划地块重叠
SELECT a.plot_id AS plot_a, b.plot_id AS plot_b,
ST_Area(ST_Intersection(a.geom, b.geom)) AS overlap_area
FROM planned_plots a, planned_plots b
WHERE a.plot_id < b.plot_id
AND ST_Overlaps(a.geom, b.geom);
5.10 ST_Equals 和 ST_OrderingEquals
-- ST_Equals:几何相等(不考虑坐标顺序)
SELECT ST_Equals(
ST_GeomFromText('POLYGON((0 0, 2 0, 2 2, 0 2, 0 0))'),
ST_GeomFromText('POLYGON((1 1, 1 -1, -1 -1, -1 1, 1 1))') -- 旋转后的同一个面
);
-- 输出: TRUE (如果表示同一区域)
-- ST_OrderingEquals:坐标完全相同
SELECT ST_OrderingEquals(
ST_GeomFromText('LINESTRING(0 0, 1 1)'),
ST_GeomFromText('LINESTRING(1 1, 0 0)')
);
-- 输出: FALSE (方向相反)
5.11 空间关系函数汇总
二元谓词(返回 Boolean)
| 函数 | 说明 | 索引加速 |
|---|---|---|
ST_Intersects(A, B) | 相交 | ✅ |
ST_Contains(A, B) | A 包含 B | ✅ |
ST_Within(A, B) | A 在 B 内 | ✅ |
ST_Covers(A, B) | A 覆盖 B | ✅ |
ST_CoveredBy(A, B) | A 被 B 覆盖 | ✅ |
ST_Touches(A, B) | 边界接触 | ✅ |
ST_Crosses(A, B) | 交叉穿过 | ✅ |
ST_Overlaps(A, B) | 部分重叠 | ✅ |
ST_Equals(A, B) | 几何相等 | ✅ |
ST_Disjoint(A, B) | 不相交 | ❌ (需取反) |
ST_DWithin(A, B, dist) | 距离范围内 | ✅ |
度量函数(返回数值)
| 函数 | 说明 | Geography 支持 |
|---|---|---|
ST_Distance(A, B) | 最短距离 | ✅ |
ST_MaxDistance(A, B) | 最远距离 | ❌ |
ST_HausdorffDistance(A, B) | Hausdorff 距离 | ❌ |
ST_Area(geom) | 面积 | ✅ |
ST_Length(geom) | 长度/周长 | ✅ |
ST_Perimeter(geom) | 周长 | ✅ |
5.12 复杂空间查询示例
示例 1:最近设施查询
-- 为每个居民区找到最近的医院
WITH hospitals AS (
SELECT id, name, geom
FROM facilities
WHERE facility_type = '医院'
),
residential AS (
SELECT id, name, geom
FROM communities
)
SELECT r.name AS community,
h.name AS nearest_hospital,
ROUND(ST_Distance(r.geom::geography, h.geom::geography)) AS distance_m
FROM residential r
CROSS JOIN LATERAL (
SELECT h.name, h.geom
FROM hospitals h
ORDER BY r.geom <-> h.geom
LIMIT 1
) h;
示例 2:覆盖范围分析
-- 计算所有门店的 3 公里配送覆盖区域
SELECT ST_Union(
ST_Buffer(geom::geography, 3000)::geometry
) AS coverage_area
FROM stores;
-- 计算覆盖面积(平方公里)
SELECT ST_Area(
ST_Union(ST_Buffer(geom::geography, 3000)::geometry)::geography
) / 1000000 AS coverage_km2
FROM stores;
示例 3:叠加分析
-- 计算两个图层的交集
SELECT
a.id AS region_id,
b.id AS zone_id,
ST_Intersection(a.geom, b.geom) AS intersection_geom,
ST_Area(ST_Intersection(a.geom, b.geom)::geography) / 1000000 AS area_km2
FROM regions a
JOIN zones b ON ST_Intersects(a.geom, b.geom)
WHERE NOT ST_IsEmpty(ST_Intersection(a.geom, b.geom));
5.13 空间查询性能注意事项
| 要点 | 说明 |
|---|---|
| 始终创建空间索引 | 没有索引的空间查询将全表扫描 |
使用 ST_DWithin 代替 ST_Distance 过滤 | 前者利用索引,后者不利用 |
用 && 做粗过滤 | 先用边界框快速过滤,再用精确函数 |
| 注意 Geography 的性能 | 椭球体计算慢,小范围用 Geometry + 投影 |
避免对大量数据做 ST_Buffer | 缓冲区计算开销大,考虑预计算 |
-- 优化技巧:两步过滤法
-- 步骤 1: 用边界框快速过滤(利用索引)
-- 步骤 2: 用精确几何函数验证
SELECT name
FROM stores
WHERE geom && ST_Buffer(target_geom, 0.01) -- 粗过滤(边界框)
AND ST_DWithin(geom::geography, target_geom::geography, 1000); -- 精确过滤
5.14 本章小结
| 要点 | 说明 |
|---|---|
| DE-9IM | 空间拓扑关系的理论基础 |
| ST_Intersects | 最通用的相交判断 |
| ST_Contains / ST_Within | 包含关系判断 |
| ST_Distance | 距离计算,Geometry 用坐标单位,Geography 用米 |
| ST_DWithin | 带索引加速的距离过滤 |
| KNN 查询 | <-> 操作符 + ORDER BY ... LIMIT |