发布日期:2026-04-04
如何选择合适的分区列
可以按某一列对 Delta 表进行分区。最常用的分区列是日期。在决定按哪一列分区时,请遵循以下两条经验法则:
- 如果某列的基数非常高,请不要将其用作分区列。例如,如果按 userId 列分区,而可能的用户 ID 数量高达 100 万个,那么这种分区策略是不可取的。
- 每个分区中的数据量:如果预计某个分区的数据量至少为 1 GB,就可以按该列进行分区。
文件压缩
如果持续向 Delta 表中写入数据,随着时间的推移,表会积累大量文件,尤其是在以小批次方式增量写入数据的情况下。这会对表的读取效率产生不利影响,还可能影响文件系统的性能。理想的做法是,定期将大量小文件重写为数量较少的大文件,这一过程称为压缩(compaction)。
可以通过对表进行重新分区,将其重写为更少数量的文件来实现压缩。此外,还可以将 dataChange 选项设置为 false,表示该操作并不改变数据本身,只是重新组织数据的存储布局。这样可以确保压缩操作对其他并发操作的影响最小化。
例如,以下操作可以将一张表压缩为 16 个文件:
val path = "..."
val numFiles = 16
spark.read
.format("delta")
.load(path)
.repartition(numFiles)
.write
.option("dataChange", "false")
.format("delta")
.mode("overwrite")
.save(path)
如果表是分区表,并且只想根据某个谓词对单个分区进行重新分区,可以使用 where 仅读取该分区,然后使用 replaceWhere 将数据写回该分区:
val path = "..."
val partition = "year = '2019'"
val numFilesPerPartition = 16
spark.read
.format("delta")
.load(path)
.where(partition)
.repartition(numFilesPerPartition)
.write
.option("dataChange", "false")
.format("delta")
.mode("overwrite")
.option("replaceWhere", partition)
.save(path)
注意: 此操作不会删除旧文件。如需删除,请运行 VACUUM 命令。
替换表的内容或结构
有时可能需要替换一个 Delta 表。例如:
- 发现表中的数据不正确,想要替换其内容。
- 希望重写整个表以进行不兼容的结构变更(例如更改列类型)。
虽然可以删除 Delta 表的整个目录,然后在同一路径上创建新表,但这种方法不推荐,原因如下:
- 删除目录效率低下。包含非常大文件的目录可能需要数小时甚至数天才能删除完成。
- 删除文件后其中的所有内容都会丢失;一旦误删了错误的表,很难恢复。
- 目录删除操作不是原子性的。在删除表的过程中,并发的查询操作可能会失败,或者只能看到部分表数据。
如果不需要更改表结构,可以直接从 Delta 表中删除数据并插入新数据,或者更新表来修正错误的值。
如果需要更改表结构,可以采用原子方式整体替换表。例如:
dataframe.write
.format("delta")
.mode("overwrite")
.option("overwriteSchema", "true")
.partitionBy(<your-partition-columns>)
.saveAsTable("<your-table>") // Managed table
dataframe.write
.format("delta")
.mode("overwrite")
.option("overwriteSchema", "true")
.option("path", "<your-table-path>")
.partitionBy(<your-partition-columns>)
.saveAsTable("<your-table>") // External table
这种方法有多重好处:
- 覆盖表的速度要快得多,因为它不需要递归地列出目录内容,也不需要删除任何文件。
- 表的旧版本仍然存在。如果您误删了错误的表,可以轻松地使用时间旅行(Time Travel)功能恢复旧数据。
- 这是一个原子操作。在删除表的过程中,并发查询仍然可以正常读取表。
- 由于 Delta Lake 的 ACID 事务保证,如果覆盖表失败,表将保持之前的状态不变。
此外,如果在覆盖表之后想要删除旧文件以节省存储成本,可以使用 VACUUM 命令来删除它们。该命令针对文件删除进行了优化,通常比删除整个目录要快。
Spark 缓存
不建议使用 Spark 缓存,原因如下:
- 缓存的 DataFrame 如果在其基础上添加了额外的筛选条件,本可以享受的数据跳过(data skipping)优势将会丢失。
- 如果通过不同的标识符访问表,缓存的数据可能不会被更新(例如,通过 spark.table(x).cache() 缓存了数据,但随后使用 spark.write.save("/some/path") 向表中写入数据)。