3. Delta Lake表设计与特征存储基础

在特征工程流水线中,特征数据的存储方式直接决定了后续训练读取的效率、特征回溯的可靠性以及线上线下一致性。Delta Lake 作为构建在云对象存储之上的开放表格式,提供了 ACID 事务、Schema 演进、时间旅行和高效的数据跳过能力,是 Databricks 特征存储的天然底座。本节从 Delta 表的创建与物理布局出发,逐步过渡到特征表的概念与初步实践。

Delta 表的创建与基本结构

Delta 表在物理上是一组 Parquet 数据文件加上一个 _delta_log 事务日志目录。日志以 JSON 格式按版本递增记录每次提交的元数据与操作,使表具备原子性和可追溯性。创建表时,我们通常先定义 Schema,再写入数据。以下示例创建一个用户行为特征表:

CREATE TABLE IF NOT EXISTS feature_store.user_behavior (
  user_id       STRING NOT NULL,
  event_date    DATE   NOT NULL,
  click_cnt_7d  INT,
  order_cnt_30d INT,
  avg_amount_30d DOUBLE,
  update_ts     TIMESTAMP
)
USING DELTA
PARTITIONED BY (event_date)
LOCATION 's3://bucket/feature_store/user_behavior';

这里 event_date 被选为分区列,因为特征查询几乎总是按时间窗口过滤。分区列的选择应遵循“高基数但查询频繁”的原则:基数太低(如性别)会导致分区过大,基数太高(如 user_id)会产生海量小文件。日期、区域、业务线通常是合适的分区维度。

分区与 Z-Order:两种不同的数据跳过策略

分区是目录级别的物理隔离,查询时通过分区裁剪直接跳过无关目录。但分区无法解决分区内部的数据过滤问题。Z-Order 则是在文件内部对数据进行多维聚簇,使相关数据在物理上相邻,从而在读取时通过 Parquet 的 min/max 统计信息跳过大量文件。两者是互补关系,而非替代关系。

执行 Z-Order 的语法如下:

OPTIMIZE feature_store.user_behavior
ZORDER BY (user_id);

如果查询模式是“按 user_id 查最近 30 天特征”,那么分区列用 event_date,Z-Order 列用 user_id,可以同时获得分区裁剪和文件级跳过两重收益。需要注意的是,Z-Order 会重写文件,应配合 OPTIMIZE 定期执行,而不是每次写入都触发。

Delta 表分区与 Z-Order 数据跳过示意 分区目录(event_date) event_date=2024-06-01 file_1.parquet … file_n.parquet event_date=2024-06-02 file_1.parquet … file_n.parquet event_date=2024-06-03 file_1.parquet … file_n.parquet Z-Order 后文件内 user_id 聚簇 file_A: user_id 1000–1200(min/max 统计) file_B: user_id 1201–1400(min/max 统计) file_C: user_id 1401–1600(min/max 统计) 查询 user_id=1350 → 仅读 file_B,跳过 A/C 分区裁剪跳过目录,Z-Order 跳过文件,两者叠加减少 I/O

时间旅行:特征回溯与版本管理

Delta 的事务日志天然支持时间旅行。通过 VERSION AS OF 或 TIMESTAMP AS OF,可以查询表在任意历史版本的状态:

SELECT * FROM feature_store.user_behavior VERSION AS OF 12;
SELECT * FROM feature_store.user_behavior TIMESTAMP AS OF '2024-06-01 08:00:00';

这在特征工程中有两个关键用途。第一,训练集回溯:当模型上线后发现某次训练使用了错误特征,可以回到当时的表版本重新生成训练集,保证实验可复现。第二,特征一致性排查:对比线上推理时读取的特征版本与训练时使用的版本,定位线上线下不一致的根因。需要注意的是,时间旅行依赖日志与数据文件保留,默认保留 30 天,可通过 delta.logRetentionDuration 和 delta.deletedFileRetentionDuration 调整。

特征表的概念

特征表(Feature Table)是 Delta 表在特征工程语境下的特化形式。它不仅仅是“存特征的 Delta 表”,而是具备以下约束的实体:

  • 主键明确:通常由实体键(如 user_id、item_id)加时间戳组成,保证每行特征可唯一定位。
  • 时间戳列:记录特征的计算时间或事件时间,用于 point-in-time 正确连接,防止特征穿越。
  • Schema 稳定:特征列有明确的类型和语义,支持 Schema 演进但不随意删除列。
  • 可注册:在 Databricks Feature Store 中注册后,可被训练和推理统一引用,自动处理训练/服务一致性。

一个典型的特征表主键设计为 (user_id, event_date),其中 event_date 既是分区列,也是时间戳列。这样在生成训练集时,可以用 AS OF JOIN 或时间窗口聚合,确保每个样本只看到该时间点之前已知的特征。

用 Delta 表作为特征存储的初步实践

下面给出一个从原始行为日志构建特征表并注册的完整示例。假设原始表 raw_events 包含 user_id, event_time, event_type, amount。

-- 1. 计算 7 天点击数和 30 天订单数
CREATE OR REPLACE TABLE feature_store.user_behavior AS
SELECT
  user_id,
  event_date,
  SUM(CASE WHEN event_type='click' THEN 1 ELSE 0 END) AS click_cnt_7d,
  SUM(CASE WHEN event_type='order' THEN 1 ELSE 0 END) AS order_cnt_30d,
  AVG(CASE WHEN event_type='order' THEN amount END) AS avg_amount_30d,
  current_timestamp() AS update_ts
FROM (
  SELECT *, to_date(event_time) AS event_date
  FROM raw_events
  WHERE event_time >= current_date() - INTERVAL 30 DAYS
)
GROUP BY user_id, event_date;

-- 2. 优化布局
OPTIMIZE feature_store.user_behavior ZORDER BY (user_id);

-- 3. 注册为特征表(Databricks Feature Store)
-- 在 Notebook 中使用 Python API
from databricks.feature_store import FeatureStoreClient
fs = FeatureStoreClient()
fs.create_table(
  name="feature_store.user_behavior",
  primary_keys=["user_id", "event_date"],
  timestamp_keys=["event_date"],
  description="用户行为特征表"
)

注册后,训练时通过 fs.create_training_set() 指定标签表和特征表,Feature Store 会自动完成 point-in-time 连接,避免特征穿越。推理时通过 fs.score_batch() 或在线存储读取同一份特征定义,保证线上线下一致。

分区列与 Z-Order 列的选择应基于实际查询模式。如果查询同时包含时间范围和实体过滤,建议分区用时间、Z-Order 用实体键;如果查询以实体为主且时间范围很宽,可考虑反过来。没有万能组合,应以查询日志中的过滤条件分布为依据。
时间旅行虽然强大,但并非无限期。默认 30 天后旧版本数据文件可能被清理,届时 VERSION AS OF 会失败。对于需要长期复现的训练集,建议将关键版本的数据物化到独立路径,或延长保留周期并评估存储成本。

总结来看,Delta 表通过分区实现目录级裁剪,通过 Z-Order 实现文件级跳过,通过时间旅行实现版本回溯;特征表在此基础上增加了主键、时间戳和注册语义,使特征从“一张表”升级为“可治理、可复用、可一致读取”的资产。掌握这四者的关系,是构建可靠特征存储的第一步。