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 的事务日志天然支持时间旅行。通过 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() 或在线存储读取同一份特征定义,保证线上线下一致。
VERSION AS OF 会失败。对于需要长期复现的训练集,建议将关键版本的数据物化到独立路径,或延长保留周期并评估存储成本。
总结来看,Delta 表通过分区实现目录级裁剪,通过 Z-Order 实现文件级跳过,通过时间旅行实现版本回溯;特征表在此基础上增加了主键、时间戳和注册语义,使特征从“一张表”升级为“可治理、可复用、可一致读取”的资产。掌握这四者的关系,是构建可靠特征存储的第一步。