本章探讨了使用 Delta Lake 进行数据管理的基本操作。由于 Delta Lake 既作为存储层,也参与数据应用的交互层,因此从持久化存储系统的基础操作开始是非常合理的。你已经知道 Delta Lake 提供 ACID 保证,然而,重点关注 CRUD 操作(见图 3-1)将更多地引导我们思考“如何使用 Delta Lake?”如果这就是你需要了解的全部内容,那么故事会显得非常简短(因此本书也会很短),然而这远远不足以完全掌握 Delta Lake。因此,我们还将探讨与 Delta Lake 表交互时至关重要的几个额外内容:合并操作、从所谓的原始 Parquet 文件转换以及表的元数据。
注意:
除非另有说明,SQL 语法将指代 Spark SQL 语法,为了简化起见。如果你使用的是 Trino 或其他 SQL 引擎与 Delta Lake 进行交互,可以在第 4 章中找到更多关于 Delta Lake 生态系统的详细信息,或者查阅相关文档。所有 Python 示例都将使用基于 Spark 的 Delta Lake API,同样的例子也会在其中呈现。如果需要,使用 PySpark 进行等效操作也是可行的,相关示例将在合适的地方展示。
我们可以通过 Delta Lake 表的顶级目录路径或通过目录(例如 Apache Spark 常用的 Hive Metastore 或更高级的 Unity Catalog)来执行操作。在本章中,你将看到这两种方法的使用;选择使用哪种方法主要取决于个人偏好和你所使用系统的功能。通常来说,如果你的环境中有可用的目录,它可以简化代码的可读性以及未来可能的事务(比如如果你更改了表的位置)。需要注意的是,如果你使用目录,你可以为数据库对象或每个表单独设置位置。
创建
在执行其他操作之前,你需要创建一个表,这样才能进行交互。实际的创建操作可以通过不同的方式进行,因为许多引擎会在某些操作(例如 Spark SQL 中的附加操作)过程中自动创建一个不存在的表。那么,“在这个过程中创建了什么?”你可能会问。核心是,Delta Lake 如果没有 Parquet 就无法存在,因此你会看到创建的是 Parquet 文件目录和数据文件,仿佛你用 Parquet 创建了这个表。然而,你应该注意到的一项新内容是另一个名为 _delta_log 的文件。
创建 Delta Lake 表
要创建一个空的 Delta Lake 表,你需要定义表的 schema。在 SQL 中,这看起来就像任何数据库表的定义,只不过你还需要通过添加 USING DELTA 参数来指定表是基于 Delta Lake 的:
-- SQL
CREATE TABLE exampleDB.countries (
id LONG,
country STRING,
capital STRING
) USING DELTA;
注意
本章的所有示例和其他支持代码可以在本书的 GitHub 仓库中找到。
在 Python 中,你将从 DeltaTable.create 方法返回的 TableBuilder 对象开始,然后添加表名和列的定义。execute 命令将定义合并为查询计划并执行:
# Python
from pyspark.sql.types import *
from delta.tables import *
delta_table = (
DeltaTable.create(spark)
.tableName("exampleDB.countries")
.addColumn("id", dataType=LongType(), nullable=False)
.addColumn("country", dataType=StringType(), nullable=False)
.addColumn("capital", dataType=StringType(), nullable=False)
.execute()
)
无论是 SQL 还是 Python 定义表,过程本质上只是创建一个命名的表对象,并指定列名和类型。你可能在 Python 语法中注意到的另一个元素是,我们还可以指定 Apache Spark 中的可空性(nullability)。但是,这个设置对 Delta Lake 表来说是无效的,因为它只适用于 JDBC 数据源。在 SQL 中,另一个常见的创建语句是 IF NOT EXISTS 限定符,或者在 Python 中使用 createIfNotExists 方法。它们的使用完全取决于你的选择。
注意
本章中的许多示例假定通过目录访问的表对象是可用的,但大多数核心操作也很好地支持直接文件访问方法。对于 Spark SQL,主要的区别在于,它使用路径访问器 delta.<TABLE>(注意反引号)来代替表名。对于 DeltaTable API,你通常只需要将 forPath 方法替换为 forName。在 PySpark 中,有时你也需要使用其他方法,例如使用带路径参数的 save 方法代替带表名的 saveAsTable 方法。如有需要,参见 Delta Lake 的 Python 文档,了解在某些情况下基于路径的访问可能需要配置的额外细节(例如,云服务提供商特定的安全配置参数)。
将数据加载到 Delta Lake 表中
假设你已经有了一个 Delta 表,最常见的操作将包括读取或写入数据。自然地,在你从表中读取数据之前,可能需要先写入数据,以便有数据可以读取。这引出了使用 SQL 和 Python API 与 Delta Lake 的一个显著差异。无论使用哪种方法,你都需要先定义表,然后将行插入到该表中,但两者的语法有所不同。在 SQL 中,你需要使用 INSERT 语句,而在 Python 中,你可以使用类似的 insertInto 方法,或者使用 Spark 的追加操作。
INSERT INTO
当你有一个空的 Delta Lake 表时,可以使用 INSERT INTO 命令将数据加载到表中。这个操作的思路是定义数据的插入位置,然后通过为每一行提供 VALUES 来定义插入的内容:
-- SQL
INSERT INTO exampleDB.countries VALUES
(1, 'United Kingdom', 'London'),
(2, 'Canada', 'Toronto')
在 PySpark DataFrame 语法中,你只需要通过 insertInto 指定将记录插入到特定表中作为写入操作的目标(注意,列是按位置对齐的,因此该方法不会考虑列名):
# Python
data = [
(1, "United Kingdom", "London"),
(2, "Canada", "Toronto")
]
schema = ["id", "country", "capital"]
df = spark.createDataFrame(data, schema=schema)
(
df
.write
.format("delta")
.insertInto("exampleDB.countries")
)
有时,你可能已经有了需要的数据(具有相同的 schema 和列名),而这些数据可能存储在其他格式(如 CSV 或 Parquet)中。你可以指定数据源为文件,并从中选择数据,甚至直接指定另一个表。通过 SELECT 语句,这些数据可以替代 INSERT INTO 操作中的 VALUES 参数。你需要指定从新数据源选择哪些列,或者指定选择整个表,像这样:
-- SQL
INSERT INTO exampleDB.countries
SELECT * FROM parquet.`countries.parquet`;
这提供了一种将已有数据附加到 Delta Lake 表的方式。另一种方法是通过 Spark DataFrame 写入操作的追加模式。
追加
除了使用 DataFrame 的 insertInto 方法外,我们还可以通过追加模式将新数据添加到 Delta Lake 表中。在 SQL 中,这通常作为 INSERT INTO 操作的一部分进行,但在 DataFrameWriter 中,你需要显式地设置写入模式,通过 .mode(append) 或其更长的写法 .option("mode", "append")。这会告诉 DataFrameWriter 你只是在向表中添加额外的记录。当设置为追加模式并且表已经存在时,数据会被追加到表中;如果表之前不存在,则会创建该表:
# Python
# 示例数据
data = [(3, 'United States', 'Washington, D.C.') ]
# 定义 Delta 表的 schema
schema = ["id", "country", "capital"]
# 创建 DataFrame
df = spark.createDataFrame(data, schema=schema)
# 将 DataFrame 写入 Delta 表,采用追加模式
# (如果表不存在,它将被创建)
(df
.write
.format("delta")
.mode("append")
.saveAsTable("exampleDB.countries")
)
警告
如果没有设置模式,Delta Lake 默认假定你正在创建一个新表,但如果该表已经存在,则会收到以下错误信息:
AnalysisException: [TABLE_OR_VIEW_ALREADY_EXISTS] Cannot create table or view `exampleDB`.`countries` because it already exists.
你可以选择不同的表名,删除或替换现有对象,添加 IF NOT EXISTS 子句来容忍预先存在的对象,或者添加 OR REFRESH 子句来刷新现有的流表。
对于 PySpark 用户来说,这是将数据追加到表中的最常见方法,因为它提供了更多的灵活性,以便在开发的不同阶段指定不同的写入模式。与 insertInto 方法不同,它还使用表的规范来对齐来自传入 DataFrame 的列名。
使用 CREATE TABLE AS SELECT
在 PySpark 中的追加操作中,我们注意到,如果你将数据追加到一个尚不存在的表中,则该表会被创建。在 SQL 中使用 INSERT INTO 时,情况并非如此。你必须在尝试插入数据之前,首先定义目标表。要使行为更像追加操作,可以使用 CTAS(Create Table As Select)语句,将创建表和插入数据合并为一个操作:
-- SQL
CREATE TABLE exampleDB.countries2 AS
SELECT * FROM exampleDB.countries
使用 CTAS 语句创建表为表定义提供了一些额外的简便性。最大的好处之一是,如果你不需要对列类型或其他列级信息进行精细控制,你可以跳过定义 schema 的步骤。无论你选择使用这种方法还是标准的 CREATE 和 INSERT 操作,通常它们会产生相同的结果。两者的主要区别在于所需的事务数量不同,它们在事务日志中的表现也会有所不同。
事务日志
当你创建一个 Delta 表时,会在 Parquet 结构中生成一个名为 _delta_log 的子目录。这个目录就是事务日志,它追踪表的所有变更历史。如果你检查 _delta_log 目录的文件结构,你会发现它包含 JSON 文件:
!tree countries.delta/_delta_log
countries.delta/_delta_log
└── 00000000000000000000.json
这些文件记录了所有对表进行操作并产生变化的记录(即,非读取操作)。每次创建、插入或追加操作都会向事务日志中添加一个 JSON 文件,并增加表的版本号。事务日志的具体结构因实现方式不同而有所差异,但你通常会在事务记录中找到关于表创建的信息(例如,用于创建表的处理引擎、记录的数量或其他写入操作的度量),维护操作的记录,以及删除信息。
尽管这看起来可能是个小细节,但你应该明白,事务日志是 Delta Lake 的核心组件。有人甚至认为事务日志就是 Delta Lake。事务记录以及处理引擎与它的交互方式是 Delta Lake 与 Parquet 区别开来的地方,也是它能够提供 ACID 保证、精确一次流处理以及 Delta Lake 所提供的所有其他功能的根源。一个例子就是时间旅行(time travel),这将在下一节中进行描述。
具体细节可能因你使用 Delta Lake 的位置和方式而有所不同,但关键要点是,你需要知道事务日志的存在以及如何找到它。由于事务日志中通常包含丰富的信息,你可能会发现它是调查流程、诊断错误和监控数据管道健康状况的宝贵工具。不要忽视你手头的这些信息!
读取
读取是数据处理中的基础操作,以至于几乎可以假设它不需要特别关注。然而,关于从 Delta Lake 表中读取数据,还是有一些值得关注的内容,包括对分区过滤工作原理的高层理解(这一点将在第 5 章和第 10 章中更深入探讨)以及事务日志如何允许查询历史版本的数据视图(即时间旅行)。
从 Delta Lake 表中查询数据
就像你过去可能接触过的丰富的数据库系统一样,你可以使用许多 SQL 技巧来操作从 Delta Lake 表中读取的数据。然而,要掌握更高级的实践,关键是理解所有操作都建立在基本的读取操作之上。在 SQL 中,这通常以 SELECT 语句的形式呈现,而在 DeltaTable API 中,你将加载一个对象并将其转换为 Spark DataFrame:
-- SQL
SELECT * FROM exampleDB.countries
# Python
from delta.tables import DeltaTable
delta_table = DeltaTable.forName(spark, "exampleDB.countries")
delta_table.toDF()
这两种方法都会返回表中所有的记录,输出的限制仅受你使用的处理引擎(例如,Spark 默认的 show 输出 20 条记录)影响。通常,你可能只想选择数据的一个子集;这可以是单条记录、整个文件分区或来自表中不同位置的任意集合。为了实现这一点,你只需要在查询或 DataFrame 定义中添加一个过滤条件:
-- SQL
SELECT * FROM exampleDB.countries
WHERE capital = "London"
# Python
delta_table_df.filter(delta_table_df.capital == 'London')
这将返回表中所有符合过滤条件的结果。即使没有符合条件的值,也不会出错。如果指定的过滤条件值在表中不存在,结果将是没有返回任何记录。在 Spark 中,这将是一个空的 DataFrame 对象,仍然保留来源表的模式。你还可以选择与过滤器一起选择一部分列,或者仅执行其他操作。为此,只需指定所需的列:
-- SQL
SELECT
id,
capital
FROM
exampleDB.countries
# Python
delta_table_df.select("id", "capital")
虽然操作本身很简单,但在背后有更多的事情发生。与 Parquet 文件类似,Delta Lake 在底层文件中包含了统计信息,以便尽可能提高这些查询的效率。统计信息的数量可以控制,并且因实现方式而异(第 12 章将重点讨论这些统计信息及如何优化性能)。Parquet 本身是列式文件结构,因此读取部分列通常会更高效。事务日志在其中的作用,提供了相较于传统 Parquet 文件的显著优势,特别是在读取历史数据方面。
使用时间旅行进行读取
得益于 Delta Lake 中的事务日志,你可以使用一些额外的表参数来完成一些否则较为困难的任务。通过日志,你可以查看或恢复表的历史版本。这意味着你可以查看表中数据在某一时间点的状态,而无需费力地创建文件备份,也无需依赖本地云服务的备份工具。
注意: 本章中使用的 DeltaTable API 并不直接支持时间旅行。然而,PySpark 用户仍然可以通过 PySpark 访问这一特性。该 API 支持恢复操作,这将在“修复、恢复和替换表数据”一节中讨论。你还会找到一些关于删除数据的更高级操作。鉴于这一限制,时间旅行的等效表达式将在 SQL 和 PySpark 中并行呈现。
要查看表的某个历史版本,在 SQL 中,只需在查询中添加限定符。你可以通过两种方式来指定这一点。一种是使用 VERSION AS OF 来指定特定的版本号。例如,如果你想查看某个特定版本的表中 id 的值,可以结合 DISTINCT 查询与时间旅行到表的版本 1:
-- SQL
SELECT DISTINCT id FROM exampleDB.countries VERSION AS OF 1
# Python
(
spark
.read
.option("versionAsOf", "1")
.load("countries.delta")
.select("id")
.distinct()
)
或者,如果你想查看在当前日期之前存在的记录数量,而不需要检查版本号,你可以使用 TIMESTAMP AS OF,并指定当前日期:
-- SQL
SELECT count(1) FROM exampleDB.countries TIMESTAMP AS OF "2024-04-20"
# Python
(
spark
.read
.option("timestampAsOf", "2024-04-20")
.load("countries.delta")
.count()
)
时间旅行作为一个特性非常有用,但它只是表版本管理的副产品。尽管如此,它确实体现了 Delta Lake 在事务保证和原子性方面对数据提供的保护。因此,时间旅行并不是最终的好处,而是展现了 Delta Lake 中 ACID 事务所提供保护的一扇窗。
更新
能够创建表、向表中添加数据并从中读取数据是非常强大的功能。然而,有时在读取数据时,可能会发现某个名称存在错误。或者,也许与业务系统的集成要求对国家名称进行缩写。
假设你的销售团队决定使用国家的缩写代替全名,因为缩写更短,在销售报告的图表中显示效果更好。在这种情况下,你需要将表中“United Kingdom”的值更新为“U.K.”。为了进行这些更改,你只需要一个 UPDATE 语句,指定你想要更改的内容(使用 SET)以及更改的条件(使用 WHERE 子句):
-- SQL
UPDATE exampleDB.countries
SET country = 'U.K.'
WHERE id = 1;
# Python
delta_table_df.update(
condition = "id = 1",
set = { "country": "'U.K.'" }
)
使用 UPDATE 可以轻松地修复表中的特定值。你也可以通过使用更不具体的过滤条件来更新表中的多个值。完全省略 WHERE 子句将允许你更新整个 Delta Lake 表中的值。每次更新操作都会在事务日志中增加表的版本号。
删除
删除表中的数据是 CRUD 操作中最后一个需要探讨的内容。数据删除可能有多种原因,但一些常见的原因包括删除特定记录(例如,用户的“被遗忘权”)、替换错误或过时的数据(例如,每日表格刷新),或裁剪表格的时间窗口(通常是当相同的数据可能在其他地方可用,但你希望保持一个报告表格或类似的表格来提高性能或作为计算的基础)。对于这些情况,你可能需要显式命令来删除值;而在其他情况下,你也许可以让系统代替你处理删除操作。通常实现删除的两种方法是使用 DELETE 命令或指定覆盖行为。
从 Delta Lake 表中删除数据
从 Delta Lake 表中删除记录所需的就是 DELETE 语句。其功能与 SELECT 语句非常相似,都是通过 WHERE 子句应用过滤条件来选择需要删除的记录。通过这种方式,你可以方便地先用 SELECT 查询,查看即将被删除的记录,然后再切换为 DELETE 查询进行删除。在 Python API 中,你没有这种可以直接交换 SELECT 和 DELETE 查询来轻松查看记录的能力,但可以通过指定删除的条件来实现,条件仍然是一个表达式,指定了操作的匹配标准。使用 Python API 的一个增强功能是,表达式本身可以是包含 SQL 表达式的字符串,也可以使用 PySpark 库中的函数,这为你编写代码提供了更多灵活性:
-- SQL
DELETE FROM exampleDB.countries
WHERE id = 1;
# Python
from pyspark.sql.functions import col
delta_table.delete("id = 1") # 使用 SQL 表达式
delta_table.delete(col("id") == 2) # 使用 PySpark 表达式
警告
在指定 WHERE 子句时要特别小心,以防不小心删除了额外的数据。如果没有包含 WHERE 子句,将会删除表中的所有记录。使用覆盖模式写入表时也应当小心。如果发生此类错误,你将需要恢复表的先前版本(有关如何操作的详细信息,请参见第 5 章)。
从表中删除多条记录与删除单条记录类似。如果表达式中的值匹配多条记录,那么所有这些记录都会被删除。你也可以使用基于不等式的表达式来根据阈值删除数据。例如,像“transaction_date <= date_sub(current_date(), 7)”这样的表达式,可以将表中的值裁剪到仅包含过去一周的数据。删除大量数据通常与将该表中的数据替换为一组全新的记录有关。与其将这个过程分为两个步骤,不如在某些情况下直接用新的数据覆盖现有数据。
在 Delta Lake 表中覆盖数据
Delta Lake 使得覆盖表中的数据变得非常容易。这既是一个特性,也是一项警告。覆盖模式允许你用新的结果集替换表中的数据,无论数据的大小或文件数量如何。唯一的例外是当你指定只覆盖数据的特定分区时,但即使在这种情况下,你也应该明确知道自己在做什么,因为分区也可能包含许多文件,这些文件在过程中可能会被覆盖。考虑到这一点,覆盖表格是一个相当常见的过程,它可以用于定期重新计算时更新表中的记录,当出现错误时,替换表中部分或全部数据,或者当你希望更改表格的结构时。在本节中讨论覆盖操作,因为任何上述方法的核心组成部分都隐含了删除先前存在的数据。与 Delta Lake 交互的不同方法使用不同的方式来覆盖数据。DeltaTable API 有一个独特的 replace 方法,而 PySpark 和 Spark SQL 都有一种方法可以将覆盖设置为操作模式。
replace 方法
在使用 DeltaTable Python API 时,有一些方法可以用来替换表的全部内容。你可以使用 replace 或 createOrReplace 来替换表的内容。这两种方法都是直接处理器,允许你使用相同的 TableBuilder 对象在现有表结构之上定义新的表结构:
# Python
delta_table2 = (
DeltaTable.replace(spark)
.tableName("countries.delta")
.addColumns(data_df.schema)
.execute()
)
使用 DeltaTable API 可以让你通过从名为 data_df 的 DataFrame 中来的列定义来覆盖表的架构。总体而言,如果你已经在使用 Spark,可能会觉得使用 Spark DataFrameWriter 的覆盖模式规格更为方便。
覆盖模式
通过更改 Spark DataFrameWriter 的输出模式来覆盖数据,可以成为快速而高效的替代方法,用于完全替换 Delta Lake 数据集中的部分或全部数据。覆盖模式参数与用于将数据添加到 Delta Lake 表中的追加模式参数类似。在这种情况下,数据不会添加到表的现有数据中,而是当前 DataFrame 的内容将直接替换表中已有的内容。所有之前的数据将被移除,未来只会有当前的数据,除非你恢复到先前的版本:
# Python
(
spark
.createDataFrame(
[
(1, 'India', 'New Delhi'),
(4, 'Australia', 'Canberra')
],
schema=["id", "country", "capital"]
)
.write
.format("delta")
.mode("overwrite") # 指定输出模式
.saveAsTable("exampleDB.countries")
)
使用这种方法,你可以通过只改变一个词来在两种不同的输出模式之间切换,这在开发和测试过程中尤其有用。在 Spark SQL 中,你也可以通过 INSERT OVERWRITE 实现类似的操作。
INSERT OVERWRITE
作为 INSERT INTO 的配套命令,INSERT OVERWRITE 可以与 PySpark DataFrame 语法中的覆盖模式一样使用。这两个基于查询的命令与 PySpark 中的追加模式和覆盖模式功能相同;也就是说,它们允许你在查询中切换 INTO 和 OVERWRITE 参数,而不需要对其他部分做修改:
-- SQL
INSERT OVERWRITE exampleDB.countries
VALUES (3, 'U.S.', 'Washington, D.C.');
与覆盖模式或 replace 方法一样,使用 INSERT OVERWRITE 会删除目标表中的所有先前数据。这意味着在使用时要小心,并确保你知道自己在覆盖什么数据。与 INSERT INTO 命令一样,你在向目标表插入数据时拥有很大的自由度。你可以使用具体的值、其他表或文件作为覆盖目标表的数据来源。
MERGE
在数据处理过程中,组合插入、更新和/或删除操作是非常常见的,因此需要为这些操作创建“快捷方式”。MERGE 就是这样一个很好的例子,它允许你将多个操作链式连接在一起,通过统一的匹配条件来处理一组数据。它使你能够根据匹配程度或不匹配的条件,控制操作的执行。当操作仅限于插入和更新的组合时,MERGE 也通常被称为“upsert”(插入或更新)。这种方法在实际操作中非常有用,因为许多日常的数据工程模式都与 MERGE 行为相契合。
如果你需要向一个表插入很多记录,同时还需要更新先前已经存在的记录,那么你可能需要组合执行多个不同的查询。为此,你需要先识别出哪些记录已经在表中,更新这些记录,然后再插入新的记录。MERGE 让你可以通过基于查询中的匹配条件,将这些操作合并成一个单一的操作。实际上,你可以根据某些值是否已经存在于表的关键列中来指定不同的操作。
在 MERGE 查询中创建组合的方式非常多,但通常情况下,你会定义目标表(你希望进行更改的表)和源数据(比如来自文件或其他表)的匹配标准。通过定义匹配条件,你可以根据源数据中每条记录的匹配状态采取不同的操作:
WHEN MATCHED
当条件匹配时,你可以删除匹配的记录,或者使用整个新记录或指定的列来更新记录。
WHEN NOT MATCHED
当条件不匹配时,你可以插入不匹配的记录,或者使用指定的列插入部分记录。
WHEN NOT MATCHED BY SOURCE
当源数据中的新记录没有匹配目标表中的记录时,你可以删除这些记录,或者使用整个新记录或指定的列进行更新。
在 SQL 中,你只需将这些操作组合在一起,构建整个 MERGE 查询,并将其作为一个单一的语句执行。首先,你指定要合并的目标表、要合并的源数据以及你希望用于匹配的条件。然后,对于 upsert 操作,你只需定义更新操作和插入操作的细节:
-- SQL
MERGE INTO exampleDB.countries A
USING (select * from parquet.`countries.parquet`) B
ON A.id = B.id
WHEN MATCHED THEN
UPDATE SET
id = A.id,
country = B.country,
capital = B.capital
WHEN NOT MATCHED
THEN INSERT (
id,
country,
capital
)
VALUES (
B.id,
B.country,
B.capital
)
在 DeltaTable API 中,你将使用一个名为 DeltaMergeBuilder 的新类来指定这些条件和操作。与 SQL 语法不同,每种匹配状态和随后的操作都有自己的方法可以使用。你可以在文档中找到支持的所有组合的完整列表。我们建议你将多个操作组合在一起,并将它们链接成一个单一的事务,以帮助你更好地分解每条记录的逻辑路径。如果你想进行 upsert 操作并使用包含新记录的 DataFrame,你可以这样操作:首先应用 MERGE 来指定新记录的来源和匹配条件,然后使用 whenMatchedUpdate 和 whenNotMatchedInsert 来处理这两种情况:
# Python
idf = (
spark
.createDataFrame([
(1, 'India', 'New Delhi'),
(4, 'Australia', 'Canberra')],
schema=["id", "country", "capital"]
)
)
delta_table.alias("target").merge(
source = idf.alias("source"),
condition = "source.id = target.id"
).whenMatchedUpdate(set =
{
"country": "source.country",
"capital": "source.capital"
}
).whenNotMatchedInsert(values =
{
"id": "source.id",
"country": "source.country",
"capital": "source.capital"
}
).execute()
总体来说,使用 MERGE 可以帮助你简化原本可能需要多个不同查询和联合逻辑的复杂操作。
其他有用的操作
Delta Lake 还有一些其他重要的操作需要了解。一种是能够简化从其他文件格式迁移到 Delta Lake 的转换操作,另一种是查看与表的元数据相关的功能。这两者对于许多应用场景都非常有价值。
Parquet 转换
即使你将 Delta Lake 作为所有数据操作的底层文件格式,仍然可能会遇到来自遗留系统、第三方提供者或其他来源的数据集,这些数据集使用的是不同的格式。对于几种文件类型,特别是 Parquet 和基于 Parquet 的 Iceberg 格式,有一种简单的转换方法,可以简化一些操作。CONVERT TO DELTA 命令是将 Iceberg 或 Parquet 目录转换为 Delta 表的推荐方法。
常规 Parquet 转换
由于 Delta Lake 表内部由 Parquet 文件组成,事务日志是将 Parquet 表转换为 Delta Lake 表时最大的区别。要为现有的 Parquet 文件创建日志,只需要在 SQL 中运行 CONVERT TO DELTA,或使用 DeltaTable API 的 convertToDelta 方法,并指定目录路径:
-- SQL
CONVERT TO DELTA parquet.`countries.parquet`
# Python
from delta.tables import DeltaTable
delta_table = (
DeltaTable
.convertToDelta(
spark,
"parquet.`countries.parquet`"
)
)
该命令会扫描指定目录中的所有 Parquet 文件,从 Parquet 文件中推断出表的模式,并构建 Delta Lake 的事务日志 _delta_log。如果 Parquet 目录已分区,还需要在 SQL 查询中使用 PARTITIONED BY 参数,或者在 convertToDelta 的额外参数中指定分区列。
Iceberg 转换
与 Delta Lake 类似,Apache Iceberg 内部也由 Parquet 文件组成。是否可以再次使用 CONVERT TO DELTA 来转换 Iceberg 文件?部分支持。DeltaTable API 不支持 Iceberg 转换,但 Spark SQL 支持使用 CONVERT TO DELTA 来转换 Iceberg 文件,但你需要在 Spark 环境中安装对 Iceberg 格式的支持:
-- SQL
CONVERT TO DELTA iceberg.`countries.iceberg`
你可以通过在集群中安装额外的 JAR 文件(如 delta-iceberg)来实现此操作。与 Parquet 文件不同,转换 Iceberg 时不需要指定表的分区结构,因为它会从源数据中推断出这些信息。
关于此转换过程,还有一个有趣的副作用。由于 Iceberg 和 Delta Lake 都维护独立的事务日志,因此通过 Delta Lake 进行的所有新增文件操作不会在 Iceberg 侧注册。然而,由于 Iceberg 日志没有被删除,新的 Delta Lake 表仍然可以作为 Iceberg 表进行读取和访问。
Delta Lake 元数据和历史
通常,你可能希望快速查看与 Delta Lake 表相关的元数据信息。查看表的模式、表的读取或写入版本,或其他可能设置的属性都非常有用。要查看这些信息,你只需要在 Spark SQL 中使用 DESCRIBE DETAIL,或在 DeltaTable API 中使用 detail 方法:
-- SQL
DESCRIBE DETAIL exampleDB.countries
# Python
delta_table.detail()
这将显示所有列出的元数据细节,以及其他信息,例如表的最后修改时间或表中的文件数量。你可以在文档中找到返回的整个模式的参考信息。
同样,你可能不仅希望查看这些元数据的最新值,还可能希望查看每个事务的元数据。要轻松访问存储在事务日志中的信息,这些信息可以提供表随时间变化的丰富历史。这个功能有许多潜在的应用场景,例如监控表的追加操作频率和大小,或检查某次删除的来源。
在这种情况下,你将需要查看表的历史记录,而不是元数据:
-- SQL
DESCRIBE HISTORY exampleDB.countries
# Python
delta_table.history()
与元数据类似,表的历史记录包含大量的不同层次的事务级元数据,并且根据事务的类型,可能会有许多相关的度量指标。你可以在文档中找到关于可用信息的概述。
总结
Delta Lake 的核心操作为在表中创建、读取、更新和删除数据提供了一个强大的交互层,远超传统数据湖的功能。通过支持 ACID 事务、时间旅行、合并操作,以及从 Parquet 和 Iceberg 格式的简单转换,Delta Lake 提供了一个强大的存储和数据管理层。通过理解本章所涵盖的核心操作——从基础的 CRUD 操作到更高级的合并逻辑和事务日志 introspection——你可以有效地使用 Delta Lake 来构建可靠的、高性能的数据管道和应用程序。