生产环境中的Apache Iceberg

559 阅读16分钟

数据工程师负责以高效、可靠和安全的方式收集、存储和处理数据。在将数据投入生产时,他们需要遵循一系列最佳实践,以确保数据的准确性、一致性和可访问性。在本章中,我们将讨论许多用于帮助监控和维护生产环境中的Apache Iceberg表的工具。我们将从讨论Apache Iceberg元数据表开始,您可以利用这些元数据表更好地理解Iceberg表。然后我们将介绍确保数据质量的方法,包括在表或目录级别分支以隔离摄取;目录版本控制以执行多表事务;以及当出现问题时回滚表或目录的状态。

本章讨论的所有实践都可以以被动或主动的方式应用。被动方法是对已经存在的情况做出反应,例如重写已经变得过大的分区或回滚已经摄取了错误数据的表。主动技术尝试预防这些问题的发生,包括在分区大小影响查询性能之前使用元数据表进行监控,以及使用分支隔离摄取以确保在质量检查发生之前没有错误数据进入生产环境。

您将希望同时应用这两种方法,以便预防问题的发生并解决那些潜入的问题。本章中的技术应能在这两种情况下帮助您。

Apache Iceberg元数据表

Apache Iceberg的一个强大功能是能够从其丰富的元数据中生成多个元数据表,这些元数据表可用于帮助监控表的健康状况并诊断瓶颈所在。过去,为了查看Hive等格式的表数据,您必须依赖特定引擎实现的一次性命令,如SHOW PARTITIONS。由于Apache Iceberg将这些数据作为支持SQL的传统表公开,您在处理这些表时可以使用SQL的全部功能,允许排序、聚合、连接等操作,甚至是时间旅行。

SELECT * FROM catalog.table.history AS OF VERSION 1059035530770364194

这些表是在查询时从Apache Iceberg元数据文件中生成的。让我们首先介绍现有的元数据表及其模式,然后讨论它们可以用于监控Apache Iceberg表的新颖方式。请记住,内置的元数据表在不同的查询引擎中使用的语法略有不同。我们将使用Spark、Dremio和Trino的语法来介绍每个相关的元数据表。

在介绍元数据表时,您可以在本文中看到模式概述。在本书的GitHub仓库中,我们还提供了示例表数据。

历史元数据表

历史元数据表记录了表的演变情况。该表中的四个字段提供了关于表历史的独特见解。

第一个字段,made_current_at,表示相应快照成为当前快照的确切时间戳。这为表更改的提交时间提供了精确的时间标记。

接下来,snapshot_id字段作为每个快照的唯一标识符。该标识符使您能够跟踪和引用表历史中的特定快照。

随后,parent_id字段提供了当前快照的父快照的唯一ID。这样有效地绘制了每个快照的谱系,从而便于跟踪表随时间的演变。

最后,is_current_ancestor字段表示快照是否是表当前快照的祖先。这个布尔值(true或false)有助于识别属于表当前状态谱系的快照和那些因表回滚而失效的快照。

表10-1列出了历史元数据表的模式。

表10-1:历史元数据表的模式

字段名称数据类型示例值
made_current_atTimestamp2023-02-08 03:29:51.215
snapshot_idInt5179289226185056830
parent_idInt 或 null5781947345336215154
is_current_ancestorBooleantrue

您可以使用历史元数据表进行数据恢复和版本控制,以及识别表的回滚。通过快照ID,您可以恢复数据并最大限度地减少潜在的数据丢失。如果出现任何问题或错误,用户可以通过参考快照历史检索早期版本的数据。您只需查询表以获取灾难发生前的快照,然后使用快照ID回滚表,使用本章稍后讨论的方法之一。

在以下代码片段中,我们运行一个查询以获取7月11日之前所有快照的snapshot_id,我们可以使用这些快照ID将表回滚到该日期之前的快照。回滚将在本书稍后讨论。

SELECT snapshot_id
FROM catalog.table.metadata_log_entries
WHERE made_current_at < '2023-07-11 00:00:00'
ORDER BY made_current_at ASC

该表中的数据可用于识别表的回滚。这有助于在尝试建立表历史的背景时识别何时采取了恢复操作。要识别表回滚,有两个信号需要在历史表中查找:

  1. 两个或更多快照具有相同的parent_id。
  2. 只有一个快照的is_current_ancestor设置为true(true表示它是当前表历史的一部分)。

例如,根据之前提供的表信息,可以推断在给定的快照ID(296410040247533565和2999875608062437345)处表的历史中有一次回滚。这个结论是基于快照296410040247533565不是当前祖先且与快照2999875608062437345共享一个父快照的事实得出的。

以下代码片段显示了如何从历史元数据表中查询所有条目:

-- Spark SQL
SELECT * FROM my_catalog.table.history;

-- Dremio
SELECT * FROM TABLE(table_history('catalog.table'));

-- Trino
SELECT * FROM "table$history";

元数据日志条目元数据表

元数据日志条目元数据表通过记录表更新期间生成的元数据文件来跟踪表的演变。该表中的每个字段都包含有关特定时间点表状态的重要信息。

timestamp字段记录元数据更新的确切日期和时间。这个时间戳为特定时刻的表状态提供了时间标记。

接下来,file字段指示与特定元数据日志条目对应的数据文件的位置。这个位置作为访问与元数据条目关联的实际数据的参考点。

latest_snapshot_id字段提供元数据更新时最新快照的标识符。这是理解元数据更新时数据状态的有用参考点。

随后,latest_schema_id字段包含创建元数据日志条目时使用的模式ID。这提供了关于元数据更新时数据结构的上下文。

最后,latest_sequence_number字段表示元数据更新的顺序。这是一个递增的计数,有助于跟踪随时间变化的元数据更改的顺序。

表10-2列出了元数据日志条目元数据表的模式。

表10-2:元数据日志条目元数据表的模式

字段名称数据类型示例值
timestampTimestamp2023-07-28 10:43:57.487
fileString…/v1.metadata.json
latest_snapshot_idInt180260833656645300
latest_schema_idInt0
latest_sequence_numberInt1

您可以使用元数据日志条目元数据表找到具有先前模式的最新快照。例如,也许您对模式进行了更改,现在您想回到先前的模式。您将需要找到使用该模式的最新快照,可以通过一个查询来确定,该查询将对每个schema_id的快照进行排名,然后仅返回每个schema_id的最高排名快照:

WITH Ranked_Entries AS (
    SELECT 
        latest_snapshot_id, 
        latest_schema_id, 
        timestamp, 
        ROW_NUMBER() OVER(PARTITION BY latest_schema_id ORDER BY timestamp DESC) as row_num
    FROM 
        catalog.table.metadata_log_entries
    WHERE 
        latest_schema_id IS NOT NULL
)
SELECT 
    latest_snapshot_id,
    latest_schema_id,
    timestamp AS latest_timestamp
FROM 
    Ranked_Entries
WHERE 
    row_num = 1
ORDER BY 
    latest_schema_id DESC;

以下代码片段将查询该表中的所有条目:

-- Spark SQL
SELECT * FROM my_catalog.table.metadata_log_entries;

快照元数据表

快照元数据表对于跟踪数据集的版本和历史记录至关重要。它维护了每个表的每个快照的元数据,代表了特定时间点数据集的一致视图。关于每个快照的详细信息作为更改的历史记录,并描绘了快照创建时的数据集状态。该表包含几个字段,每个字段都有独特的作用。

首先,committed_at字段表示快照创建的精确时间戳,指示快照及其关联的数据状态何时提交。

snapshot_id字段是每个快照的唯一标识符。这个字段对于区分不同的快照以及进行特定操作(如快照检索或删除)至关重要。

