强曰为道

与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

第 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 模式说明
EqualsTF**FFF完全相等
DisjointFFFF***完全不相交
IntersectsT*T******相交(非 Disjoint)
TouchesFT*******, F**T*****, F***T****边界接触
CrossesT*T****** (线/面), T*****T** (线/线)交叉穿过
WithinT*F**F***被包含于
ContainsT*****FF*包含
OverlapsT*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

扩展阅读