メインコンテンツに移動

builders.flash

データレイクの「見えない問題」を可視化する — Amazon S3 + Apache Iceberg メダリオンアーキテクチャの検証ガイド

2026-08-03 | Author : 織田 繁 (AWS Community Hero)

Missing alt text value

はじめに

こんにちは、AWS Community Hero の織田繁です。

2026 年 6 月 25 日~26 日に幕張メッセで開催された AWS Summit Japan 2026 にて、『データレイクの「見えない問題」を可視化する — Amazon S3 + Apache Iceberg メダリオンアーキテクチャのオブザーバビリティ実践ガイド』というタイトルで登壇させて頂きました。
この記事では、AWS Summit Japan 2026 で語れなかった点を深く掘り下げたいと思います。

AWS Summit Japanでの当時の発表資料は こちら になります。

 

X ポスト » | Facebook シェア » | はてブ »

builders.flash メールメンバー登録

builders.flash メールメンバー登録で、毎月の最新アップデート情報とともに、AWS を無料でお試しいただけるクレジットコードを受け取ることができます。

今すぐ登録 »

課題について

「データレイクにデータを入れたけど、その後どうなっているか把握できていない」。これはデータ基盤チームが抱える課題の一つです。パイプラインは動いている。Amazon Athena でクエリも返ってくる。でも裏側では、こんなことが起きているかもしれません。

  • 課題 A : 1行ずつ INSERT された数KBの小さな Parquet ファイルが Bronze 層に数千個溜まっている
  • 課題 B : Silver 層の UPDATE/DELETE のたびに増え続けるスナップショットがメタデータを肥大化させている
  • 課題 C : DELETE したはずの個人情報が古いデータファイルに残っている(個人情報保護違反リスク)

課題と対応する検証

本記事では、これらの「見えない問題」への対応を検証します。

課題
対応する検証
課題 A : スモールファイルの蓄積

検証 1 : スモールファイル問題の可視化

課題 B : スナップショットの肥大化

検証 2 : スナップショットの肥大化と expire_snapshots

課題 C : 個人情報が古いファイルに残存

検証 3 : 個人情報保護リスク — DELETE しても消えないデータ

検証環境

メダリオンアーキテクチャの 3 層

本検証では、Raw テーブルおよびメダリオンアーキテクチャ (Bronze / Silver / Gold) の 3 層構造で、すべてのテーブルを Iceberg v2 で構成します。

各層の役割と操作

テーブル
役割
主な操作
Raw

external_nyc_taxi

NYC Taxi 元データ参照 (外部テーブル)

SELECT のみ

Raw

external_taxi_zones

 Taxi Zone マスター参照 (外部テーブル)

SELECT のみ

Bronze

bronze_taxi_trips

Taxi 乗車データの取り込み (append-only)

INSERT

Bronze

bronze_taxi_zones

Taxi Zone マスターの取り込み

INSERT (初回のみ)

Silver

silver_trips_enriched

Zone 情報を結合・クレンジング・擬似 PII 付与

INSERT / UPDATE / DELETE

Gold

gold_daily_borough_summary

Borough 別・日別集計

INSERT (集計結果)

データソース : 本記事では NYC Taxi & Limousine Commission (TLC) Trip Record Data を使用しています。

  • Yellow Taxi Trip Records : 乗車記録データ (Parquet 形式、月次約 300 万件・50 MB)。1 時間あたり約 4,000 ~ 5,000 件。PULocationID (乗車地点) と DOLocationID (降車地点) で Taxi Zone と結合可能。
  • Taxi Zone Lookup : 265 ゾーンのマスターデータ (CSV 形式)。LocationID で Trip Records と結合し、Borough (Manhattan, Brooklyn, Queens, Bronx, Staten Island) や Zone 名を取得。

Bronze 層では、外部テーブルから Iceberg テーブルへ 1 時間単位で INSERT を繰り返すことでスモールファイル問題を再現します。これは Amazon Kinesis Data Firehose や AWS Glue Streaming のマイクロバッチ処理を模したシナリオです。
Silver 層で両テーブルを JOIN し、擬似的な個人情報 (passenger_email) を付与して個人情報保護の検証に使用します。
Gold 層では Borough 別・日別の乗車数と売上を集計します。

