16.4 空间谓词与索引
空间查询不应从函数名猜语义。先把业务问题归类:
布尔拓扑:是否相交/覆盖/包含?
范围邻近:是否在给定距离内?
度量:具体距离或面积是多少?
排名:最近的 K 个是谁?这四类问题的返回类型、边界、单位、索引方式和停止条件不同。
16.4.1 包含、相交、邻近与最近邻
拓扑谓词回答关系,不回答距离
常用关系:
| 谓词 | 问题 |
|---|---|
ST_Intersects(a,b) | 两者是否共享任何点? |
ST_Disjoint(a,b) | 两者是否完全不相交? |
ST_Contains(a,b) | B 是否位于 A 内部,且内部有共同点? |
ST_Within(a,b) | A 是否位于 B 内,是 contains 的反向关系 |
ST_Covers(a,b) | B 是否没有任何点位于 A 外部? |
ST_Touches(a,b) | 是否只在边界接触、内部不相交? |
对“事件属于围栏”:
ST_Covers(zone.zone_geom, event.location)参数顺序是:
area first, point second若使用反向表达,可写 ST_CoveredBy(point, area)。不要因为
ST_Intersects 参数对称,就假设所有空间谓词都对称。
边界政策决定 contains 还是 covers
固定探针:
SELECT
ST_Covers(zone_geom, location),
ST_Contains(zone_geom, location),
ST_Touches(zone_geom, location)
FROM ...
WHERE event_id = 'e003';结果:
covers=true, contains=false, touches=true这不是函数争论,而是业务选择:
| 业务规则 | 更接近的表达 |
|---|---|
| 边界也算在服务区 | ST_Covers |
| 必须严格位于内部 | ST_Contains |
| 只找边界点 | ST_Touches |
| 任何接触都算 | ST_Intersects |
boundary-semantics.sql 还证明同一点在
围栏扩张前后得到不同结果,说明时间版本不能被省略。
邻近筛选使用 ST_DWithin
“在中心 1 km 内”是布尔资格:
WHERE ST_DWithin(
event.location_geog,
hub.location::geography,
1000
)对 geography,距离参数以米为单位。对 geometry,距离参数使用 CRS 单位。
PostGIS
ST_DWithin
说明该函数包含可利用索引的包围盒比较,然后执行距离判断。
不要写成:
WHERE ST_Distance(a, b) <= 1000再期待同样的索引路径。ST_Distance 必须为候选计算具体值;ST_DWithin
针对阈值问题设计,规划器可使用对应空间操作符缩小候选。
距离值与距离资格分开
常见响应同时需要资格与显示距离:
SELECT
event_id,
ST_Distance(location_geog, :hub_geog) AS meters
FROM shop_ch16.delivery_event
WHERE ST_DWithin(location_geog, :hub_geog, 1000)
ORDER BY meters, event_id;先用 ST_DWithin 过滤,再为较小集合计算/排序距离。SQL 表达式仍可能被
优化器重排,但逻辑合同清楚,且索引条件可见。
距离函数还涉及:
- geography 使用 spheroid 还是 sphere;
- 2D 还是 3D;
- 误差容忍;
- 坐标精度与 GPS 噪声;
- 是否按路网距离而非直线距离。
本章只验证 2D 地表直线距离,不做路线规划。
最近邻是排名问题
找最近的 K 个:
SELECT hub_id, hub_name
FROM shop_ch16.delivery_hub
ORDER BY location <-> :query_geometry, hub_id
LIMIT 2;<-> 是距离排序操作符,适配的索引访问方法/operator class 可以执行 KNN
路径。它与 ST_DWithin 不同:
ST_DWithin -> 资格:所有半径内对象
<-> LIMIT K -> 排名:最近 K 个,不保证在某半径内生产常组合:
WHERE ST_DWithin(location, :point, :radius)
ORDER BY location <-> :point, hub_id
LIMIT :k;radius 控制业务资格,k 控制返回深度。
稳定 tie-breaker 不能省
多个对象可能与查询点等距。实验和分页都要追加:
ORDER BY distance, event_id否则同距离对象的顺序未定义。空间索引也不会替你创造业务唯一顺序。
最近点不等于最近路径
两点直线很近,可能隔着河流、围墙或单行路网。若问题是配送 ETA/路径,应 引入:
- 权威路网;
- 拓扑连接;
- 交通规则和时间版本;
- 路径算法;
- 地图匹配;
- 实际行驶数据校准。
PostGIS 点距离只能作为几何近似,不能自动变成路由引擎。
16.4.2 包围盒过滤与精确计算
空间索引保存可搜索近似
复杂 Polygon 可能有成千上万个顶点。每行都执行精确拓扑会很贵。常见路径:
bounding box candidate filter
-> exact geometry predicate包围盒是包住对象的轴对齐矩形。两个对象的包围盒不相交,则对象必不相交; 包围盒相交,只说明它们可能相交。
因此:
bbox reject = 可以安全排除
bbox match = 仍需精确判断命名谓词会加入索引友好的初筛
PostGIS 对一组常见命名谓词自动加入包围盒条件,例如本章使用的:
ST_Covers
ST_DWithin固定 GiST 计划中可以看见:
Index Cond:
location_geog && _st_expand(query_geography, 1500)
Filter:
st_dwithin(location_geog, query_geography, 1500, true)Index Cond 缩小候选,Filter 执行精确距离。本章联合计划中:
Index Cond:
location @ zone.zone_geom
Filter:
st_covers(zone.zone_geom, location)内部操作符展示可能随版本和计划格式变化,工程上应关注“索引候选 + 精确谓词”结构,而不是把某一行文本当 API。
PostGIS 官方 空间索引与查询 列出会自动利用空间索引的函数,并解释两阶段比较。
手工 && 只回答包围盒
WHERE geometry_a && geometry_b只测试二维包围盒重叠。它适合:
- 明确只需要视窗候选;
- 分阶段调试;
- 为后续自定义精确计算生成候选。
它不等于 ST_Intersects。对凹多边形、带洞区域或长斜线,包围盒会包含大量
实际不相交对象。
不要为了“更快”把精确谓词删掉,除非业务合同本来就只需要 bbox。
lossy/recheck 是正常行为
GiST 等索引可能返回需要 heap recheck 的候选。看到计划中的:
Recheck Cond
Rows Removed by Filter不表示索引错误,而是近似索引和精确关系的正常分工。应观察:
- 候选数量与最终命中数量;
- recheck 比例;
- 几何复杂度;
- 选择率估计;
- heap page 命中;
- 查询半径和区域大小。
若一个巨大 Polygon 的 bbox 覆盖整座城市,空间索引无法凭 bbox 排除很多 点。可考虑细分几何、预计算层级网格或业务分区,但任何近似都要保留精确 复核或明确误差合同。
无效几何会破坏前提
官方 ST_Covers 文档提醒不要对无效 geometry 期待可靠结果。索引只会让
错误候选更快地产生。接入时:
CHECK (ST_IsValid(zone_geom))并保留 ST_IsValidReason 证据,比查询时临时修复更可控。
扩张半径与单位必须一致
geometry 上:
ST_Expand(point_4326, 1000)会按度扩张 1000,不是 1000 米,几乎覆盖全球。不要把 geography 的米参数 直觉套到 geometry 函数。
本章 geometry SP-GiST probe 使用:
ST_DWithin(location, point_4326, 0.02)这里 0.02 是度,仅用于证明 operator class 路径;业务 1 km 查询使用
geography 和 1000 米。
二阶段也适用于跨类型方案
若业务必须在局部投影做高精度计算,可以:
cheap canonical-CRS bbox
-> smaller candidate set
-> ST_Transform
-> exact projected calculation但粗筛边界必须是保守的,不能漏掉真值。跨 CRS 的安全包围盒设计需要处理 投影非线性和区域边缘,不能简单转换两个角点就默认安全。
16.4.3 GiST/SP-GiST 计划与选择率验证
访问方法不是单独的“空间索引类型”
PostgreSQL 索引能力由:
access method + operator class + data type + operator/query共同决定。本章目录:
| 对象 | access method | operator class |
|---|---|---|
| 围栏 geometry | GiST | gist_geometry_ops_2d |
| 事件 geometry | GiST | gist_geometry_ops_2d |
| 事件 geography | GiST | gist_geography_ops |
| 中心 Point | SP-GiST | spgist_geometry_ops_2d |
| 围栏有效期约束 | GiST | gist_text_ops, range_ops |
因此“建了 GiST”信息不完整。还要知道列、opclass、维度和目标查询。
GiST 与 SP-GiST 的直觉边界
GiST 是通用搜索树框架,PostGIS 常用它管理几何包围盒,也支持 geography 和 KNN 等相应 operator class 能力。
SP-GiST 将空间递归划分,适合某些可分区的数据结构与分布。本章用
spgist_geometry_ops_2d 为三个 Point 建索引,并只证明 ST_DWithin
产生该索引路径。
不要从 access method 名字推导所有能力。本章未声称这个 SP-GiST opclass
服务 <-> KNN;最近邻是否走索引必须对目标版本、类型、opclass 和实际
查询看计划。
建索引
CREATE INDEX event_20260308_location_gist_idx
ON shop_ch16.delivery_event_20260308
USING gist (
location shop_ch16_ext.gist_geometry_ops_2d
);
CREATE INDEX event_20260308_geog_gist_idx
ON shop_ch16.delivery_event_20260308
USING gist (
location_geog shop_ch16_ext.gist_geography_ops
);
CREATE INDEX delivery_hub_location_spgist_idx
ON shop_ch16.delivery_hub
USING spgist (
location shop_ch16_ext.spgist_geometry_ops_2d
);本地实验把 PostGIS 安装到 shop_ch16_ext,所以 opclass 与操作符都显式
schema 限定。生产可选择 public 或受控扩展 schema,但搜索路径、迁移工具
和 ORM 必须与之兼容。
目录验收
psql "service=pg36-admin" \
-f static/labs/ch16/index-catalog.sqlindex-catalog.sql 固定 13 个管理索引,并
验证:
access method
all operator classes
indisvalid
indisready
indislive
index bytes
object marker其中:
3 geography GiST
4 geometry GiST (3 event + 1 geofence)
1 geometry SP-GiST
1 mixed text/range GiST exclusion
4 B-tree主键自动索引另由 34 个关系对象白名单验收,不混进“本章主动选择的 13 个 索引”计数。
为什么计划探针关闭顺序扫描
fixture 只有 3 个中心、4 个围栏和 12 个事件。正常成本模型选择 Seq Scan 很合理。为了证明路径存在,探针执行:
SET enable_seqscan = off;
EXPLAIN (ANALYZE, BUFFERS, COSTS OFF, ...);固定结果:
spatial-gist-plan
-> event_20260308_geog_gist_idx
spatial-spgist-plan
-> delivery_hub_location_spgist_idx
joint-plan
-> geofence_version_no_overlap
-> event_20260308_location_gist_idx这只证明:
query/operator/opclass/index are compatible不证明:
planner should choose it at realistic scale
index is faster
estimated selectivity is accurate
cache/WAL/write cost is acceptable生产选择率验证
生产候选应在代表性数据上运行:
EXPLAIN (
ANALYZE,
BUFFERS,
WAL,
SETTINGS,
VERBOSE
) ...对比:
estimated rows vs actual rows
index candidates vs exact matches
heap/index blocks
cache warm/cold
radius/区域大小分布
不同租户与城市的数据倾斜
并发下延迟空间选择率高度依赖数据分布。城市中心密集、郊区稀疏;巨大围栏和小围栏的 bbox 过滤能力不同。一个平均值无法代表所有查询。
统计与维护
装载或大批更新后:
ANALYZE shop_ch16.delivery_event;
ANALYZE shop_ch16.geofence_version;还要观察:
- autovacuum/analyze 是否覆盖每个叶分区;
- 历史分区统计是否陈旧;
- PostGIS 列统计目标是否足够;
- 索引膨胀与重建窗口;
- 写放大与 WAL;
- 副本 replay 延迟;
- 新分区是否漏建空间索引。
父表有索引声明不等于每个未来分区都满足预期,自动化应从目录持续核对。
本节反例清单
以下说法都不足以作为上线结论:
“EXPLAIN 里出现 GiST,所以很快”
“用了 geography,所以最准确”
“SP-GiST 比 GiST 新,所以更好”
“ST_Distance 能算距离,所以能用索引筛半径”
“包围盒命中就是空间相交”
“12 行强制 Index Scan 比 Seq Scan 快”正确结论必须包含语义、类型/SRID、operator class、计划、代表性规模和实际 测量。
上一节:空间类型与坐标参考 · 返回本章目录 · 下一节:时空联合查询是本章收束目标 · 查看全书目录 · 查看索引中心