3. Delta Lake表设计与建模落地
在 Databricks 上做 AI 数据建模,Delta Lake 不只是一个存储格式,它同时承担了“表结构治理”和“数据版本治理”两件事。事实表与维度表能否长期可演化,取决于五个设计动作:分区、Z-order、合并模式、Schema 演进、时间旅行。它们不是五个独立开关,而是一条从写入到查询、从结构到历史的协同链。
一、分区:先问“过滤条件长什么样”
分区的本质是用目录切分数据,让查询在扫描前就能裁掉无关文件。因此分区键必须来自高频过滤列,而不是“看起来像分类”的列。典型事实表按 event_date 做日期分区,维度表若数据量不大,通常不分区,避免小文件。
分区粒度要克制。按天分区适合日增量百万到千万级的表;若单日数据只有几万行,按天分区会产生大量小文件,反而拖慢查询。此时可改为按月分区,或干脆不分区、依赖文件跳过。判断依据是:单分区文件数控制在合理范围,单文件大小尽量接近 128MB~1GB。
CREATE TABLE IF NOT EXISTS silver.fact_order (
order_id BIGINT,
user_id BIGINT,
product_id BIGINT,
order_amount DECIMAL(18,2),
order_status STRING,
event_date DATE
)
USING DELTA
PARTITIONED BY (event_date)
TBLPROPERTIES (
'delta.autoOptimize.optimizeWrite' = 'true',
'delta.autoOptimize.autoCompact' = 'true'
);
这里开启 optimizeWrite 与 autoCompact,是为了在写入阶段就抑制小文件,而不是等查询变慢后再补救。
二、Z-order:在分区内部再排一次“物理顺序”
分区解决“扫哪些目录”,Z-order 解决“同一分区内先读哪些文件”。它把多个高基数列的位交错映射到文件排序中,使这些列上的过滤能命中更少的文件。适合 Z-order 的列是高频等值或范围过滤列,例如 user_id、product_id。低基数列如 order_status 收益有限。
OPTIMIZE silver.fact_order
ZORDER BY (user_id, product_id);
注意:Z-order 是重写数据的代价换查询收益,不宜每次写入都执行。常见做法是按天或按周对近期分区做一次 OPTIMIZE,并配合 VACUUM 清理旧文件。分区键本身不建议再放进 Z-order,因为分区已经完成了粗粒度裁剪。
三、合并模式:事实表用 MERGE,维度表用 SCD 策略
事实表通常按业务主键做 upsert,用 MERGE 保证幂等重跑。维度表则要区分“覆盖式”与“保留历史式”:前者直接更新属性,后者用生效时间与当前标志保留版本。
MERGE INTO silver.fact_order AS t
USING staging.fact_order_inc AS s
ON t.order_id = s.order_id
WHEN MATCHED AND s.op = 'U' THEN UPDATE SET *
WHEN MATCHED AND s.op = 'D' THEN DELETE
WHEN NOT MATCHED THEN INSERT *;
维度表的 SCD2 合并要同时处理“关闭旧版本”和“插入新版本”,并保证同一业务键只有一条 is_current = true。合并条件必须走主键,否则会退化为全表笛卡尔匹配。
四、Schema 演进:让表结构跟着业务长
上游新增字段是常态。Delta 提供两种演进方式:写入时自动合并新列,或显式覆盖表结构。前者适合加列,后者适合类型放宽。默认情况下,MERGE 的 UPDATE SET * 不会自动加列,需要显式开启。
-- 写入时自动合并新列
SET spark.databricks.delta.schema.autoMerge.enabled = true;
-- 显式演进表结构
ALTER TABLE silver.fact_order
ADD COLUMNS (coupon_code STRING COMMENT '优惠券编码');
演进的原则是“只加不删、类型放宽不收紧”。删除列或收窄类型会破坏下游读取,应通过新表或视图过渡,而不是直接改历史表。
五、时间旅行:把“可回滚”变成默认能力
每次写入都会生成新的 Delta 版本,历史版本可被查询、比对和恢复。排查“某天数据突然变少”时,先看版本历史,再对比相邻版本的行数差异,最后决定是否回滚。
DESCRIBE HISTORY silver.fact_order;
SELECT COUNT(*) FROM silver.fact_order VERSION AS OF 12;
SELECT COUNT(*) FROM silver.fact_order VERSION AS OF 13;
RESTORE TABLE silver.fact_order TO VERSION AS OF 12;
时间旅行依赖保留的历史文件,因此 VACUUM 的保留期决定了可回溯窗口。默认 7 天,若合规要求更长,应显式设置并评估存储成本。
六、落地顺序与判断依据
实际建模时,建议按“先定主键与分区、再定合并策略、最后补演进与回溯”的顺序推进。判断一张表是否设计合理,可以看四个信号:查询是否只扫必要分区;同一分区内过滤列是否命中少量文件;重跑是否幂等;结构变更后下游是否无需改代码即可读取。
| 设计动作 | 作用对象 | 关键参数/语句 | 判断依据 |
|---|---|---|---|
| 分区 | 事实表 | PARTITIONED BY (event_date) | 过滤列命中分区,单分区文件数合理 |
| Z-order | 分区内文件 | OPTIMIZE ... ZORDER BY | 高基数列过滤命中文件数下降 |
| 合并模式 | 事实/维度表 | MERGE ON 主键 | 重跑幂等,SCD2 仅一条当前记录 |
| Schema 演进 | 表结构 | autoMerge / ALTER ADD COLUMNS | 加列不破坏下游读取 |
| 时间旅行 | 历史版本 | VERSION AS OF / RESTORE | 可定位异常版本并回滚 |
把上述五项落到一张事实表和一张维度表上,Delta Lake 就从“能存数据”升级为“能长期演进、能安全回滚、能高效查询”的建模底座。后续无论是特征表、指标表还是 AI 训练样本表,都可以沿用这套设计语言。