検証環境構築

本記事の Terraform テンプレートと SQL スクリプトは GitHub リポジトリinfra/ ディレクトリにまとまっています。以下の手順で同じ環境を構築できます。

前提条件

  • Terraform >= 1.15
  • AWS CLI v2 (認証済み)
  • リージョン: ap-northeast-1

1. Terraform デプロイ

bash
cd infra
terraform init
terraform plan
terraform apply

2. NYC Taxi データのダウンロード、テーブル作成、データ投入

bash
# データダウンロード → Iceberg テーブル作成 → データ投入を一括実行
./scripts/reset_and_load.sh

個別に実行する場合 :

bash
./scripts/01_download_raw_data.sh   # NYC Taxi データ (Parquet/CSV) を Amazon S3 raw/ にダウンロード
./scripts/02_run_ddl.sh             # Iceberg v2 テーブル (Bronze/Silver/Gold) を作成
./scripts/03_load_monthly.sh        # 2024年1〜3月分を投入 (月初1日は1時間単位INSERTでスモールファイルを再現) 
./scripts/04_load_zones.sh          # Taxi Zone マスタデータを投入
./scripts/05_load_silver.sh         # Bronze (trips + zones) をJOINし擬似PII付与してSilverへ投入
./scripts/06_correct_silver.sh      # Silverに対する10パターンの補正UPDATE (検証2: スナップショット肥大化シナリオ) 
./scripts/07_load_gold.sh           # Silverを日別×Borough別に集計してGoldへ投入

検証 1 : スモールファイル問題の可視化

なぜ問題なのか

Iceberg では、1回の INSERT (コミット) ごとに新しいデータファイルとスナップショットが作られます。ストリーミングやマイクロバッチのように高頻度・小刻みに INSERT を繰り返すと、コミット回数分の小さなファイルが積み重なります。メダリオンアーキテクチャでは、ソースからの取り込みを担う Bronze 層が Amazon Kinesis Data Firehose などのマイクロバッチ処理を受け止める入口になりやすく、この問題が顕著に現れます。

これが引き起こす問題:

  • クエリ性能の劣化 : ファイルを開く I/O オーバーヘッドがファイル数に比例
  • Amazon S3 API コストの増加 : GET リクエスト数がファイル数に比例
  • メタデータの肥大化 : マニフェストファイルが全ファイルを追跡

再現方法

外部テーブル (external_nyc_taxi) から Iceberg テーブル (bronze_taxi_trips) へのデータ投入は、環境構築手順の scripts/03_load_monthly.sh で実施済みです。このスクリプトは、Amazon Kinesis Data Firehose や AWS Glue Streaming のマイクロバッチ処理を模したシナリオとして、以下の投入パターンを 2024 年 1 月 ~ 3 月の 3 ヶ月分について実行します。

  • 各月の 1 日分 : 1 時間単位で 24 回に分けて INSERT (スモールファイルを意図的に再現)
  • 各月の 2 日 ~ 月末 : 一括で 1 回 INSERT (比較用の正常パターン)

NYC Yellow Taxi のデータは 1 時間あたり約 4,000 ~ 5,000 件のため、1 時間単位の INSERT では各ファイルが約 50 ~ 100 KB にしかならず、推奨サイズ (128 MB ~ 512 MB) には遠く及びません。一方、月初以外の日をまとめた一括 INSERT では、月あたり 1 個のファイルとしてまとまった大きさになります。

3 ヶ月分ではこのパターンが月ごとに繰り返され、月初 1 日の 24 回 INSERT ×  3 ヶ月 = 72 回に加え、月 2 日〜月末の一括 INSERT × 3 ヶ月 = 3 回、合計約 75 回の書き込みが bronze_taxi_trips に対して行われています。

事前検証 : rewrite_data_files

Iceberg の $files メタデータテーブルで Bronze テーブルの 2024 年 1 月パーティションにおけるファイルサイズの分布を確認します。 Amazon Athena で以下の Query を実行します。