operation字段列出了发生的操作类型字符串,如APPEND和OVERWRITE。

parent_id字段链接到快照的父快照的ID,提供关于快照谱系的上下文,并允许重建快照的历史序列。

此外,manifest_list字段提供了快照包含的文件的详细信息。它像一个目录或清单,记录了与给定快照关联的所有数据文件。

最后,summary字段包含有关快照的度量标准,如添加或删除的文件数量、记录数量和其他统计数据,提供了快照内容的一瞥。

表10-3总结了快照元数据表的模式。

表10-3:快照元数据表的模式

字段名称数据类型示例值
committed_atTimestamp2023-02-08 03:29:51.215
snapshot_idInt57897183625154
parent_idInt 或 nullNULL
operationStringappend
manifest_listString…/table/metadata/snap-57897183999154-1.avro
summaryMap/struct{ added-records -> 400404, total-records -> 3000000, added-data-files -> 300, total-data-files -> 500, spark.app.id -> application_1520379268916_155055 }

快照元数据表有许多可能的用途。一个用例是了解向表中添加数据的模式,这对于容量规划或理解数据增长趋势可能很有用。以下SQL查询显示了每个快照添加的总记录数:

SELECT 
    committed_at,
    snapshot_id,
    summary['added-records'] AS added_records
FROM 
    catalog.table.snapshots;

另一个快照元数据表的用例是监控表随时间进行的操作类型和频率,这对于理解表的工作负载和使用模式可能很有用。以下SQL查询显示了随时间进行的每种操作类型的计数:

SELECT 
    operation,
    COUNT(*) AS operation_count,
    DATE(committed_at) AS date
FROM 
    catalog.table.snapshots
GROUP BY 
    operation, 
    DATE(committed_at)
ORDER BY 
    date;

Apache Iceberg中的快照元数据表是管理数据集版本、支持时间旅行查询和执行增量处理、优化和复制的宝贵资源。它使用户能够有效跟踪更改、访问历史状态并高效管理数据集。

以下SQL将允许您查询快照表以查看其所有数据:

-- Spark SQL
SELECT * FROM my_catalog.table.snapshots;

-- Dremio
SELECT * FROM TABLE(table_snapshot('catalog.table'));

-- Trino
SELECT * FROM "table$snapshots";

文件元数据表

文件元数据表展示了表内的当前数据文件,并提供了每个文件的详细信息,从其位置和格式到其内容和分区细节。

第一个字段,content,表示文件中的内容类型,0表示数据文件,1表示位置删除文件,2表示相等删除文件。

接下来,file_path提供每个文件的确切位置。这有助于在需要时访问每个数据文件。

file_format字段指示数据文件的格式,例如它是Parquet、Avro还是ORC文件。

spec_id字段对应于文件遵循的分区规范ID,提供了数据如何分区的参考。

partition字段提供了数据文件特定分区的表示,指示文件中的数据如何划分以优化访问和查询性能。

record_count字段报告每个文件中包含的记录数,提供了文件数据量的度量。

file_size_in_bytes字段提供文件的总大小(以字节为单位),而column_sizes提供各个列的大小。

value_countsnull_value_countsnan_value_counts字段分别提供每列中的非空值、空值和NaN(不是数字)值的计数。

lower_boundsupper_bounds字段包含每列中的最小值和最大值,提供文件中数据范围的重要见解。

key_metadata字段包含特定于实现的元数据(如果有)。

split_offsets字段提供文件分割为较小段以便并行处理的偏移量。

equality_idssort_order_id字段对应于与相等删除文件相关的ID(如果有)以及表的排序顺序ID(如果有)。

表10-4总结了文件元数据表的模式。

表10-4:文件元数据表的模式

字段名称数据类型示例值
contentInt0
file_pathString…/table/data/00000-3-8d6d60e8-d427-4809-bcf0-f5d45a4aad96.parquet
file_formatStringPARQUET
spec_idInt0
partitionStruct{1999-01-01, 01}
record_countInt1
file_size_in_bytesInt597
columns_sizesMap[1 -> 90, 2 -> 62]
value_countsMap[1 -> 1, 2 -> 1]
null_value_countsMap[1 -> 0, 2 -> 0]
nan_value_countsMap[]
lower_boundsMap[1 -> , 2 -> c]
upper_boundsMap[1 -> , 2 -> c]
key_metadataBinarynull
split_offsetsList[4]
equality_idsListnull
sort_order_idIntnull

文件元数据表有许多可能的用例,包括确定是否应重写分区、识别需要数据修复的分区、查找快照的总大小以及获取先前快照的文件列表。

如果分区有许多小文件,可能是压缩以提高性能的好候选,如第4章讨论的那样。以下查询可以帮助您分解每个分区的文件数量和平均文件大小,以识别需要重写的分区:

SELECT 
    partition,
    COUNT(*) AS num_files,
    AVG(file_size_in_bytes) AS avg_file_size
FROM 
    catalog.table.files
GROUP BY 
    partition
ORDER BY 
    num_files DESC, 
    avg_file_size ASC;

某些字段在您的数据中可能不应有空值。使用文件元数据表,您可以识别分区或文件可能有缺失值,而这比扫描实际数据要轻量得多。以下查询返回第三列中有空数据的文件的分区和文件名:

SELECT 
    partition, file_path
FROM 
    catalog.table.files
WHERE 
    null_value_counts['3'] > 0
GROUP BY 
    partition;

您还可以使用文件元数据表汇总所有文件大小,以获得快照的总大小:

SELECT sum(file_size_in_bytes) FROM catalog.table.files;

使用时间旅行,您可以获取先前快照的文件列表:

SELECT file_path, file_size_in_bytes 
FROM catalog.table.files 
VERSION AS OF <snapshot_id>;

Apache Iceberg中的文件元数据表提供了关于单个数据文件的详细信息,使得细粒度数据处理、模式管理、谱系跟踪和数据质量保证成为可能。它是各种用例的宝贵资源,为用户提供了增强的数据理解和控制。以下SQL允许您查询文件表以查看其数据:

-- Spark SQL
SELECT * FROM my_catalog.table.files;

-- Dremio
SELECT * FROM TABLE(table_files('catalog.table'));

-- Trino
SELECT * FROM "table$files";

清单元数据表

清单元数据表详细描述了表的当前清单文件。该表提供了一系列有用的信息,有助于了解表的结构及其随时间的变化。

path 字段提供了存储清单文件的文件路径,便于快速访问文件。length 字段显示了清单文件的大小。

partition_spec_id 字段表示与清单文件关联的分区的规范ID,这对于跟踪分区表中的变化非常有价值。added_snapshot_id 字段提供了添加该清单文件的快照ID,建立了快照和清单之间的联系。

三个计数字段——added_data_files_countexisting_data_files_countdeleted_data_files_count——分别传递了在该清单中添加的新文件数、在先前快照中添加的现有数据文件数以及在该清单中删除的文件数。这三个字段对于理解数据的演变至关重要。

最后,partition_summaries 字段是一个 field_summary 结构体数组,总结了分区级别的统计信息。它包含以下信息:contains_nullcontains_nanlower_boundupper_bound。这些字段指示分区是否包含空值或NaN值,并提供分区内数据的下限和上限。需要注意的是,当文件元数据中没有可用信息时(通常在读取V1表时),contains_nan 字段可能返回null。表10-5总结了清单元数据表的模式。

表10-5:清单元数据表的模式

字段名称数据类型示例值
pathString…/table/metadata/45b5290b-ee61-4788-b324-b1e2735c0e10-m0.avro
lengthInt4479
partition_spec_idInt0
added_snapshot_idInt6668963634911763636
added_data_files_countInt8
existing_data_files_countInt0
deleted_data_files_countInt0
partition_summariesList[[false,null,2019-05-13,2019-05-15]]

