由于经常会忘记WHERE子句并意外运行DELETE或UPDATE语句导致全表操作,或者需要查看特定时间点的数据或模式,因此对数据库和表进行了操作的人往往会感到恐慌。我们都有过这样的经历。或者您可能会想知道用于审核、跟踪或分析目的的数据或模式在特定时间点是什么样子。
鉴于数据不断变化,历史上很难解决或回答以下常见情况:
- 法规:审核和法规合规性可能要求存储和检索数据多年,或者要求跟踪数据的某些更改(例如,GDPR)。
- 重现实验和报告:数据科学家或分析师通常需要根据特定时间点的一组数据重新创建报告或机器学习实验和模型输出。
- 回滚:意外或不良的DML操作,例如INSERT、UPDATE、DELETE和MERGE,可能需要修复并回滚到以前的状态。
- 时序分析:报告需求可能要求您回顾或分析一段时间内的数据,例如一个月内新增了多少个客户。
- 调试:排除ETL流水线、数据质量问题或只有在历史状态下才能观察到的破裂过程的特定原因。
轻松浏览数据的不同版本并跟踪特定时间点的数据版本是Delta Lake中的一个关键功能,称为Delta Lake时间旅行。在第2章中,您学到了事务日志会自动对Delta表中的数据进行版本控制,这种版本控制有助于访问数据的任何历史版本。通过版本控制和数据保留,您将学会如何使用这些强大的Delta Lake功能,同时还能利用数据管理和存储优化。
Delta Lake时间旅行
Delta Lake的时间旅行功能允许您访问和还原存储在Delta Lake中的先前数据版本,为版本控制、审计和数据管理提供了强大的机制。您可以跟踪数据的变化并在需要时回滚到以前的版本。
让我们通过一个示例来详细了解。首先,执行第6章的"Chapter Initialization"笔记本,创建taxidb.tripData Delta表。接下来,打开第6章的"01 - Time Travel"笔记本。假设我们需要将VendorId从1更新为10。然后,我们需要删除所有满足WHERE VendorId = 2条件的记录。使用"01 - Time Travel"笔记本中的脚本,执行以下命令来应用这些更改:
sql
%sql
--update records in table
UPDATE taxidb.tripData
SET VendorId = 10
WHERE VendorId = 1;
--delete records in table
DELETE FROM taxidb.tripData
WHERE VendorId = 2;
--describe the table history
DESCRIBE HISTORY taxidb.tripData;
您将看到以下输出(仅显示相关部分):
sql
+---------+-----------+--------------------------------------------------------+
| version | operation | operationParameters |
+---------+-----------+--------------------------------------------------------+
| 2 | DELETE | {"predicate": |
| | | "["(spark_catalog.taxidb.tripData.VendorId = 2L)"]"} |
+---------+-----------+--------------------------------------------------------+
| 1 | UPDATE | {"predicate": "(VendorId#5081L = 1)"} |
+---------+-----------+--------------------------------------------------------+
| 0 | WRITE | {"mode": "Overwrite", "partitionBy": "[]"} |
+---------+-----------+--------------------------------------------------------+
继续的输出并进行了修改以显示 operationMetrics(仅显示相关部分):
sql
+---------------------+---------------------------------------------------------+
| version | operation | operationMetrics |
+---------------------+---------------------------------------------------------+
| 2 | DELETE | {"numRemovedFiles": "10", "numCopiedRows": "9643805", |
| | | "numAddedChangeFiles": "0", |
| | | ..."numDeletedRows": "23360027"..., |
| | | "numAddedFiles": "10"...} |
+---------------------+---------------------------------------------------------+
| 1 | UPDATE | {"numRemovedFiles": "10", "numCopiedRows": "23414817", |
| | | "numAddedChangeFiles": "0","..."numAddedFiles": "10".} |
+---------------------+---------------------------------------------------------+
| 0 | WRITE | {"numFiles": "10", "numOutputRows": "33003832"...} |
+---------------------+---------------------------------------------------------+
从输出中,我们可以看到表的总共有三个版本,每个版本对应一个提交,其中版本2是最近的更改:
- 版本0:使用overwrite和无分区编写了初始Delta表
- 版本1:使用谓词VendorId = 1更新了Delta表
- 版本2:使用谓词VendorId = 2从Delta表中删除了数据
请注意,DESCRIBE HISTORY命令显示了有关版本、事务时间戳、操作、操作参数和操作指标的详细信息。输出中的operationMetrics还显示了操作更改的行数和文件数。 图6-1说明了表的不同版本和底层数据的示例。
还原表
现在,假设我们想要撤销先前在taxidb.tripData上执行的UPDATE和DELETE操作,将其还原到原始状态(即版本0)。我们可以使用RESTORE命令将表回滚到所需的版本:
css
%sql
--restore table to previous version
RESTORE TABLE taxidb.tripData TO VERSION AS OF 0;
--describe the table history
DESCRIBE HISTORY taxidb.tripData;
输出(仅显示相关部分):
sql
+----------------------------------------------------------------------------+
| version | operation | operationParameters |
+----------------------------------------------------------------------------+
| 3 | RESTORE | {"version": "0", "timestamp": null} |
+----------------------------------------------------------------------------+
| 2 | DELETE | {"predicate": |
| | | "[\"(spark_catalog.taxidb.tripData.VendorId IN 5,6L) |
| | | \"]"} |
+----------------------------------------------------------------------------+
| 1 | UPDATE | {"predicate": "(VendorId#5081L = 1)"} |
+----------------------------------------------------------------------------+
| 0 | WRITE | {"mode": "Overwrite", "partitionBy": "[]"} |
+----------------------------------------------------------------------------+
将表恢复到版本0后,运行DESCRIBE HISTORY命令,您将看到表的现在多了一个版本,即版本3,记录了RESTORE操作。图6-2说明了表的不同版本以及底层数据的示例。
现在您可以在数据和图6-2中看到,表的最新版本现在反映了版本0的数据。
通过时间戳进行恢复
在先前的示例中,我们将表还原到特定版本,但我们还可以将表还原到特定的时间戳。恢复到较早状态的时间戳格式是 yyyy-MM-dd HH:mm:ss。仅提供日期(yyyy-MM-dd)字符串也是支持的。
sql
%sql
--restore table to a specific timestamp
RESTORE TABLE taxidb.tripData TO TIMESTAMP AS OF '2023-01-01 00:00:00';
--restore table to the state it was in an hour ago
RESTORE TABLE taxidb.tripData
TO TIMESTAMP AS OF current_timestamp() - INTERVAL '1' HOUR;
我们还可以导入 delta 模块,使用 PySpark 和 DataFrame API 进行表的还原。我们可以使用 restoreToVersion(version: int) 方法来还原到特定版本,就像我们之前做的那样,或者我们可以使用 restoreToTimestamp(timestamp: str) 方法来还原到指定的时间戳:
arduino
--import delta module
from delta.tables import *
--restore table to a specific timestamp using PySpark
deltaTable = DeltaTable.forName(spark, "taxidb.tripData")
deltaTable.restoreToTimestamp("2023-01-01")
时间旅行的背后原理
版本历史可以保存在Delta表中,因为事务日志会跟踪在对表执行操作时应该或不应该读取哪些文件。执行DESCRIBE HISTORY命令时,它还将返回operationMetrics,该指标告诉您在操作期间添加和删除了多少个文件。在对表执行UPDATE、DELETE或MERGE操作时,数据不会从底层存储中物理删除。相反,这些操作会更新事务日志,指示哪些文件应该或不应该读取。类似地,当您将表还原到以前的版本时,它不会物理添加或删除数据,只会更新事务日志中的元数据,以指示应该读取哪些文件。
在第2章中,您了解了_delta_log目录中的JSON文件和检查点文件。检查点文件保存了表在某个时间点的整个状态,并通过将JSON提交合并到Parquet文件中来自动生成,以维护读取性能。检查点文件和随后的提交可以读取以获取表的当前状态,以及在时间旅行的情况下,之前的状态,而无需列出和重新处理所有提交。
事务日志提交检查点文件,以及数据文件仅在逻辑上删除而不是物理删除的事实,构成了Delta Lake如何轻松启用对Delta表的时间旅行的基础。图6-3显示了taxidb.tripData表在不同事务和版本中的每个操作的事务日志条目。
图6-3中的编号步骤显示:
- Version 0:创建初始表并在事务日志中添加文件。
- Version 1和2:add file和remove file充当元数据条目,Delta Lake使用它们来确定应该为每个版本读取哪些文件。remove file并不会物理删除数据;它只会从表中逻辑上移除文件。
- Version 3:这个版本被还原到版本0,因此事务日志还原了文件A,即在版本0中添加的原始文件,并逻辑上删除了文件C。
如果查看我们之前还原到版本0的taxidb.tripData表的表历史,您将注意到在operationMetrics中捕获的已还原、已添加和已删除文件的数量:
sql
%sql
--describe table history
DESCRIBE HISTORY taxidb.tripData
输出(仅显示相关部分):
sql
+-----------------------------------------------------------------------+
| version | operation | operationMetrics |
+-----------------------------------------------------------------------+
| 3 | RESTORE | {"numRestoredFiles": "10"..."numRemovedFiles": |
| | | "10"..."numOfFilesAfterRestore": "10" |
+-----------------------------------------------------------------------+
| 2 | DELETE | {"numRemovedFiles": "10"..."numAddedChangeFiles": |
| | | "0"..."numAddedFiles": "10" |
+-----------------------------------------------------------------------+
| 1 | UPDATE | {"numRemovedFiles": "10"..."numAddedChangeFiles": |
| | | "0"..."numAddedFiles": "10" |
+-----------------------------------------------------------------------+
| 0 | WRITE | {"numFiles": "10", "numOutputRows": "33003832", |
| | | "numOutputBytes": "715810450"} |
+-----------------------------------------------------------------------+
您将在本章后面的部分中了解如何保留和删除先前的数据版本。
RESTORE注意事项和警告
RESTORE是一个会改变数据的操作,也就是dataChange = true。这意味着它有可能会影响下游的作业,比如在第8章中会学到的结构化流作业。
考虑这样一种情况,流查询只处理对Delta表的更新。如果我们将表还原到以前的版本,那么流作业可能会重新处理表的先前更新,因为事务日志会使用dataChange = true的add file操作来还原以前的数据版本。流作业会将这些记录识别为新数据。
请注意,在表6-1中,OPTIMIZE操作删除了与版本1和2相关的文件,并为版本3添加了一个文件。在运行RESTORE命令后,该操作为它们各自的版本添加了文件1和文件2,这被视为数据更改操作。
查询表的旧版本
默认情况下,每当您查询 Delta 表时,您总是查询表的最新版本。但 Delta Lake 时间旅行允许您还可以执行对表的先前版本的读取操作,而无需恢复它们。请记住,数据本身并没有从底层存储中物理删除,只是逻辑上删除。逻辑删除而不是物理删除意味着时间旅行不仅允许您将表恢复到特定时间点,还可以直接查询表的以前版本,而无需还原。
您可以以两种不同的方式访问以前的数据版本:
- 使用时间戳
- 使用版本号
类似于我们如何使用版本号还原表到以前的版本,我们也可以使用版本号来查询特定时间点的表。在前面的示例中,我们将表还原到以前的状态,但在版本 2 中使用了谓词 WHERE VendorId = 2 删除了记录。我们可以使用时间旅行来查找表版本 2 中 VendorId 记录的计数:
sql
%sql
--count records where VendorId = 1 using version number
SELECT COUNT(*) AS count FROM taxidb.tripData VERSION AS OF 2 WHERE VendorId = 1;
--count records where VendorId = 1 using operation timestamp
SELECT COUNT(*) AS count FROM taxidb.tripData
VERSION AS OF '2023-01-01 00:00:00' WHERE VendorId = 1;
--count records where VendorId = 1 using operation timestamp and using @ syntax
--timestamp must be in yyyyMMddHHmmssSSS format
SELECT COUNT(*) AS count FROM taxidb.YellowTaxi@20230101000000000
WHERE VendorId = 1;
--count records where VendorId = 1 using version number and using @ syntax
SELECT COUNT(*) AS count FROM taxidb.tripData@v2 WHERE VendorId = 1;
输出:
diff
+-------+
| count |
+-------+
| 0 |
+-------+
正如前面的示例所示,我们可以使用不同类型的语法访问不同版本的数据;我们可以使用时间戳或版本号,语法可以是 "VERSION AS OF" 或在表名后面添加 "@"。
时间旅行不仅可以通过 SQL 访问,还可以通过 DataFrame API 使用 .option() 方法进行时间旅行:
sql
# count records where VendorId = 1 using version number
spark.read.option("versionAsOf", "0").table("taxidb.tripData").filter(
"VendorId = 1"
).count()
# count records where VendorId = 1 using timestamp
spark.read.option("timestampAsOf", "0").table("taxidb.tripData").filter(
"VendorId = 1"
).count()
通过时间戳查询可以轻松进行时间序列分析,因为我们可以比较同一表在两个不同时间点的数据。尽管我们可以遵循其他 ETL 模式来捕获历史数据并实现时间序列分析(例如,缓慢变化的维度和变更数据源),但时间旅行为那些可能没有这些 ETL 模式的表提供了一种快速而便捷的进行即席分析的方法。例如,如果我们想要快速查看本周与上周 taxidb.tripData 的版本历史,了解本周上车的乘客数量与上周相比,我们可以运行以下查询:
sql
%sql
--count number of new passengers from 7 days ago
SELECT sum(passenger_count) - (
SELECT sum(passenger_count)
FROM taxidb.tripData TIMESTAMP AS OF date_sub(current_date(), 7)
)
FROM taxidb.tripData
数据保留时间
Delta表后面的数据文件永远不会自动删除,但日志文件会在检查点写入后自动清除。从根本上来说,使得可以进行特定版本的表的时间旅行是该表版本的数据和日志文件的保留。默认情况下,Delta表会保留30天的提交历史,因此您可以在Delta表上进行最多30天的时间旅行,除非您修改了数据或日志文件。
在本节和后续章节中,您将看到保留阈值(retention thresholds)这个术语。保留阈值是指文件必须在被物理删除前保留的时间间隔(例如天数)。例如,如果表的保留阈值为七天,则文件必须比当前表版本旧至少七天才能被删除的候选文件。接下来的章节将涵盖两种类型的保留,即数据和日志文件保留。
数据文件保留期
数据文件保留期指的是数据文件在Delta表中的保留时间。默认的保留期为七天,适用于通过VACUUM命令来物理删除数据文件的候选文件。简而言之,VACUUM会删除不再由Delta表引用并且年龄超过保留期的数据文件。除非手动删除,数据文件只会在运行VACUUM时被删除。此命令不会删除Delta日志文件,只会删除数据文件。您将在本章后面了解有关VACUUM命令及其工作原理的更多信息。
通常需要将数据文件保留更长的时间,而不是默认的保留期。表属性delta.deletedFileRetentionDuration = "interval <interval>"
控制了文件在成为VACUUM的候选文件之前必须被删除多久。如果需要保留并访问一年的历史数据,可以设置delta.deletedFileRetentionDuration = "interval 365 days"
。
然而,保留过多的数据文件会导致云存储成本随着时间的推移增加,同时还可能对处理元数据的性能产生影响。 为了演示数据文件如何随时间增长,我们可以使用taxiDb.tripData表的DESCRIBE HISTORY命令,利用operationMetrics中的numFiles和numAddedFiles指标来显示每次操作添加了多少个文件:
sql
%sql
--describe table history
DESCRIBE HISTORY taxidb.tripData
输出(仅显示相关部分):
sql
+-----------------------------------------------------------------------+
| version | operation | operationMetrics |
+-----------------------------------------------------------------------+
| 3 | RESTORE | {"numRestoredFiles": "10"..."numRemovedFiles": |
| | | "10"..."numOfFilesAfterRestore": "10" |
+-----------------------------------------------------------------------+
| 2 | DELETE | {"numRemovedFiles": "10"..."numAddedChangeFiles": |
| | | "0"..."numAddedFiles": "10" |
+-----------------------------------------------------------------------+
| 1 | UPDATE | {"numRemovedFiles": "10"..."numAddedChangeFiles": |
| | | "0"..."numAddedFiles": "10" |
+-----------------------------------------------------------------------+
| 0 | WRITE | {"numFiles": "10", "numOutputRows": "33003832", |
| | | "numOutputBytes": "715810450"} |
+-----------------------------------------------------------------------+
根据numFiles和numAddedFiles指标,您可以看到已向此表添加了30个文件。如果您的ETL流程每天运行并在单个表上执行INSERT、UPDATE或DELETE操作,那么一年后,您可能会有10,950个文件(30 x 365)!而这只是单个表的文件数量。想象一下在整个数据平台上可能会有多少文件。每次操作添加的文件数量显然取决于所执行的操作、每次操作中包含的行数和其他变量,但这有助于说明您的数据文件如何随时间增长。
幸运的是,云数据湖在存储数据方面非常具有成本效益,但随着数据文件的增长,这些成本也会增加。这就是为什么在保留数据文件一段时间时仍然重要,以及在设置保留期时要考虑成本和业务需求的原因。
日志文件保留
日志文件保留是指Delta表中日志文件的保留时间。默认保留期限为30天。您可以使用表属性delta.logRetentionDuration
来更改文件的保留时间。例如,如果您需要保留一张表的提交历史记录一年,可以设置delta.logRetentionDuration = "interval 365 days"
。
在第2章中,您学到每经过10次提交,就会写入一个检查点(截止到目前为止;未来版本的Delta Lake可能会有所改变)。每次生成新的检查点时,Delta Lake会根据保留间隔自动清理日志文件。
保留日志文件的风险较小,因为日志文件不会影响对表的读/写操作的性能;它们只会影响利用表历史记录的操作的性能。在保留文件时,您应该始终考虑存储成本,但日志文件通常很小。
设置文件保留期示例
使用taxidb.tripData表,例如,有一个要求是为时间序列分析或监管目的保留表的整个历史记录一年。为了确保我们可以在过去一年中的任何时间点进行时光旅行,我们可以设置以下表属性:
sql
%sql
--set log retention to 365 days
ALTER TABLE taxidb.tripData
SET TBLPROPERTIES(delta.logRetentionDuration = "interval 365 days");
--set data file retention to 365 days
ALTER TABLE taxidb.tripData
SET TBLPROPERTIES(delta.deletedFileRetentionDuration = "interval 365 days");
--show the table properties to confirm data and log file retention
SHOW TBLPROPERTIES taxidb.tripData;
输出(仅显示相关部分):
sql
+------------------------------------+-------------------+
| key | value |
+------------------------------------+-------------------+
| delta.deletedFileRetentionDuration | interval 365 days |
+------------------------------------+-------------------+
| delta.logRetentionDuration | interval 365 days |
+------------------------------------+-------------------+
由于delta.deletedFileRetentionDuration和delta.logRetentionDuration是表属性,我们可以在初始创建表时设置这些属性,或者在创建后更改表的属性。
在前面的示例中,您可以看到在更改表的属性后执行SHOW TBLPROPERTIES命令后,它返回了taxidb.tripData的已删除文件和日志文件保留的时间间隔。通过将两个间隔都设置为365天,我们现在可以确保在过去一年内的任何时间都可以进行时间旅行,以满足业务需求和法规要求。
数据归档
对于法规或归档目的,您可能需要保留数据多年。使用时间旅行和文件保留存储这些数据可能会因存储成本而变得昂贵。为了帮助降低成本,一种替代解决方案是通过使用"CREATE TABLE AS SELECT"模式,以每天、每周或每月的方式创建一个新的表来归档数据:
sql
%sql
--archive table by creating or replace
CREATE OR REPLACE TABLE archive.tripData USING DELTA AS
SELECT
*
FROM
taxidb.tripData
以这种方式创建的表与源表相比具有独立的历史记录;因此,基于归档频率,源表和新表上的时间旅行查询可能返回不同的结果。
清理(VACUUM)
在上一节中,您了解到可以设置保留阈值并删除已被逻辑删除且不再被Delta表引用的数据文件。 这是一个提醒:除非运行VACUUM命令,这些数据文件永远不会被自动物理删除。 VACUUM旨在允许用户物理删除不再需要的旧版本数据文件和目录,同时考虑表的保留阈值。
使用VACUUM命令物理删除旧版本数据文件具有主要两个重要原因:
- 成本
存储旧的和未使用的数据文件可能导致云存储成本呈指数增长,特别是对于经常更改的数据。 通过删除未使用的数据文件来减少这些成本。
- 法规
审计和法规合规性(例如,GDPR)可能要求某些记录被永久删除并不再可用。 物理删除包含这些记录的文件可以有助于满足这些法规要求。
图6-4显示了Delta表中在不同版本之间的日志和数据文件的精简版本,以展示VACUUM的效果。
图中的编号步骤显示:
- 表的第0个版本已经过了保留阈值(超过了七天)。 这个表版本包含了日志文件、当前表版本中使用的数据文件以及不再被当前表版本使用的已删除数据文件。
- 执行了VACUUM命令。
- 已删除数据文件的默认保留期为七天。
- 运行VACUUM命令后,版本0的逻辑删除数据文件被物理删除,因为它们已经超过了默认的已删除文件保留期七天。
- 日志文件没有被删除,只有已删除数据文件被删除。
- 仍然被当前表版本使用的数据文件没有被删除。
VACUUM 语法和示例
当对表进行清理时,可以不带任何参数地指定 VACUUM 命令,以清理不再被默认保留期内的版本所需的文件。还可以使用 RETAIN num HOURS 参数来清理不再被大于参数中指定小时数的版本所需的文件。
要清理表,指定表名或文件路径,然后添加任何额外的参数:
sql
%sql
--vacuum files not required by versions older than the default retention period
VACUUM taxidb.tripData;
VACUUM './chapter06/YellowTaxisDelta/'; --vacuum files in path-based table
VACUUM delta.`./chapter06/YellowTaxisDelta/`;
--vacuum files not required by versions more than 100 hours old
VACUUM delta.`./chapter06/YellowTaxisDelta/` RETAIN 100 HOURS;
在尝试清理表之前,我们还可以运行带有 DRY RUN 参数的 VACUUM 命令,以查看要删除的文件列表,然后再执行实际的删除操作:
css
%sql
VACUUM taxidb.tripData DRY RUN --dry run to get the list of files to be deleted
输出(仅显示相关部分):
bash
+--------------------------------------------------------------------+
| path |
+--------------------------------------------------------------------+
| dbfs:/xxx/chapter06/YellowTaxisDelta/part-xxxx.c000.snappy.parquet |
| dbfs:/xxx/chapter06/YellowTaxisDelta/part-xxxx.c000.snappy.parquet |
| dbfs:/xxx/chapter06/YellowTaxisDelta/part-xxxx.c000.snappy.parquet |
+--------------------------------------------------------------------+
您可以从输出中看到,如果运行VACUUM,将从Delta表taxidb.tripData中删除的文件列表。 VACUUM还会提交到Delta事务日志,这意味着您还可以使用DESCRIBE HISTORY查看先前的VACUUM提交和operationMetrics:
sql
%sql
DESCRIBE HISTORY taxidb.tripData --view the previous vacuum commit(s)
输出(仅显示相关部分):
sql
+--------------------+-------------------------+---------------------------+
|version| operation | operationParameters | operationMetrics |
+--------------------+-------------------------+---------------------------+
| x |VACUUM END |{"status": "COMPLETED"} | {"numDeletedFiles": "100" |
| | | | "numVacuumedDirectories": |
| | | | "1"} |
+-------+------------+-------------------------+---------------------------+
| x |VACUUM START|{"retentionCheckEnabled":| |
| | |"true"...} | {"numFilesToDelete": |
| | | | "100"} |
+--------------------+-------------------------+---------------------------+
在输出中,请注意operationParameters中的retentionCheckEnabled是true还是false。您还会注意到operationMetrics显示了已删除的文件数和已清理的目录数。
多久应该运行VACUUM和其他维护任务?
建议定期在所有表上运行VACUUM,而不是在主要的ETL工作流之外,以减少多余的云数据存储成本。没有确切的科学方法来指示您应该多久运行VACUUM。相反,您对运行频率的决策应主要基于您的预算存储成本以及业务和法规需求。强烈建议安排一个定期的维护作业来运行VACUUM,以适当满足这些因素。
这个维护作业,还可以包括其他文件清理操作,如OPTIMIZE,应该作为主要ETL工作流之外的一个单独工作流运行,原因有以下几点:
- 资源利用 文件清理操作可能会占用大量资源,并且可能与主要工作流竞争资源,导致整体性能下降。因此,您应该在非高峰时段之外指定维护时间窗口。这些类型的操作还需要不同的集群大小建议,例如自动缩放,而不是通常使用固定集群大小的常规工作流。关于集群大小建议,您将在本章末尾阅读更多内容。
- 隔离 最好将执行文件清理和合并的过程隔离,以便它们能够独占Delta表,避免潜在的冲突。
- 监控 通过隔离这些进程,更容易监视性能,以便跟踪进展和资源消耗进行调整。隔离的过程还减少了并行运行进程时的任何调试复杂性,并且更容易识别任何瓶颈。
通过为VACUUM等维护任务安排单独的工作流,您可以更好地管理资源、隔离、监视和整体控制您的作业和工作流。
与维护作业的频率相关的一个重要设置是默认的保留期。VACUUM的默认保留阈值为七天。您可以根据需要随时增加Delta表的保留阈值。不建议降低保留阈值。即使您定期在所有表上运行VACUUM,它也只会根据表的保留设置删除有资格删除的数据文件。设置较高的阈值可以让您查看表的更大历史记录,但会增加存储在云提供商那里的数据文件数量,导致更高的存储成本。因此,始终需要平衡保留阈值与需求和预算。
VACUUM警告和注意事项
虽然VACUUM旨在是一种低影响操作,可以在不中断正常数据操作的情况下执行,但还有一些要考虑的事项,以避免冲突或甚至损坏表格:
不建议将保留间隔设置为小于七天。如果在具有较短保留间隔的表上运行VACUUM,仍然处于活动状态的文件,例如需要读取器或写入器的未提交文件,可能会被删除。如果VACUUM删除了未提交的文件,可能会导致读取操作失败,甚至损坏表格。
Delta Lake具有安全检查,以防止您运行危险的VACUUM命令。如果您确定在此表上没有执行长于您计划指定的保留间隔的操作,可以通过设置Spark配置属性spark.databricks.delta.retentionDurationCheck.enabled = false来关闭此安全检查。
如果将spark.databricks.delta.retentionDurationCheck.enabled设置为false,必须选择一个长于最长运行的并发事务和任何流落后于表格的最新更新的最长时间的间隔。不要禁用spark.databricks.delta.retentionDurationCheck.enabled并运行配置为RETAIN 0 HOURS的VACUUM。
如果运行VACUUM RETAIN num HOURS,则必须将RETAIN num HOURS设置为大于或等于保留期间。否则,如果spark.databricks.delta.retentionDurationCheck.enabled = true,将收到错误。如果您确定在此表上没有执行操作,例如INSERT/UPDATE/DELETE/OPTIMIZE,可以通过设置spark.databricks.delta.retentionDurationCheck.enabled = false来关闭此检查,以避免异常错误。
当在Delta表上运行VACUUM时,它会从底层文件系统中删除以下文件: 任何不由Delta Lake维护的数据文件,忽略以下划线开头的目录,比如_delta_log。如果您在Delta表目录中存储其他元数据,比如结构化流检查点,您将在第8章中了解更多信息,使用目录名称,如_checkpoints。
过时的数据文件(不再由Delta表引用的文件),其年龄大于保留期。由于吸尘删除文件,因此这个过程可能需要一些时间,具体取决于表的大小和要删除的文件数量。
定期运行OPTIMIZE以消除小文件,并减少需要删除的文件数量。当您将OPTIMIZE与定期运行的VACUUM相结合时,您可以确保最小化陈旧数据文件的数量。
在运行VACUUM之后,失去了回溯到保留期之前版本的能力。
将已删除文件的保留期设置为日志保留期,以维护可以回溯到整个历史记录的完全兼容性。
这意味着如果您使用默认设置运行VACUUM,则只能从运行VACUUM的时间起回溯七天的时间。
根据需要识别和删除多余文件的数量,VACUUM命令可能需要一段时间来执行。为了优化Spark集群的成本和性能,建议使用根据VACUUM执行操作的以下步骤自动调整大小和配置的集群:
VACUUM操作的第1步使用Spark集群上的工作节点标识未使用的文件,而驱动节点保持空闲。因此,应使用至少8个内核的1到4个工作节点。
VACUUM操作的第2步使用Spark集群上的驱动节点删除已识别的文件。因此,为了避免内存不足错误,应使用具有8到32个内核的驱动节点。
更改数据源
到目前为止,在本章中,您已经学到,通过数据和文件保留,时间旅行使您能够在特定时间点遍历数据的不同版本。但是,时间旅行不会跟踪行级更改,或者说不会跟踪如何在不同版本之间插入、更新或删除行级数据。Delta Lake提供了更高效的方式来查看不同版本中的这些更改,而不仅仅是比较整个表的不同版本。跨版本高效跟踪行级更改的功能称为Change Data Feed (CDF)。
启用Delta表的CDF后,Delta Lake会记录写入表的所有数据的"更改事件"。这包括行数据和元数据,指示指定的行是插入、删除还是更新的。下游消费者还可以使用SQL和DataFrame API以及.readStream在批量查询和流查询中读取更改事件。您将在第9章中详细了解流查询如何使用CDF。
有了CDF,您可以捕获对数据的更改,而无需处理Delta表文件中的每个记录或查询整个表的版本。因此,如果只有一个记录发生更改,您不再需要读取文件或表中的所有记录。CDF存储在一个名为_change_data的单独目录中,与_delta_log并排,用于维护Delta表文件的更改。CDF支持多种用例,可以补充Delta Lake的时间旅行和版本管理:
- ETL操作
识别和处理仅在操作后需要行级更改的记录,可以极大地加快和简化ETL操作。在ETL操作中,增量加载记录至关重要,对于任何高效的ETL流程都是必不可少的。
例如,如果您有一个包含所有用于报告的销售订单信息的大型非规范化表,该表是通过从多个上游表连接创建的,您希望避免每次处理表时处理所有记录。有了CDF,您可以跟踪来自上游表的行级更改,以确定哪些信息或销售订单记录是新的、已更新或已删除,并随后使用它来增量处理包含销售订单信息的表。
- 传递更改给下游消费者
其他下游系统和消费者,如Spark Structured Streaming或Kafka,可以使用CDF来处理数据。例如,流查询,您将在第8章中详细了解,可以读取更改源以流式传输数据进行近实时分析和报告。
如果您有一个事件驱动的应用程序,事件流平台,如Kafka,可以读取更改源并触发下游应用程序或平台的操作。例如,如果您拥有一个电子商务平台,Kafka可以读取更改源并根据捕获在Delta表中的产品库存更改来触发平台上的近实时操作。
- 审计跟踪表
CDF相比于时间旅行提供了更高的效率,特别是在随时间查询行级数据的更改方面,以便您可以轻松查看已更新或删除的数据以及时间。这提供了数据的完整审计跟踪。
许多法规要求某些行业跟踪这些行级更改,并保留完整的审计跟踪。例如,在医疗保健领域,HIPAA和审计控制要求系统跟踪围绕电子受保护健康信息(ePHI)的活动或更改。2 Delta Lake中的CDF有助于支持跟踪更改的法规要求。
启用CDF
我们可以通过设置以下的 Spark 配置属性,为所有新的表启用 CDF (Change Data Feed):
ini
%sql
set spark.databricks.delta.properties.defaults.enableChangeDataFeed = true
如果您不希望为环境中的所有表启用 CDF (Change Data Feed),您可以在创建表或更改现有表时使用表属性来指定它。在本章的开头,当我们执行"Chapter Initialization" notebook 时,创建了一个包含有关乘客数量和供应商(或出租车)的车费信息的外部 Delta 表。在接下来的示例中,可以在"02 - Change Data Feed" notebook 中找到,我们创建了一个新表并启用了 CDF:
sql
%sql
--create new table with change data feed
CREATE TABLE IF NOT EXISTS taxidb.tripAggregates
(VendorId INT, PassengerCount INT, FareAmount INT)
TBLPROPERTIES (delta.enableChangeDataFeed = true);
--alter existing table to enable change data feed
ALTER TABLE myDeltaTable SET TBLPROPERTIES (delta.enableChangeDataFeed = true);
修改 CDF 中的记录
为了演示 CDF,首先让我们在刚刚创建的 taxidb.tripAggregates 表中插入、更新和删除一些数据,以便我们稍后查看 CDF:
sql
%sql
--insert record in the table
INSERT INTO taxidb.tripAggregates VALUES
(4, 500, 1000);
--update records in the table
UPDATE taxidb.tripAggregates SET TotalAmount = 2500 WHERE VendorId = 1;
-- delete record in the table
DELETE FROM taxidb.tripAggregates WHERE VendorId = 3;
现在表格已经发生了变化,CDF 已经捕获了行级别的更改。如果我们查看 Delta 表格存储的位置,我们会注意到新的 _change_data 目录:
bash
%sh
ls -al /dbfs/mnt/datalake/book/chapter06/TripAggregatesDelta/
输出(仅显示相关部分):
diff
drwxrwxrwx 2 root _change_data
drwxrwxrwx 2 root _delta_log
-rwxrwxrwx 1 root part-00000-...-c000.snappy.parquet
-rwxrwxrwx 1 root part-00000-....snappy.parquet
-rwxrwxrwx 1 root part-00000-...-c000.snappy.parquet
-rwxrwxrwx 1 root part-00001-....snappy.parquet
既然我们可以看到新的 _change_data 目录,我们可以查看该目录以查看包含数据更改的数据文件:
bash
%sh
ls -al /dbfs/mnt/datalake/book/chapter06/TripAggregatesDelta/_change_data
输出:
diff
-rwxrwxrwx 1 root cdc-00000-....snappy.parquet
-rwxrwxrwx 1 root cdc-00001-....snappy.parquet
_change_data 目录是另一个元数据目录,其中包含数据文件中的变更捕获数据。未来每次对数据进行更改时,都会同时更新当前版本的数据文件以及_change_data目录中的文件。需要注意的是,CDF目录仅在_change_data目录中存储更新和删除操作,对于插入操作,直接从事务日志中计算CDF更加高效。这并不意味着插入操作不会被捕获在CDF中,它们只是不存储在_change_data目录中。
查看CDF(Change Data Feed)
为了帮助识别发生的行级更改,CDF(Change Data Feed)包含附加的元数据以标记事件类型和提交信息。以下表显示了附加的元数据列的模式。
利用来自CDF的附加元数据列,我们可以轻松查看表的行级更改。为了查看这些更改和CDF的元数据列,我们可以使用TABLE_CHANGES(table_str, start [, end]) SQL 命令。以下表详细说明了此命令的参数。
现在回到taxidb.tripAggregates表,已经执行了多个DML操作来插入、更新和删除现有表中的数据。您可以指定一个版本或时间戳,类似于时间旅行,来使用TABLE_CHANGES() SQL 命令查看表格更改:
sql
%sql
SELECT *
FROM table_changes('taxidb.tripAggregates', 1, 4)
ORDER BY _commit_timestamp
输出:
sql
+----------------+-------------+-------------------+-----------------+
| PassengerCount | FareAmount | _change_type | _commit_version |
+----------------+-------------+-------------------+-----------------+
| 1000 | 2000 | update_preimage | 2 |
+----------------+-------------+-------------------+-----------------+
| 1000 | 2500 | update_postimage | 2 |
+----------------+-------------+-------------------+-----------------+
| 7000 | 10000 | delete | 3 |
+----------------+-------------+-------------------+-----------------+
| 500 | 1000 | insert | 4 |
+----------------+-------------+-------------------+-----------------+
+----------+------------------------+
| VendorId | _commit_timestamp |
+----------+------------------------+
| 1 | 2023-07-09T19:17:54 |
+----------+------------------------+
| 1 | 2023-07-09T19:17:54 |
+----------+------------------------+
| 3 | 2023-07-09T19:17:54 |
+----------+------------------------+
| 4 | 2023-07-09T19:17:54 |
+----------+------------------------+
在这个示例中,当查看行级更改时,您可以通过查看_commit_version来看到特定记录何时插入、更新或删除的版本。_change_type指示记录上的操作类型,对于已更新的记录,请注意它指示更新前的行级数据,由update_preimage指示,以及更新后的行级数据,由update_postimage指示。
您还可以使用DataFrame API查看相同的表格更改,方法是使用.option()方法,并将"readChangeFeed"设置为"true":
lua
%python
# view CDF table changes using versions
spark.read.format("delta") \
.option("readChangeFeed", "true") \
.option("startingVersion", 1) \
.option("endingVersion", 4) \
.table("taxidb.tripAggregates")
# view CDF table changes using timestamps
spark.read.format("delta")\
.option("readChangeFeed", "true")\
.option("startingTimestamp", "2023-01-01 00:00:00")\
.option("endingTimestamp", "2023-01-31 00:00:00")\
.table("taxidb.tripAggregates")
现在,如果我们想要查看记录的审计轨迹并了解其随时间的变化,我们可以简单地使用CDF和TABLE_CHANGES()来高效地捕获这些信息。例如,如果我们想要查看taxidb.tripAggregates中特定供应商的值随时间的变化,比方说WHERE VendorId = 1,我们可以使用以下查询:
sql
%sql
SELECT *
FROM table_changes('taxidb.tripAggregates', 1, 4)
WHERE VendorId = 1 AND _change_type = 'update_postimage'
ORDER BY _commit_timestamp
输出:
diff
+------------+-------------+------------------+-----------------+
| FareAmount | TotalAmount | _change_type | _commit_version |
+------------+-------------+------------------+-----------------+
| 10000 | 25000 | update_postimage | 2 |
+------------+-------------+------------------+-----------------+
+----------+-----------------------------+
| VendorId | _commit_timestamp |
+----------+-----------------------------+
| 1 | 2023-07-09T19:17:54.000+000 |
+----------+-----------------------------+
这提供了一个特定供应商的数据随时间如何更新的审计记录。虽然这是一个简单的示例,但你可以看到对于具有许多不断更新的值的大型表格,这可能非常强大且高效,比起使用时间旅行功能。
或者,假设我们想执行时间序列分析,查看自特定时间点以来有多少新供应商已经添加(假设表格的粒度是VendorId),以及他们的FareAmount自那时以来生成了多少。我们可以使用一个WHERE子句来指定这些信息并高效地读取CDF:
sql
%sql
SELECT *
FROM table_changes('taxidb.tripAggregates', '2023-01-01')
WHERE VendorId = 1 AND _change_type = 'insert'
ORDER BY _commit_timestamp
输出:
lua
+------------+-------------+------------------+-----------------+
| FareAmount | TotalAmount | _change_type | _commit_version |
+------------+-------------+------------------+-----------------+
| 500 | 1000 | insert | 4 |
+------------+-------------+------------------+-----------------+
+----------+-----------------------------+
| VendorId | _commit_timestamp |
+----------+-----------------------------+
| 4 | 2023-07-09T19:17:54.000+000 |
+----------+-----------------------------+
这演示了你可以查看表格的更改,具体来说是自2023年01月01日以来的更改,条件是WHERE VendorId = 1 AND _change_type = 'insert'。
CDF是一种高效且强大的功能,可以捕捉数据随时间的更改。这可以与其他ETL操作一起使用,以轻松构建类型2的缓慢变化维度,或者可以在MERGE、UPDATE或DELETE操作后仅处理行级更改,以加速ETL操作并逐渐将数据加载到下游。如前所述,CDF还有其他用例,其中之一是流处理,这将在第9章中详细介绍。
CDF警告和注意事项
虽然CDF是一个强大的功能,但还有一些需要考虑的事项:
- 更改记录遵循与表的数据文件相同的保留策略。这意味着如果CDF文件超出表的保留策略,它将成为VACUUM的候选对象,并在运行VACUUM时将被删除。
- CDF不会对表处理造成显著的额外开销,因为一切都是内联生成的,作为DML操作。通常,写入_change_data目录中的数据文件比表操作的重写文件的总大小要小得多,因为它们只包含对已更新或已删除的记录的操作。如前所述,插入到表中的记录不会在_change_data目录中捕获。
- 由于更改数据与其他操作一起发生,因此更改数据在新数据提交后立即可用并在表中可用。
- CDF不记录发生在启用CDF之前的记录更改。
- 一旦为表启用了CDF,您将无法再使用Delta Lake 1.2.1或更低版本写入表,但仍然可以读取表。
总之,虽然CDF是一个有用的功能,但在使用它时需要考虑这些事项,以确保它与您的需求和工作流程相匹配。
总结
在本章中,您了解了Delta Lake如何使用版本控制,从而使您能够在特定时间点遍历数据的不同版本,并使用CDF跟踪数据随时间的逐行更改。Delta Lake中的时间旅行和CDF是强大的功能,允许用户跟踪数据的变化,可以用于启用下游消费者、ETL操作、时间序列分析、版本控制、审计和数据管理等各种用例。
在了解了如何轻松还原或查询表的以前版本之后,您了解到可以使用版本号或时间戳来回滚、审计和满足各种用例。通过使用诸如DESCRIBE HISTORY之类的命令,您可以轻松查看表的提交历史。所有这些都是通过交易日志和文件保留实现的。如果要更改默认设置,您可以使用Spark配置或表属性进一步定义这些保留设置,用于日志和数据文件。然后,由于数据文件不会被自动删除,您可以使用VACUUM命令删除旧数据文件。
为了补充时间旅行,Delta Lake还为表提供了Change Data Feed(CDF)。这个功能允许您以高效的方式捕获逐行的变化,而不需要比较每个表历史的整个版本来识别变化。
通过使用内置的Delta Lake功能,如时间旅行、保留、维护和CDF,您可以节省宝贵的时间和资源,并满足法规和审计要求。这些功能是通过_change_data目录以及更重要的是交易日志实现的。在下一章中,您将了解更多关于交易日志如何存储表的模式并使用它来更新和修改表模式的内容。