引言
图大多在「某一时刻」的视图下建模,但现实数据带着时间:一个人换了公司、一笔钱流经多个账户、一段关系有起止。时态图(Temporal Graph)就是把时间维度显式建模进图,让「当时的图长什么样」可以被查询。本文讲时态图建模:先厘清两种时间的本质差异(valid time 与 transaction time),再讲关系/节点上时间属性的建模策略,然后是 bitemporal 建模(同时维护两种时间)、事件时序图(边即事件)、历史状态查询(point-in-time 回溯)、图版本与快照机制、时态路径与时序分析(资金链/传播链的时序路径)、最后是 Neo4j 等图库的时间建模实践与性能取舍。目标:你能设计「带时间」的图模型,并回答「某个时刻的图是什么样」。
前置:/graphdb-data-model-basics/(属性图模型)、/graphdb-modeling-patterns/(建模模式,含时序图)、/graphdb-graph-import-etl/(数据导入与增量)。
目录
- 1. 两种时间:valid time 与 transaction time
- 2. 时间属性的建模位置:节点、关系还是事件
- 3. 关系的生命周期建模:起止时间
- 4. bitemporal 建模:同时维护两种时间
- 5. 事件时序图:边即事件
- 6. 历史状态查询:point-in-time 回溯
- 7. 图版本与快照机制
- 8. 时态路径与时序分析
- 9. 时间索引与性能取舍
- 10. 速查表
- 延伸阅读
1. 两种时间:valid time 与 transaction time
时间维度有两种,容易混淆:
Valid time(有效时间):
事实「在现实世界」成立的时间区间
例:张三「2020-2024」在某公司任职
→ 现实语义:我什么时候在这里上班
Transaction time(事务时间):
事实「被记录进数据库」的时间
例:这条任职记录「2024-06-01 10:00」写入
→ 系统语义:我什么时候知道了这件事
两者的差异:
- valid time:现实时间(可未来、可追溯改)
- transaction time:数据库时间(不可改,追加式)
- 例:2025 年才补录「2020 年任职」
→ valid = 2020-2024(现实)
→ transaction = 2025(记录时刻)
→ 同一事实有两个时间轴
只用一种时间的问题:
- 只记 valid:无法回答「当时系统里记录的是什么」
(审计:当时数据库显示什么?)
- 只记 transaction:无法回答「现实什么时候成立」
(业务:这段关系实际生效期?)
→ 审计类场景需要 transaction,业务需要 valid
选择策略:
- 大多数业务:valid time 足够(现实生效期)
- 审计/合规/可追溯:需要 transaction time
- 两者都要 → bitemporal(第 4 节)
→ 先明确「你要回答哪类时间问题」
心智:valid time = 现实生效时间(可改),transaction time = 记录入库时间(不可改追加)——审计要 transaction、业务要 valid,两者都要用 bitemporal;先明确「你要回答哪类时间问题」。
2. 时间属性的建模位置:节点、关系还是事件
时间可以挂在三个位置,含义不同:
节点上的时间属性:
(n:Employee {since: 2020, until: 2024})
→ 表示「该实体的状态/存在期」
问题:实体改状态 → 覆盖还是新增节点?
关系上的时间属性:
(a)-[r:WORKS_AT {since: 2020, until: 2024}]->(b)
→ 表示「关系/事实的有效期」
最常用(关系本身就是事实)
独立事件节点:
(:Event {type:'TRANSFER', at: 2024-06-01})
→ 时间作为「事件本身」而非属性
适合时序分析(事件序列)
选择的依据:
- 时间描述「实体的属性值」→ 节点属性
- 时间描述「实体间的关系」→ 关系属性
- 时间描述「一次发生的事件」→ 事件节点
→ 看「时间属于谁」,就挂在哪
实体「变了」怎么办:
例:员工的部门从 A 变 B
方案 1:改节点属性(丢失历史)
方案 2:新增关系 + 有效期(保留历史)
方案 3:节点版本化(同实体多版本节点)
→ 保历史 → 用「关系版本/节点版本」
推荐模式:
- 变化的属性 → 拆成关系(含 since/until)
- 稳定属性 → 留在节点
- 事件 → 独立事件节点(带时间戳 + 参与实体)
→ 时间建模 = 「把变化变成带时间的边」
心智:时间挂节点(实体状态期)、挂关系(事实有效期,最常用)、挂事件节点(发生事件);「变化的属性拆成带时间的边、稳定的留节点、事件独立成节点」是核心模式——时间属于谁就挂在哪。
3. 关系的生命周期建模:起止时间
关系带 since/until = 生命周期建模:
(a:Person {name:'张三'})
-[:WORKS_AT {since: date('2020-01-01'), until: date('2024-12-31')}]
->(b:Company {name:'X公司'})
// 查询「2022 年他在哪家公司」
MATCH (p:Person {name:'张三'})-[r:WORKS_AT]->
(c:Company)
WHERE r.since <= date('2022-06-01') <= r.until
RETURN c.name
建模要点:
- since/until 用 date 类型(可比较、可索引)
- until = null → 表示「仍有效」(开放区间)
- 区间边界约定:闭区间 vs 半开区间(一致约定)
- 时间粒度:天/月/秒(一致)
→ 区间建模 = 类型 + 边界约定 + 开放区间
区间查询的模式:
- 点在区间:WHERE since <= t <= until(第 6 节回溯)
- 区间重叠:两个关系区间相交(重叠检测)
AND a.since <= b.until AND b.since <= a.until
- 区间包含:一个区间在另一个内
→ 区间查询有固定模式,建对索引就快
关系的「变化」与版本:
- 关系结束:新关系(新区间)而非删除
- 删除语义:软删除(until = 结束时间)
- 多个连续关系:同类型时间相邻(历史链)
- 查询「当时有效的关系」→ 按时间过滤
→ 保留历史 = 不删除,只标区间
生命周期建模的陷阱:
- 忘了 until 更新(区间重叠)
- null 语义不统一(仍在职 vs 未知)
- 时间粒度不一致(2020 vs 2020-01-01)
- 索引缺失 → 区间查询全扫
→ 约定先行 + 索引配合
心智:关系生命周期 = since/until 区间属性(date 类型、until=null 开放区间、边界约定)——历史靠「软删除 + 新区间」而非删除;区间查询(点在区间/重叠/包含)有固定模式,建对时间索引才快。
4. bitemporal 建模:同时维护两种时间
bitemporal = valid + transaction 两个时间轴:
每条事实同时带两组时间:
valid since/valid until (现实生效期)
tx since/tx until (记录生效期)
例:2025-06 补录「2020-2024 任职」
valid: 2020-01-01 ~ 2024-12-31(现实)
tx: 2025-06-01 ~ null (何时被记录)
→ 一条事实 = 二维时间区间(valid × tx)
bitemporal 的查询:
- 现实视角:按 valid 查「当时实际如何」
- 审计视角:按 tx 查「当时系统记录了啥」
- 全历史:valid × tx 的组合(按两者过滤)
- 修正记录:更正不删原记录,而是「结束旧 tx 段 + 新 tx 段」
→ bitemporal 能回答「现实与记录的所有组合」
bitemporal 的建模:
关系属性上放 4 个时间字段:
v_since, v_until, t_since, t_until
或分离:把「版本的版本」建模为多层
(Fact)-[:VERSION {tx...}]->(:Version {valid...})
→ 简单场景:4 字段属性
→ 复杂审计:分层版本节点
为什么需要 bitemporal:
- 合规:数据变更可追溯(谁、何时、改成什么)
- 纠错:更正与原始都保留
- 审计报告:任何时刻「系统看到什么」
- 数据回放:重建任意时刻的系统状态
→ bitemporal = 「时间维度上的不可篡改」
bitemporal 的代价:
- 建模复杂(4 字段、版本逻辑)
- 查询复杂(二维时间过滤)
- 存储增长(保留所有版本)
- 写入逻辑(结束旧段 + 开新段)
→ 只有「审计必需」才上 bitemporal
心智:bitemporal 同时维护 valid(现实生效)与 tx(记录生效)两个时间轴——能回答现实与记录的所有组合、支持纠错不删原始、审计可追溯;代价是 4 字段 + 版本逻辑 + 存储增长,审计必需才上。
5. 事件时序图:边即事件
当「发生」本身就是主体 → 事件时序图:
模型:
(:Account)-[:TRANSFER {at, amount}]->(:Account)
或独立事件节点:
(:Account)-[:SOURCE]->(:Transfer {at, amount})-[:TARGET]->(:Account)
特点:
- 每条边/事件带时间戳(at)
- 时间戳即「发生时刻」(不是区间)
- 可排序:沿时间轴的边序列
→ 事件图 = 「时间戳边」的集合
事件图的典型场景:
- 资金流转:转账记录(时序资金链)
- 用户行为:点击/浏览/下单(行为序列)
- 供应链:物料流动(批次时序)
- 网络:连接/请求日志
→ 事件图回答「按时间发生了哪些事」
建模选择:边属性 vs 事件节点:
边属性(TRANSFER {at, amount}):
- 简单,直接「从一个节点到另一个」
- 无法给事件自身挂更多信息/多个参与方
事件节点(:Transfer + SOURCE/TARGET):
- 事件可挂任意属性/关系(参与方、关联事件)
- 更适合「事件为中心」的分析
→ 事件节点更灵活,边属性更简单
时序查询:
// 2024 年的转账序列
MATCH (a)-[t:TRANSFER {at, amount}]->(b)
WHERE date(t.at) >= date('2024-01-01')
AND date(t.at) < date('2025-01-01')
RETURN a, t, b ORDER BY t.at
→ 时间过滤 + 排序 = 事件序列
心智:事件时序图 = 时间戳边/事件节点的集合(转账/行为/物流/日志),事件节点更灵活(可挂多参与方与属性)、边属性更简单;时序查询 = 时间过滤 + 排序,回答「按时间发生了哪些事」。
6. 历史状态查询:point-in-time 回溯
核心能力:给定一个时刻,图是什么样:
问题:「2022-06-01 时,张三的关系网络是什么样的?」
解法:
对每条关系/节点,判断它在那个时刻是否「有效」:
WHERE r.since <= $t AND (r.until IS NULL OR $t <= r.until)
→ 把所有「当时有效」的边过滤出来 = 当时的图
point-in-time 查询的实现:
// 快照查询:2022-06-01 的所有任职
MATCH (p:Person)-[r:WORKS_AT]->(c:Company)
WHERE r.since <= date('2022-06-01')
AND (r.until IS NULL OR date('2022-06-01') <= r.until)
RETURN p, r, c
回溯的变体:
- 单时刻回溯:WHERE 时间过滤(如上)
- 时间区间查询:当时所有有效关系(区间重叠)
- 历史演变:把多个时刻的结果对比(演变分析)
- 「最近一次变更」:按 tx 时间排序取最新
→ 回溯是「区间过滤」,演变是「多次回溯对比」
演变分析:
- 对比 t1 与 t2 的图:新增/消失/保持的关系
- 实体轨迹:一个人随时间的位置/角色变化
- 指标演变:网络密度、社区划分随时间的漂移
→ 历史状态是「静态切片」,演变是「切片序列」
性能关键:
- 时间过滤要在索引上做(时间戳索引)
- 区间查询的索引设计(第 9 节)
- 频繁回溯 → 快照缓存(第 7 节)
→ 回溯能力 = 建模 + 索引 + 缓存
心智:point-in-time 回溯 = 对所有带区间属性判断「当时是否有效」(since ≤ t ≤ until),把有效边过滤出来即「当时的图」;演变 = 多次回溯对比(新增/消失/保持);性能靠时间索引 + 快照缓存。
7. 图版本与快照机制
频繁回溯太慢 → 预计算快照:
快照 = 某个时刻图的「固定副本」(物化)
- 定期快照:每天/每小时存一份当时的图
- 查询快照:WHERE 版本 = 某天 → 直接读
- 对比快照:diff 两张快照 → 演变
→ 用「空间换时间」,避免每次回溯现算
快照的建模:
方案 1:全图版本(大快照)
- 每个版本存整图(成本高,简单)
方案 2:增量版本(delta)
- 基快照 + 变更日志(低存储,重建耗时)
方案 3:版本属性(就地)
- 节点/关系带 valid/tx 区间,回溯时过滤
→ 这不是「快照」而是「区间即版本」
→ 物化快照 vs 区间过滤是「空间 vs 时间」权衡
快照的适用:
- 高频回溯同一时刻(如每日报表)
- 图较小(全量快照可行)
- 需要「不可变版本」审计(合规)
→ 快照适合「查询模式固定、重复回溯」
版本管理实践:
- 快照命名:日期/版本号
- 快照生命周期:保留最近 N 个 + 归档
- 快照一致性:同一时刻的所有节点/边一致
- 快照验证:与实时图抽样对账
→ 快照也要「像备份一样管理」
区间建模 vs 快照的选择:
- 单事实可改:区间建模(每事实带时间)
- 整图状态需要:快照(物化时刻)
- 混合:区间建模 + 定期物化快照加速
→ 建模优先区间,快照做加速/审计层
心智:快照 = 预物化的图状态副本(定期/增量/版本属性三方案),用空间换时间、适合高频回溯同一时刻;区间建模是「事实级时间」,快照是「图级时刻」——建模优先区间,快照做加速与审计层。
8. 时态路径与时序分析
时态路径:路径上的边有时间约束:
普通路径:任意连接的节点序列
时态路径:路径上边的时间「连贯」
例:资金 A→B→C→D 的转账链
要求:每条转账的时间递增(钱的流动时序)
若 B→C 发生在 A→B 之前 → 不构成时序资金链
→ 时态路径 = 「时间上可连通的路径」
时态路径的查询:
// 时序资金链:每一步时间递增
MATCH p = (a)-[t1:TRANSFER]->(b)-[t2:TRANSFER]->(c)-[t3:TRANSFER]->(d)
WHERE t1.at <= t2.at <= t3.at
RETURN p
- 长度受限:变长时序路径(启发式遍历)
- 时间窗口:只在窗口内的边参与
- 严格递增 vs 非递减:约定
→ 时序约束 = 路径查询里加「时间单调」条件
时序分析的类型:
- 时序路径:资金链/传播链/物流链(时间连通)
- 时序社区:随时间演化的群体(见社区发现动态篇)
- 时序中心性:随时间变化的节点重要性
- 延迟分析:事件间的时间间隔(路径时延)
→ 时序 = 「时间」作为路径的第四维
实现策略:
- 显式约束:查询里比较边时间(简单、深度有限)
- 图算法扩展:时序版 BFS/Dijkstra(时间感知遍历)
- 事件图 + 时间索引:按时间推进遍历
→ 深度浅用查询约束,深度深用专用遍历器
业务价值:
- 反欺诈:资金流转的时序链(洗钱路径)
- 传播:信息/传染的时间传播路径
- 物流:物料流动的时序优化
→ 时态路径回答「怎么在时间上连起来」
心智:时态路径 = 边时间「时序连贯」的路径(资金/传播/物流链)——查询里加时间单调条件,深度浅够用、深度深用时间感知遍历器;时序分析(路径/社区/中心性/延迟)把时间作为路径的第四维,反欺诈资金链是典型场景。
9. 时间索引与性能取舍
时间查询的性能关键 = 时间索引:
- 单时间戳(at):普通索引(btree 范围查询)
CREATE INDEX FOR ()-[r:TRANSFER]-() ON (r.at)
- 区间(since/until):复合索引 + 区间过滤
或「区间树/有效时间索引」
- 时间+实体:复合(节点 + 时间)
→ 时间查询要能「用索引」而不是全扫
时间索引的类型:
- B-tree(btree):标准范围查询(Neo4j 原生)
- 全文/字符串时间:不推荐(要 date 类型)
- 区间索引:since/until 的范围剪枝
- 时间桶:按天/月分桶(聚合预计算)
→ 类型/桶化 + 时间索引 = 时间查询加速
查询的索引利用:
- 时间过滤「下推」:先按时间缩小范围再遍历
- EXPLAIN/PROFILE 确认走了索引
- 复合索引顺序:等值列在前、时间列在后
- 避免函数包裹:date(t.at) 可能不走索引
→ 用参数化 date('...') 比较而非转换
→ 时间查询也要「先过滤再遍历」
存储与时间的权衡:
- 区间建模 → 属性多、存储涨
- 快照 → 空间翻倍(全量)
- 事件图 → 边多(每条事件一条边)
- 压缩/归档:冷数据归档到时间分区
→ 时间维度是有成本的,按热度分级存储
性能预算:
- 常用回溯时刻 → 快照加速
- 实时时间查询 → 索引 + 小范围
- 全时分析 → 离线批处理(大数据栈)
→ 按「查询模式」分配性能预算
心智:时间查询性能靠时间索引(btree 范围、复合 节点+时间、区间剪枝)+ 时间过滤下推 + 避免函数包裹;时间维度有存储成本(区间属性/快照/事件边),按热度分级(快照加速常用时刻、索引保实时、离线批处理全时分析)。
10. 速查表
全篇速查:
| 主题 | 结论 |
|---|---|
| 两种时间 | valid(现实)vs tx(记录) |
| 时间挂哪 | 节点状态 / 关系事实 / 事件节点 |
| 生命周期 | since/until 区间 + 软删除保历史 |
| bitemporal | valid×tx 双轴,审计必需 |
| 事件图 | 时间戳边/事件节点,时序序列 |
| 回溯 | 区间过滤 = 当时的图 |
| 快照 | 物化时刻,空间换时间 |
| 时态路径 | 边时间连贯,时序连通 |
| 索引 | 时间 btree + 过滤下推 |
| 取舍 | 区间建模优先,快照加速 |
一句话记忆:时态图把时间显式建进图——valid time(现实生效,可改)与 transaction time(记录入库,不可改)是两种本质不同的时间轴,审计要 tx、业务要 valid、两者都要用 bitemporal(valid×tx 双轴 4 字段,纠错不删原始、可完全追溯);时间挂哪看「时间属于谁」:实体状态挂节点、关系事实挂关系(since/until 区间 + until=null 开放区间 + 软删除保历史)、发生的事件独立成事件节点(时间戳边序列);point-in-time 回溯 = 区间过滤(since ≤ t ≤ until)得到「当时的图」,演变 = 多次回溯对比;高频回溯用快照(物化时刻/增量/版本属性三方案)空间换时间;时态路径 = 边时间时序连贯的路径(资金链/传播链),查询加时间单调条件、深度深用时间感知遍历;性能靠时间索引(btree/复合/区间剪枝)+ 过滤下推 + 避免函数包裹,时间维度有存储成本按热度分级(快照加速常用时刻、索引保实时、离线批处理全时分析)。
延伸阅读
- /graphdb-data-model-basics/ — 属性图模型基础
- /graphdb-modeling-patterns/ — 建模模式与反模式(含时序图)
- /graphdb-graph-import-etl/ — 数据导入与增量更新
- /graphdb-graph-query-optimization/ — 时间过滤与查询优化
- /graphdb-fraud-detection/ — 时序资金链与风控
- 数据工程专题 — 时序数据与事件流处理
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。