利用清单元数据表,用户可以执行各种操作,包括查找需要重写的清单、汇总每个快照添加的文件总数、查找删除文件的快照以及确定表是否排序良好。

通过以下查询,您可以找到哪些清单文件小于清单文件的平均大小,这可以帮助您发现哪些清单文件可以通过 rewrite_manifests 进行压缩:

WITH avg_length AS (
    SELECT AVG(length) as average_manifest_length
    FROM catalog.table.manifests
)
SELECT 
    path,
    length
FROM 
    catalog.table.manifests
WHERE 
    length < (SELECT average_manifest_length FROM avg_length);

您可能会对表中的文件增长速度感兴趣。通过此查询,您可以查看每个快照添加了多少文件:

SELECT 
    added_snapshot_id,
    SUM(added_data_files_count) AS total_added_data_files
FROM 
    catalog.table.manifests
GROUP BY 
    added_snapshot_id;

也许您想监控删除模式,以便遵守清理个人身份信息(PII)的请求。知道哪些快照有删除操作可以帮助您监控需要到期以硬删除数据的快照:

SELECT 
    added_snapshot_id
FROM 
    catalog.table.manifests
WHERE
    deleted_data_files_count > 0;

以下代码片段检查清单的上下限,以查看它们是否排序良好,或者是否需要重写以实现更好的聚类:

SELECT path, partition_summaries 
FROM db.table.manifests;

清单元数据表还可以在管理、分析和优化存储在Apache Iceberg中的数据集方面发挥作用:

-- Spark SQL
SELECT * FROM my_catalog.table.manifests;

-- Dremio
SELECT * FROM TABLE(table_manifests('catalog.table'));

-- Trino
SELECT * FROM "table$manifests";

分区元数据表

Apache Iceberg中的分区元数据表提供了表中数据如何划分为不同、不重叠的区域(称为分区)的快照。每一行代表表内的一个特定分区。

第一个字段,partition,表示实际的分区值,通常基于数据的某些列。这使得数据能够以有意义的方式进行组织,并且使查询处理更加高效,因为数据可以基于特定的分区值进行检索。

接下来是 record_count 字段,指示给定分区内的总记录数。这个指标有助于理解各分区之间的数据分布,并且可以指导重新分区和重新平衡等优化策略。

file_count 字段给出了分区内数据文件的总数。在管理和优化存储时,这一点至关重要,因为过多的小文件会影响查询性能。

最后,spec_id 字段对应于生成该分区的分区规范的ID。分区规范定义了数据如何分割为分区,拥有这个ID有助于理解所使用的分区策略。

值得注意的是,对于未分区的表,分区元数据表将只有一条记录,其中只包含 record_countfile_count 字段,因为未对这些表进行分区。还包括删除文件记录计数和文件计数,分别在 position_delete_record_countposition_delete_file_countequality_delete_record_countequality_delete_file_count 字段中。

虽然分区元数据表提供了分区当前状态的快照,但需要注意的是,删除文件并未应用。因此,在某些情况下,即使所有数据行都已被删除文件标记为删除,分区仍可能列出。表10-6总结了分区元数据表的模式。

表10-6:分区元数据表的模式

字段名称数据类型示例值
partitionList{20211001, 11}
spec_idInt0
record_countInt1
file_countInt1
position_delete_record_countInt0
position_delete_file_countInt0
equality_delete_record_countInt0
equality_delete_file_countInt0

分区元数据表有许多用例,包括查找分区中的文件数量、汇总分区的总大小(以字节为单位)以及查找每个分区方案的分区数量。例如,您可能希望查看分区中有多少文件,因为如果某个特定分区有大量文件,它可能是压缩的候选对象。以下代码实现了这一点:

SELECT partition, file_count 
FROM catalog.table.partitions;

除了查看文件数量之外,您还可能希望查看分区的大小。如果某个分区特别大,您可能希望更改分区方案以更好地平衡分布,如下所示:

SELECT partition, SUM(file_size_in_bytes) AS partition_size 
FROM catalog.table.files 
GROUP BY partition;

随着分区演变,您可能会随着时间的推移拥有不同的分区方案。如果您想了解不同分区方案如何影响数据写入时的分区数量,以下查询应该会有所帮助:

SELECT 
    spec_id,
    COUNT(*) as partition_count
FROM 
    catalog.table.partitions
GROUP BY 
    spec_id;
-- Spark SQL
SELECT * FROM my_catalog.table.partitions;

-- Trino
SELECT * FROM "test_table$partitions";

全部数据文件元数据表

Apache Iceberg中的全部数据文件元数据表提供了有关表中所有有效快照的每个数据文件的详细信息。

第一个字段,content,表示文件的类型。值为0表示数据文件,值为1表示位置删除文件,值为2表示相等删除文件。

