16.3 空间类型与坐标参考
PostGIS 不只是“经纬度函数包”。它把空间对象、坐标参考、拓扑关系、距离 模型、索引操作符和目录元数据带进 PostgreSQL。
学习顺序应当是:
业务对象
-> 坐标参考与单位
-> geometry / geography
-> 合法性与边界规则
-> 谓词
-> 索引若从“建一个 GiST”开始,很容易得到能够执行却单位错误的查询。
16.3.1 geometry、geography 与测量语义
两种类型回答不同计算问题
PostGIS 的核心空间类型:
| 类型 | 计算表面 | 距离/面积单位 | 典型用途 |
|---|---|---|---|
geometry | 指定 CRS 的平面坐标 | CRS 单位 | 拓扑、局部投影、丰富函数、空间索引 |
geography | 地球曲面模型 | 米/平方米 | 全球经纬度上的距离、半径、面积 |
geometry(Point, 4326) 的坐标是经纬度角度。下面这条距离:
ST_Distance(point_a_4326, point_b_4326)返回的是坐标系单位,也就是角度,不是米。把结果乘一个固定“每度米数”只在 非常有限的局部近似下成立,且经度尺度随纬度变化。
将同一点作为 geography:
ST_Distance(
point_a_4326::geography,
point_b_4326::geography
)才得到以米为单位的地表距离语义。PostGIS 官方 空间查询章节 说明 geometry 的平面计算与 geography 的大地计算差异。
不是所有列都要存两份
本章将规范值保存在 geometry:
location geometry(Point, 4326) NOT NULL再生成 geography:
location_geog geography(Point, 4326)
GENERATED ALWAYS AS (
location::geography
) STORED好处:
- 只有一个可写坐标事实;
- geography 与 geometry 不会因应用漏更新而漂移;
- geography 可建独立 GiST,米制查询不必每次临时转换;
- 生成表达式和类型可以从目录审计。
代价:
- 多一列存储;
- 写入要计算生成值;
- 多一个索引意味着更多 WAL、磁盘和缓存;
- schema 将业务允许的 CRS 固定为 4326。
若米制查询很少,可以只存 geometry 并在查询中转换;若大多数查询都在一个 适当局部投影内,也可以统一使用投影 geometry。要由查询和单位合同决定, 不是机械地“双列最保险”。
typmod 把对象类型与 SRID 放进 schema
比较:
location geometry和:
location geometry(Point, 4326)后者让数据库拒绝非 Point 或非 4326 的值,使表结构本身表达坐标合同。本章 围栏同样固定:
zone_geom geometry(Polygon, 4326)如果业务允许 MultiPolygon,应明确写:
geometry(MultiPolygon, 4326)或在接入时将 Polygon 规范化为 MultiPolygon。不要直到某个区域含离岛才临时 修改客户端。
X/Y 是坐标轴,不是自动的“纬/经”
EPSG:4326 常见 WKT/GeoJSON 使用:
X = longitude
Y = latitude本章点:
ST_SetSRID(
ST_MakePoint(-74.00000, 40.71000),
4326
)即经度 -74、纬度 40.71。把两者颠倒仍可能落在各自合法数值范围内,
数据库不一定能发现。
接入合同应写清:
format: longitude,latitude
x: longitude
y: latitude
srid: 4326
longitude_range: [-180, 180]
latitude_range: [-90, 90]并用已知控制点做端到端验证,而不是只做数值范围检查。
geography 不是“更准确”的万能开关
geography 很适合:
- “距离配送中心 1 km 内”;
- 跨较大区域的地表距离;
- 用经纬度数据直接得到米制结果。
但 geometry 仍常用于:
ST_Covers、ST_Intersects等拓扑;- 本地工程坐标和高精度投影;
- 更广的 PostGIS 函数集合;
- 需要明确平面模型的地图与分析。
问题不是哪种类型高级,而是哪种计算模型与业务问题一致。
本章距离证据
distance-semantics.sql 对三个中心寻找最近
事件,并用 geography 计算米:
| hub | 最近事件 | 距离(四舍五入米) |
|---|---|---|
| airport | e007 | 0 |
| central | e001 | 0 |
| east | e004 | 423 |
三者都在 1 km 内。这里的 423 只验证单位与查询链,不是测绘级距离承诺。
16.3.2 SRID、投影、单位与坐标转换
SRID 是坐标参考身份
同样一对数:
(500000, 4500000)在不同 CRS 中代表完全不同位置和单位。SRID 让 PostGIS 知道坐标属于哪个 参考系统,并能查找转换定义。
检查:
SELECT
ST_SRID(location),
GeometryType(location)
FROM shop_ch16.delivery_event;本章所有事件、中心和围栏都是 4326。
ST_SetSRID 只贴标签
ST_SetSRID(geom, 4326)不会改变任何坐标数值。它适用于“这些数本来就是 4326,只是对象没有声明” 的场景。
真正转换:
ST_Transform(geom, target_srid)会依据源 CRS 与目标 CRS 重新计算坐标。PostGIS
ST_Transform 文档
明确区分转换坐标与仅修改 SRID 标签。
危险反例:
-- 原始数值其实是 Web Mercator,却被错误贴成 WGS84
ST_SetSRID(mercator_numbers, 4326)这不是近似误差,而是数据身份损坏。以后再 ST_Transform 只会把错误输入
转换得更复杂。
混合 SRID 应当显式失败
本章故意执行:
SELECT ST_Intersects(
ST_SetSRID(ST_MakePoint(0, 0), 4326),
ST_SetSRID(ST_MakePoint(0, 0), 3857)
);固定结果:
SQLSTATE XX000
Operation on mixed SRID geometriessrid-mismatch.sql 把错误作为验收证据。不要
捕获这个错误后自动 ST_SetSRID 到另一边;系统无法仅凭数值知道哪边身份
正确。
CRS 决定单位与失真
常见选择:
| 选择 | 优点 | 风险 |
|---|---|---|
| EPSG:4326 geometry | 交换广泛,保存经纬度自然 | 平面距离是角度 |
| EPSG:4326 geography | 米制地表距离直接 | 函数/性能模型与 geometry 不同 |
| 本地投影 geometry | 局部距离、面积和形状可控 | 适用区域有限,需转换治理 |
| EPSG:3857 geometry | Web 地图显示生态常见 | 距离/面积失真,非通用测量 CRS |
Web Mercator 适合瓦片显示,不应仅因为前端地图使用它,就把业务距离也定义在 3857 平面上。
选投影需要:
- 业务覆盖区域;
- 容许失真;
- 距离、面积、方向还是拓扑;
- 数据供应者的 CRS;
- 跨区/跨国查询;
- 权威测绘要求。
这通常要由 GIS 专业人员与业务共同评审,而不是数据库管理员猜一个 EPSG 编号。
转换表达式与索引要一致
若查询反复写:
WHERE ST_DWithin(
ST_Transform(location, :local_srid),
:query_point,
1000
)原始 location GiST 通常不能直接服务这个转换表达式。选项包括:
- 存储/生成规范投影列并建索引;
- 建表达式索引;
- 先用原 CRS 的安全包围盒缩小候选,再精确转换;
- 使用 geography 的米制谓词。
表达式索引必须与查询表达式结构、SRID 和函数可索引条件一致。每行临时转换 还会增加 CPU。不能只验证结果,不看计划。
坐标转换也要版本化
CRS 定义、网格文件和转换路径可能随 PROJ/PostGIS/操作系统包变化。高精度 业务应在证据中保存:
PostGIS_Full_Version()
PROJ version
source and target SRID
transformation method/grid availability
control points and expected tolerance本章没有外部权威坐标或网格文件,因此只固定 PostGIS 版本、SRID 和合成 控制点,不声称测绘精度。
16.3.3 点、线、面、边界与有效几何
对象维度决定问题
| 对象 | 本章/配送中的例子 | 典型问题 |
|---|---|---|
| Point | 配送事件、中心 | 在哪里、离多远 |
| LineString | 轨迹、道路 | 长度、相交、沿线位置 |
| Polygon | 围栏 | 覆盖、包含、面积 |
| Multi* | 多片区域、分段轨迹 | 多个几何组成一个业务对象 |
用两列经纬度只能表示 Point,且无法让数据库知道它们共同构成一个空间对象。 PostGIS 类型让约束、函数和索引围绕整个对象工作。
Polygon 有内部、边界和外部
点对面的关系至少有三种:
interior
boundary
exterior所以:
ST_Contains(polygon, point)
ST_Covers(polygon, point)不是同义词。本章 e003 在边界:
ST_Covers = true
ST_Contains = false
ST_Touches = truePostGIS
ST_Covers
说明它允许 B 的点位于 A 的内部或边界,并会自动利用包围盒比较。
业务要先决定:
- 边界订单属于两个区、任一区还是某个优先区;
- 边界容差如何处理 GPS 噪声;
- 围栏之间是否允许重叠;
- 点恰好在洞边界如何解释;
- 版本换挡时边界与时间边界谁先判定。
SQL 只能实现已选规则。
有效几何是谓词的前提
自相交 Polygon、未闭合 ring、错误洞关系等无效几何可能让拓扑谓词产生意外 结果。写入时至少验证:
CHECK (ST_IsValid(zone_geom))本章还固定:
CHECK (NOT ST_IsEmpty(zone_geom))
CHECK (ST_SRID(zone_geom) = 4326)必要时用:
ST_IsValidReason(geom)定位原因。ST_MakeValid 可以尝试修复,但修复可能改变对象类型、拆成多个面
或改变业务边界,不能在生产接入中无审计地自动覆盖原值。
空、NULL 与未知不同
NULL geometry -> 未提供/未知
EMPTY geometry -> 已知为空的空间对象它们在函数和聚合中的行为不同。本章业务事件和围栏都要求 NOT NULL 且
非空,避免把“没有位置”误当成“位于任何区域之外”。如果设备可能不上传定位,
应另设质量状态:
location_status = missing / invalid / approximate / verified不要用 (0,0) 作为缺失哨兵;它是几内亚湾中的真实坐标。
轨迹不是无序点集
若把多个 Point 组成 LineString,顺序必须来自稳定合同:
ST_MakeLine(location ORDER BY occurred_at, event_id)还应分段:
- courier/session;
- 最大时间间隔;
- 设备重启;
- 不合理速度跳变;
- SRID/质量状态。
跨越长间隔直接连线会制造从 A 到 B 的虚假直线。轨迹的 LineString 是一种 派生产品,应可从原始事件和版本化规则重建。
经纬度数据也有许可证与版本
真实边界、道路、地址或兴趣点通常来自外部数据源。上线前要保存:
provider and dataset
version / snapshot date
license and attribution
allowed redistribution/use
source CRS
transformation chain
import checksum
update and rollback policy本章坐标全部为项目自造的合成数据,
fixture-manifest.json 明确
external_geodata=false。这让实验离线可复现,也意味着它不能证明真实地图
数据质量。
建表前的空间合同
business_object: delivery event
geometry_type: Point
canonical_srid: 4326
coordinate_order: longitude, latitude
geometry_role: topology and index
geography_role: meter distance
null_policy: forbidden for canonical event
validity_policy: ST_IsValid and non-empty
boundary_policy: ST_Covers
external_dataset: none这份合同完整后,下一节的谓词与索引才有确定含义。
本节验收
psql "service=pg36-admin" \
-f static/labs/ch16/boundary-semantics.sql
psql "service=pg36-admin" \
-f static/labs/ch16/distance-semantics.sql再检查生成列:
SELECT
attname,
attgenerated,
format_type(atttypid, atttypmod)
FROM pg_attribute
WHERE attrelid =
'shop_ch16.delivery_event'::regclass
AND attname IN ('location', 'location_geog');verify.sql 要求 location_geog.attgenerated = 's',所有 geometry 与
geography 的 SRID 都是 4326,围栏全部非空且有效。任一项漂移,时空查询
即使返回“看起来正确”的五行也不能通过。
上一节:时序表与时间分区 · 返回本章目录 · 下一节:空间谓词与索引 · 查看全书目录 · 查看索引中心