Delta Lake 是什么?为什么我们需要它?

聊 Lakehouse,绕不开 Delta Lake。我刚开始接触数据湖时,最头疼的问题就是:数据写到一半任务挂了,留下一堆脏数据;或者今天跑的报告和昨天对不上,查了半天发现是有人改了历史文件。说白了,传统数据湖缺少数据库那样的事务保障。

Delta Lake 就是来解决这个问题的。它是一个开源存储层,跑在 Parquet 之上,给数据湖加上了 ACID 事务、版本控制、Schema 校验这些能力。你可以把它理解成「带数据库特性的文件系统」。

一句话总结:Delta Lake = Parquet 文件 + 事务日志(Transaction Log)+ 版本快照。

核心特性一:ACID 事务

ACID 是数据库的四个基本特性:原子性、一致性、隔离性、持久性。Delta Lake 把这些带到了数据湖里。

举个例子。你正在往一张表里写入 100 个文件,写到第 50 个时集群挂了。没有 Delta Lake 的话,这 50 个文件就留在那了,下次读数据的人会看到半成品。有了 Delta Lake,这次写入要么全部成功,要么全部回滚——不会留下中间状态。

我遇到过最典型的场景是:多个任务同时往同一张表写数据,互相覆盖。用了 Delta Lake 之后,每个写入都是一个独立事务,冲突时会自动重试或报错,不会静默覆盖。

我的习惯:生产环境里,我建议所有写入操作都走 Delta Lake 的 merge 或 upsert,而不是直接覆盖分区。这样能避免并发写入时的数据丢失。

核心特性二:时间旅行(Time Travel)

时间旅行是 Delta Lake 最酷的特性之一。它允许你查询某个时间点的数据快照,或者回滚到之前的版本。

为什么会需要这个?

  • 数据回滚:有人误删了数据,你可以直接回滚到一小时前的版本。
  • 审计追溯:监管要求提供上个月某天的数据状态,直接查那个时间点就行。
  • 实验对比:跑新模型时,可以对比新旧两版数据对结果的影响。

实现原理其实不复杂。Delta Lake 的每个操作都会在事务日志里记录一条「提交」。每次查询时,引擎会读取日志,重建出那个版本的文件列表。旧文件不会被立即删除,而是保留一段时间(默认 7 天或 168 小时)。

-- 查询某个时间点的数据
SELECT * FROM my_table TIMESTAMP AS OF '2024-01-15 10:00:00'

-- 查询某个版本的数据
SELECT * FROM my_table VERSION AS OF 42

-- 查看历史版本
DESCRIBE HISTORY my_table

避坑指南:我曾经因为没配清理策略,导致事务日志膨胀到几百 GB。记得定期执行 VACUUM 清理旧文件,但别把保留期设得太短——至少留够 7 天,方便回滚。

核心特性三:Schema 演进(Schema Evolution)

数据湖里,Schema 变化是家常便饭。业务加字段、改类型、删列……传统 Parquet 表遇到 Schema 不匹配就直接报错。Delta Lake 允许你优雅地处理这些变化。

Delta Lake 支持以下几种 Schema 演进操作:

  • 新增列:默认允许,新列会填充 null。
  • 列类型变更:需要显式设置 mergeSchema 为 true。
  • 列重命名:通过 ALTER TABLE RENAME COLUMN 实现。
  • 列删除:通过 ALTER TABLE DROP COLUMN 实现。
-- 写入时自动合并 Schema
df.write \
  .mode("append") \
  .option("mergeSchema", "true") \
  .format("delta") \
  .save("/path/to/table")

-- 手动修改 Schema
ALTER TABLE my_table ADD COLUMNS (new_col STRING)
ALTER TABLE my_table ALTER COLUMN old_col TYPE BIGINT

我个人习惯是:在开发阶段用自动合并 Schema,生产环境里手动管理 Schema 变更,避免意外引入不兼容的改动。

Delta Lake vs Parquet:一张表说清楚

很多人问:Delta Lake 不就是 Parquet 吗?其实不是。Delta Lake 底层确实用 Parquet 存数据,但多了很多关键能力。我整理了一张对比表:

特性 Parquet Delta Lake
存储格式 列式存储 Parquet + 事务日志
ACID 事务 不支持 支持(可序列化隔离级别)
时间旅行 不支持 支持(按版本或时间戳查询)
Schema 演进 手动处理 自动或手动演进
并发控制 无(文件级覆盖) 乐观并发控制
数据版本管理 无 事务日志记录每次变更
性能优化 列裁剪、谓词下推 以上 + Z-Order、数据跳过、文件压缩
适用场景 静态数据、只读分析 实时写入、增量处理、数据治理

说白了,如果你只是存一批历史数据,跑一次分析就完事,Parquet 够用了。但如果你需要多人协作、增量写入、数据回滚、Schema 灵活变化——Delta Lake 是更好的选择。

Delta Lake 的核心架构

下面这张图展示了 Delta Lake 的核心组件和读写流程:

Delta Lake 核心架构 用户 / 应用程序 Delta Lake 读写 API(Spark / Flink / Presto) 事务日志(Transaction Log) _delta_log/ 目录下的 JSON 文件 数据文件(Parquet) 实际列式存储数据 缓存 / 快照(Snapshot Cache) 每次读取时,Delta Lake 先读事务日志重建文件列表,再读取对应的 Parquet 文件

这张图展示了 Delta Lake 的读写流程。用户通过 Spark、Flink 或 Presto 发起请求,Delta Lake 的 API 层会做两件事:一是更新事务日志(记录这次操作),二是读写实际的 Parquet 文件。每次查询时,引擎先读事务日志,重建出当前版本的文件列表,再读取对应的数据文件。

嗯,这里要注意:事务日志是 Delta Lake 的「大脑」。它记录了每一次提交的元数据,包括新增了哪些文件、删除了哪些文件、Schema 是什么版本。没有事务日志,Delta Lake 就只是一个普通的 Parquet 目录。

什么时候用 Delta Lake?

根据我的经验,以下场景特别适合引入 Delta Lake:

  • 实时数据管道:需要频繁写入和读取,且要求数据一致性。
  • 多团队协作:多个任务同时操作同一张表,需要并发控制。
  • 数据回滚需求:业务上需要「后悔药」,能回到某个时间点。
  • 增量 ETL:每天只处理新增或变更的数据,而不是全量覆盖。
  • Schema 频繁变化:业务字段经常增减,不想每次手动改表结构。

反过来说,如果你的场景是「一次性导入数据,之后只读不写」,或者数据量极小(几百 MB),那直接用 Parquet 就够了,没必要上 Delta Lake 的复杂度。

我的建议:刚开始接触 Lakehouse 时,可以先从一张 Delta 表开始试水。把最核心的、写入最频繁的那张表迁移过来,感受一下 ACID 事务和版本管理带来的变化。等熟悉了再逐步推广。