sql
SELECT
    regexp_extract(file_path, 'tpep_pickup_datetime_month=([0-9]{4}-[0-9]{2})', 1) AS partition_month,
    CASE
        WHEN file_size_in_bytes < 1048576 THEN 'small (<1MB)'
        WHEN file_size_in_bytes < 134217728 THEN 'medium (1-128MB)'
        ELSE 'large (>128MB)'
    END AS size_category,
    COUNT(*) AS file_count,
    SUM(file_size_in_bytes) / 1048576.0 AS total_size_mb,
    AVG(file_size_in_bytes) / 1048576.0 AS avg_size_mb
FROM s3_datalake_obs_db."bronze_taxi_trips$files"
WHERE regexp_extract(file_path, 'tpep_pickup_datetime_month=([0-9]{4}-[0-9]{2})', 1) = '2024-01'
GROUP BY 1, 2
ORDER BY 1, 2

クエリの実行結果

1 時間単位の INSERT で24 個のスモールファイル (平均約 0.08MB) が生成されました。これは初日に 24 回の 1 時間単位 INSERT を実行した結果です。一方、月初 1 日を除く残りの日をまとめた一括 INSERT では、約 57.19MB のファイルが 1 個生成されており、スモールファイルとの差が顕著です。

#
partition_month
size_category
file_count
total_size_mb
avg_size_mb
1

2024-01

medium (1-128MB)

1

57.19

57.19

2

2024-01

small (< 1MB) 

24

1.92

0.08

対処 : rewrite_data_files

rewrite_data_filesは scripts/11_run_rewrite_data_files.sh から実行します。内部では AWS Glue Job (glue_rewrite_data_files_job.py) を起動し、指定した月のパーティションに対して Spark SQL 経由で rewrite_data_files を呼び出します。

bash
# 第1引数 : コンパクション対象月 (YYYY-MM形式、省略時は 2024-01) 
./scripts/11_run_rewrite_data_files.sh 2024-01

対処結果 : rewrite_data_files

11_run_rewrite_data_files.sh 2024-01 の実行結果は以下のようになりました。(before / after の比較、対象は 2024 年 1 月のパーティションのみ)

#
状態
partition_month
size_category
file_count
total_size_mb
avg_size_mb
1

before

2024-01

medium (1 - 128MB) 

1

57.19

57.19

2

before

2024-01

small (< 1MB)

24

1.92

0.08

3

after

2024-01

medium (1 - 128MB)

1

60.34

60.34

25 個のファイル (24 個のスモールファイル + 1 個の中サイズファイル、合計 59.11 MB) が、1 個のファイル (60.34MB) に統合されました。ファイル数は 25 → 1 に削減されています。合計サイズはコンパクションによる再エンコードでわずかに増加しています。

ファイル数が減ったことで、クエリ性能 (メタデータスキャン削減) と API コスト (GET リクエスト削減) の両方が改善します。

運用のポイント : rewrite_data_files は Amazon EMR や AWS Glue Job から手動実行するほか、Amazon EventBridge Scheduler でジョブを定期起動して自動化できます。ただし実行自体も Amazon S3 への書き込みと既存ファイルの読み込みを発生させるため、実行タイミングの調整が必要です。やむを得ず書き込みと並行して実行する場合は、where オプションでパーティションを絞り、書き込み中のパーティション (例: 当日分) を避けて過去パーティションだけを対象にすると衝突を回避できます。

事前検証: rewrite_manifests

rewrite_data_files はデータファイル (Parquet) 自体のコンパクションであり、マニフェストファイル (メタデータ) は対象外です。高頻度なコミット (本記事では 1 時間単位のマイクロバッチ INSERT) が続くと、コミット回数分のマニフェストファイルが積み重なり、データファイル数を上回るマニフェスト断片化が起こることがあります。マニフェストが断片化すると、クエリ実行時のスキャンプランニング (対象データファイルの特定処理) で読み込むメタデータファイル数が増え、プランニング性能が劣化します。

クエリを実行

Iceberg の $manifests メタデータテーブルで、bronze_taxi_trips テーブル全体のマニフェストファイル数・サイズを確認します。Amazon Athena で以下の Query を実行します。

