财务问“这个季度收入为什么是这个数”时,工程师往往要翻 ETL 脚本、追视图依赖、核对调度任务,再猜测两年前留下的处理逻辑。PostgreSQL 19 引入的 SQL/PGQ 图查询能力提供了另一种思路:把数据血缘作为数据库中的可查询数据管理,用一条图模式查询,从报表行一路追溯到原始事件。
数据血缘不只是表依赖
数据血缘通常需要回答四类问题:
- 来源追溯:某个值来自哪些原始记录?
- 依赖关系:哪些下游对象依赖当前数据?
- 转换历史:数据经历了加载、过滤、聚合还是汇总?
- 影响分析:修改上游记录或处理逻辑后,哪些报表会变化?
Airflow、dbt、Dagster 等工具能够记录任务或模型之间的依赖,但它们通常只能看到自己管理的工作流。临时 SQL、遗留脚本以及任务之外的手工处理可能不在依赖图中。Wiki 和电子表格的问题更直接:它们需要人工同步,往往很快过期。
更可靠的做法是把血缘关系存进表中。业务表保存数据,边表保存“哪条记录生成了哪条记录”。SQL/PGQ 再把这些关系声明为属性图,使工程师可以用图模式表达追溯路径,同时继续得到普通 SQL 行集。
用边表记录行级流向
假设电商分析管道包含四层:
raw_clickstream
-> stg_events_raw
-> fact_sales
-> report_monthly
原始点击事件经过清洗,购买事件按商品和日期聚合,最后按月份和品类生成财务报表。下面是一个可以改造的最小模式;需要先换成实际业务中的表名、主键和金额字段。
CREATE TABLE raw_clickstream (
event_id bigint PRIMARY KEY,
product_id int NOT NULL,
action text NOT NULL,
amount numeric(10, 2),
ts timestamptz NOT NULL
);
CREATE TABLE stg_events_raw (
event_id bigint PRIMARY KEY,
product_id int NOT NULL,
action text NOT NULL,
amount numeric(10, 2),
loaded_at timestamptz NOT NULL DEFAULT now()
);
CREATE TABLE fact_sales (
product_id int NOT NULL,
sale_date date NOT NULL,
revenue numeric(12, 2) NOT NULL,
PRIMARY KEY (product_id, sale_date)
);
CREATE TABLE report_monthly (
report_month date NOT NULL,
category text NOT NULL,
total_revenue numeric(14, 2) NOT NULL,
PRIMARY KEY (report_month, category)
);
CREATE TABLE edge_loads_into (
src_event_id bigint NOT NULL REFERENCES raw_clickstream(event_id),
dst_event_id bigint NOT NULL REFERENCES stg_events_raw(event_id),
PRIMARY KEY (src_event_id, dst_event_id)
);
CREATE TABLE edge_aggregates_into (
src_event_id bigint NOT NULL REFERENCES stg_events_raw(event_id),
dst_product int NOT NULL,
dst_date date NOT NULL,
FOREIGN KEY (dst_product, dst_date)
REFERENCES fact_sales(product_id, sale_date),
PRIMARY KEY (src_event_id, dst_product, dst_date)
);
CREATE TABLE edge_rollup_into (
src_product int NOT NULL,
src_date date NOT NULL,
dst_month date NOT NULL,
dst_category text NOT NULL,
FOREIGN KEY (src_product, src_date)
REFERENCES fact_sales(product_id, sale_date),
FOREIGN KEY (dst_month, dst_category)
REFERENCES report_monthly(report_month, category),
PRIMARY KEY (src_product, src_date, dst_month, dst_category)
);
这些边表不是额外的业务事实,而是转换过程留下的证据。例如,一次聚合作业处理事件 1001 时,应在写入或更新 fact_sales 的同时,向 edge_aggregates_into 写入事件与目标事实行的对应关系。
边表会增加存储量和写入成本。对于点击流等高吞吐数据,可以只保留受审计指标的行级血缘,其他数据记录批次级血缘,例如用 load_id 或 run_id 连接一次任务的输入与输出。
把关系声明成属性图
下面的语法以 PostgreSQL 19 的 SQL/PGQ 能力为前提。实际试验前,需要确认所使用的 PostgreSQL 19 构建版本已经包含相应语法,客户端和迁移工具也能正确处理属性图对象。
CREATE PROPERTY GRAPH lineage
VERTEX TABLES (
raw_clickstream
KEY (event_id)
LABEL clickstream_source
PROPERTIES (event_id, product_id, action, amount, ts),
stg_events_raw
KEY (event_id)
LABEL staging
PROPERTIES (event_id, product_id, action, amount, loaded_at),
fact_sales
KEY (product_id, sale_date)
LABEL fact
PROPERTIES (product_id, sale_date, revenue),
report_monthly
KEY (report_month, category)
LABEL report
PROPERTIES (report_month, category, total_revenue)
)
EDGE TABLES (
edge_loads_into
SOURCE KEY (src_event_id) REFERENCES raw_clickstream (event_id)
DESTINATION KEY (dst_event_id) REFERENCES stg_events_raw (event_id)
LABEL loads_into,
edge_aggregates_into
SOURCE KEY (src_event_id) REFERENCES stg_events_raw (event_id)
DESTINATION KEY (dst_product, dst_date)
REFERENCES fact_sales (product_id, sale_date)
LABEL aggregates_into,
edge_rollup_into
SOURCE KEY (src_product, src_date)
REFERENCES fact_sales (product_id, sale_date)
DESTINATION KEY (dst_month, dst_category)
REFERENCES report_monthly (report_month, category)
LABEL rollup_into
);
属性图不会复制业务数据。顶点仍然来自原表,边仍然来自血缘表;图对象负责声明哪些表构成顶点、哪些表构成边,以及连接端点所使用的键。
标签也比单纯的外键更有表达力。loads_into 表示直接加载,aggregates_into 表示聚合,rollup_into 表示进一步汇总。审计人员看到的是转换语义,而不只是“两个字段之间存在引用”。
一条查询解释报表金额
当财务需要解释某个月某个品类的收入时,可以从报表顶点反向匹配到原始点击事件:
SELECT *
FROM GRAPH_TABLE (
lineage
MATCH
(r IS report)
<-[IS rollup_into]-(f IS fact)
<-[IS aggregates_into]-(s IS staging)
<-[IS loads_into]-(c IS clickstream_source)
WHERE r.report_month = DATE '2024-01-01'
AND r.category = 'electronics'
COLUMNS (
r.report_month,
r.category,
r.total_revenue,
f.product_id,
f.sale_date,
f.revenue,
s.event_id AS staging_event_id,
s.action,
s.amount,
c.event_id AS source_event_id,
c.ts AS source_ts
)
)
ORDER BY source_ts;
图模式几乎就是问题本身:从 report 经 rollup_into 到 fact,再经聚合和加载边回到源数据。GRAPH_TABLE 返回普通关系结果,因此还可以把它连接到数据质量告警、dbt 运行记录或 CDC 日志中。
正向查询则适合变更影响分析。例如,在修改原始加载器之前,可以检查指定事件最终进入了哪些报表行:
SELECT DISTINCT report_month, category
FROM GRAPH_TABLE (
lineage
MATCH
(c IS clickstream_source)
-[IS loads_into]->(s IS staging)
-[IS aggregates_into]->(f IS fact)
-[IS rollup_into]->(r IS report)
WHERE c.event_id IN (1001, 1002, 1003)
COLUMNS (
c.event_id AS source_event_id,
r.report_month,
r.category
)
)
ORDER BY report_month, category;
这类查询回答的是“哪些具体结果会受影响”,而不仅是“哪张表依赖哪张表”。后者属于表级血缘,前者才是定位财务差异时真正有用的行级血缘。
落地时要守住的边界
SQL/PGQ 并不会自动理解 ETL。属性图只能查询已经记录的关系,因此最重要的工程工作仍然是让流水线稳定地产生边数据。可以这样实践:
- 从一条受审计的收入或风险报表开始,不要一次覆盖全部数据平台。
- 在同一事务或同一任务提交阶段写业务结果和血缘边,避免两者状态分离。
- 给每次运行附加
run_id、代码版本和转换时间,补足“由哪次任务生成”的证据。 - 将
CREATE PROPERTY GRAPH放进数据库迁移,并要求流水线变更同步更新图声明。 - 增加完整性检查,例如验证每一条财务报表记录至少存在一条可回溯路径。
- 对个人信息和敏感财务数据设置权限;血缘图本身也可能暴露数据来源、客户标识和内部系统结构。
按照来源所述,PostgreSQL 19 当前的 GRAPH_TABLE 尚不支持可变长度路径和路径聚合。固定三四层的管道可以直接写图模式;面对五层以上且深度不固定的血缘链,仍需在底层边表上使用 WITH RECURSIVE,或者由应用生成固定长度查询。
属性图不是过期文档的自动替代品,它的价值来自更强的约束:转换代码、边数据和图声明必须一起演进。一旦做到这一点,“这个数字从哪里来”就不再是一场代码考古,而是一条可以执行、测试和交付给审计人员的 SQL 查询。