经天纬地:时序、空间与时空查询
16 经天纬地:时序、空间与时空查询
“时间”与“空间”都很容易被压缩成错误的表结构:
这三个字段看起来够用,却没有回答最关键的问题:
一旦业务需要处理夏令时、迟到、乱序、重复写入、围栏换版或距离筛选,这些 未回答的问题就会从“数据建模细节”变成错误结果。
本章建立一条统一原则:
先固定时间与空间语义,再选择分区、扩展和索引;先证明逻辑答案,再证明 物理路径;最后才讨论容量和性能。
本章完成后
你应当能够:
- 区分事件时间、接收时间、处理时间和业务有效时间;
- 选择
timestamptz与timestamp,解释 PostgreSQL 的存储、输入和显示 时区职责; - 用一次夏令时跳变说明“墙上时间差”为什么不等于实际经过时间;
- 识别迟到、乱序和重复是三个不同问题,并分别设计水位线、重算与幂等合同;
- 用
tstzrange和半开区间[)表达无歧义的有效期; - 选择事件时间作为分区键,写出可裁剪的半开范围谓词;
- 从计划中区分“只访问一个分区”与“扫描所有分区后再过滤”;
- 解释原生分区、聚合与 TimescaleDB 解决的问题边界;
- 区分 PostGIS
geometry与geography的计算模型和单位; - 说明 SRID 是坐标参考身份,
ST_SetSRID不会转换坐标; - 在
ST_Covers、ST_Contains、ST_Intersects、ST_DWithin、ST_Distance与<->之间按业务语义选择; - 解释包围盒候选与精确几何判断的二阶段关系;
- 用 GiST/SP-GiST 计划证明路径存在,同时不把小表强制计划冒充性能基准;
- 把事件时间裁剪、围栏有效期与空间谓词组合成可审计的时空查询;
- 在 Pigsty 中区分扩展装包、preload、
CREATE EXTENSION、版本核对与 L1 节点一致性; - 把 PostGIS 纳入备份恢复、大版本升级、WAL、索引和副本成本;
- 交付一个有冻结输入、反例、计划、权限、校验和、ADR 与精确复位路径的 配送事件 PoC。
贯穿本章的配送事件
实验固定三种时间:
| 列 | 语义 | 用途 |
|---|---|---|
occurred_at |
配送事件实际发生时刻 | 业务排序、分区、历史归属 |
received_at |
该写入尝试被接收的时刻 | 迟到、乱序、重放审计 |
valid_during |
围栏版本生效区间 | 历史时点连接 |
固定两种空间表示:
| 表示 | 本章职责 |
|---|---|
geometry(..., 4326) |
拓扑谓词、边界判断、空间索引 |
geography(..., 4326) |
以米为单位的距离判断 |
冻结 fixture 包含:
13 次尝试中,e003 被发送两次;数据库保留尝试事实,再选出唯一规范事件。
三张日分区分别得到 1 / 7 / 4 行。e008 发生在
2026-03-08 23:59:59Z,e009 正好发生在次日 00:00:00Z,用来证明
半开分区边界。
一个十分钟却跨过两小时刻度的例子
纽约在 2026-03-08 进入夏令时。fixture 中:
| 事件 | UTC | America/New_York 显示 |
|---|---|---|
e002 |
06:55Z |
01:55 |
e003 |
07:05Z |
03:05 |
墙上时间从 01:55 跳到 03:05,看起来相隔 70 分钟;两个绝对时刻实际只相隔 600 秒。实验把两项都保存为证据:
PostgreSQL 的日期时间类型与时区转换规则以官方 Date/Time Types 和 Date/Time Functions 为准。应用程序不应自己维护一份简化时区规则。
围栏边界不是实现细节
e003 位于 central v1 的东边界,也位于 east v1 的西边界。固定结果是:
| 区域 | ST_Covers |
ST_Contains |
ST_Touches |
|---|---|---|---|
| central v1 | true | false | true |
| east v1 | true | false | true |
本章选择 ST_Covers,所以边界算命中,e003 会同时属于两个区域。这是业务
合同,不是 PostGIS 替业务做出的唯一正确选择。若配送系统要求唯一归属,还要
增加优先级、分区化面集或确定性消歧。
另一个版本例子:
e004 在扩张前位于旧围栏外;同一点的 e005 正好在 12:00 发生,按 [)
落入 v2 并位于新围栏内。同一 zone_id 的有效期由 btree_gist 排他约束
禁止重叠,重叠写入固定失败为 SQLSTATE 23P01。
逻辑正确与物理路径分开
时间查询的正确写法是直接约束分区键:
固定计划只出现:
把列包进表达式:
逻辑答案仍是七行,但计划通过 Append 访问三张分区。PostgreSQL 官方分区
文档强调,裁剪依据是分区边界,而不是分区上的普通索引;写法必须让规划器或
执行器能够把谓词与分区键对应。参见
Table Partitioning。
空间计划分别证明:
这些计划在 12 行 fixture 上关闭顺序扫描,仅证明路径可用。正常规划器选择 顺序扫描并不表示索引失效,更不能用强制计划声称生产更快。
实验资产
规范与决策:
冻结输入:
实现与证据:
三份 CSV 不只是示例附件。自动化从数据库重新导出并逐字节比较;
fixture-manifest.json 还固定行数、SHA-256、时间合同、坐标合同和许可证
边界。
快速运行
本地开发数据库应先完成第 4 章的角色与物理模型:
all 会:
- 验证数据库、可写状态、PostgreSQL 14–18、管理员、owner/app 角色和
ch04-v1模型; - 核对本机恰好可供应 PostGIS 3.6.4 与
btree_gist1.8; - 只接管带精确 owner、marker、版本与扩展依赖的两个 schema;
- 在单事务中安装扩展、创建三张日分区、四类数据表和 13 个管理索引;
- 导入 13 次尝试,验证重复 payload 一致,再生成 12 个规范事件;
- 从数据库回读三份冻结 CSV 并逐字节比较;
- 采集 DST、迟到、乱序、分区路由、时间桶、围栏版本、边界和距离证据;
- 采集扩展、索引、权限、对象体积和五份执行计划;
- 证明混合 SRID、重叠有效期、应用写入分别以
XX000、23P01、42501失败; - 运行 34 个关系对象、扩展依赖、数据事实、索引、权限与业务校验和的 完整断言;
- 证明错误 token、错误 target、活跃 worker 时 reset 分别以
P3660/P3661/P3663被拒绝; - 在单事务中不用
CASCADE精确复位,确认第 14 章扩展保留,再完整 重建第二次。
正式 Homebrew PostgreSQL 18.6 双周期证据得到:
安全边界
task.sh all会删除并重建带本章精确 marker 的shop_ch16、shop_ch16_ext和其中两项扩展,只适合本书本地/开发 fixture。生产环境 必须使用经过评审的扩展供应、在线分区与索引发布、备份恢复验证和回退流程。
学习路径
16.1 时间语义先于时序扩展
先学会给“时间”命名和验收。未固定语义时,引入任何时序扩展只会更快地得到 不确定答案。
16.2 时序表与时间分区
把事件时间落实到原生分区,理解裁剪、路由、生命周期和引入时序扩展的决策 门槛。
16.3 空间类型与坐标参考
先固定坐标身份、表示与单位,再允许业务写空间谓词。
16.4 空间谓词与索引
从“问题是什么”推导谓词,再从谓词和数据分布推导索引,不反过来。
16.5 时空联合查询是本章收束目标
把事件时间、围栏有效时间和空间命中合成同一条可解释查询。
16.6 时空扩展的交付与观察
把本地 SQL 映射到 Pigsty 的装包、配置、建库、节点一致性和运行证据。
16.7 实战:配送事件的时空 PoC
- 16.7.1 生成确定性事件与地理数据
- 16.7.2 验证 SRID 错误、裁剪失效和空间索引
- 16.7.3 输出 ADR、PoC 证据与生产代价清单
- 16.7.4 超预算时先删扩展专属细节,不删基础判断力
最后完整执行两周期 PoC,并明确哪些结论已证明、哪些仍需生产规模测试。
权威参考
- PostgreSQL 18: 日期时间类型、 日期时间函数、 范围类型、 声明式分区
- PostGIS:
数据管理与坐标参考、
空间查询、
ST_Covers、ST_DWithin、ST_Transform - Pigsty 4.5: 扩展概览、 包别名、 创建扩展、 PostGIS、 TimescaleDB
上一章:见微知著:全文、模糊与向量检索 · 返回上卷导读 · 下一章:合纵连横:分析加速与分布式选型 · 查看全书目录 · 查看索引中心
16.1 时间语义先于时序扩展
时序系统首先是时间语义系统,其次才是高吞吐写入、压缩或连续聚合系统。
如果一张表只有 created_at,读者无法判断:
- 它由设备、应用还是数据库生成;
- 它表示业务发生、消息到达、事务提交还是规则生效;
- 它能否用于重建业务顺序;
- 它是否适合成为分区键;
- 迟到事件应修改旧聚合,还是被丢弃;
- 修改历史维表后,旧事件是否要重新归属。
本节先建立一套可以写入 schema、查询和验收证据的时间词汇。
16.1.1 事件时间、处理时间与有效时间
一条事实至少可能有四只钟
| 时间 | 回答的问题 | 常见来源 |
|---|---|---|
事件时间 occurred_at |
业务世界何时发生? | 设备、业务服务、领域事件 |
接收时间 received_at |
本系统何时看见这次尝试? | API/消息消费者入口 |
处理时间 processed_at |
某处理阶段何时完成? | worker、ETL、聚合任务 |
有效时间 valid_during |
某规则/版本何时适用? | 业务配置、主数据版本 |
接收时间是处理时间的一种边界,但不要因此把所有处理阶段都压成一个字段。 例如:
每一项都可能有诊断价值,却不都应成为业务查询的默认时间。配送事件的“当天
发生量”通常按 occurred_at;消息积压通常按 received_at - occurred_at
或 processed_at - received_at;围栏归属还要用 valid_during。
本章的最小模型
setup.sql 创建:
原始尝试与规范事件分开,保留两种真相:
若直接把 event_id 设为事件表唯一键并使用 ON CONFLICT DO NOTHING,
数据库能做到幂等,却会丢失重试次数和 payload 冲突证据。本章先保存
ingest_attempt,再要求同一 event_id 的业务 payload 完全一致,选择最早
到达的尝试作为 canonical。
timestamptz 表示绝对时刻
PostgreSQL 有两种常被混淆的 timestamp:
| 类型 | 语义 | 是否保留输入时区名称 |
|---|---|---|
timestamp with time zone / timestamptz |
时间线上的绝对时刻 | 否 |
timestamp without time zone |
没有时区解释的日期与墙上时间 | 不适用 |
timestamptz 输入会依据显式偏移、时区名称或会话 TimeZone 解释成绝对
时刻;内部以统一形式保存,输出时再按当前 TimeZone 显示。原始的
America/New_York、Asia/Shanghai 名称不会随值保存。
因此,下面两个输入表示同一个时刻:
结果是 true。若业务还要知道用户选择的法定时区,应另存经过校验的 IANA
时区名:
不要试图从 UTC offset 反推时区。-04:00 同时可能对应多个地区,也不能
表达未来或过去的夏令时规则。
PostgreSQL 官方
日期时间类型
详细描述输入、存储与输出行为。特别要注意:一个没有偏移的字符串写入
timestamptz 时依赖会话 TimeZone,所以 API 合同应要求显式偏移。
timestamp 也有正当用途
不能把规则简化为“永远用 timestamptz”。以下值本来就不是一个已确定的绝对 时刻:
- 商店每天
09:00开门; - 用户生日
1990-05-06; - “2027 年 5 月第一周一上午十点”这项待排程规则;
- 一张历史文档只记录了当地时间但未知地区。
它们应使用 date、time、timestamp 或“本地日期时间 + IANA 时区 +
解析状态”的复合模型。只有在规则被具体化到某个地区和日期后,才能解析出
timestamptz。
有效时间是区间,不是两个互不相关字段
本章围栏版本:
与 valid_from、valid_to 两列相比,range 把“是否包含端点、是否为空、
是否重叠”变成类型和操作符可见的合同:
业务连接因而直接表达为:
这回答的是“事件发生时哪个版本有效”,而不是“现在最新版本是什么”。
事务时间是另一条轴
本章没有实现完整双时态表。现实中还可能需要:
例如 3 月 10 日才补录“围栏从 3 月 8 日开始生效”,业务有效期与系统记录期 不同。若需要审计“当时系统认为什么”,应增加系统版本、审计表或不可变事件 日志,而不是覆盖旧行后只保留最终答案。
选择默认时间的判断表
| 问题 | 应优先使用 |
|---|---|
| 某日发生多少配送事件 | occurred_at |
| 消息积压/链路延迟 | received_at - occurred_at |
| worker 吞吐与处理延迟 | processed_at - received_at |
| 历史事件属于哪个围栏 | valid_during @> occurred_at |
| 何时把修订写入数据库 | 审计/事务时间 |
| 数据保留按到达还是发生 | 由法规和回补合同明确,不能猜 |
一张表可以有多只钟,但每个查询只能在合同中明确自己使用哪一只。
16.1.2 时区、迟到、乱序与重复事件
时区是显示规则,也是输入解析规则
本章连接上下文固定:
这让导出和校验和稳定,不意味着用户只能看 UTC。展示时显式转换:
AT TIME ZONE 的返回类型取决于输入:
前者把绝对时刻投影为某地墙上时间;后者把无时区的墙上时间按指定地区解释为 绝对时刻。方向相反,代码审查时必须看输入类型,不能只看函数名字。
夏令时反例
fixture 中:
纽约本地显示:
正确的实际时长:
错误模式是先把两边转成无时区本地时间再相减:
第二个结果计算的是墙上刻度差,不是经过时长。在秋季回拨时还可能出现同一
本地时刻两次。绝对持续时间应在 timestamptz 上运算;按本地日历排程则要
先明确地区,再接受 DST 带来的 23/25 小时日。
temporal-analysis.sql 将两个本地显示与
600 秒实际差同时固化,防止只验证其中一面。
迟到不等于乱序
本章定义超过五分钟为“迟到”:
固定迟到事件:
迟到比较事件时间与接收/处理时间。乱序比较多个事件在两条时间轴上的 顺序。本章:
e004 先发生却后到达,因此 e004 -> e005 是乱序对。一个事件可以迟到但
不造成当前批次乱序,也可以只晚几秒却翻转相邻事件顺序。
迟到策略不能藏在 SQL 里
流式或增量聚合常见策略:
| 策略 | 好处 | 代价 |
|---|---|---|
| 永远接受并重算 | 最接近最终事实 | 旧分区、缓存和下游长期可变 |
| 水位线内重算 | 成本可控 | 水位线外需要补偿路径 |
| 迟到旁路/人工处理 | 主链稳定 | 两套状态与操作流程 |
| 直接丢弃 | 简单 | 数据损失,必须有明确业务授权 |
水位线不是 now() - interval '5 minutes' 这么简单。还要定义:
- 以哪个来源、分区或租户推进;
- 空闲来源如何处理;
- 来源时钟漂移多大;
- 重放是否让水位线倒退;
- 聚合、缓存、物化视图和外部消费者如何更正;
- 超过水位线的数据被保留、补偿还是拒绝。
本章只标记迟到,不模拟完整流处理平台。它建立的是数据库中可重算的事实 基础。
重复也有两种
传输重复:同一 event_id、同一 payload 被发送多次。本章 e003
有 a003/a004 两次尝试,注册表记录:
业务冲突:同一 event_id 带来不同 payload。不能将它静默视为普通
重复。本章 loader 先计算 payload variant 数,只有恰好一个版本的
event_id 才进入注册表;生产应把冲突写入隔离表并报警。
一个可靠幂等键应来自领域身份,而不是接收时间或随机重试 ID:
若来源只能提供“近似重复”,需要另设去重窗口、payload hash 与误合并风险, 不能假装获得 exactly-once。
来源序列补足时间排序
两条事件可能具有相同 timestamp 精度,设备时钟也可能回拨。本章保留:
同一 courier 内的业务顺序可用:
它不替代时间:序列通常只能在单一来源内比较,也无法回答真实时长。稳健模型 同时保存领域序列、事件时间、接收时间和唯一身份。
输入时钟也要被观测
本章约束 received_at >= occurred_at 是教学简化。生产设备的时钟可能快于
服务器,直接拒绝会丢数据。更现实的处理是:
原始时间不可覆盖;任何校正都要带算法版本,才能在规则变化后重算。
16.1.3 范围类型、窗口与时间对齐
半开区间让相邻边界只有一个归属
本章统一采用:
左端包含,右端不包含。因此:
正好 12:00 只属于 v2。日分区:
23:59:59 与次日 00:00:00 也不会重叠或漏掉。不要用
23:59:59.999999 人工制造闭区间上界:精度变化、类型转换和代码生成很容易
产生缝隙。
PostgreSQL range 支持包含、重叠、相邻、交集等操作,并可由 GiST/SP-GiST 索引。参见官方 Range Types。
用约束保护有效期
同一围栏版本不能重叠:
普通 B-tree 能找 zone_id,却不能单独表达“同 zone 的两个 range 不得
重叠”。btree_gist 为标量等值提供 GiST operator class,使它能与 range
重叠操作符组合在一个排他约束中。
故意写入:
会与 v1/v2 冲突并返回 23P01。这比在应用中先 SELECT 再 INSERT
可靠,因为并发事务仍由数据库约束仲裁。
时间桶必须固定原点
本章用原生 date_bin 对齐 15 分钟:
三个参数分别是:
不固定 origin,就没有完整的桶合同。不同服务若使用不同原点,即使桶宽相同 也无法合并。
date_trunc('hour', ...) 适合自然日历单位;date_bin 可表达 15 分钟这类
任意固定长度,但不能把“一个月”当成固定秒数。月份、季度、当地营业日应使用
明确日历与时区规则。
官方
Date/Time Functions
给出 date_trunc、date_bin 与 AT TIME ZONE 的类型和行为。
UTC 桶与本地日历桶不是同一产品
UTC 15 分钟监控桶:
“纽约当地营业日”则需要先定义本地日期边界,再转换成两个绝对时刻作为 查询范围。不要简单写:
这虽然逻辑可读,却可能包裹分区键而失去裁剪。更好的应用流程:
DST 切换日的两个 UTC 边界可能相差 23 或 25 小时,这恰好是正确的当地日。
窗口函数不是时间窗口状态机
SQL 窗口函数:
能在当前查询快照中比较相邻事件,适合轨迹间隔、停留候选和乱序审计。但它 不会自动:
- 等待迟到事件;
- 维护跨批水位线;
- 修正已发送给外部系统的结果;
- 把无限事件流变成有界状态。
数据库增量表、物化视图、TimescaleDB continuous aggregate 或外部流系统 可以承接不同职责。选择前仍要先固定 event time、lateness 与 correction 合同。
可执行验收
运行:
关键事实:
时间桶共 11 个,事件总数仍为 12,迟到总数仍为 2;12:00 桶含
e005/e006 两行。聚合后的总量与原始事实不守恒时,应先定位过滤、边界或
重复处理,而不是调整索引。
本节判断线
进入下一节前,至少能完整回答:
回答不完整时,不应先争论日分区还是小时分区,也不应先安装时序扩展。
返回本章目录 · 下一节:时序表与时间分区 · 查看全书目录 · 查看索引中心
16.2 时序表与时间分区
分区不是“数据带时间戳以后自然要做的事”。它是一项物理设计决策:
第 4 章先建立类型、约束与分区 ADR。本节只把那套判断落实到事件时间场景, 不把“按天分区”包装成默认答案。
16.2.1 从 ch04 的分区 ADR 选择时间键
分区键首先决定数据归属
配送事件有两个候选:
按 received_at 分区的好处是写入几乎总落到最新分区,创建和冻结历史分区
容易;坏处是“某业务日发生的事件”会散布到以后到达的分区。
按 occurred_at 分区使业务日查询与保留自然对应,也能只扫描目标事件时间
范围;代价是迟到和重放会写旧分区,旧分区不能简单变成永久只读。
本章 ADR 选择:
理由不是“事件表都该这么做”,而是本 PoC 的主要查询与历史围栏连接都以事件 发生时间为准。若法规要求按接收时间保留原始消息,可让 raw ingest 与规范 事件采用不同分区键。
父表合同
核心定义摘自 setup.sql:
主键包含 occurred_at,不是为了业务身份。PostgreSQL 在分区父表上建立
UNIQUE/PRIMARY KEY 时,约束列必须包含所有分区键列;这样每个叶分区的
局部唯一索引才能共同证明父表范围内不重复。
业务要求的全局 event_id 唯一性由未分区的:
承担。这个模式把两个合同分开:
若只在每个叶分区上建 UNIQUE(event_id),同一个 ID 仍可出现在不同分区。
应用重试改变 occurred_at 时尤其危险。
半开日分区
边界含义是:
fixture 验证:
若没有可接收某值的分区,向父表写入会失败。生产应提前创建未来分区并监控 覆盖范围,不应依赖事故发生后手工补表。
UTC 边界与当地业务日
本章日分区是 UTC 日,不等于纽约当地日。纽约 2026-03-08 当地日可能跨越 两个 UTC 分区:
查询仍然写两个绝对边界:
规划器可能保留两个相关 UTC 分区,其余分区被裁剪。这比为每个用户时区建立 分区可控得多。
裁剪取决于谓词与分区边界
正例:
time-pruned-plan.sql 的固定计划只有:
这已经是成功的分区裁剪。叶表只有七行,顺序扫描是合理选择;“没有使用 B-tree”不影响裁剪已经生效。
反例:
逻辑答案一样,物理工作不同。不要用“给表达式建索引”代替分区裁剪;索引可 减少每张叶表内的扫描,却不一定让规划器排除叶表。
官方 声明式分区文档 区分规划期与执行期裁剪,也明确指出裁剪由分区边界驱动而非普通索引。
从目录验证,而不是从表名猜
partition-catalog.sql 同时采集边界、行数、
最早/最晚事件、owner 与 marker。正式验收不应只检查三张名字像日期的表。
16.2.2 写入模式、冷热生命周期与保留
写入链先处理身份,再路由事实
本章确定性流程:
顺序很重要。如果先向分区事件表写入,再尝试全局注册,两个并发事务可能把
同一业务 ID 写进不同叶表。生产可使用单事务、注册表 INSERT ... ON CONFLICT、显式状态机或消息 inbox/outbox 协调,但必须让全局身份争用发生在
可证明唯一的位置。
为迟到写入保留窗口
按事件时间分区时,“旧”不等于“不再写”。生产 ADR 至少定义:
数字取决于业务,重要的是分开:
- 正常迟到:自动接受并更新聚合;
- 允许迟到:接受但触发更正或告警;
- 超窗回补:需要显式作业、审计和容量窗口;
- 冻结:应用角色不再写,但受控管理员可能回补;
- 保留到期:可删除或归档。
若把“昨天的分区”每天 00:00 立刻设只读,e001 这类迟到事件会在正常链路
失败。
不要让 DEFAULT 分区变成垃圾桶
DEFAULT 分区可以避免缺分区导致写入失败,但会引入新的责任:
- 为什么正常日期落入 default?
- 补建正式分区时怎样迁移而不长时间阻塞?
- default 上的约束是否允许 attach 新分区?
- 查询是否意外长期扫描 default?
- 异常未来时间和损坏年份是否被悄悄接受?
一种稳健策略:
另一种策略可以保留受控 DEFAULT,但必须把行数、年龄和迁移作业作为一等 监控。不存在普适答案。
分区粒度由约束共同决定
按小时、日、周或月选择时,至少估算:
例如一年按日 365 张、每张 4 个索引,已经有约 1,460 个叶索引;若按小时, 一年约 8,760 张表和数万个索引。小分区不自动更快,空或微小分区也有目录、 锁、统计与规划成本。
本章用三张日分区只为让边界和计划可见,不构成生产粒度建议。
索引是叶分区成本
本章每张事件分区维护:
三个叶表一共 12 个索引对象。分区越多,DDL、REINDEX、统计、磁盘 inode、 缓存和故障面越大。
父分区索引能管理对应的叶索引集合,但 PostGIS、不同历史策略或在线构建流程
仍可能需要逐叶控制。无论自动还是手工创建,都要从 pg_index 验证
indisvalid/indisready/indislive,不能只看 DDL 命令返回成功。
热、温、冷不是表空间颜色
可以按生命周期决定:
| 状态 | 可能动作 |
|---|---|
| 热 | 正常写、完整索引、频繁 analyze |
| 温 | 低频回补、限制写角色、保留关键索引 |
| 冷 | detach/归档、压缩、外部存储或只读集群 |
| 到期 | 经审批删除并保留删除证据 |
但每个动作都要回答恢复路径。DROP TABLE old_partition 很快,却会同时删除
数据和局部索引;只有备份、归档与法规合同允许时才是正确保留策略。
PostgreSQL 支持 DETACH PARTITION,可让数据先脱离父表再归档或处理。在线
动作的锁、并发、约束验证和版本差异应在接近生产的环境验证,本章 PoC 不演示
线上表迁移。
删除与回补会影响 vacuum
时间序列通常“追加为主”,不等于没有 MVCC 成本:
- 重复处理可能执行冲突更新;
- 迟到修正会更新旧聚合;
- 围栏重算可能写结果表;
- 保留若使用大批
DELETE会产生死元组和 WAL; - 索引页仍会分裂、膨胀或缓存失衡。
分区级删除可以避免海量行删除,但活跃分区仍需 vacuum/analyze。后续 第 28 章 专门处理 vacuum、冻结与膨胀;本章只要求 把这些成本列入 ADR。
16.2.3 原生分区、聚合与可选时序扩展
先列需求,再选能力
“这是时序数据,所以安装 TimescaleDB”不是决策。先问:
| 需求 | PostgreSQL 原生基线 | 何时考虑专用扩展 |
|---|---|---|
| 时间范围裁剪 | RANGE 分区 |
分片/自动 chunk 管理明显减负 |
| 普通时间聚合 | date_bin、GROUP BY |
continuous aggregate 有量化收益 |
| 预计算 | 物化视图、增量任务 | 自动刷新与失效模型更合适 |
| 保留 | detach/drop 分区 | policy 自动化降低运维风险 |
| 压缩/列式收益 | 外部归档、其他扩展/方案 | 压缩率与查询代价已实测 |
| 高写入 | 批量、COPY、schema/索引优化 | chunk 并行与架构收益已验证 |
原生方案的优势:
- 能力随 PostgreSQL 一起交付;
- 依赖与升级边界较小;
- SQL、备份和故障模型更接近核心数据库;
- 可以先建立可信基线。
扩展的优势可能包括自动 chunk、保留策略、压缩、时间函数和连续聚合,但也 增加:
“少写运维脚本”有价值,但必须与新依赖成本一起衡量。
本章为何推迟 TimescaleDB
fixture 只有 12 行、三天数据。它能证明:
- 事件时间分区合同;
- 半开边界;
- 裁剪正反例;
- 聚合语义;
- 迟到和历史围栏连接。
它不能证明:
- 压缩比;
- continuous aggregate 刷新成本;
- 大规模 chunk 规划;
- 写吞吐或副本延迟;
- 自动保留比受控分区作业更可靠。
所以 spatiotemporal-adr.md 把
TimescaleDB 标为 deferred,而不是反对。重开条件是压缩、保留、连续聚合或
运维收益出现量化证据。
可选扩展仍要完整走交付链
在 Pigsty 中,时序扩展不是一句 CREATE EXTENSION:
Pigsty 当前 TimescaleDB 扩展页 应作为目标 release 的供应入口;实际版本和 PG major/OS 可用性要在 inventory 中核对,不能从本章快照推断未来版本。
聚合真值仍来自时间合同
无论使用:
都必须固定:
- event time 还是 processing time;
- bucket origin 和 timezone;
[)边界;- 迟到水位线;
- 更正是否回写旧桶;
- 去重在哪一层发生;
- 结果版本与重建方式。
扩展能自动化计算,不会替业务决定这些语义。
用 A/B 迁移而不是信仰迁移
若要引入时序扩展,建议保留原生基线:
然后分别比较:
只有语义结果一致后,性能数字才可比较。
16.2.4 不在本章重复在线分区化和维护细节
本章边界
本章从空 schema 创建三张固定分区,目的是教学验证,不处理已有大表的在线 分区化。以下主题需要单独的迁移设计:
- 在写入不中断时建立新分区父表;
- 双写、触发器或逻辑复制;
- 历史数据分批回填;
ATTACH PARTITION前的约束证明;- 索引并发构建与父索引 attach;
- 外键、序列、权限、RLS 和依赖对象迁移;
- 切流、回退、校验和与旧表退役。
它们属于迁移、锁和运维章节,而不是时空语义入门。这里不提供一条貌似通用的 “在线改分区”命令,以免读者在生产大表上照抄。
分区维护也不应塞进应用请求
不要让第一条新日期写入在业务事务里执行 CREATE TABLE。DDL 会涉及锁、
catalog、权限、审计和副本传播。更合适的是受控作业:
删除旧分区也需要独立审批、备份/归档确认和 active worker 防护。
本章必须保留的判断力
即使篇幅有限,也不能删掉:
- event/ingest/valid time 分离;
- 选择分区键的 ADR;
[)边界;- 全局
event_id与分区主键分离; - 直接谓词与包裹谓词的计划对照;
- 迟到写旧分区的成本;
- 原生能力与扩展收益的证据门槛;
- 分区数量乘以索引数量的运维成本。
可以删的是某一扩展的参数百科或某一版本的命令清单。基础判断一旦省掉,读者 会把工具选择误当成时间模型。
本节验收
应看到:
如果逻辑行数正确但计划扫描全部分区,先修谓词和边界;如果行路由错误,先修 分区定义或事件时间。不要用更多索引掩盖语义错误。
上一节:时间语义先于时序扩展 · 返回本章目录 · 下一节:空间类型与坐标参考 · 查看全书目录 · 查看索引中心
16.3 空间类型与坐标参考
PostGIS 不只是“经纬度函数包”。它把空间对象、坐标参考、拓扑关系、距离 模型、索引操作符和目录元数据带进 PostgreSQL。
学习顺序应当是:
若从“建一个 GiST”开始,很容易得到能够执行却单位错误的查询。
16.3.1 geometry、geography 与测量语义
两种类型回答不同计算问题
PostGIS 的核心空间类型:
| 类型 | 计算表面 | 距离/面积单位 | 典型用途 |
|---|---|---|---|
geometry |
指定 CRS 的平面坐标 | CRS 单位 | 拓扑、局部投影、丰富函数、空间索引 |
geography |
地球曲面模型 | 米/平方米 | 全球经纬度上的距离、半径、面积 |
geometry(Point, 4326) 的坐标是经纬度角度。下面这条距离:
返回的是坐标系单位,也就是角度,不是米。把结果乘一个固定“每度米数”只在 非常有限的局部近似下成立,且经度尺度随纬度变化。
将同一点作为 geography:
才得到以米为单位的地表距离语义。PostGIS 官方 空间查询章节 说明 geometry 的平面计算与 geography 的大地计算差异。
不是所有列都要存两份
本章将规范值保存在 geometry:
再生成 geography:
好处:
- 只有一个可写坐标事实;
- geography 与 geometry 不会因应用漏更新而漂移;
- geography 可建独立 GiST,米制查询不必每次临时转换;
- 生成表达式和类型可以从目录审计。
代价:
- 多一列存储;
- 写入要计算生成值;
- 多一个索引意味着更多 WAL、磁盘和缓存;
- schema 将业务允许的 CRS 固定为 4326。
若米制查询很少,可以只存 geometry 并在查询中转换;若大多数查询都在一个 适当局部投影内,也可以统一使用投影 geometry。要由查询和单位合同决定, 不是机械地“双列最保险”。
typmod 把对象类型与 SRID 放进 schema
比较:
和:
后者让数据库拒绝非 Point 或非 4326 的值,使表结构本身表达坐标合同。本章 围栏同样固定:
如果业务允许 MultiPolygon,应明确写:
或在接入时将 Polygon 规范化为 MultiPolygon。不要直到某个区域含离岛才临时 修改客户端。
X/Y 是坐标轴,不是自动的“纬/经”
EPSG:4326 常见 WKT/GeoJSON 使用:
本章点:
即经度 -74、纬度 40.71。把两者颠倒仍可能落在各自合法数值范围内,
数据库不一定能发现。
接入合同应写清:
并用已知控制点做端到端验证,而不是只做数值范围检查。
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 是坐标参考身份
同样一对数:
在不同 CRS 中代表完全不同位置和单位。SRID 让 PostGIS 知道坐标属于哪个 参考系统,并能查找转换定义。
检查:
本章所有事件、中心和围栏都是 4326。
ST_SetSRID 只贴标签
不会改变任何坐标数值。它适用于“这些数本来就是 4326,只是对象没有声明” 的场景。
真正转换:
会依据源 CRS 与目标 CRS 重新计算坐标。PostGIS
ST_Transform 文档
明确区分转换坐标与仅修改 SRID 标签。
危险反例:
这不是近似误差,而是数据身份损坏。以后再 ST_Transform 只会把错误输入
转换得更复杂。
混合 SRID 应当显式失败
本章故意执行:
固定结果:
srid-mismatch.sql 把错误作为验收证据。不要
捕获这个错误后自动 ST_SetSRID 到另一边;系统无法仅凭数值知道哪边身份
正确。
CRS 决定单位与失真
常见选择:
| 选择 | 优点 | 风险 |
|---|---|---|
| EPSG:4326 geometry | 交换广泛,保存经纬度自然 | 平面距离是角度 |
| EPSG:4326 geography | 米制地表距离直接 | 函数/性能模型与 geometry 不同 |
| 本地投影 geometry | 局部距离、面积和形状可控 | 适用区域有限,需转换治理 |
| EPSG:3857 geometry | Web 地图显示生态常见 | 距离/面积失真,非通用测量 CRS |
Web Mercator 适合瓦片显示,不应仅因为前端地图使用它,就把业务距离也定义在 3857 平面上。
选投影需要:
- 业务覆盖区域;
- 容许失真;
- 距离、面积、方向还是拓扑;
- 数据供应者的 CRS;
- 跨区/跨国查询;
- 权威测绘要求。
这通常要由 GIS 专业人员与业务共同评审,而不是数据库管理员猜一个 EPSG 编号。
转换表达式与索引要一致
若查询反复写:
原始 location GiST 通常不能直接服务这个转换表达式。选项包括:
- 存储/生成规范投影列并建索引;
- 建表达式索引;
- 先用原 CRS 的安全包围盒缩小候选,再精确转换;
- 使用 geography 的米制谓词。
表达式索引必须与查询表达式结构、SRID 和函数可索引条件一致。每行临时转换 还会增加 CPU。不能只验证结果,不看计划。
坐标转换也要版本化
CRS 定义、网格文件和转换路径可能随 PROJ/PostGIS/操作系统包变化。高精度 业务应在证据中保存:
本章没有外部权威坐标或网格文件,因此只固定 PostGIS 版本、SRID 和合成 控制点,不声称测绘精度。
16.3.3 点、线、面、边界与有效几何
对象维度决定问题
| 对象 | 本章/配送中的例子 | 典型问题 |
|---|---|---|
| Point | 配送事件、中心 | 在哪里、离多远 |
| LineString | 轨迹、道路 | 长度、相交、沿线位置 |
| Polygon | 围栏 | 覆盖、包含、面积 |
| Multi* | 多片区域、分段轨迹 | 多个几何组成一个业务对象 |
用两列经纬度只能表示 Point,且无法让数据库知道它们共同构成一个空间对象。 PostGIS 类型让约束、函数和索引围绕整个对象工作。
Polygon 有内部、边界和外部
点对面的关系至少有三种:
所以:
不是同义词。本章 e003 在边界:
PostGIS
ST_Covers
说明它允许 B 的点位于 A 的内部或边界,并会自动利用包围盒比较。
业务要先决定:
- 边界订单属于两个区、任一区还是某个优先区;
- 边界容差如何处理 GPS 噪声;
- 围栏之间是否允许重叠;
- 点恰好在洞边界如何解释;
- 版本换挡时边界与时间边界谁先判定。
SQL 只能实现已选规则。
有效几何是谓词的前提
自相交 Polygon、未闭合 ring、错误洞关系等无效几何可能让拓扑谓词产生意外 结果。写入时至少验证:
本章还固定:
必要时用:
定位原因。ST_MakeValid 可以尝试修复,但修复可能改变对象类型、拆成多个面
或改变业务边界,不能在生产接入中无审计地自动覆盖原值。
空、NULL 与未知不同
它们在函数和聚合中的行为不同。本章业务事件和围栏都要求 NOT NULL 且
非空,避免把“没有位置”误当成“位于任何区域之外”。如果设备可能不上传定位,
应另设质量状态:
不要用 (0,0) 作为缺失哨兵;它是几内亚湾中的真实坐标。
轨迹不是无序点集
若把多个 Point 组成 LineString,顺序必须来自稳定合同:
还应分段:
- courier/session;
- 最大时间间隔;
- 设备重启;
- 不合理速度跳变;
- SRID/质量状态。
跨越长间隔直接连线会制造从 A 到 B 的虚假直线。轨迹的 LineString 是一种 派生产品,应可从原始事件和版本化规则重建。
经纬度数据也有许可证与版本
真实边界、道路、地址或兴趣点通常来自外部数据源。上线前要保存:
本章坐标全部为项目自造的合成数据,
fixture-manifest.json 明确
external_geodata=false。这让实验离线可复现,也意味着它不能证明真实地图
数据质量。
建表前的空间合同
这份合同完整后,下一节的谓词与索引才有确定含义。
本节验收
再检查生成列:
verify.sql 要求 location_geog.attgenerated = 's',所有 geometry 与
geography 的 SRID 都是 4326,围栏全部非空且有效。任一项漂移,时空查询
即使返回“看起来正确”的五行也不能通过。
上一节:时序表与时间分区 · 返回本章目录 · 下一节:空间谓词与索引 · 查看全书目录 · 查看索引中心
16.4 空间谓词与索引
空间查询不应从函数名猜语义。先把业务问题归类:
这四类问题的返回类型、边界、单位、索引方式和停止条件不同。
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_CoveredBy(point, area)。不要因为
ST_Intersects 参数对称,就假设所有空间谓词都对称。
边界政策决定 contains 还是 covers
固定探针:
结果:
这不是函数争论,而是业务选择:
| 业务规则 | 更接近的表达 |
|---|---|
| 边界也算在服务区 | ST_Covers |
| 必须严格位于内部 | ST_Contains |
| 只找边界点 | ST_Touches |
| 任何接触都算 | ST_Intersects |
boundary-semantics.sql 还证明同一点在
围栏扩张前后得到不同结果,说明时间版本不能被省略。
邻近筛选使用 ST_DWithin
“在中心 1 km 内”是布尔资格:
对 geography,距离参数以米为单位。对 geometry,距离参数使用 CRS 单位。
PostGIS
ST_DWithin
说明该函数包含可利用索引的包围盒比较,然后执行距离判断。
不要写成:
再期待同样的索引路径。ST_Distance 必须为候选计算具体值;ST_DWithin
针对阈值问题设计,规划器可使用对应空间操作符缩小候选。
距离值与距离资格分开
常见响应同时需要资格与显示距离:
先用 ST_DWithin 过滤,再为较小集合计算/排序距离。SQL 表达式仍可能被
优化器重排,但逻辑合同清楚,且索引条件可见。
距离函数还涉及:
- geography 使用 spheroid 还是 sphere;
- 2D 还是 3D;
- 误差容忍;
- 坐标精度与 GPS 噪声;
- 是否按路网距离而非直线距离。
本章只验证 2D 地表直线距离,不做路线规划。
最近邻是排名问题
找最近的 K 个:
<-> 是距离排序操作符,适配的索引访问方法/operator class 可以执行 KNN
路径。它与 ST_DWithin 不同:
生产常组合:
radius 控制业务资格,k 控制返回深度。
稳定 tie-breaker 不能省
多个对象可能与查询点等距。实验和分页都要追加:
否则同距离对象的顺序未定义。空间索引也不会替你创造业务唯一顺序。
最近点不等于最近路径
两点直线很近,可能隔着河流、围墙或单行路网。若问题是配送 ETA/路径,应 引入:
- 权威路网;
- 拓扑连接;
- 交通规则和时间版本;
- 路径算法;
- 地图匹配;
- 实际行驶数据校准。
PostGIS 点距离只能作为几何近似,不能自动变成路由引擎。
16.4.2 包围盒过滤与精确计算
空间索引保存可搜索近似
复杂 Polygon 可能有成千上万个顶点。每行都执行精确拓扑会很贵。常见路径:
包围盒是包住对象的轴对齐矩形。两个对象的包围盒不相交,则对象必不相交; 包围盒相交,只说明它们可能相交。
因此:
命名谓词会加入索引友好的初筛
PostGIS 对一组常见命名谓词自动加入包围盒条件,例如本章使用的:
固定 GiST 计划中可以看见:
Index Cond 缩小候选,Filter 执行精确距离。本章联合计划中:
内部操作符展示可能随版本和计划格式变化,工程上应关注“索引候选 + 精确谓词”结构,而不是把某一行文本当 API。
PostGIS 官方 空间索引与查询 列出会自动利用空间索引的函数,并解释两阶段比较。
手工 && 只回答包围盒
只测试二维包围盒重叠。它适合:
- 明确只需要视窗候选;
- 分阶段调试;
- 为后续自定义精确计算生成候选。
它不等于 ST_Intersects。对凹多边形、带洞区域或长斜线,包围盒会包含大量
实际不相交对象。
不要为了“更快”把精确谓词删掉,除非业务合同本来就只需要 bbox。
lossy/recheck 是正常行为
GiST 等索引可能返回需要 heap recheck 的候选。看到计划中的:
不表示索引错误,而是近似索引和精确关系的正常分工。应观察:
- 候选数量与最终命中数量;
- recheck 比例;
- 几何复杂度;
- 选择率估计;
- heap page 命中;
- 查询半径和区域大小。
若一个巨大 Polygon 的 bbox 覆盖整座城市,空间索引无法凭 bbox 排除很多 点。可考虑细分几何、预计算层级网格或业务分区,但任何近似都要保留精确 复核或明确误差合同。
无效几何会破坏前提
官方 ST_Covers 文档提醒不要对无效 geometry 期待可靠结果。索引只会让
错误候选更快地产生。接入时:
并保留 ST_IsValidReason 证据,比查询时临时修复更可控。
扩张半径与单位必须一致
geometry 上:
会按度扩张 1000,不是 1000 米,几乎覆盖全球。不要把 geography 的米参数 直觉套到 geometry 函数。
本章 geometry SP-GiST probe 使用:
这里 0.02 是度,仅用于证明 operator class 路径;业务 1 km 查询使用
geography 和 1000 米。
二阶段也适用于跨类型方案
若业务必须在局部投影做高精度计算,可以:
但粗筛边界必须是保守的,不能漏掉真值。跨 CRS 的安全包围盒设计需要处理 投影非线性和区域边缘,不能简单转换两个角点就默认安全。
16.4.3 GiST/SP-GiST 计划与选择率验证
访问方法不是单独的“空间索引类型”
PostgreSQL 索引能力由:
共同决定。本章目录:
| 对象 | 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 和实际
查询看计划。
建索引
本地实验把 PostGIS 安装到 shop_ch16_ext,所以 opclass 与操作符都显式
schema 限定。生产可选择 public 或受控扩展 schema,但搜索路径、迁移工具
和 ORM 必须与之兼容。
目录验收
index-catalog.sql 固定 13 个管理索引,并
验证:
其中:
主键自动索引另由 34 个关系对象白名单验收,不混进“本章主动选择的 13 个 索引”计数。
为什么计划探针关闭顺序扫描
fixture 只有 3 个中心、4 个围栏和 12 个事件。正常成本模型选择 Seq Scan 很合理。为了证明路径存在,探针执行:
固定结果:
这只证明:
不证明:
生产选择率验证
生产候选应在代表性数据上运行:
对比:
空间选择率高度依赖数据分布。城市中心密集、郊区稀疏;巨大围栏和小围栏的 bbox 过滤能力不同。一个平均值无法代表所有查询。
统计与维护
装载或大批更新后:
还要观察:
- autovacuum/analyze 是否覆盖每个叶分区;
- 历史分区统计是否陈旧;
- PostGIS 列统计目标是否足够;
- 索引膨胀与重建窗口;
- 写放大与 WAL;
- 副本 replay 延迟;
- 新分区是否漏建空间索引。
父表有索引声明不等于每个未来分区都满足预期,自动化应从目录持续核对。
本节反例清单
以下说法都不足以作为上线结论:
正确结论必须包含语义、类型/SRID、operator class、计划、代表性规模和实际 测量。
上一节:空间类型与坐标参考 · 返回本章目录 · 下一节:时空联合查询是本章收束目标 · 查看全书目录 · 查看索引中心
16.5 时空联合查询是本章收束目标
时空查询不是“时间 WHERE + 空间 WHERE”这么简单。历史围栏场景至少有三项 同时成立:
第一项选择事件分区,第二项选择当时规则版本,第三项执行空间关系。少任何 一项,答案都可能看起来合理却在历史边界上出错。
16.5.1 某时段、某区域内的配送事件
先把业务问题写完整
目标:
找出 2026-03-08 UTC 日内,事件发生时属于
central围栏的配送事件。
完整 SQL:
固定结果:
e004 与 e005 位于同一点附近:
如果查询只连接 max(version),两条都会按 v2 判断,历史答案被今天的规则
重写。
时间范围约束放在事件时间
应用可能请求“纽约当地 3 月 8 日”。接口层先将当地日解析为两个
timestamptz 参数:
SQL 仍是:
不要在列上转换时区或取 date。参数计算与存储查询分层后,既保留当地日 语义,也保留分区裁剪机会。
围栏版本也使用半开区间
@> 依据 range 自身端点规则。v1 的上界不包含 12:00,v2 的下界包含
12:00,因此不需要:
BETWEEN 两端都包含,会让相邻版本在换挡时刻同时命中。用 range 可以把
端点合同保存在数据中。
空间边界可能产生多归属
本章允许相邻围栏共享边界,ST_Covers 又包含边界,所以 e003 同时命中:
这意味着:
可以大于事件数。固定 12 个事件得到 14 条 membership。若聚合“各区事件数” 后求和,不能假设等于全局事件数。
需要唯一归属时,可以定义:
但任何规则都会改变业务含义,应版本化并进入 ADR,而不是在报表 SQL 中随机
DISTINCT ON。
视图是可复用语义,不是性能保证
本章创建:
应用读取:
普通 view 保存查询定义,规划器通常会展开优化;它不缓存结果,也不保证
每次选择相同计划。权限上,本章只授予 pg36_app 对父表、中心和三个视图的
SELECT,不授予任何写权限。
app-query.sql 以应用角色返回固定五行;
app-write.sql 更新事件固定失败为 SQLSTATE
42501。
参数、权限与租户必须先过滤
真实查询还可能需要:
空间命中不能越过租户和授权边界。若使用 RLS,要验证:
- view 的 security invoker/definer 行为;
- 空间函数是否泄露错误或执行时间信息;
- 查询计划是否在权限过滤后仍可接受;
- plan cache 对不同租户选择率的影响。
本章单租户 fixture 不声称覆盖这些生产边界。
空间输入也要设限
若 API 允许用户上传任意 Polygon:
- 顶点数可能巨大;
- geometry 可能无效;
- SRID 可能错误;
- bbox 可能覆盖全球;
- 拓扑计算可消耗大量 CPU;
- WKT/GeoJSON 大小可能成为滥用入口。
接口应限制字节、顶点、对象类型、SRID、区域范围和 statement timeout,并在 受控流程中验证/规范化。不能因为 PostGIS 函数是 SQL,就把它当廉价谓词。
16.5.2 轨迹、停留、地理围栏与迟到修正
轨迹首先是有序事件序列
最小查询:
稳定顺序由三项共同提供:
单用 timestamp 可能同值;单用来源序列无法跨来源解释实际时间;event ID 用于最后确定 tie。
先分段,再连线
生成轨迹:
只对已确定的 segment 安全。分段条件可能包括:
- courier/session 改变;
- 相邻事件间隔超过阈值;
- 设备重启或 sequence 回退;
- 推算速度超过物理上限;
- 位置质量从 verified 变成 missing;
- 数据跨过不可连接的业务状态。
若从 10:00 的北京点直接连到 18:00 的上海点,LineString 只画出一条直线, 并没有证明实际路径。
停留是派生规则
“在一个区域停留十分钟”需要同时定义:
一种简单候选:
但稀疏采样只能证明观测点,不能证明两点之间始终停留。生产应把结果标为推断, 保存算法版本和输入范围。
地理围栏事件有三种生成方式
| 方式 | 特点 |
|---|---|
| 查询时计算 membership | 总能使用最新修正,查询成本高 |
| 写入时计算并存结果 | 读快,但迟到/围栏修订要更正 |
| 批/流增量派生 | 可控重算,增加状态与作业 |
本章 view 使用查询时计算,最容易证明语义。生产可以物化:
不能只存 event_id, zone_id。至少要知道使用哪个围栏版本、哪套边界算法和
哪批输入。
入围/出围不是两个独立点
若连续位置从 outside 变成 inside,可派生 enter;inside 变 outside 可派生 exit。但 GPS 抖动会在边界反复切换。常见稳健化:
- 进入与退出使用不同阈值(hysteresis);
- 要求连续 N 个样本;
- 使用定位精度圆而不是无误差 Point;
- 对边界附近状态标记 uncertain;
- 限制最大采样间隔;
- 保存原始点以便重算。
ST_Covers 只定义单点关系,不自动解决状态机抖动。
迟到事件会插入历史中间
本章 e004 比 e005 早发生却后到。若系统先看到 e005 并已生成轨迹/围栏
状态,e004 到达后应:
不能只在列表末尾追加。否则 processing order 被误当成 event order。
围栏修订也会重写历史
若业务在 3 月 10 日修订“central v2 从 3 月 8 日 12:00 生效”,至少有两种 政策:
前者适合最终业务事实,后者适合审计。需要两者时,应同时建 valid time 与 system time,而不是在原行上静默覆盖。
重算范围要可证明
对于变更围栏 Polygon:
这正是时空联合过滤的另一个用途。先用时间与 bbox 缩小候选,再对旧/新几何 执行精确关系,可避免全表重算;但必须保留旧 geometry 或可恢复版本。
派生结果不应覆盖原始事实
建议层次:
每层保存:
- 输入版本或 checksum;
- 算法/规则版本;
- 计算时刻;
- 可重建路径;
- 更正/撤回身份。
把“是否在围栏内”直接写回唯一事件行且不留版本,会让历史无法审计。
16.5.3 时间裁剪、空间索引与二阶段过滤
两条独立缩小路径
联合查询的候选空间可以理解为:
固定联合计划:
最关键的不是 Nested Loop,而是:
joint-plan.sql 保存完整证据。
SQL 书写顺序不等于执行顺序
把时间谓词写在 WHERE 第一行不会强制数据库先执行它。PostgreSQL 规划器会 根据等价变换与成本选择路径。我们能做的是:
- 写出可推导的直接分区键范围;
- 使用有索引语义的空间谓词;
- 保持统计新鲜;
- 在真实参数分布下检查计划;
- 必要时调整模型、索引或查询边界;
- 不把关闭 planner 开关当生产提示。
“先时间后空间”是逻辑与候选设计,不是靠 SQL 行顺序控制算子。
prepared statement 也要看参数计划
应用通常使用参数:
PostgreSQL 可能使用 custom 或 generic plan。执行期裁剪可以根据参数移除 分区,但不同参数选择率仍可能使通用计划不理想。生产验证应包括:
不要只在 psql 常量查询上验收,然后假设 ORM prepared statement 完全相同。
先过滤围栏还是先过滤事件取决于基数
本章只有两个 central 版本和七个 day8 事件,Nested Loop 很自然。现实中:
- 一个 zone + 短时间:先找 zone 再扫事件空间索引可能好;
- 许多 zone + 一个事件:对事件点查围栏索引可能好;
- 巨大 polygon:bbox 候选可能很多;
- 大半径 geography:空间选择率可能很低;
- 多租户:tenant/zone 复合过滤会改变基数。
应从业务参数分布测量,不应固定 join order。
范围排他索引兼任查找路径
geofence_version_no_overlap 原本为约束创建:
联合计划也用它查 zone_id。一个索引可以同时承担约束与查询,但这不保证它
覆盖所有查询。若主要模式是:
应在真实规模验证该复合 GiST 的选择率和代价,再决定是否需要其他索引。
大查询要显式预算
若请求:
时间与空间索引都无法制造高选择率。接口必须限制:
- 最大时间跨度;
- 最大区域/半径;
- zone 数;
- 返回行数与分页;
- statement timeout;
- 并发与资源组;
- 是否异步导出。
索引不是资源治理替代品。
验收逻辑结果与计划结果
先验收结果:
应为:
再验收计划:
最后验收全量 membership:
必须是 14 行,并保留 e003、e005、e006 的双区域命中。只对五行
central 结果做截图不足以证明边界、多归属和版本语义。
本节收束
一条可交付的时空查询结论应包含:
这十项比“用了 PostGIS + 分区”更接近生产合同。
上一节:空间谓词与索引 · 返回本章目录 · 下一节:时空扩展的交付与观察 · 查看全书目录 · 查看索引中心
16.6 时空扩展的交付与观察
本地执行一条 CREATE EXTENSION postgis,只能证明当前实例已有可用控制文件与
动态库。生产交付要回答:
Pigsty 提供扩展供应与数据库声明的实现路径;PostgreSQL/PostGIS 目录仍是最终 验收事实。
16.6.1 安装 PostGIS 与可选时序扩展
四个阶段不能合并
Pigsty 把扩展生命周期概括为:
对应工程问题:
| 阶段 | 验收 |
|---|---|
| 下载/解析 | 目标 Pigsty、OS、PG major 有哪个包版本 |
| 安装 | 每个 L1 节点都有控制文件、SQL 和动态库 |
| 配置 | preload、GUC、重启和资源参数一致 |
| 创建 | 目标数据库 pg_extension 中有正确对象 |
只做 Create,在当前主库可能成功,但切换到缺二进制的副本后函数会失败;只装 包,则数据库里还没有类型、函数和 operator class。
参考 Pigsty 当前 扩展概览、 包别名 与 创建扩展。
package alias 与 SQL extension name 不一定相同
例子:
不要从 SQL 名猜操作系统包名。包还随:
变化。生产 inventory 应同时记录 package alias、解析后的实际包、版本和 SQL extension。
本章 Pigsty 声明
pigsty-declaration.example.yml
是合并片段,不是完整生产配置:
三层含义:
btree_gist 属于 PostgreSQL contrib,通常随主包集合供应;仍要从目标节点的
pg_available_extension_versions 验证,不能只根据经验省掉。
本地实验为何不用 public
本地 PoC 安装到:
并将数据放在:
好处是扩展对象与业务对象边界清楚,reset 可分别验证依赖;代价是操作符、 类型和 opclass 常要显式 schema 限定:
生产可选择 public、app_ext 或其他标准,但要评审:
- extension 是否支持指定/迁移 schema;
- ORM、迁移器和 SQL 是否会限定类型/操作符;
search_path是否包含可被低权限用户写入的 schema;- 备份恢复是否重建同一 namespace;
- 多数据库是否遵循同一约定。
PostGIS 在本章版本中不可 relocatable,创建时 schema 选择更应提前确定。
trusted 与 superuser 边界
目录快照:
| extension | version | trusted | relocatable | 本地 owner |
|---|---|---|---|---|
btree_gist |
1.8 | true | true | pg36_owner |
postgis |
3.6.4 | false | false | 管理员 |
btree_gist 是 trusted extension,满足数据库权限的非超级用户可以安装;
PostGIS 非 trusted,本章由管理员创建。应用角色 pg36_app 永远不获得
CREATE 或扩展 owner 权限,只得到两个 schema 的 USAGE 与受控对象 SELECT。
目录证据来自:
不要把“应用需要调用 PostGIS 函数”误解为“应用要拥有 PostGIS”。
PostGIS 不要求 preload,TimescaleDB 要单独评审
本章 PostGIS 路径不修改 shared_preload_libraries。可选 TimescaleDB 分支
示意:
这段故意没有在基线启用。TimescaleDB 涉及包、preload、重启和数据库对象, 必须走集群变更窗口。以目标 Pigsty release 的 TimescaleDB 扩展页 为准。
声明后回到 SQL 验收
功能探针至少包括:
再执行本章边界、距离、索引计划和排他约束。版本存在不等于业务路径可用。
16.6.2 核对版本、依赖、备份和升级边界
版本是矩阵,不是一个数字
发布证据应保存:
本章正式证据固定:
最后一行很重要:直接 PostgreSQL 验收不能冒充 Pigsty 集群验收。
Pigsty 当前 PostGIS 扩展目录页 用于查看目标 release 的包可用性;版本会演进,不能把本章数字当长期默认。
主备所有 L1 节点必须一致
物理复制会把数据库页和 WAL 变更带到副本,却不会分发操作系统扩展包。 备库执行扩展查询、恢复后开放查询或升主继续服务时,仍依赖本地兼容的控制 文件、动态库及其依赖。
上线前为每个节点保存矩阵:
| host | role | PG | package | control file | shared library | preload |
|---|---|---|---|---|---|---|
| pg-1 | primary | |||||
| pg-2 | replica | |||||
| pg-3 | replica |
任一行不同,应先修供应层。不要等故障切换后才发现新主缺 postgis 动态库。
扩展依赖是数据库对象图
CREATE EXTENSION postgis 注册大量:
它们通过 pg_depend 与 pg_extension 关联。本章 reset 在删除扩展前验证
shop_ch16_ext 的关系、类型、函数、操作符和 opclass 都是合法扩展成员或
扩展表的自动对象。若出现外来对象,停止而不是 DROP ... CASCADE。
这避免两个风险:
- 把用户误建在扩展 schema 的对象一起删除;
- 扩展对象身份漂移后仍声称复位安全。
备份不是只备 geometry 列
恢复要同时具备:
pg_dump 会按扩展成员关系处理对象;恢复环境必须先能供应相容扩展。物理
备份同样要求目标运行环境可加载相应库。
发布前至少做一次隔离恢复:
- 新建与生产隔离的 Pigsty/PG 环境;
- 安装声明版本;
- 恢复角色、schema、扩展和数据;
- 核对
PostGIS_Full_Version(); - 运行 SRID、有效性、边界、距离与空间索引计划;
- 对关键表做逻辑行数和 checksum;
- 演练主备切换后的相同查询。
“备份任务成功”不证明 PostGIS 查询已可恢复。
扩展升级与 PostgreSQL 大版本升级分开设计
可能的变化轴:
一次同时改变所有轴,失败后很难归因。稳健流程:
某些 extension update 不可简单降级。回退可能依赖恢复旧集群/备份或蓝绿
切流,不能默认执行 ALTER EXTENSION 反向版本。
扩展 schema 与 search_path 是安全边界
本章上下文固定:
所有数据对象、类型、函数与操作符显式限定。这样可以避免低权限用户在
search_path 前端 schema 创建同名函数,影响管理员脚本解析。
生产未必需要如此冗长,但管理员自动化应:
- 使用可信固定
search_path; - 显式限定关键对象;
- 禁止 PUBLIC 在扩展/应用 schema CREATE;
- 审计 extension owner;
- 不让应用角色成为 schema owner。
空间函数调用量大,名称解析安全不能被“写起来太长”省掉。
版本断言要分兼容与精确
本章教学实验要求精确 PostGIS 3.6.4,因为计划文本、依赖目录和 checksum 需要可复现。生产策略可以是:
不要在 setup 中悄悄接受“任何 3.x”。也不要把本章精确版本断言误当成 PostGIS 永远只能使用 3.6.4。
16.6.3 观察分区、索引、写入与聚合成本
先建对象清单
本章的固定对象规模:
verify.sql 使用精确白名单和 marker。生产不一定
需要把所有对象硬编码进单个 DO block,但必须有期望状态与漂移检测。
分区覆盖与行分布
每日检查:
监控:
本章固定 1/7/4 只用于回归;生产应关注趋势和异常分布。
父分区大小可能是零
size-catalog.sql 得到:
分区父表不存 heap 行,只查:
可能严重低估整棵分区树。容量查询要遍历 pg_partition_tree/
pg_inherits 汇总叶表与叶索引。
写入成本不是一行 heap
每个事件写入:
本章为了可见性保留完整链;生产应测每一项是否需要。删除一个索引可能降低 写放大,却也改变关键查询。决策来自读写 workload,不来自“空间列都建 GiST”。
观察索引状态与使用
目录状态:
运行统计:
但 idx_scan = 0 不能立刻证明索引无用:
- 统计可能重置;
- 它可能为约束服务;
- 查询可能只在事故/月底运行;
- 小表规划器合理选择 Seq Scan;
- standby 查询不一定反映在 primary 指标。
删除前应结合查询样本、约束职责、时间窗口和回退计划。
空间候选比率
对代表性 query 记录:
候选/命中比很高,说明 bbox 粗筛弱。可能原因:
- 巨大或细长 geometry;
- 查询区域过大;
- 数据高度密集;
- 无效/异常 geometry;
- 不合适的 CRS/opclass;
- 统计估计失真。
不要只盯索引大小。
聚合与迟到更正
监控时间桶:
本章 quarter_hour_volume 是普通 view,每次现算。若改成物化或 continuous
aggregate,还要观察刷新窗口、失效范围、后台 worker、锁、WAL 与旧结果
更正。
PostgreSQL/Pigsty 观测面
常用原生证据:
Pigsty 将其中许多指标接入监控与仪表盘。平台视图适合发现趋势,SQL 与系统 目录适合确认对象和查询事实。告警链接应能回到具体 cluster/database/schema/ partition/index,而不是只有一个“PostGIS 慢”标签。
生产基准矩阵
至少覆盖:
| 维度 | 样本 |
|---|---|
| 时间范围 | 15 分钟、1 日、30 日、全保留 |
| 空间范围 | 小半径、城市区、多边形、超大区域 |
| 数据密度 | 中心区、郊区、极端热点 |
| 状态 | 热缓存、冷缓存、并发写 |
| 事件 | 正常、迟到、批量回补 |
| 计划 | 常量、prepared custom/generic |
| 节点 | primary、read replica、failover 后 |
记录 P50/P95/P99、吞吐、CPU、I/O、WAL、锁、副本延迟和结果 checksum。
观测不能改变语义
若性能不达标,优化顺序应是:
- 结果与时间/空间合同是否正确;
- 参数范围是否合理;
- 分区裁剪是否生效;
- 候选/精确阶段是否存在;
- 类型、SRID、谓词和 opclass 是否匹配;
- 统计是否可信;
- 索引、分区粒度或预计算是否需要调整;
- 是否有引入扩展/分片/异步路径的量化理由。
不要为了让曲线好看,把 ST_Covers 换成 bbox-only 或丢弃迟到事件而不修改
业务合同。
上一节:时空联合查询是本章收束目标 · 返回本章目录 · 下一节:实战:配送事件的时空 PoC · 查看全书目录 · 查看索引中心
16.7 实战:配送事件的时空 PoC
本节把前六节压成一个可审计的 1.4-proposal:
正式证据来自 Homebrew PostgreSQL 18.6 的受控开发数据库。Pigsty 4.5 的声明 与交付职责已经映射,但没有执行 Pigsty L1,因此结果明确标为:
破坏边界
task.sh all会删除并重建带精确 marker 的shop_ch16、shop_ch16_ext,以及后者中的 PostGIS/btree_gist。它只适用于本书 本地/开发 fixture。生产环境不得执行这条删后重建路径。
16.7.1 生成确定性事件与地理数据
前置连接
沿用第 4 章的 libpq service:
不要把密码写进命令行、脚本、Git 或 evidence。
context.sql 要求:
任何一项不符都停止。脚本不会“接受最接近的 PostGIS”后继续生成一套无法与 golden 比较的证据。
资产清单
核心文件:
另有时间、分区、边界、距离、目录、权限与计划探针。
冻结文件身份
这些 SHA-256 识别输入;release candidate checksum 识别发布合同,两者不是 同一个东西。
为什么使用合成坐标
冻结数据像一个简化城市网格,但不是权威地图:
这样做可以:
- 离线运行;
- 不引入地图许可证;
- 人工看懂边界点与围栏扩张;
- 每次得到相同 WKT 和 checksum;
- 隔离数据库机制与外部地理数据质量。
因此本章不能证明地址、道路、行政区或真实 GPS 精度。
单步建立
setup 的碰撞保护先验证已有对象:
通过后,整个重建位于一个事务:
第一次开发执行曾在视图语法处失败,这促使 setup 加入事务边界。如今任何 中途错误都会回滚,不留下会挡住下一次运行的半成品。
扩展创建
PostGIS 由管理员创建,btree_gist extension owner 是业务 owner。两个
extension 和 schema 都带相同精确 marker:
marker 不是安全令牌的替代品;reset 还会核对 target、完整对象清单、依赖、 数据 checksum 和活跃 worker。
时间与空间 schema
数据层:
关键约束:
确定性去重
loader 先统计同一 event_id 的 payload variant:
只有 payload_variants = 1 才进入 registry。canonical 按:
选择。固定:
再由 canonical attempt 生成 Point 并写入父分区表。
建成摘要
这只是 setup 摘要,不是完整验收。
逐字节回读
自动化从数据库执行三条 COPY ... TO STDOUT CSV HEADER,再:
导出显式固定:
- 行顺序;
- UTC RFC3339 风格时间;
- 经纬度五位小数;
- WKT;
- CSV header。
没有 ORDER BY 的数据库导出不具备逐字节比较意义。
16.7.2 验证 SRID 错误、裁剪失效和空间索引
先看时间事实
应为:
任何一个值改变,都意味着 fixture、时区、去重或路由合同漂移。
验证分区目录
固定:
| partition | bound | rows |
|---|---|---|
delivery_event_20260307 |
[03-07,03-08) |
1 |
delivery_event_20260308 |
[03-08,03-09) |
7 |
delivery_event_20260309 |
[03-09,03-10) |
4 |
同时验证每张叶表 owner 与 marker。
对照裁剪正反例
正例:
反例:
两条 SQL 都返回七行。计划对照证明“结果正确”与“裁剪正确”是两项验收。
验证围栏边界与换版
固定:
| scenario | event | zone/version | covers | contains | touches |
|---|---|---|---|---|---|
| at expansion | e005 | central/2 | t | t | f |
| before expansion | e004 | central/1 | f | f | f |
| shared boundary | e003 | central/1 | t | f | t |
| shared boundary | e003 | east/1 | t | f | t |
这四行比一个“地图截图”更容易自动回归。
混合 SRID 必须失败
预期进程退出码 3,stderr 含:
自动化将“正确拒绝”当成功证据。若 SQL 意外返回 false/true,说明坐标身份 保护失效。
重叠有效期必须失败
预期:
失败语句不留下 version 99,随后 verify.sql 仍要求围栏恰好四行。
应用写入必须失败
预期:
应用可读取 central day8 五行,却不能更新事件或绕过生成/分区/去重链。
GiST 与 SP-GiST 路径
固定计划包含:
两条探针临时关闭 Seq Scan;它们只证明路径可用。
时空联合计划
必须同时出现:
且不出现 20260307/20260309 事件分区。
索引目录不能只看名字
13 行全部要求:
特别是:
完整 SQL 断言
verify.sql 检查:
- 两个 schema 的 owner/marker;
- 34 个关系对象精确白名单;
- extension schema 中没有未管理成员;
- 两项扩展版本、owner、trusted/relocatable 边界;
- fixture 身份与 13/12/4/3 基数;
- 去重 canonical;
- 1/7/4 路由与两个边界事件;
- DST 600 秒、迟到和乱序;
[)、非重叠、有效 geometry;- 14 条 membership 和四个边界事实;
- generated geography;
- 13 个索引状态/opclass;
- 应用权限;
- 业务校验和。
成功:
16.7.3 输出 ADR、PoC 证据与生产代价清单
一键双周期验收
all 执行:
第二周期不是重复表演。它证明 reset 后:
- 数据 schema 消失;
- 扩展 schema 消失;
- PostGIS/
btree_gist被精确移除; - 第 14 章
pg_trgm/vector保留; - 同一输入能重建同一业务 checksum;
- 计划、权限和失败边界仍成立。
evidence 结构
manifest 身份
每周期 manifest 保存:
动态采集时间不进入业务 checksum。它说明“证据何时采集”,不改变数据真值。
最终状态
final-state.sql 固定:
checksum 覆盖:
它不包含执行计划、物理 OID、索引页或采集时间,因此 reset/rebuild 后仍应 相同。
自动审校
review.py 不连接数据库,只审查 evidence 与冻结
source:
- 三份导出字节相同且 hash/行数匹配 manifest;
- DST、迟到、乱序与路由事实精确;
- 分区、桶与 membership 守恒;
- 边界、距离结果精确;
- 扩展、索引、权限、体积目录满足合同;
- 直接/包裹时间计划形成反例;
- GiST/SP-GiST/联合计划包含目标路径;
- 三个失败的退出码与 SQLSTATE 正确;
- final state 与完整 verify 通过;
- baseline JSON 与业务 checksum 一致。
这样可以把“数据库输出的确生成了”与“输出符合我们预先定义的结论”分开。
reset 三道动作护栏
精确复位要求:
错误 token:
错误 target:
活跃 pg36-ch16-* worker:
通过动作护栏后,reset 在事务内先 \ir verify.sql。也就是说,只有完整状态
仍与本章合同相同时才开始 DROP。
为什么不用 CASCADE
删除顺序:
全部使用 RESTRICT 默认语义。若外部对象意外依赖本章扩展,DROP 会失败,事务 整体回滚,数据和扩展不会处于半删状态。
ADR 的核心决策
同时列出否决方案、代价与重开条件。ADR 的价值不是替 SQL 写说明,而是保留 “为什么这样选”和“什么新证据会让我们重选”。
生产代价清单
本 PoC 未证明:
发布前应将这些项目变成有负责人、环境、阈值、证据路径和停止线的验收计划。
最终正式输出
两轮 Homebrew PostgreSQL 18.6 结果:
这个 checksum 对应
baseline-v1.4-proposal.json
的规范化 JSON。修改合同后必须生成新 proposal checksum,不能继续引用旧
结果。
16.7.4 超预算时先删扩展专属细节,不删基础判断力
本章的教学最小闭环
如果书稿、课程或项目时间不足,最小闭环仍必须保留:
删掉其中任一组,读者很可能只记住命令,不会形成判断力。
第一优先可删:扩展参数百科
可压缩:
- TimescaleDB 某一版本的全部 GUC;
- 所有 PostGIS 子扩展列表;
- 每个索引 opclass 的完整矩阵;
- 某发行版每个包文件名;
- 罕见 geometry 类型函数目录。
它们变化快,也可从目标版本官方文档查到。正文应保留如何核对,而不是复制 百科。
第二优先可删:未实测的高级方案
本章没有假装实现:
- 路网最短路;
- 地图匹配;
- 轨迹压缩;
- 3D/4D 几何;
- raster;
- 全球多投影治理;
- 双时态修订系统;
- continuous aggregate 基准。
这些可以成为后续项目,但不应挤掉本章已经可复现的基础闭环。
不能把 Pigsty 映射删成一句话
即使篇幅少,也至少保留:
否则读者会把本地 CREATE EXTENSION 当成生产交付。
不能把反例全删掉
本章四个关键反例:
- 夏令时墙上差 70 分钟,实际 600 秒;
- 包裹分区键后逻辑结果相同,但三分区全扫;
- 边界点
covers=true、contains=false; - 混合 SRID 必须失败。
成功路径告诉读者“怎么写”;反例让读者知道“为什么这样写”。若只留成功 截图,认知无法迁移到新业务。
生产预算不足时的正确停止线
如果没有预算完成:
结论应停在:
不能因为 PoC 代码整洁就降低生产验收标准。
迁移练习
读者可复制 fixture 为 ch16-spatiotemporal-v2,任选一项扩展:
- 改用一个真实但许可明确的公开边界数据集;
- 增加 GPS accuracy 与 uncertain membership;
- 实现围栏修订的 system time;
- 对 native partition 与 TimescaleDB 做同输入 A/B;
- 加入真实数量级并比较 GiST/SP-GiST;
- 为当地业务日生成可裁剪 UTC 边界;
- 增加唯一归属消歧规则。
必须:
- 新建 manifest/version;
- 保留旧 fixture;
- 写明新许可证与生成方法;
- 更新 expected facts/checksum;
- 加入至少一个新反例;
- 重新做 reset/rebuild 两周期;
- 不沿用本章 release checksum。
做到这一步,读者不只是会调用 PostGIS,而是能把时空需求变成可验证的 PostgreSQL/Pigsty 工程合同。
上一节:时空扩展的交付与观察 · 返回本章目录 · 下一章:合纵连横:分析加速与分布式选型 · 查看全书目录 · 查看索引中心