file_path 字段是一个字符串,表示数据文件的完整路径。通常包括存储系统位置(例如,s3://my-bucket/folder/subfolder/myfile.xyz)、表名和唯一文件标识符。

file_format 字段表示数据文件的格式。在我们的示例中,它是Parquet,但也可以是其他文件格式,如AVRO或ORC。

spec_id 字段对应于生成该分区的分区规范的ID。

partition 字段表示此数据文件所属的分区。通常基于为表定义的分区方案。

record_count 字段给出了文件内的总记录数,而 file_size_in_bytes 表示数据文件的大小(以字节为单位)。这两个指标对于理解数据量并可以在查询优化策略中使用非常重要。

column_sizes 字段提供了列ID与该列大小(以字节为单位)之间的映射。

value_counts 字段给出了表示数据文件中每列值总数的映射。同样,null_value_countsnan_value_counts 分别提供了每列的空值和NaN值的计数。

lower_boundsupper_bounds 字段是存储数据文件中每列最小值和最大值的映射。这些字段在查询执行期间用于修剪数据。

key_metadata 字段包含特定于实现的元数据。

split_offsets 字段提供了文件内分割点的信息。它是一个长整型值的数组,在分布式处理场景中非常有用,数据文件可以分割成较小的块以进行并行处理。

equality_ids 字段与相等删除相关,有助于识别被相等删除删除的行。

sort_order_id 字段包含用于写入数据文件的排序顺序的ID。

readable_metrics 字段是一个派生字段,提供文件元数据的可读表示,包括列大小、值计数、空值计数以及下限和上限。

请记住,全部数据文件元数据表可能会为每个数据文件生成多个行,因为一个文件可能是多个表快照的一部分。该表有助于在细粒度级别理解数据的状态和组织情况。表10-7总结了全部数据文件元数据表的模式。

表10-7:全部数据文件元数据表的模式

字段名称数据类型示例值
contentInt0
file_pathString…/dt=20210103/00000-0-26222098-032f-472b-8ea5...
file_formatStringPARQUET
spec_idInt0
partitionList{20210102}
record_countInt14
file_size_in_bytesInt2444
column_sizesMap{1 -> 94, 2 -> 17}
value_countsMap{1 -> 14, 2 -> 14}
null_value_countsMap{1 -> 0, 2 -> 0}
nan_value_countsMap{1 -> 0, 2 -> 0}
lower_boundsMap{1 -> 1, 2 -> 20210102}
upper_boundsMap{1 -> 2, 2 -> 20210102}
key_metadataBinaryNULL
split_offsetsList[4]
equality_idsListNULL
sort_order_idInt0
readable_metricsList{{48, 2, 0, null, Benjamin, Brandon}}

全部数据文件元数据表有许多用例,包括查找所有快照中的最大表、查找所有快照的总文件大小以及评估所有快照中的分区。

以下查询首先确保您只有唯一文件,因为同一文件可能有多个记录。然后,它返回这些唯一文件列表中的五个最大文件:

WITH distinct_files AS (
    SELECT DISTINCT file_path, file_size_in_bytes 
    FROM catalog.table.all_data_files
)
SELECT file_path, file_size_in_bytes 
FROM distinct_files
ORDER BY file_size_in_bytes DESC
LIMIT 5;

如果您想查看所有快照中文件数量、这些文件的大小和记录数的总体情况,可以运行此查询:

WITH unique_files AS (
    SELECT DISTINCT file_path, record_count, file_size_in_bytes
    FROM catalog.table.all_data_files
)
SELECT COUNT(*) as num_unique_files, 
       SUM(record_count) as total_records,
       SUM(file_size_in_bytes) as total_file_size
FROM unique_files;

通过以下查询,您可以查看所有快照中每个分区的文件数、记录数和总文件大小。您可以使用这些信息来帮助了解分区的数据存储状态:

WITH unique_files AS (
    SELECT DISTINCT file_path, partition, record_count, file_size_in_bytes
    FROM catalog.table.all_data_files
)
SELECT partition, 
       COUNT(*) as num_unique_files, 
       SUM(record_count) as total_records,
       SUM(file_size_in_bytes) as total_file_size
FROM unique_files
GROUP BY partition;
-- Spark SQL
SELECT * FROM my_catalog.table.all_data_files;

全部清单元数据表

Apache Iceberg中的全部清单元数据表提供了有关表中所有有效快照的每个清单文件的详细见解。

第一个字段,content,表示文件的类型,类似于全部数据文件表。值为0表示清单跟踪数据文件;值为1表示跟踪删除文件。

path 字段是一个字符串,表示清单文件的完整路径。与全部数据文件表一样,这包括存储系统位置(例如,s3://...)、表名和唯一文件标识符。

length 字段表示清单文件的大小(以字节为单位)。这可以提供有关存储在清单中的元数据量的见解。

partition_spec_id 字段对应于用于写入此清单文件的分区规范的ID。此字段指示清单中列出的数据文件的分区方式。

added_snapshot_id 字段表示创建清单时的快照ID。

added_data_files_countexisting_data_files_countdeleted_data_files_count 字段提供了该清单文件表示的数据文件更改摘要。added_delete_files_countexisting_delete_files_countdeleted_delete_files_count 字段提供了删除文件的类似摘要。

partition_summaries 字段是一个结构体数组,其中每个结构体提供清单文件中特定分区的摘要。每个结构体指示分区是否包含空值或NaN值,以及分区的上下限。

reference_snapshot_id 字段表示与该记录关联的快照的ID。您会看到清单在其有效的每个快照中列出一次。

请记住,全部清单元数据表可能会为每个清单文件生成多个行,因为一个清单文件可能是多个表快照的一部分。该表有助于在比全部数据文件表更全面的层次上理解数据的状态和组织情况。表10-8总结了全部清单元数据表的模式。

表10-8:全部清单元数据表的模式

字段名称数据类型示例值
contentInt0
pathString…/metadata/a85f78c5-3222-4b37-b7e4-faf944425d48-m0.avro
lengthInt6376
partition_spec_idInt0
added_snapshot_idInt6272782676904868561
added_data_files_countInt2
existing_data_files_countInt0
deleted_data_files_countInt0
added_delete_files_countInt2
existing_delete_files_countInt0
deleted_delete_files_countInt0
partition_summariesList[{false, false, 20210101, 20210101}]
reference_snapshot_idInt6272782676904868561

全部清单元数据表有许多用例,包括查找特定快照的所有清单、监控快照之间的清单增长以及获取所有有效清单的总大小。

虽然清单表将告诉您当前快照的所有清单,但您可以使用全部清单表为任何快照生成此数据,如以下查询所示:

SELECT * 
FROM catalog.table.all_manifests 
WHERE reference_snapshot_id = 1059035530770364194;

以下查询返回每个快照的总清单大小和数据文件计数,以查看从一个快照到下一个快照的文件和清单大小的增长:

SELECT reference_snapshot_id, SUM(length) as manifests_length,
SUM(added_data_files_count + existing_data_files_count) AS total_data_files 
FROM catalog.table.example.all_manifests 
GROUP BY reference_snapshot_id;

通过此查询,您可以获取所有有效清单的存储使用情况:

SELECT 
    SUM(length) AS total_length
FROM (
    SELECT DISTINCT path, length
    FROM catalog.table.all_manifests
);
-- Spark SQL
SELECT * FROM my_catalog.table.all_manifests;

引用元数据表

Apache Iceberg中的引用元数据表提供了Iceberg表中所有命名引用的列表。命名引用可以被视为指向表数据特定快照的指针,提供了书签或版本控制表状态的能力。

第一个字段,name,表示命名引用的唯一标识符。命名引用分为两类,这将引出第二个字段,typetype可以是两种值之一:BRANCH,一种可变引用,可以移动到新的快照;或者 TAG,一种不可变引用,一旦创建,始终指向同一个快照。

max_reference_age_in_ms 字段表示可以引用快照的最大持续时间(以毫秒为单位)。该持续时间从快照添加到表的时间开始计算。如果快照的年龄超过了这个持续时间,它将不再有效,并在维护操作期间成为清理的候选对象。

min_snapshots_to_keep 字段提供了表历史中保持的快照数量的下限。即使它们比 max_snapshot_age_ms 设置的年龄大,Iceberg表也将始终保持至少这么多快照。

最后,max_snapshot_age_in_ms 字段表示表中任何快照的最大年龄(以毫秒为单位)。除非受到 min_snapshots_to_keep 设置的保护,否则超过此年龄的快照可能会在维护操作中删除。

请记住,引用元数据表有助于您了解和管理表的快照历史和保留策略,使其成为维护数据版本控制和确保表大小可控的重要部分。表10-9总结了引用元数据表的模式。

表10-9:引用元数据表的模式

字段名称数据类型示例值
nameStringmain
typeStringBRANCH
snapshot_idInt4686954189838128572
max_reference_age_in_msInt10
min_snapshots_to_keepInt20
max_snapshot_age_in_msInt30

引用元数据表有许多用途,包括查找有失去快照风险的引用和查找特定引用的最新快照。

此外,您可能想知道特定分支的规则是否可能导致快照在下一次更新时无效。此查询应有助于仅过滤具有最大快照规则的引用:

SELECT name, min_snapshots_to_keep, max_snapshot_age_in_ms
FROM catalog.table.refs
WHERE min_snapshots_to_keep IS NOT NULL AND max_snapshot_age_in_ms IS NOT NULL;

以下查询将仅为每个引用返回快照ID:

SELECT name, snapshot_id 
FROM catalog.table.refs;
-- Spark SQL
SELECT * FROM my_catalog.table.refs;

条目元数据表

Apache Iceberg中的条目元数据表提供了有关所有快照中对表数据和删除文件执行的每个操作的详细信息。该表中的每一行捕获了在表历史的某个时间点影响许多文件的操作,使其成为理解数据集演变的基本资源。

第一个字段,status,是一个整数,指示文件在快照中是添加还是删除。值为0表示现有文件,值为1表示添加文件,值为2表示删除文件。此字段允许您跟踪每个文件的生命周期,提供对数据集随时间变化的见解。

接下来,snapshot_id 是执行操作的快照的唯一标识符。此ID允许您将每个文件操作连接到特定快照,这在跟踪特定版本表中所做的更改时非常有用。

sequence_number 字段指示操作的顺序。这是表所有快照中的全局计数器,每次更改都会递增,无论是添加、修改还是删除。通过了解序列号,您可以重建导致表当前状态的精确操作系列。

最后,data_file 是一个结构体,封装了有关操作中涉及的文件的详细信息。结构体包含以下字段:

  • file_path:文件在存储系统中的完整路径
  • file_format:文件格式,例如Parquet或AVRO
  • partition:文件所属的分区信息
  • record_count:文件中的总记录数
  • file_size_in_bytes:文件的大小(以字节为单位)
  • column_sizes:每列大小(以字节为单位)的映射
  • value_counts:表示每列值总数的映射
  • null_value_counts:每列空值的计数
  • nan_value_counts:每列NaN值的计数
  • lower_boundsupper_bounds:每列最小值和最大值的映射
  • key_metadata:特定于实现的元数据
  • split_offsets:文件内分割点的信息

通过查询条目表,您可以跟踪应用于表的每个操作,提供数据演变的综合审计跟踪。表10-10总结了条目元数据表的模式。

表10-10:条目元数据表的模式

字段名称数据类型示例值
statusInt1
snapshot_idInt1059035530770364194
sequence_numberInt0
data_fileList{0, s3://….parquet, PARQUET, 0, {A}, 6, 609, {1 -> 83}, {1 -> 6}, {1 -> 0}, {}, {1 -> Adriana}, {1 -> Antonio}, null, null, null, 0}

条目元数据表有许多用例,包括识别特定快照中添加的文件、跟踪文件随时间的变化以及跟踪表大小随时间的变化。

例如,以下查询将找到与代表添加文件的快照匹配的所有条目:

SELECT data_file 
FROM catalog.table.entries
WHERE snapshot_id = <your_snapshot_id> AND status = 1;

此查询将返回特定文件的所有记录,无论是现有、添加还是删除的文件:

SELECT snapshot_id, sequence_number, status, data_file 
FROM catalog.table.entries
WHERE data_file.file_path = '<your_file_path>'
ORDER BY sequence_number ASC;

通过以下查询,您将获得每个快照添加文件的大小,以查看存储需求在快照之间的增长:

SELECT snapshot_id, SUM(data_file.file_size_in_bytes) as total_size_in_bytes
FROM catalog.table.entries
WHERE status = 1
GROUP BY snapshot_id
ORDER BY snapshot_id ASC;
-- Spark SQL
SELECT * FROM my_catalog.table.entries;

结合使用元数据表

通过将这些元数据表结合起来,您可以提取更多有价值的见解,并更有效地定制您的数据操作。让我们考虑几个如何将这些元数据表连接起来以满足不同用例的示例。

获取快照中添加的所有文件的数据

您可以评估快照中添加的数据,以验证添加的记录数量是否正确,查看特定快照中文件存储的增长等。要调出特定快照的所有文件元数据,请使用以下查询:

SELECT f.*, e.snapshot_id
FROM catalog.table.entries AS e
JOIN catalog.table.files AS f
ON e.data_file.file_path = f.file_path
WHERE e.status = 1 AND e.snapshot_id = <your_snapshot_id>;

获取特定数据文件生命周期的详细概述

您可能想知道文件何时被添加、删除以及存在的状态,以及每个时间点表操作的状态。使用以下查询,您可以构建特定数据文件历史的详细日志,允许您查看文件生命周期中每个点添加和删除的文件数量。此外,使用文件路径,您可以识别文件所涉及的每个操作,并使用条目和清单表收集有关这些操作的更多信息和上下文:

SELECT e.snapshot_id, e.sequence_number, e.status, m.added_snapshot_id,
m.deleted_data_files_count, m.added_data_files_count
FROM catalog.table.entries AS e
JOIN catalog.table.manifests AS m
ON e.snapshot_id = m.added_snapshot_id
WHERE e.data_file.file_path = '<your_file_path>'
ORDER BY e.sequence_number ASC;

按快照跟踪分区在表中的演变

您可能希望查看分区如何在快照之间演变,例如添加了多少文件。以下是如何构建该数据视图的示例查询。您可以将其用作评估分区中添加的文件数量和文件大小的基础:

SELECT e.snapshot_id, f.partition, COUNT(*) AS files_added
FROM catalog.table.entries AS e
JOIN catalog.entries.files AS f
ON e.data_file.file_path = f.file_path
WHERE e.status = 1
GROUP BY e.snapshot_id, f.partition;

监控与特定分支关联的文件

如果您使用表分支,可能希望监控这些分支以跟踪存储和优化需求。使用此查询,您可以调出特定分支当前快照的文件:

SELECT r.name as branch_name, f.*
FROM catalog.table.refs AS r
JOIN catalog.table.entries AS e
ON r.snapshot_id = e.snapshot_id
JOIN catalog.table.files AS f
ON e.data_file.file_path = f.file_path
WHERE r.type = 'BRANCH' AND r.name = '<your_branch_name>';

查找表两个分支之间的文件差异

如果您想查看两个分支之间不共享的文件,可以使用此查询。它联合两个查询的结果,以获取两个分支中的唯一文件:

-- branch1中的文件但不在branch2中
SELECT 'branch1' as branch, f.*
FROM catalog.table.refs AS r1
JOIN catalog.table.entries AS e1
ON r1.snapshot_id = e1.snapshot_id
JOIN catalog.table.files AS f
ON e1.data_file.file_path = f.file_path
WHERE r1.type = 'BRANCH' AND r1.name = 'branch1'
AND f.file_path NOT IN (
   SELECT f2.file_path
   FROM catalog.table.refs AS r2
   JOIN catalog.table.entries AS e2
   ON r2.snapshot_id = e2.snapshot_id
   JOIN catalog.table.files AS f2
   ON e2.data_file.file_path = f2.file_path
   WHERE r2.type = 'BRANCH' AND r2.name = 'branch2'
)
UNION ALL
-- branch2中的文件但不在branch1中
SELECT 'branch2' as branch, f.*
FROM catalog.table.refs AS r1
JOIN catalog.table.entries AS e1
ON r1.snapshot_id = e1.snapshot_id
JOIN catalog.table.files AS f
ON e1.data_file.file_path = f.file_path
WHERE r1.type = 'BRANCH' AND r1.name = 'branch2'
AND f.file_path NOT IN (
   SELECT f2.file_path
   FROM catalog.table.refs AS r2
   JOIN catalog.table.entries AS e2
   ON r2.snapshot_id = e2.snapshot_id
   JOIN catalog.table.files AS f2
   ON e2.data_file.file_path = f2.file_path
   WHERE r2.type = 'BRANCH' AND r2.name = 'branch1'
);

通过每个分支的最新快照查找存储增长

分支非常适合隔离和实验,但随着时间的推移,实验数据已摄取的许多分支可能会带来您可能希望监控的存储成本。此查询将允许您查看每个分支当前快照中添加的数据量:

SELECT r.name as branch_name, e.snapshot_id, SUM(f.file_size_in_bytes) as
total_size_in_bytes
FROM catalog.table.refs AS r
JOIN catalog.table.entries AS e
ON r.snapshot_id = e.snapshot_id
JOIN catalog.table.files AS f
ON e.data_file.file_path = f.file_path
WHERE r.type = 'BRANCH'
GROUP BY r.name, e.snapshot_id
ORDER BY r.name, e.snapshot_id;

通过使用Apache Iceberg元数据表,您可以更好地监控表的状态,以避免性能瓶颈和其他问题,从而在生产环境中更好地利用Apache Iceberg。

使用分支隔离更改

在现代数据工作流中,像Git一样在分支中隔离对数据的更改具有重要价值,因此更多从业者应该开始采用这种方法。这种方法允许分离不同的工作线,使开发人员能够独立进行更改,而不会干扰其他开发人员的工作或破坏主代码库。这就像拥有多个平行宇宙,一个宇宙中的变化不会影响其他宇宙。您可以进行实验、犯错并学习,而不必担心影响整个系统。

在Apache Iceberg表的上下文中,有两种实现此概念的方法:在表级别实现,这种方法是Apache Iceberg原生支持的,无论使用何种目录都可以使用;在目录级别实现,当使用Project Nessie目录时可以实现。

第一种方法是在表级别隔离更改,涉及为特定表创建分支。每个分支包含对该表所做更改的完整历史记录。这种方法允许并行模式演变、回滚和其他高级用例。它是处理特定表更改的强大工具,但无法提供整个数据目录中更改的整体视图。

在目录级别隔离更改使您能够将整个数据湖作为一个单一实体进行管理,捕捉一个分支中多个表的变化。使用Nessie,您可以在特定时间点对整个目录进行快照。这种做法促进了更全面的版本控制策略,使您能够测试数据转换、跟踪数据谱系,并维护多个表的完整性。

这两种方法各有优劣。表级隔离为单个表提供了细粒度的控制和灵活性,但在大规模数据环境中可能变得复杂。目录级隔离提供了全面的统一视图,但对于小规模或单表场景可能显得过度。

在Git-like分支中隔离数据更改的价值是多方面的。它为开发人员提供了自由,能够进行实验和更改而不必担心广泛的影响,允许对更改进行版本控制和回滚,并促进更高的数据完整性和谱系跟踪。无论您选择在表级别还是在目录级别实现这种方法,使用Project Nessie将取决于您的具体用例(数据摄取、生产环境测试、实验环境)和数据环境的复杂性。

表分支和标记

内置在Apache Iceberg规范中的功能是能够在不同路径下跟踪快照,称为分支,或者为特定快照命名,称为标记。这使得在单个表中进行隔离、重现和实验成为可能。

表分支

Apache Iceberg中的表分支允许您创建独立的快照世系,每个分支都有自己的生命周期。分支本质上是指向不同快照链的命名引用。每个分支指向分支的头部,即分支快照历史中最近的快照。每个分支还具有设置,用于确定快照的最大年龄和应保留的最小快照数。

考虑一个数据管理场景,您有一个数据管道要将数据摄取到现有表中。在将数据合并到主表之前,您希望将其隔离以进行验证和质量检查。为了实现这种隔离,您可以利用Apache Iceberg的分支机制。

在此场景中,可以将传入数据定向到一个单独的分支(例如,“ingestion-validation-branch”),而不会干扰主表。您可以使用Iceberg的Java API通过toBranch操作在写入表时实现这一点(Java API由Apache Iceberg项目的一部分Java库组成,使得对Iceberg表进行常见操作成为可能)。这种方法将传入数据隔离,使其在合并到主表数据之前进行验证和检查。

以下是演示此过程的Java代码片段:

// 使用Iceberg Java API
// 字符串用于作为分支名称
String branch = "ingestion-validation-branch";

// 创建一个分支
table.manageSnapshots()
    // 从特定快照创建分支
    .createBranch(branch, 3)
    // 指定要保留的快照数量
    .setMinSnapshotsToKeep(branch, 2)
    // 指定这些快照的最大年龄
    .setMaxSnapshotAgeMs(branch, 3600000)
    // 设置分支的最大年龄
    .setMaxRefAgeMs(branch, 604800000)
    .commit();

// 将传入数据写入新分支
table.newAppend()
    // 将传入文件附加到分支
    .appendFile(INCOMING_FILE)
    // 指定要在此操作的分支
    .toBranch(branch)
    .commit();

// 从分支进行验证读取
TableScan branchRead = table
    .newScan()
    .useRef(branch);

创建“ingestion-validation-branch”允许对传入的新数据进行测试和验证,使其成为数据工程工作流中的宝贵工具。一旦新分支上的数据通过验证和所有质量检查,可以使用fastForward操作将主分支更新为“ingestion-validation-branch”的头部:

// 更新主分支以合并来自新分支的验证更改
table
  .manageSnapshots()
  // 设置主分支的最新提交应与新分支匹配
  .fastForward("main", "ingestion-validation-branch")
  .commit();

通过这种方式,Apache Iceberg中的分支提供了一种有效机制,用于隔离、验证和合并传入数据,从而在主表中维护数据质量和完整性。

要使用SQL实现相同的隔离和验证工作流,您可以使用Apache Iceberg提供的ALTER TABLE语句。第一步是在表上创建一个新分支“ingestion-validation-branch”。假设您正在使用一个名为sales_data的表,该表位于名为my_catalog的目录和名为my_db的数据库中。该分支被配置为保留快照七天并始终保留至少两个快照。以下是实现此操作的代码:

-- 创建新分支
ALTER TABLE my_catalog.my_db.sales_data
  CREATE BRANCH ingestion-validation-branch
  RETAIN 7 DAYS 
  WITH RETENTION 2 SNAPSHOTS;

接下来,使用SET命令将新创建的分支设置为活动写入分支:

-- 将新分支设置为写入分支
SET spark.wap.branch = 'ingestion-validation-branch';

现在,您可以将传入数据写入“ingestion-validation-branch”。这将隔离新数据,并允许您在合并到主数据之前进行验证和质量检查。术语WAP(Write Audit Publish)是一种模式,其中您写入数据,审核数据的质量问题,然后在完成时发布。对于此示例,假设您正在向sales_data表插入一些数据:

-- 将传入数据写入新分支
INSERT INTO my_catalog.my_db.sales_data (column1, column2, column3) 
VALUES (value1, value2, value3), (value4, value5, value6);

完成任何验证后,您需要使用Java运行fastForward过程。

表标记

虽然分支提供了一种创建数据独立世系的方法,但Iceberg也提供了标记机制。Iceberg中的标记允许对快照进行命名引用,从而促进重现。

在供应链管理的背景下,考虑一个场景,您需要在季度末重现表的状态以进行审计。标记使您能够保留重要的历史快照,从而允许状态重现。可以使用createTag操作为快照创建标记,并且可以指定标记应保留的时间:

// 创建标记
String tag = "end-of-quarter-Q3FY23";
table.manageSnapshots()
    // 从快照8创建标记
    .createTag(tag, 8)
    // 设置标记的最大年龄
    .setMaxRefAgeMs(tag, 1000 * 60 * 60 * 2486400000)
    .commit();

在此示例中,在快照8处创建了一个名为“end-of-quarter-Q3FY23”的标记,并保留一天。从标记读取数据就像在设置表扫描时将其传递给useRef API一样简单,如下所示:

// 从标记读取数据
String tag = "end-of-quarter-Q3FY23";
Table tagRead = table
    .newScan()
    .useRef(tag);

这也可以通过SQL完成,如下所示:

-- Spark SQL
-- 创建保留14天的标记
ALTER TABLE catalog.db.closed_invoices 
  CREATE TAG 'end-of-quarter-Q3FY23' 
  AS OF VERSION 8 
  RETAIN 14 DAYS;

分支和标记共同构成了管理大数据集的强大组合。它们允许隔离测试、简化审计,并能够在任何给定时间重现数据状态。无论您是处理《通用数据保护条例》(GDPR)的要求,还是在供应链管理的复杂性中航行,Apache Iceberg的这些功能都提供了高效数据操作所需的灵活性和控制。但是,当处理许多表时,在目录级别使用抽象可能是一个更好的选择。

目录分支和标记

Project Nessie,通常被称为数据湖的Git,引入了强大的功能,如目录分支和标记,以增强您对Apache Iceberg表的管理。Nessie本质上为管理大量数据提供了一种更有组织的方法,同时保持数据的完整性和一致性。

Nessie的区别在于它能够在目录中的所有表之间保持始终一致的数据视图。通过隔离和独立处理更改,Nessie确保用户不会遇到不完整的更改。一旦所有更改完成,就可以一致且原子地应用,从而增强整体数据管理体验。

使用Nessie,跟踪单个数据文件变得轻而易举。它知道哪些数据文件正在使用,哪些可以安全删除。它允许多个环境(如生产、暂存和开发)在同一个数据湖中共存,而不会损害生产数据的完整性。

关键是,Nessie通过避免不必要的重复来优化数据管理。Nessie使用对现有不可变数据文件的引用系统,而不是复制数据。此特性类似于Git,使Nessie能够记录数据湖中的所有修改为提交,而不需要复制实际数据。

Nessie的一个显著优势是其目录级别的版本控制,相对于单个表的版本控制提供了便利。当处理许多表时,单独管理每个表的版本控制可能变得复杂和麻烦。相反,使用Nessie的目录级别版本控制允许您同时更高效地处理许多表,大大简化了您的数据管理过程。

让我们进一步探讨如何使用Nessie和Apache Iceberg的目录级分支和标记来增强您的数据管理策略。

目录分支

当与Apache Iceberg一起使用时,Project Nessie的分支功能提供了一个安全的环境,用于在将新数据纳入目录之前进行测试。通过创建一个新分支,您可以安全地摄取和验证多个表中的数据批次,减少跨目录中的错误条目的风险。

想象一下,您正在处理一个与正在进行的项目相关的大型数据集系列,并且您每周收到一批数据。您可以先将这些数据摄取到一个单独的分支(例如“weekly_ingest_branch”),而不是直接将其添加到目录中。这样做允许您在将数据与目录的主生产分支合并之前进行验证。

以下是使用Spark SQL的这种工作流程示例:

-- 为每周数据摄取创建新分支
CREATE BRANCH IF NOT EXISTS weekly_ingest_branch IN catalog;

-- 切换到新分支
USE REFERENCE weekly_ingest_branch IN catalog;

-- 从分支中摄取新数据到每个表
INSERT INTO table_name (...);

在验证了表中的数据后,您可以将其合并到主分支中:

MERGE BRANCH weekly_ingest_branch INTO main IN catalog;

目录标记

在Project Nessie目录中使用标记与Apache Iceberg配对,使您能够标记数据的特定版本,从而提供一种简单的方法来跟踪和重现不同时点的数据状态。这在进行季度分析时特别有用,您需要在每个季度末重现数据的状态。

例如,假设您每个季度进行财务分析。通过创建如Q1_end_snapshot和Q2_end_snapshot的标记,您可以轻松检索每个季度末的数据状态。以下Spark SQL命令可用于创建这些季度标记:

-- 为第一季度末创建标记
CREATE TAG IF NOT EXISTS Q1_end_snapshot IN catalog;

要检索第一季度末的数据,您可以切换到以下标记并查询数据:

-- 切换到第一季度末的快照
USE REFERENCE Q1_end_snapshot IN catalog;

-- 查询第一季度末的数据
SELECT * FROM table_name;

使用Apache Iceberg和Project Nessie的目录分支和标记提供了以下好处:

  • 促进新数据批次的安全和隔离测试和验证
  • 使定期间隔(例如每季度末)的数据重现变得简单,提高分析的可靠性
  • 有助于维护数据更改的审计跟踪
  • 有助于为不同的分析需求识别表的不同版本

目录分支和标记是Apache Iceberg和Project Nessie的重要功能,提供了一种稳健高效的方法来管理和控制目录级别的数据表。使用这些工具,您可以确保准确、可靠的数据摄取,并轻松重现历史数据以进行分析。

多表事务

多表事务是支持跨多个表的一致性和原子性操作的数据库中的基本概念。在多表事务中,涉及不同表的多个操作被视为一个单一的原子工作单元。这意味着事务中的所有操作要么全部成功,要么如果任何操作失败,则会回滚事务中所有的更改,从而使数据库保持一致的状态。

多表事务的重要性主要在于它们能够维护数据一致性,这是数据库管理系统中的一个关键方面。考虑一个供应链管理系统,其中下订单涉及更新Orders表并减少Inventory表中的库存。如果这些操作不属于单个事务的一部分,那么在更新Orders表后更新Inventory表失败可能导致数据不一致——系统会显示已下订单,但库存不会反映库存减少。

通过多表事务,整个过程被视为一个单一的原子操作。这意味着如果更新Inventory表失败,Orders表所做的更改也会被回滚,从而确保数据保持一致。

此外,多表事务对于隔离性也是必不可少的,这是可靠数据库系统的另一个关键属性。它们确保并发事务不会互相干扰,从而避免潜在的数据不一致和冲突。

总之,多表事务对于维护数据库系统中的数据一致性和隔离性至关重要。它们允许涉及不同表的多个操作被视为一个单一的原子工作单元,确保所有操作成功或者在失败时回滚所有更改。这在供应链管理等复杂系统中尤为重要,因为这些系统的数据完整性和一致性至关重要。

您可以通过使用Project Nessie目录来实现多表事务,方法是分支目录,然后在分支上对多个表运行事务。来自分支的所有操作,无论涉及哪个表、引擎或用户运行事务,都会与主生产分支隔离开来。

一旦您对分支的状态和所有目录表感到满意,您可以将这些更改合并回主分支,如以下代码所示。或者,如果您对所有这些不满意,可以删除分支,这样就不会有任何更改被任何人看到:

-- 创建分支
CREATE BRANCH IF NOT EXISTS etl IN catalog;

-- 切换到分支(Spark SQL)
USE REFERENCE ingest IN catalog;

-- 在多个表上运行事务
INSERT INTO catalog.db.tableA ...;
INSERT INTO catalog.db.tableB ...;

-- 完成后,同时将所有事务合并到生产分支
MERGE BRANCH etl INTO main IN catalog;

回滚更改

目录级和表级回滚是数据管理中的关键概念,对于保持高数据质量具有重要作用。

表级回滚是一种特定的数据管理技术,允许将表的更改恢复或“回滚”到以前的状态。这在数据处理过程中发生错误或更改导致意外结果时特别有用。通过回滚更改,可以将数据恢复到已知良好质量的状态,从而减轻错误或问题更改的影响。

目录级和表级回滚共同提供了一种防范错误和可能影响数据质量的更改的保护措施。这种组合使得维护高质量、可靠的数据变得更容易,这在数据密集型领域如数据科学、机器学习和业务分析中特别重要。数据质量直接影响从数据中得出的洞察力的可靠性,因此这些概念对于确保基于数据的准确可靠决策至关重要。

表级回滚

回滚允许由于错误的数据摄取作业而处于不良状态的表基本上拥有一个“撤销”按钮。这种能力在发生错误数据更新并需要将表恢复到之前一致状态的场景中至关重要。Apache Iceberg有四个Spark过程可用于管理表的当前状态:rollback_to_snapshotrollback_to_timestampset_current_snapshotcherrypick_snapshot。元数据表是发现要回滚到的快照的绝佳工具,可以使用我们之前讨论的许多元数据表查询。

rollback_to_snapshot

Apache Iceberg提供了一个名为rollback_to_snapshot的过程,用于基于快照ID回滚表。要运行rollback_to_snapshot过程,您需要提供两个信息:表名和要回滚到的快照ID。例如,如果您有一个名为orders的表,并希望回滚到快照12345,可以使用以下命令:

spark.sql("CALL catalog.database.rollback_to_snapshot('orders', 12345)")

当调用此过程时,当前表状态将更改为指向提供的快照ID。表及其快照的元数据和数据文件保持不变,这意味着此操作是非破坏性的。它设计为可安全地与表上的其他操作同时运行,防止任何潜在冲突。

例如,假设您对orders表进行了某些更新,并意识到更新逻辑中的错误引入了一些记录的不正确值。如果您在错误更新之前对表进行了快照,可以回滚表到此快照,从而恢复数据的正确状态:

# 假设错误更新之前的快照ID是12345
spark.sql("CALL catalog.database.rollback_to_snapshot('orders', 12345)")

还可以通过更改Apache Iceberg中的某些设置来调整回滚操作的行为。例如,您可以将rollback_to_snapshot.expire_snapshots.enabled属性设置为false,以防止回滚后自动删除超过表到期时间的快照。您还可以设置rollback_to_snapshot.expire_snapshots.snapshot_age_ms属性,以控制回滚后删除快照的年龄阈值:

spark.sql("""
CALL catalog.database.alter_table_properties(
    'orders', 
    map(
        'rollback_to_snapshot.expire_snapshots.enabled', 'false',
        'rollback_to_snapshot.expire_snapshots.snapshot_age_ms', '172800000' -- 2 days in milliseconds
    )
)
""")

此设置确保在回滚后,不会自动删除超过两天的快照,从而为您提供更多的控制权。

rollback_to_timestamp

有时,您不知道要回滚到的快照的确切ID,但您知道时间点,这时rollback_to_timestamp过程非常有用。此过程需要提供要更新的表名和要回滚到的时间戳作为参数。它会使引用受影响表的所有缓存的Spark计划无效,确保任何后续操作都基于表的更新状态。此过程的输出包括回滚前的当前快照ID(previous_snapshot_id)和新的当前快照ID(current_snapshot_id)。

考虑一个场景,您有一个名为orders的表,您希望将此表回滚到某个特定时间点的状态,例如2023-06-01 00:00:00。在Apache Spark和Iceberg中,您可以使用带有rollback_to_timestamp过程的CALL语句:

// 这段代码是Scala代码

// 使用过程回滚表
spark.sql(s"CALL iceberg.system.rollback_to_timestamp('db.orders', timestamp('2023-06-01 00:00:00'))")

此语句调用rollback_to_timestamp过程,在orders表上回滚到2023-06-01 00:00:00时当前的快照。需要注意的是,您需要有执行此操作的必要权限。

set_current_snapshot

set_current_snapshot过程设置表的当前快照ID。与回滚不同的是,您不是将表设置为其历史中的某个快照,而是设置为任何可用的任意快照,这些快照可能位于不同的分支或标签上。这种能力允许用户在表的不同版本之间切换,即使它们不是顺序相关的。在使用set_current_snapshot过程之前,您需要提供两个信息:表名和应设为“当前”快照的快照ID。输出类似于rollback_to_timestamp,提供更改前的快照ID和新的当前快照ID。

假设您有一个名为inventory的表,并希望将其当前快照设置为特定的快照ID,例如123456789。在Apache Spark和Iceberg中,您可以使用带有set_current_snapshot过程的CALL语句,如下代码所示:

// 这段代码是Scala代码

// 指定要更新的表
val tableName = "db.inventory"

// 指定要设为当前快照的快照ID
val snapshotId = 123456789L

// 使用过程设置当前快照
spark.sql(s"CALL iceberg.system.set_current_snapshot('$tableName', $snapshotId)")

此语句调用set_current_snapshot过程,在inventory表上将当前快照设置为ID 123456789。请记住,您需要有执行此操作的必要权限。

cherrypick_snapshot

cherrypick_snapshot过程通过元数据操作创建一个新的快照,合并来自另一个快照的更改(不创建新的数据文件)。要运行cherrypick_snapshot过程,您需要提供两个参数:要更新的表名和应基于的快照的ID。此事务将返回cherrypick操作前后的快照ID,分别为source_snapshot_idcurrent_snapshot_id

例如,假设您有一个名为products的表,希望从ID为987654321的快照中挑选更改。在Apache Spark和Iceberg中,您可以使用带有cherrypick_snapshot过程的CALL语句。以下是示例:

// 这段代码是Scala代码

// 使用过程挑选快照
spark.sql(s"CALL iceberg.system.cherrypick_snapshot('db.products', 987654321)")

此语句调用cherrypick_snapshot过程,在products表上挑选ID为987654321的快照中的更改。

这些过程提供了管理和操作Apache Iceberg表状态的强大工具。它们提供了对表版本的灵活控制,允许用户回滚更改、设置特定快照为当前状态或从一个快照中挑选更改。这些功能促进了现代数据科学和分析工作流程中的有效数据版本控制和历史分析。

目录级回滚

使用Nessie作为Apache Iceberg目录的一个关键好处是能够在目录级别回滚数据。就像版本控制系统允许软件开发人员将整个代码库回滚到以前的版本一样,Nessie使数据工程师和数据科学家能够将整个数据环境回滚到以前的状态。这种功能在各种场景中非常有价值。

例如,假设一个数据工程师运行一个批处理作业,修改了大量数据集,但后来意识到转换逻辑中有错误。没有像Nessie这样的工具,纠正这个错误可能需要逐一回滚每个Iceberg表。然而,使用Nessie,工程师只需将整个目录回滚到错误批处理作业运行之前的状态,从而在瞬间撤销每个表的错误。

在Nessie中,可以通过与Apache Hive、Apache Spark和Dremio的SQL查询引擎集成来使用SQL执行回滚数据。以下是如何使用Project Nessie在SQL中回滚数据的示例。首先,您需要找到要回滚到的提交的哈希值。这可以通过SHOW LOG命令完成:

SHOW LOG nessie.main

这将显示主分支上所有提交的列表及其哈希值。一旦确定了要回滚到的提交哈希值,可以使用SET REF命令将分支的头部更改为该提交:

SET REF nessie.main TO 'commitHash'

此命令将主分支的头部更改为指定的提交哈希值之前的状态,从而有效地回滚该提交之后的所有更改。

请注意,此命令不会做出永久更改;它只会在当前会话中生效。如果要使回滚永久生效,可以使用ASSIGN命令而不是SET REF

ASSIGN nessie.main AT 'commitHash'

此命令将永久回滚主分支到指定的提交,使所有用户都能看到该提交时的数据。

Nessie在目录级别回滚数据的能力提供了一种强大的工具来管理数据。它使从错误中恢复更容易,促进数据转换的实验,并简化了在不同环境中保持一致性的过程。当需要快速在生产中恢复数据并且涉及许多表时,目录级回滚是一种极好的解决方案。

结论

Apache Iceberg提供的强大功能,包括元数据表、分支和标记、多个表的事务以及表级回滚,在有效的生产数据管理中起到了基础性作用,允许用户采用主动和被动的方法。

实际上,Apache Iceberg元数据表在主动和被动的数据管理中都是不可或缺的,它们为用户提供了对数据环境的深入理解,跟踪模式更改和分区演变。这些关键见解使团队能够做出明智的决策,优化数据架构,并预测需求。

分支和标记进一步增强了主动管理,提供了安全的环境来测试更改或新功能而不影响生产数据,从而实现迭代开发和持续改进。这些功能还有助于维护版本控制和促进实验的可重现性。

在被动方面,多表事务提供了更高的控制和一致性。它们确保跨多个表的相关更改作为一个原子操作处理,防止部分更新,确保数据环境的完整性。

最后,Iceberg表回滚在出现错误或意外后果时提供了关键的恢复机制。这确保了能够恢复到之前的状态,使其成为风险缓解和数据完整性的重要工具。

这些功能共同构成了一个有效管理生产数据的稳健框架,平衡了前瞻性策略和响应性措施,以维护数据生态系统的健康、完整性和效率。

在第11章中,我们将探索使用Apache Iceberg处理流数据的不同方法。