跳至内容

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 methodoperator class
围栏 geometryGiSTgist_geometry_ops_2d
事件 geometryGiSTgist_geometry_ops_2d
事件 geographyGiSTgist_geography_ops
中心 PointSP-GiSTspgist_geometry_ops_2d
围栏有效期约束GiSTgist_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.sql

index-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、计划、代表性规模和实际 测量。


上一节:空间类型与坐标参考 · 返回本章目录 · 下一节:时空联合查询是本章收束目标 · 查看全书目录 · 查看索引中心

最后更新于