Apache Iceberg on AWS - Iceberg表数据查询及优化

0 阅读7分钟

数据文件说明

image.png

public_orders中数据文件数量为1000000.

image.png

public_orders 表和 public_order_items 表之间存在一对多关系。

一个订单可以有多个订单项,但至少有一个。public_order_items 通过外键列 order_id 引用 public_orders 表。

查询数据

SELECT  o.payment_method,
        SUM(oi.unit_price * oi.quantity) AS item_sum,
        order_totals.order_sum
FROM "future_pawmart_db"."public_orders" AS o 
JOIN "future_pawmart_db"."public_order_items" AS oi 
    ON o.order_id = oi.order_id 
JOIN (
    SELECT payment_method, SUM(total_amount) AS order_sum
    FROM "future_pawmart_db"."public_orders"
    GROUP BY payment_method
) AS order_totals
    ON o.payment_method = order_totals.payment_method
GROUP BY o.payment_method, order_totals.order_sum;

结果如下:

image.png

目前可以看到item_sum和order_sum数据并不一致。 为了调查这个问题,让我们使用 Iceberg 的时间旅行功能来了解这种不一致性是否也可以在表格的先前版本中发现。

时间旅行功能使用

运行以下查询以获取 public_orders 表的快照。

SELECT snapshot_id, committed_at, operation, summary
FROM "future_pawmart_db"."public_orders$snapshots"
ORDER BY committed_at DESC;

image.png

重新运行本实验一开始的查询,但针对之前的快照。为此,请将占位符 <snapshot_id> 替换为表的第二个和第三个最新快照的 ID。

SELECT  o.payment_method,
        SUM(oi.unit_price * oi.quantity) AS item_sum,
        order_totals.order_sum
FROM "future_pawmart_db"."public_orders" FOR VERSION AS OF <snapshot_id> AS o 
JOIN "future_pawmart_db"."public_order_items" AS oi 
    ON o.order_id = oi.order_id 
JOIN (
    SELECT payment_method, SUM(total_amount) AS order_sum
    FROM "future_pawmart_db"."public_orders" FOR VERSION AS OF <snapshot_id>
    GROUP BY payment_method
) AS order_totals
    ON o.payment_method = order_totals.payment_method
GROUP BY o.payment_method, order_totals.order_sum;

image.png

可以看到之前的快照中,数据是一致的,意味着最新的插入数据存在异常。

通过Glue进行Iceberg表优化

初始化环境

通过如下SQl查询当前Iceberg表:

SELECT  o.payment_method,
        sum(oi.unit_price * oi.quantity) as "total"
FROM future_pawmart_db.public_orders as o 
JOIN future_pawmart_db.public_order_items as oi 
    ON o.order_id = oi.order_id 
GROUP BY payment_method;

查看打印出来的耗时。虽然查询相对简单,但耗时可能相对较长,大约在两位数秒左右。这是因为 Iceberg 表当前包含大量小型数据文件(KB)。像您编写的查询那样,扫描较大值域的查询会扫描大量此类数据文件。与扫描少量大型数据文件(MB)相比,这会导致 I/O 操作次数增加。

image.png

Compaction优化

Apache Iceberg 提供了各种元数据表,用于分析表的值分布、数据结构或数据文件等方面的信息。

元数据表 all_data_files 概述了与表当前快照关联的数据文件

创建Glue Notebook,并初始化spark session

%idle_timeout 2880
%glue_version 3.0
%worker_type G.4X
%number_of_workers 50
%%configure
{
  "--conf" : "spark.sql.extensions=org.apache.iceberg.spark.extensions.IcebergSparkSessionExtensions",
  "--datalake-formats" : "iceberg"
} 

from pyspark.sql import SparkSession
spark = SparkSession.builder \
   .config("spark.sql.warehouse.dir", "<FutureDataLakeLocation>") \
   .config(f"spark.sql.catalog.glue_catalog", "org.apache.iceberg.spark.SparkCatalog") \
   .config(f"spark.sql.catalog.glue_catalog.warehouse", "<FutureDataLakeLocation>/") \
   .config(f"spark.sql.catalog.glue_catalog.catalog-impl", "org.apache.iceberg.aws.glue.GlueCatalog") \
   .config(f"spark.sql.catalog.glue_catalog.io-impl", "org.apache.iceberg.aws.s3.S3FileIO") \
   .config("spark.sql.extensions", "org.apache.iceberg.spark.extensions.IcebergSparkSessionExtensions") \
   .getOrCreate()

查询文件数量

spark.sql("""
     SELECT count(distinct file_path)
     FROM glue_catalog.future_pawmart_db.public_orders.all_data_files;
""").show(truncate=False)

image.png

查询summary字段

spark.sql("""
     SELECT snapshot_id, operation, summary
     FROM glue_catalog.future_pawmart_db.public_orders.snapshots;
 """).show(truncate=False)