sql
SELECT
    COUNT(*) AS manifest_count,
    SUM(length) / 1024.0 AS total_size_kb,
    AVG(length) / 1024.0 AS avg_size_kb,
    SUM(added_data_files_count + existing_data_files_count) AS total_tracked_data_files
FROM s3_datalake_obs_db."bronze_taxi_trips$manifests";

実行結果

マニフェストファイルが 76 個存在するのに対し、追跡しているデータファイル数は 51 個です。マニフェスト数がデータファイル数を上回っており、1 回のコミットごとに新しいマニフェストが作られる仕組み上、rewrite_data_files でデータファイルを統合した後も、過去のコミット履歴で作られた小さなマニフェストファイル自体は残ったままであることが分かります。

#
manifest_count
total_size_kb
avg_size_kb
total_tracked_data_files
1

76

625.78 

8.23

51

対処 : rewrite_manifests

rewrite_manifests は scripts/12_run_rewrite_manifests.sh から実行します。内部では AWS Glue Job (glue_rewrite_manifests_job.py) を起動し、bronze_taxi_trips テーブル全体に対して Spark SQL 経由で rewrite_manifests を呼び出します。rewrite_data_files とは異なり、対象月などのパーティション指定は不要です (データファイルではなくマニフェストファイルの再編成のため、テーブル全体を対象にします) 。

bash
./scripts/12_run_rewrite_manifests.sh

対処結果 : rewrite_manifests

12_run_rewrite_manifests.sh の実行結果は以下のようになりました。(before / after の比較、対象はテーブル全体)

#
状態
manifest_count
total_size_kb
avg_size_kb
total_tracked_data_files
1

before 

76 

625.78

8.23

51

2

after

1

21.43

21.43

51

76 個のマニフェストファイル (合計 625.78KB) が、1 個のマニフェストファイル (21.43KB) に統合されました。マニフェスト数は 76 → 1 に削減され、合計サイズも約 29 分の 1 に縮小しています。total_tracked_data_files (追跡しているデータファイル数) は 51 件のまま変化していないことから、rewrite_manifests はデータファイル自体には手を加えず、メタデータ (マニフェスト) のみを再編成していることが確認できます。

マニフェスト数が減ったことで、クエリ実行時のスキャンプランニング (対象データファイルを特定する処理) で読み込むメタデータファイル数が減り、プランニング性能が改善します。

検証 2 : スナップショットの肥大化と expire_snapshots

なぜ問題なのか

Iceberg は書き込み操作のたびにスナップショットを作成します。Bronze 層は高頻度 INSERT (本記事では 1 時間単位のマイクロバッチ) によってスナップショットが積み上がります。Silver 層は Bronze からの INSERT (クレンジング・JOIN を含む変換処理) に加え、以下のような後発的な UPDATE/DELETE が発生するため、スナップショットの増加要因が Bronze よりも多様になります。

  • 遅延データの反映 : TLC が後日訂正したレコードに対する UPDATE / MERGE
  • 重複排除 : 同一 trip_id の重複を取り除く DELETE
  • 個人情報保護対応 : 顧客からの削除依頼に応じた DELETE
  • データ品質修正 : 後から判明した異常値の UPDATE

各スナップショットはマニフェストファイルを持ち、テーブル数や書き込み頻度が増えるほど、メタデータ全体のサイズも増加していく傾向があります。

再現方法

Silver 層に対するクレンジング処理は、環境構築手順の scripts/06_correct_silver.sh で実施済みです。このスクリプトは、例外値の補正を目的として以下の 10 パターンの補正を実施しています。

  1. 距離当たりの運賃が異常に高いレコードの補正 (中央値単価 \ $7.4/mile で再計算)
  2. 距離がゼロなのに高額請求のレコードを補正 (初乗り運賃 $3.00 に補正)
  3. 長距離なのに運賃が異常に安いレコードを補正 (中央値単価 $7.4/mile で再計算)
  4. 運賃・距離が負の値のレコードを補正 (絶対値に補正)
  5. 降車時刻が乗車時刻以前のレコードを補正 (距離ベースの想定時間で再計算)
  6. 乗車時間が 3 時間超のレコードを補正 (距離ベースの想定時間で再計算)
  7. 乗客数がゼロのレコードを補正 (1 人に補正)
  8. 乗客数が 6 人超のレコードを補正 (法定最大乗車人数 6 人に補正)
  9. チップが運賃の 2 倍を超える異常値を補正 (チップ率 100% に補正)
  10. 走行距離が 100 マイル超のレコードを補正 (運賃から逆算した距離に補正)

