3. Delta Lake 基础操作:创建表、写入数据、读取数据、Schema 演化
好,咱们进入实战环节。Delta Lake 的基础操作,说白了就是四个动作:建表、写数据、读数据、改 Schema。我刚开始接触 Delta 时,觉得这不就是 Spark SQL 嘛,后来踩了几个坑才发现,细节里全是魔鬼。
今天咱们把这四个动作拆开揉碎,每个操作我都会带上实际项目里的教训。你跟着走一遍,基本就能应付日常开发了。
3.1 创建 Delta 表:两种方式,一个核心
创建 Delta 表有两种主流方式。我个人习惯用 DataFrame API,因为灵活;但如果你团队里分析师多,SQL 方式更友好。
3.1.1 方式一:DataFrame API 建表
from pyspark.sql import SparkSession
spark = SparkSession.builder \
.appName("DeltaCreateTable") \
.config("spark.sql.extensions", "io.delta.sql.DeltaSparkSessionExtension") \
.config("spark.sql.catalog.spark_catalog", "org.apache.spark.sql.delta.catalog.DeltaCatalog") \
.getOrCreate()
# 定义 Schema
from pyspark.sql.types import StructType, StructField, StringType, IntegerType, TimestampType
schema = StructType([
StructField("user_id", StringType(), False),
StructField("user_name", StringType(), True),
StructField("age", IntegerType(), True),
StructField("event_time", TimestampType(), True)
])
# 创建空 DataFrame 并写入 Delta 格式
data = []
df = spark.createDataFrame(data, schema)
df.write.format("delta").save("/data/delta/users")
StructField 的 nullable 参数。主键字段设为 False,其他字段尽量 True。为什么?因为数据源经常有脏数据,非空约束能帮你提前发现问题。
3.1.2 方式二:SQL 方式建表
spark.sql("""
CREATE TABLE IF NOT EXISTS delta.`/data/delta/users` (
user_id STRING NOT NULL,
user_name STRING,
age INT,
event_time TIMESTAMP
) USING DELTA
""")
嗯,这里要注意:SQL 方式建表时,USING DELTA 不能写成 USING DELTA LAKE。我见过有同事写错了,结果 Spark 默认用了 Parquet 格式,事务日志全没了。
3.2 写入数据:Append、Overwrite、Upsert
写入数据是 Delta 的核心能力。三种模式各有适用场景,我按项目经验给你排个优先级。
| 写入模式 | 语法 | 适用场景 | 注意事项 |
|---|---|---|---|
| Append | df.write.mode("append") |
日志、事件流、CDC 增量 | 不会覆盖已有数据 |
| Overwrite | df.write.mode("overwrite") |
全量刷新、重跑任务 | 会删除旧数据,慎用 |
| Upsert | DeltaTable.merge() |
维表更新、CDC 合并 | 需要指定匹配条件 |
3.2.1 Append 模式:最常用
# 模拟一批新用户数据
new_users = [
("U001", "张三", 28, "2024-01-15 10:00:00"),
("U002", "李四", 35, "2024-01-15 10:05:00")
]
columns = ["user_id", "user_name", "age", "event_time"]
df_new = spark.createDataFrame(new_users, columns)
df_new.write.format("delta").mode("append").save("/data/delta/users")
3.2.2 Overwrite 模式:全量替换
# 全量覆盖用户表
df_full = spark.read.parquet("/data/raw/users_full_20240115")
df_full.write.format("delta").mode("overwrite").save("/data/delta/users")
Overwrite 模式会替换整个表的数据,但事务日志会保留。这意味着你可以用 DESCRIBE HISTORY 看到每次覆盖的记录。我有个项目就是靠这个功能回滚了误操作。
3.2.3 Upsert 模式:Merge 操作
from delta.tables import DeltaTable
delta_table = DeltaTable.forPath(spark, "/data/delta/users")
# 模拟 CDC 数据
cdc_data = [
("U001", "张三丰", 29, "2024-01-16 08:00:00"), # 更新
("U003", "王五", 40, "2024-01-16 08:00:00") # 新增
]
df_cdc = spark.createDataFrame(cdc_data, columns)
delta_table.alias("target") \
.merge(df_cdc.alias("source"), "target.user_id = source.user_id") \
.whenMatchedUpdateAll() \
.whenNotMatchedInsertAll() \
.execute()
Merge 操作是 CDC 入湖的核心。你想想看,如果没有这个能力,每次更新都要全表覆盖,那效率得多低。
3.3 读取数据:时间旅行与增量读
Delta 的读取能力比普通 Parquet 强太多了。我最喜欢的是 时间旅行 和 增量读,这两个功能在数据回溯和流处理中特别有用。
3.3.1 基础读取
# 读取最新版本
df = spark.read.format("delta").load("/data/delta/users")
df.show()
3.3.2 时间旅行:回到过去
# 按版本号读取
df_v1 = spark.read.format("delta") \
.option("versionAsOf", 1) \
.load("/data/delta/users")
# 按时间戳读取
df_time = spark.read.format("delta") \
.option("timestampAsOf", "2024-01-15 12:00:00") \
.load("/data/delta/users")
3.3.3 增量读取:只读新数据
# 从指定版本开始增量读
df_incremental = spark.read.format("delta") \
.option("readChangeFeed", "true") \
.option("startingVersion", 5) \
.load("/data/delta/users")
df_incremental.select("user_id", "user_name", "_change_type", "_commit_version").show()
增量读配合 CDC 使用,效果绝佳。你只需要记录上次处理的版本号,下次从那个版本开始读,就能拿到所有变更数据。
3.4 Schema 演化:让表结构活起来
Schema 演化是 Delta 最实用的特性之一。说白了,就是允许表结构随着数据源的变化而自动调整。我刚开始做数据湖时,最头疼的就是上游加字段,下游就要改代码。有了 Schema 演化,这个问题基本解决了。
3.4.1 自动演化:加字段
# 新数据多了一个 phone 字段
new_data_with_phone = [
("U004", "赵六", 32, "2024-01-17 09:00:00", "13800138000")
]
columns_with_phone = ["user_id", "user_name", "age", "event_time", "phone"]
df_with_phone = spark.createDataFrame(new_data_with_phone, columns_with_phone)
# 开启 Schema 演化
df_with_phone.write.format("delta") \
.option("mergeSchema", "true") \
.mode("append") \
.save("/data/delta/users")
执行完后,表里会自动多出 phone 字段,旧数据的 phone 值为 null。这就是自动演化的威力。
3.4.2 手动演化:改字段类型
from delta.tables import DeltaTable
delta_table = DeltaTable.forPath(spark, "/data/delta/users")
# 将 age 字段从 IntegerType 改为 LongType
delta_table.alterTableAddColumns(
"age BIGINT"
)
StringType 转 IntegerType 会失败,因为数据可能不兼容。我建议手动演化时,先检查数据质量,再执行变更。
3.4.3 Schema 演化的最佳实践
- 生产环境默认关闭自动演化:用
mergeSchema=false,避免意外加字段导致下游任务报错。 - 加字段用 Append 模式:Overwrite 模式会覆盖整个表,Schema 演化效果不如 Append 直观。
- 删字段要谨慎:Delta 不支持直接删除字段,只能通过重写表来实现。我建议用
SELECT排除不需要的字段,而不是物理删除。
3.5 本章知识体系图
下面这张图把 Delta Lake 基础操作的四个核心环节串起来了。你可以看到,建表是起点,写入和读取是双向通道,Schema 演化贯穿始终。
3.6 避坑指南:我踩过的三个坑
- Schema 演化导致下游任务崩溃:有一次我开启了自动演化,上游加了个字段,下游的 ETL 任务直接报错,因为字段顺序变了。后来我改成手动演化,加字段前先通知下游团队。
- Merge 操作性能问题:当表数据量超过 10 亿行时,Merge 操作会变得很慢。我的解决方案是:先做分区裁剪,只 Merge 受影响的分区。
- 时间旅行版本保留时间:Delta 默认保留 30 天的历史版本。如果你需要更长的回溯能力,记得调大
delta.logRetentionDuration参数。
好了,Delta Lake 的基础操作就讲到这里。你把这些代码跑一遍,再结合自己的业务场景想一想,应该就能上手了。记住,建表时想清楚 Schema,写入时选对模式,读取时用好时间旅行,Schema 演化要谨慎。这四个点抓住了,Delta Lake 的基本功就算扎实了。