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 两行。聚合后的总量与原始事实不守恒时,应先定位过滤、边界或
重复处理,而不是调整索引。
本节判断线
进入下一节前,至少能完整回答:
回答不完整时,不应先争论日分区还是小时分区,也不应先安装时序扩展。
返回本章目录 · 下一节:时序表与时间分区 · 查看全书目录 · 查看索引中心