このパターンが月ごとに繰り返され、10 個のクレンジング処理 × 3 ヶ月 = 30 回の UPDATE が silver_trips_enriched に対して行われています。

事前検証 : expire_snapshots

Iceberg の $snapshots / $manifests メタデータテーブルで、silver_trips_enriched テーブル全体のスナップショット数・マニフェスト数を確認します。Amazon Athena で以下の Query を実行します。

sql
SELECT
    (SELECT COUNT(*) FROM s3_datalake_obs_db."silver_trips_enriched$snapshots") AS snapshot_count,
    (SELECT COUNT(*) FROM s3_datalake_obs_db."silver_trips_enriched$manifests") AS manifest_count;

実行結果

silver_trips_enriched には 33 個のスナップショットが積み重なっています。これは、環境構築手順の 05_load_silver.sh (3 ヶ月分の INSERT) と 06_correct_silver.sh (10 パターン × 3ヶ月 = 30 回の補正 UPDATE) によって作られたスナップショットです。1 回の書き込み操作ごとに新しいスナップショットとマニフェストが追加されるため、expire_snapshots を実行せずに運用を続けると、この数は書き込み回数に比例して増え続けます。

#
snapshot_count
manifest_count
1

33

63

対処 : expire_snapshots

expire_snapshots は scripts/14_run_expire_snapshots.sh から実行します。内部では AWS Glue Job (glue_expire_snapshots_job.py) を起動し、指定した月のパーティションに対して Spark SQL 経由で expire_snapshots を呼び出します。

bash
# 第 1 引数 : older_than (この時刻より古いスナップショットを削除対象にする、"YYYY-MM-DD HH :MM :SS"形式、省略時は "2024-01-08 00 :00 :00") 
# 第 2 引数 : retain_last (削除対象から除外する直近スナップショットの最小保持数、省略時は 1) 
# older_than には現在時刻 (UTC) を指定する。Iceberg の committed_at は UTC 基準のため、`date -u` で UTC の現在時刻を取得する。
OLDER_THAN=$(date -u "+%Y-%m-%d %H :%M :%S")
./scripts/14_run_expire_snapshots.sh "${OLDER_THAN}" 1

older_than に現在時刻を指定すると、直近 retain_last 件 (デフォルト 1 件 = 最新スナップショット) を除く全てのスナップショットが削除対象になります。これにより time travel (過去スナップショットへのロールバック) ができなくなる点に注意してください。

対処結果 : expire_snapshots

14_run_expire_snapshots.sh の実行結果は以下のようになりました。(before / after の比較、対象はテーブル全体)

#
状態
snapshot_count
manifest_count
1

before

33 

63

2

after

1

63

スナップショット数は 33 → 1 に削減されましたが、マニフェスト総数 ($manifests の件数) は 63 件のまま変化していません。これは、silver_trips_enriched の各コミットが既存のマニフェストファイルを再利用せず新規マニフェストを都度作成する一方、削除対象になった古いスナップショットが参照していたマニフェストの一部が、直近のスナップショット (retain_last=1 で保持される最新世代) からも引き続き参照されているためです。
expire_snapshots はスナップショット履歴の整理が主目的であり、マニフェスト自体の断片化解消には直接寄与しません。マニフェストの断片化を解消したい場合は、検証 1 で扱った rewrite_manifests を silver_trips_enriched に対しても別途実行する必要があります。

検証 3 : 個人情報保護リスク — DELETE しても消えないデータ

なぜ問題なのか

