发布日期: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") 向表中写入数据)。