image.png

Spark 的 Iceberg 库提供了存储过程 rewrite_data_files 来重写和压缩数据文件。有关背景信息,请参阅 Iceberg 文档

%%sql
CALL glue_catalog.system.rewrite_data_files(
  table => 'future_pawmart_db.public_orders',
  strategy => 'binpack',
  options => map(
    'max-concurrent-file-group-rewrites', '100',
    'partial-progress.enabled', 'true',
    'target-file-size-bytes', '2406000',
    'max-file-group-size-bytes', '2406000'
  )
)

image.png

参数解析:

  • table: 'future_pawmart_db.public_orders'
    • 指定要重写数据文件的表,这里是future_pawmart_db数据库中的public_orders表
  • strategy: 'binpack'
    • 使用"binpack"(二进制打包)策略来重组文件,这种策略会将小文件合并成更大的文件,减少文件数量,优化存储和查询效率
  • options: 一组配置选项,用于控制重写过程
    • 'max-concurrent-file-group-rewrites'
      • '100': 允许最多100个文件组并行重写
    • 'partial-progress.enabled'
      • 'true': 启用部分进度,允许操作可以部分完成并保存进度
    • 'target-file-size-bytes'
      • '2406000': 目标文件大小约为2.4MB
    • 'max-file-group-size-bytes'
      • '2406000': 文件组的最大大小也设为约2.4MB

在本例中,99,900 个数据文件被压缩为 101 个文件。

image.png

查询速率也相对提高。

可以想象,在所有情况下,自行调用或编排压缩操作可能并非理想之选。这对于其他日常管理任务也同样适用,例如删除快照或不再与任何快照关联的孤立文件。 AWS Glue 数据目录内置了专门针对这些情况的功能,称为“表优化”。

使用Glue Catalog进行表优化

Apache Iceberg 中的事务和快照被捕获为数据文件和元数据文件,它们描述了与上一个快照的差异(“增量”)。随着时间的推移,随着事务的执行,这些文件会越来越多。正如您之前所了解的,数据文件和分区可能未针对读取操作进行优化,需要随着时间的推移进行重写。此外,随着表快照的删除,旧的数据文件可能会被保留,尽管不再有元数据文件引用它们。

所有这些都导致您需要定期进行表优化(也称为“数据整理”),如果您在生产环境中运行 Iceberg。

AWS Glue 数据目录具有内置功能,可以执行以下任务,而无需担心所需的基础设施或调度:

压缩:将小文件合并为大文件。

快照删除:删除过时的表版本以释放存储空间并降低元数据复杂性,同时确保符合数据保留策略。 孤立文件删除:通过删除任何有效表快照不再引用的文件来回收存储空间,防止存储膨胀。

开启非常简单,控制台可一键部署:

image.png

分区优化

Apache Iceberg 的分区演进机制允许您随时间推移更改数据的组织方式,而无需重写现有数据或中断查询。

为此,我们采用了两种机制:

隐藏分区

Iceberg 将分区信息存储在元数据(而非物理文件夹)中,因此查询不依赖于分区布局。 版本化分区规范:更新分区策略时,现有数据将保留其原始分区,而新数据将使用更新后的分区。两种布局共存,Iceberg 会在查询期间自动处理它们。 隐藏分区 与 Hive 表(其中分区值显示在文件路径中,例如 /year=2023/month=01/)不同,Iceberg 将分区信息存储在元数据中。

这种分离提供了:

无需将分区值编码到路径中 支持复杂的分区转换 使用分区统计信息改进查询规划

分区演进

这是 Iceberg 最强大的功能,它允许在不迁移数据的情况下更改分区方案:

新数据遵循新的分区方案 旧数据保留在其原始分区中 查询可以无缝访问新旧分区方案

分区演进示例

假设您有一个以date为分区的表:

CREATE TABLE sales (
  id bigint,
  date date,
  customer_id string,
  amount decimal(10,2)
) PARTITIONED BY (date);

收集了一年的数据后,您注意到查询经常同时按日期和客户区域进行筛选。当前的分区对于这些查询来说效率低下。

您可以改进分区方案:

ALTER TABLE sales SET PARTITION SPEC (
  date,
  customer_id
);

演进后,现有数据仅按日期分区,新数据同时按日期和客户 ID 分区,查询可以跨两种分区方案无缝运行。 当您进行如下查询

SELECT * FROM sales 
WHERE date = '2023-01-15' AND customer_id = 'REGION-A';

Iceberg 的查询引擎:

对于旧数据:仅扫描日期分区“2023-01-15”,并按 customer_id 进行筛选

对于新数据:同时使用两个分区,以实现更高效的数据跳过

随着时间推移,分区功能不断演进,这使得 Iceberg 对于访问模式可能发生变化的长期表或初始分区决策随着数据集的增长而变得不理想的情况尤为有用。