Iceberg v2 の DELETE は、テーブルの write.delete.mode 設定によって挙動が異なります。デフォルトの Merge-on-Read (MoR) では、DELETE 実行時に既存のデータファイルは書き換えられず、削除対象行を記録した position delete fileだけが新規作成される論理削除になります。元のデータファイル自体は変更されず Amazon S3上にそのまま残り、読み取り時に delete file と突き合わせて対象行が除外されます (本検証はこの MoR の挙動を前提としています) 。一方 Copy-on-Write (CoW) では、削除対象行を含むデータファイルを読み込んで削除後の内容で新しいデータファイルを書き直すため delete file は生成されませんが、その場合も元のデータファイルは書き換えられず、古いスナップショットが有効な間は Amazon S3 上に残り続けます。

つまり、「顧客 A のデータを Silver で DELETE した」と思っていても、Amazon S3 上の複数の層に個人情報が残り続けます。個人情報保護法における利用停止・消去請求への対応義務に違反するリスクがあります。

再現方法と事前検証

環境構築手順の scripts/05_load_silver.sh で、個人情報と仮定した passenger_email の登録は実施済みです。このスクリプトは、月ごとに tpep_pickup_datetime 順の連番を用いて passenger_[連番]@example.com という形式の擬似個人情報 (passenger_email) を各レコードに付与しています。ドメインは実在しない example.com を使用しており、実データではありません。なお連番は月単位の INSERT ごとにリセットされるため、passenger_1@example.com は複数件 (月ごとに 1 件) 存在します。

sql
-- Silver 層で顧客データを削除
DELETE FROM silver_trips_enriched
WHERE passenger_email = 'passenger_1@example.com';

snapshot_id と parent_id を取得

Amazon Athena で現在の snapshot_id と、1世代前の snapshot_id である parent_id を取得します。

sql
-- snapshot_idとparent_idの取得
SELECT snapshot_id,parent_id
FROM "silver_trips_enriched$snapshots"
ORDER BY committed_at DESC
LIMIT 1;

取得できた値

以下は私の検証時に取得できた値です。

  • snapshot_id : 8322873667900583576
  • parent_id : 3738339397799175547

parent_id の取得結果を確認

一方、parent_id では削除前の状態となるため、件数が取得されます。

sql
-- 取得結果は 3 件
SELECT COUNT(passenger_email) FROM silver_trips_enriched FOR VERSION AS OF 3738339397799175547
WHERE passenger_email = 'passenger_1@example.com'

snapshot_id の取得結果を確認

現在の snapshot_id では、MoR により読み取り時に削除対象の行が除外されるため、取得件数はゼロ件となります。

sql
-- 取得結果はゼロ件
SELECT COUNT(passenger_email) FROM silver_trips_enriched FOR VERSION AS OF 8322873667900583576
WHERE passenger_email = 'passenger_1@example.com'

このことから、DELETE を実行してもデータファイルには passenger_1@example.com の個人情報が物理的に残っていることが確認できます。

Amazon S3 上の実データファイルに対する直接確認 (補足)

time travel クエリでの確認に加え、Amazon S3 上の実際のデータファイルに passenger_1@example.com が物理的に残っていることを、Iceberg のメタデータを介さずに直接検証します。aws s3api select-object-content (Amazon S3 Select) を使い、silver_trips_enriched テーブルの全 Parquet ファイル (656 個) に対して、passenger_email = 'passenger_1@example.com' を直接検索しました。

全データファイルの key 一覧を取得

まず aws s3api list-objects-v2 で対象テーブルの全データファイルの key 一覧を取得します。

bash
cd infra
BUCKET=$(terraform output -raw datalake_bucket_name)

aws s3api list-objects-v2 \
  --bucket "${BUCKET}" \
  --prefix "warehouse/silver/trips_enriched/data/" \
  --query 'Contents[].Key' --output text | tr '\t' '\n' > /tmp/silver_all_files.txt

wc -l /tmp/silver_all_files.txt

取得した key 一覧を並列検索

取得した key 一覧を 1 件ずつ select-object-content に渡して並列検索します。

