用 PostgreSQL 19 SQL/PGQ 追踪数据血缘:让每个报表数字都有来路

2026-07-28 15 预计阅读时间: 1 分钟
来源: postgr.es AI 摘要 Original link

Disclaimer: This article is an AI-assisted summary. Read it together with the original source when precision matters. The summary may omit context, version differences, or edge cases and is not official documentation.

预计阅读时间:10 分钟

财务问“这个季度收入为什么是这个数”时,工程师往往要翻 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_idrun_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;

图模式几乎就是问题本身:从 reportrollup_intofact,再经聚合和加载边回到源数据。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。属性图只能查询已经记录的关系,因此最重要的工程工作仍然是让流水线稳定地产生边数据。可以这样实践:

  1. 从一条受审计的收入或风险报表开始,不要一次覆盖全部数据平台。
  2. 在同一事务或同一任务提交阶段写业务结果和血缘边,避免两者状态分离。
  3. 给每次运行附加 run_id、代码版本和转换时间,补足“由哪次任务生成”的证据。
  4. CREATE PROPERTY GRAPH 放进数据库迁移,并要求流水线变更同步更新图声明。
  5. 增加完整性检查,例如验证每一条财务报表记录至少存在一条可回溯路径。
  6. 对个人信息和敏感财务数据设置权限;血缘图本身也可能暴露数据来源、客户标识和内部系统结构。

按照来源所述,PostgreSQL 19 当前的 GRAPH_TABLE 尚不支持可变长度路径和路径聚合。固定三四层的管道可以直接写图模式;面对五层以上且深度不固定的血缘链,仍需在底层边表上使用 WITH RECURSIVE,或者由应用生成固定长度查询。

属性图不是过期文档的自动替代品,它的价值来自更强的约束:转换代码、边数据和图声明必须一起演进。一旦做到这一点,“这个数字从哪里来”就不再是一场代码考古,而是一条可以执行、测试和交付给审计人员的 SQL 查询。


相关推荐