维度表最常见的丢数方式,是用最新快照覆盖同一业务键。报表若只看今天,这样刚好。审计若要问上周三的值,覆盖之后就没有那一行了。Spark 4.3 把第二型历史放进自动变更处理,问题单是 SPARK-56249。同一键保留多行,每行记录这一版从何时起、到何时止。Python、SQL 和 Spark Connect 都能声明。声明不会自动保住历史,写入靠微批上的合并。
四列各自负责什么
| 列 | 写入时 | 查询时 |
|---|---|---|
| 业务键 | 把同一实体的多行归组 | 多行不是多个实体 |
| 顺序列 | 同一批里决定先后 | 不能用到达时间代替 |
| __START_AT | 新版本开始 | 作为下界 |
| __END_AT | 旧版本结束 | 开着表示当前 |
业务键把同一实体的多行归到一起。顺序列决定同一批次里哪一次变更更晚,后来的有效变更应盖过更早的。__START_AT 是这一版开始有效的位置,__END_AT 是失效位置。当前仍有效的行,终点通常是开着的。查询某一时刻的值时,要找起点不晚于该时刻、终点晚于该时刻的那一行。直接对一个键的全部历史行求和,会把旧版本再算进去。
四列的名字定下来之后就不要在下游另起一套。有人用文件名里的日期当版本,有人用入库时间当终点。两边对不上时,审计问的是业务时间,你答的是文件落地时间。起点和终点只表示这一版的有效区间,不表示文件何时被写进目录。
合并要求表格式能改行
微批用 MERGE 把变更折进目标。目录连接器必须实现数据源第二版的行级操作,执行才能按行更新。内置 Parquet 只是文件,不具备这种行级操作,不能当目标。选它时,作业应在写入阶段失败,而不是悄悄只留下最后一批文件。常见满足行级更新的格式是 Iceberg。建表时就把格式定下来。第一条变更到来之后再改格式,已经覆盖掉的历史找不回来。
只要当前值的下游用第一型:一个键一行,新变更覆盖旧行。要历史的下游用第二型。两种表可以来自同一批事件,但不能是同一张物理表还指望两种查询都对。第一型表上没有终点列,无法回答过去。第二型表上不做时间过滤,当前汇总会偏大。
顺序错了,历史是反的
顺序列至少要在同一键内可比较,并且和来源日志的顺序一致。批次号、入库时间都不能代替,除非你已经证明它们和来源位置单调一致。一条更早的变更若因为重试而后到达,仍带着更小的来源位置,它不应该把已经生效的新版本关掉。做不到这一点,先修顺序,不要先加报表字段。
回放一条已知更新:旧行的终点被写上,新行的起点接上,按现在过滤只看到新行。再回放一条顺序更早的变更:当前行保持为较新的那条。两条都通过,这张历史表才可以给审计用。只通过第一条,说明你还没有处理重复和乱序。
当前值和历史怎样并存
给运营看的接口读第一型,给审计看的接口读第二型,并在接口说明里写清过滤条件。两套接口不要共用一个未过滤的查询。每天抽一个键,用来源库在该时刻的值对照湖里的历史行。对不上,先查顺序列和合并是否按行执行,而不是再存一份全量文件当备份。全量文件没有终点,仍然回答不了当时是哪一版。
常见问题 FAQ
声明了第二型历史,Parquet 目录能不能当目标?
不能。合并要求连接器支持数据源第二版的行级操作。内置 Parquet 只是文件。常见可选的是 Iceberg 这类能按行更新的表。
第一型和第二型可以写在同一张表里吗?
不要这么做。第一型是一个键一行,第二型是一个键多行。同一张表无法同时满足“只看当前”和“按时间点回看”,除非查询里始终带上有效区间。
终点是空的行表示什么?
表示这一版仍然有效。查询当前值时保留终点开着的行。查询过去某一刻时,还要看起点和终点是否包住那个时刻。
入库时间可以当顺序列吗?
只有在你证明它和来源日志的位置单调一致时才可以。重试会让旧事件晚到,按入库时间合并会把旧值留成当前值。
全量快照文件能代替历史表吗?
不能。快照没有每一版的终点,覆盖之后上一版就没了。历史要的是同一键上按区间排列的多行。