bash
search_one() {
  local key="$1"
  local result
  result=$(aws s3api select-object-content \
    --bucket "${BUCKET}" \
    --key "$key" \
    --expression "SELECT s.passenger_email FROM s3object s WHERE s.passenger_email = 'passenger_1@example.com'" \
    --expression-type SQL \
    --input-serialization '{"Parquet" :{}}' \
    --output-serialization '{"JSON" :{}}' \
    /dev/stdout 2>/dev/null)
  if [[ -n "$result" && "$result" == *"passenger_1@example.com"* ]]; then
    echo "FOUND : $key"
  fi
}
export -f search_one
export BUCKET

cat /tmp/silver_all_files.txt | xargs -P 20 -I{} bash -c 'search_one "$@"' _ {} > /tmp/silver_search_results.txt

cat /tmp/silver_search_results.txt

list-objects-v2 の Key (例 : warehouse/silver/trips_enriched/data/8iYWKA/pickup_borough=Manhattan/...parquet) が、そのまま select-object-content --key に渡す値になります。バケット名を含む s3 :// 形式ではなく、バケット直下からの相対パス (Key) である点に注意してください。

検索結果

656 個中、5 個のデータファイルに passenger_1@example.com が実際に含まれていることを確認しました。

bash
warehouse/silver/trips_enriched/data/8iYWKA/pickup_borough=Manhattan/...parquet
warehouse/silver/trips_enriched/data/LWtMqw/pickup_borough=Manhattan/...parquet
warehouse/silver/trips_enriched/data/p_MO5Q/pickup_borough=Manhattan/...parquet
warehouse/silver/trips_enriched/data/vGzp2Q/pickup_borough=Manhattan/...parquet
warehouse/silver/trips_enriched/data/xQZuxw/pickup_borough=Manhattan/...parquet

これらは孤児ファイルではなく、現行スナップショットからも delete file を介して引き続き参照されているファイルです。Amazon Athena で SELECT COUNT(*) を実行すると 0 件 (MoR による論理削除が反映された結果) になる一方で、Amazon S3 上には個人情報を含む生データがそのまま残存し、かつ現行スナップショットからも間接的に参照され続けていることが、確認できました。

対処 : rewrite_data_files と expire_snapshots

前節の直接検証で確認した通り、DELETE 済みの元データファイルは delete file を介して現行スナップショットから引き続き参照されているため、元データファイルを物理削除するには次の2ステップが必要です。

  1. rewrite_data_files (13_run_rewrite_silver_data_files.sh) : delete file をデータファイルにマージし、削除対象行を除いた新しいデータファイルを作成する。これにより元データファイルはどのスナップショットからも参照されなくなる。
  2. expire_snapshots (14_run_expire_snapshots.sh) : rewrite 前の古いスナップショットを失効させる。失効したスナップショットのみが参照していたデータファイル・マニフェストファイルは、この時点で Iceberg によって Amazon S3 から物理削除される。

delete file をデータファイルにマージ

まず 13_run_rewrite_silver_data_files.sh で delete file をデータファイルにマージします。

bash
./scripts/13_run_rewrite_silver_data_files.sh

delete-file-threshold の設定 : rewrite_data_files は、デフォルトでは delete file をデータファイルにマージしません (AWS Prescriptive Guidance より) 。マージ対象にするには delete-file-threshold (何個以上の delete file が紐づいたらマージ対象にするかの閾値) を明示的に指定する必要があります。さらに、min-file-size-bytes 未満の小さいファイルのみを対象にする bin packing のロジックとは独立しているため、delete file を持つ大きいファイル (min-file-size-bytes を超えるサイズ) は、delete-file-threshold を指定しない限りコンパクション対象から漏れます。
個人情報を確実に削除する目的で rewrite_data_files を実行する場合は、ファイルサイズに関わらず delete file を持つ全てのファイルを対象にするため delete-file-threshold を明示的に設定する必要があります。

古いスナップショットを削除

次に 14_run_expire_snapshots.sh で古いスナップショットを削除します。

bash
# 第 1 引数  : older_than (この時刻より古いスナップショットを削除対象にする、"YYYY-MM-DD HH :MM :SS"形式、省略時は "2024-01-08 00 :00 :00") 
# 第 2 引数  : retain_last (削除対象から除外する直近スナップショットの最小保持数、省略時は 1) 
# older_than には現在時刻 (UTC) を指定する。Iceberg の committed_at は UTC 基準のため、`date -u` で UTC の現在時刻を取得する。
OLDER_THAN=$(date -u "+%Y-%m-%d %H :%M :%S")
./scripts/14_run_expire_snapshots.sh "${OLDER_THAN}" 1

対処結果 : rewrite_data_files と expire_snapshots

14_run_expire_snapshots.sh の実行後、「Amazon S3 上の実データファイルに対する直接確認 (補足) 」で見つかった 5 ファイルに対して aws s3api head-object を実行したところ、5 ファイル全てが 404 Not Found となり、Amazon S3 上から物理削除されていることを確認できました。

bash
# 「Amazon S3 上の実データファイルに対する直接確認 (補足) 」で FOUND : と出力された
# キーの一覧を対象に、head-object で物理削除の有無を確認する
for key in $(grep '^FOUND :' /tmp/silver_search_results.txt | sed 's/^FOUND : //'); do
  echo "=== ${key} ==="
  aws s3api head-object --bucket "${BUCKET}" --key "${key}" 2>&1
done
# => 5ファイル全て An error occurred (404) when calling the HeadObject operation : Not Found

つまり本検証では、rewrite_data_files で delete file をマージして元データファイルへの参照を断ち、続けて expire_snapshots で古いスナップショットを失効させた時点で、passenger_1@example.com を含む5ファイルは物理削除まで完了していました。expire_snapshots は「失効させたスナップショットのみが参照していたデータファイル・マニフェストファイル」も合わせて削除する仕様のため、rewrite_data_files によって元データファイルへの参照が現行スナップショットから完全に切れていれば、追加で remove_orphan_files を実行するまでもなく削除が完了します。

まとめ

観点
使った機能
対象層
検出できたか
スモールファイル問題

Iceberg $files メタデータ 

Bronze

✅ 層別のファイルサイズ分布を可視化

スナップショット肥大化

Iceberg $snapshots メタデータ  

Silver

✅ 操作種別・時刻まで追跡可能

個人情報保護リスク

Iceberg $snapshots (time travel) + $files

Silver

✅ position delete file と孤児ファイルを検出

結論 : AWS のサービスだけで、メダリオンアーキテクチャのデータレイクに潜む「見えない問題」を十分に可視化し、対処できます。

メダリオンアーキテクチャの利点は、層ごとに問題の性質が異なることを構造的に整理できる点です:

  • Bronze : スモールファイル問題 + スナップショット肥大化 (append-only の高頻度 INSERT に起因) が集中
  • Silver : Bronze からの INSERT に加え、UPDATE/DELETE も加わることでスナップショット増加要因が多様化 + 個人情報保護リスクが集中
  • Gold : 比較的安定 (集計結果のみ)  

さいごに

メタデータファイルを利用した Apache Icebergにおける課題と対応を、Bronze・Silver 両層の 3 つの検証を通じて紹介しました。

$files$snapshots$manifests といったメタデータテーブルを Amazon Athena から直接クエリすることで、データレイクの中で実際に何が起きているかを追加のツールなしで可視化できます。

Gold 層で利用される価値あるデータを作成するには、これらの課題を解決する必要がありますが、Apache Iceberg を活用した DataLake の運用に取り組む方の助けになれば幸いです。

筆者プロフィール

織田 繁
Sansan株式会社
AWS Community Hero / BigData-JAWS、JAWS-UG 初心者支部 運営

AWS クラウドエンジニアとして、Apache Iceberg や AWS Glue、Spark、Terraform を活用したデータレイク基盤の設計・構築に携わっています。AWS Community Builders・AWS Heroes コミュニティのメンバーとして、技術記事の執筆やワークショップ開発などを通じて AWS コミュニティへの貢献も行っています。趣味は 4 歳児の子供と遊び成長を感じること。

X : @OutputSeq
GitHub : @shigeru-oda
zenn.dev : @shigeru_oda

A man holding a baby close to his shoulder, looking directly at the camera in a softly lit room.