Apache Iceberg 用の BigQuery テーブルを操作してみる #cm_google_cloud_adcal_2024

Apache Iceberg 用の BigQuery テーブルを操作してみる #cm_google_cloud_adcal_2024

Clock Icon2024.12.25

はじめに

データ事業本部のkobayashiです。
クラスメソッドの Google Cloud Advent Calendar 2024 の 最終日のブログです。

https://qiita.com/advent-calendar/2024/cm-google-cloud

BigQueryでApache Icebergテーブルのプレビューサポートが発表されました。この機能により、従来のBigQueryの管理機能やパフォーマンスを維持したまま、Apache IcebergフォーマットでデータをGoogle Cloud Storage(GCS)に保存できるようになります。

本記事では、この新機能の基本的な使い方と、実際に試してみて気づいた注意点などについて紹介していきます。

https://cloud.google.com/bigquery/docs/release-notes#October_10_2024

Apache Iceberg 用の BigQuery テーブルとは

Apache Iceberg用のBigQueryテーブル(以下、Icebergテーブル)は、オープンな形式でデータレイクハウスを構築するためのGoogle Cloudの機能になります。この機能により、従来のBigQueryテーブルの管理機能や使い勝手のままデータをGCSにParquet形式で保存できるようになりました。

https://cloud.google.com/bigquery/docs/iceberg-tables?hl=ja#required-roles

主な特徴は、データを移動することなくBigQueryの分析機能を活用できることです。これにより、データガバナンスの観点からデータを自社管理下に置きたいというニーズと、BigQueryの強力な分析機能を活用したいというニーズを満たせます。

  • Icebergテーブルのフルマネージド運用
    • BigQueryの標準テーブルと同様のフルマネージド体験を提供
    • 自動ストレージ最適化機能により、適応型ファイルサイズの設定や自動クラスタリング、ガベージコレクション、メタデータの最適化などが自動的に行われる
    • 列レベルのセキュリティやデータマスキングなど、エンタープライズレベルのセキュリティ機能も利用可能
  • 柔軟なデータ操作
    • GoogleSQL DMLを使用したテーブル操作が可能で、データの更新や削除も自由に行える
    • SparkやDataflowなどの外部エンジンとBigLakeコネクタを介して連携することで、バッチ処理や高スループットストリーミングの統合も可能
  • スキーマ進化のサポート
    • ビジネスニーズの変化に応じて、列の追加、削除、名前変更が可能で、既存の列のデータ型や列モードの変更も可能

一方以下のような制限もあるので使用する際はご注意ください。

  • データの整合性を保つため、以下の点に特に注意が必要
    • BigQuery外からのデータファイルの直接操作は厳禁
    • 外部からファイルを追加した場合、そのデータは追跡されずに損失する可能性がある
    • テーブル削除時、関連するデータファイルは自動的には削除されない
  • 運用上の重要な考慮事項
    • 新規テーブル作成は必ず空の接頭辞に対して行う
    • テーブルごとに一意のURIを使用する
    • バケットへの外部ツールからの書き込み権限は厳密に制限する

では実際にApache Iceberg 用の BigQuery テーブルを作成してみます。

Apache Iceberg 用の BigQuery テーブルを作成する

BigQueryにIceberg テーブルを作成するにはGCSに対して読み書きを行うのでGCSへのCloudリソース接続の設定を用意する必要があります。
はじめにこの設定を行いますが、以下のbqコマンドで作成します。

$ bq mk --connection --location=us --project_id={プロジェクトID} --connection_type=CLOUD_RESOURCE test-bq-iceberg

これでCloudリソース接続を作成できましたが接続設定に割り当てられたサービスアカウントにIcebergテーブルを作成するGCSへのオブジェクトの読み書き権限を付与する必要があります。これは外部接続を通してGCS上にIcebergのdataやmetadataを作成・読み取りするためです。

bq show --connection {プロジェクトID}.us.test-bq-iceberg
Connection {プロジェクトID}.us.test-bq-iceberg

               name                 friendlyName   description    Last modified         type        hasCredential                                            properties                                            
 --------------------------------- -------------- ------------- ----------------- ---------------- --------------- ----------------------------------------------------------------------------------------------- 
  233151396088.us.test-bq-iceberg                                16 Dec 16:42:26   CLOUD_RESOURCE   False           {"serviceAccountId": "bqcx-233151396088-7wjn@gcp-sa-bigquery-condel.iam.gserviceaccount.com"} 

このコマンドで表示されたserviceAccountIdに対してroles/storage.objectViewerroles/storage.objectCreatorを付与します。

$ gcloud storage buckets add-iam-policy-binding gs://{バケット名} \
    --member=serviceAccount:bqcx-233151396088-7wjn@gcp-sa-bigquery-condel.iam.gserviceaccount.com \
    --role=roles/storage.objectCreator
$ gcloud storage buckets add-iam-policy-binding gs://{バケット名} \
    --member=serviceAccount:bqcx-233151396088-7wjn@gcp-sa-bigquery-condel.iam.gserviceaccount.com \
    --role=roles/storage.objectViewer

これで接続設定が終わったので次にIcebergテーブルを作成します。
今回作成するテーブルですが以下のようなparquetファイルをインポートするテーブルとなります。

FL_DATE DEP_DELAY ARR_DELAY AIR_TIME DISTANCE DEP_TIME ARR_TIME
2006-01-01 5 19 350 2475 9.083 12.483
2006-01-02 167 216 343 2475 11.783 15.767
2006-01-03 -7 -2 344 2475 8.883 12.133
2006-01-04 -5 -13 331 2475 8.917 11.95
2006-01-05 -3 -17 321 2475 8.95 11.883
2006-01-06 -4 -32 320 2475 8.933 11.633
2006-01-08 -3 -2 346 2475 8.95 12.133

それではBiqQueryで以下のSQLを実行します。

-- Icebergテーブル作成
CREATE TABLE `data_set_iceberg.flights` (
    FL_DATE DATE,           -- 日付型を使用
    DEP_DELAY INT64,      
    ARR_DELAY INT64,
    AIR_TIME INT64,         -- 時間は整数として扱う
    DISTANCE INT64,         -- 距離は整数
    DEP_TIME FLOAT64,       -- 時刻は小数点形式
    ARR_TIME FLOAT64
)
CLUSTER BY FL_DATE         -- 日付でクラスタリング
WITH CONNECTION `projects/{プロジェクトID}/locations/us/connections/test-bq-iceberg`
OPTIONS (
    file_format = 'PARQUET',
    table_format = 'ICEBERG',
    storage_uri = 'gs://{バケット名}/test-bg-iceberg/flights/'
);

これでIcebergテーブルが作成出来ました。テーブルの作成先に指定したGCSのオブジェクトを確認してみます。

gcloud storage ls -l -r --readable-sizes gs://{バケット名}/test-bg-iceberg/flights
gs://{バケット名}/test-bg-iceberg/flights/:

gs://{バケット名}/test-bg-iceberg/flights/metadata/:
   107.00B  2024-12-16T07:50:49Z  gs://{バケット名}/test-bg-iceberg/flights/metadata/v0.metadata.json
TOTAL: 1 objects, 107 bytes (107.00B)

するとmetadataディレクトリにmetadataのjsonファイルが作為されています。このv0.metadata.jsonの中身を確認してみると以下のようなjsonでした。

{"properties":{"bigquery-table-id":"{プロジェクトID}.data_set_iceberg.flights"},"current-snapshot-id":-1}

Icebergテーブルにデータのデータを操作する

parquetのインポート

それではこのテーブルにparquetデータをインポートします。あたらじめインポートするparquetファイルをgs://{バケット名}/source/flights-1m.parquetにアップロードしておきます。

> LOAD DATA INTO `data_set_iceberg.flights`
FROM FILES (
uris=['gs://{バケット名}/source/flights-1m.parquet'],
format='PARQUET');

これでIcebergテーブルにデータをインポートできたので確認してみます。

> SELECT * FROM `{プロジェクトID}.data_set_iceberg.flights` LIMIT 5;
| FL\_DATE | DEP\_DELAY | ARR\_DELAY | AIR\_TIME | DISTANCE | DEP\_TIME | ARR\_TIME |
| :--- | :--- | :--- | :--- | :--- | :--- | :--- |
| 2006-01-04 | -2 | -6 | 281 | 2475 | 15.216666221618652 | 23.299999237060547 |
| 2006-01-26 | -5 | -38 | 331 | 2586 | 15.75 | 18.71666717529297 |
| 2006-01-11 | 0 | 66 | 331 | 2454 | 8.25 | 17.600000381469727 |
| 2006-01-17 | 1 | -6 | 290 | 2454 | 8.266666412353516 | 16.399999618530273 |
| 2006-01-20 | -1 | 25 | 335 | 2446 | 18.733333587646484 | 22.33333396911621 |

テーブルにデータが登録されていることが確認できました。GCSも確認してみます。

$ gcloud storage ls -l -r --readable-sizes gs://{バケット名}/test-bg-iceberg/flights
gs://{バケット名}/test-bg-iceberg/flights/:

gs://{バケット名}/test-bg-iceberg/flights/data/:
   7.98MiB  2024-12-16T08:00:38Z  gs://{バケット名}/test-bg-iceberg/flights/data/11bc4f7e-8486-4500-a722-10760457bf30-726ea64391cfcf2d-f-00000-of-00001.parquet

gs://{バケット名}/test-bg-iceberg/flights/metadata/:
   107.00B  2024-12-16T07:50:49Z  gs://{バケット名}/test-bg-iceberg/flights/metadata/v0.metadata.json
TOTAL: 2 objects, 8362806 bytes (7.98MiB)

dataディレクトリにparquetファイルが作成されていることがわかります。

csvのインポート

次にcsvデータをインポートします。あたらじめインポートするparquetファイルをgs://{バケット名}/source/flights-1m.csvにアップロードしておきます。

> LOAD DATA INTO `data_set_iceberg.flights`
FROM FILES (
uris=['gs://{バケット名}/source/flights-1m.csv'],
format='CSV',
skip_leading_rows = 1 
);

これでIcebergテーブルにcsvファイルからデータをインポートできました。
テーブルにデータが登録されていることが確認できました。GCSのdataディレクトリを確認してみます。

$ gcloud storage ls -l -r --readable-sizes gs://{バケット名}/test-bg-iceberg/flights/data | sort -k2 -r
  34.69kiB  2024-12-16T08:09:17Z  gs://{バケット名}/test-bg-iceberg/flights/data/1fb05088-1ade-4368-aff2-df75a37f65ce-3c572a0966cfc0a7-f-00000-of-00001.parquet
   7.98MiB  2024-12-16T08:00:38Z  gs://{バケット名}/test-bg-iceberg/flights/data/11bc4f7e-8486-4500-a722-10760457bf30-726ea64391cfcf2d-f-00000-of-00001.parquet

新しくcsvを読み込んだのでdataディレクトリにその差分のparquetファイル1fb05088-1ade-4368-aff2-df75a37f65ce-3c572a0966cfc0a7-f-00000-of-00001.parquetが作成されていることがわかります。

Insert文

次にInsert文を使ってデータをInsertしてみます。

> INSERT INTO `data_set_iceberg.flights` (FL_DATE,DEP_DELAY,ARR_DELAY,AIR_TIME,DISTANCE,DEP_TIME,ARR_TIME)
VALUES
('2024-12-01',5,19,350,2475,9.083,12.483),
('2024-12-02',167,216,343,2475,11.783,15.767),
('2024-12-03',-7,-2,344,2475,8.883,12.133),
('2024-12-04',-5,-13,331,2475,8.917,11.95),
('2024-12-05',-3,-17,321,2475,8.95,11.883),
('2024-12-06',-4,-32,320,2475,8.933,11.633),
('2024-12-08',-3,-2,346,2475,8.95,12.133);

Select文でデータがInsert出来たことを確認してみます。

> SELECT * FROM `{プロジェクトID}.data_set_iceberg.flights` WHERE FL_DATE >= '2024-12-01' LIMIT 10;
| FL\_DATE | DEP\_DELAY | ARR\_DELAY | AIR\_TIME | DISTANCE | DEP\_TIME | ARR\_TIME |
| :--- | :--- | :--- | :--- | :--- | :--- | :--- |
| 2024-12-02 | 167 | 216 | 343 | 2475 | 11.783 | 15.767 |
| 2024-12-01 | 5 | 19 | 350 | 2475 | 9.083 | 12.483 |
| 2024-12-05 | -3 | -17 | 321 | 2475 | 8.95 | 11.883 |
| 2024-12-06 | -4 | -32 | 320 | 2475 | 8.933 | 11.633 |
| 2024-12-03 | -7 | -2 | 344 | 2475 | 8.883 | 12.133 |
| 2024-12-04 | -5 | -13 | 331 | 2475 | 8.917 | 11.95 |
| 2024-12-08 | -3 | -2 | 346 | 2475 | 8.95 | 12.133 |

Insert文でデータを追加出来ていることがわかります。GCSのdataディレクトリを確認してみます。

$ gcloud storage ls -l -r --readable-sizes gs://{バケット名}/test-bg-iceberg/flights/data | sort -k2 -r
   2.37kiB  2024-12-16T08:13:19Z  gs://{バケット名}/test-bg-iceberg/flights/data/40a3e2fb-9c1b-4d7f-b82c-e5cc024b9281-e1ba57e0976265a5-f-00000-of-00001.parquet
  34.69kiB  2024-12-16T08:09:17Z  gs://{バケット名}/test-bg-iceberg/flights/data/1fb05088-1ade-4368-aff2-df75a37f65ce-3c572a0966cfc0a7-f-00000-of-00001.parquet
   7.98MiB  2024-12-16T08:00:38Z  gs://{バケット名}/test-bg-iceberg/flights/data/11bc4f7e-8486-4500-a722-10760457bf30-726ea64391cfcf2d-f-00000-of-00001.parquet

Insert文でデータを追加したのでdataディレクトリにその差分のparquetファイル40a3e2fb-9c1b-4d7f-b82c-e5cc024b9281-e1ba57e0976265a5-f-00000-of-00001.parquetが作成されていることがわかります。

Update文

次はUpdate分でIcebergテーブルのデータを更新してみます。

> UPDATE `{プロジェクトID}.data_set_iceberg.flights` SET DEP_DELAY = 100 WHERE FL_DATE = '2024-12-01';
> SELECT * FROM `{プロジェクトID}.data_set_iceberg.flights` WHERE FL_DATE = '2024-12-01';
| FL\_DATE | DEP\_DELAY | ARR\_DELAY | AIR\_TIME | DISTANCE | DEP\_TIME | ARR\_TIME |
| :--- | :--- | :--- | :--- | :--- | :--- | :--- |
| 2024-12-01 | 100 | 19 | 350 | 2475 | 9.083 | 12.483 |

こちらも問題なくUpdateできました。GCS上のdataディレクトリを確認してみます。

> gcloud storage ls -l -r --readable-sizes gs://{バケット名}/test-bg-iceberg/flights/data | sort -k2 -r
   2.65kiB  2024-12-16T08:46:18Z  gs://{バケット名}/test-bg-iceberg/flights/data/ev_ZL6xSW79yB3XdVY2sNxyp5J2tZAb3O88QeoE1McA3To-f102f5a02d26cfcb-f-00000-of-00001.parquet
   2.37kiB  2024-12-16T08:13:19Z  gs://{バケット名}/test-bg-iceberg/flights/data/40a3e2fb-9c1b-4d7f-b82c-e5cc024b9281-e1ba57e0976265a5-f-00000-of-00001.parquet
  34.69kiB  2024-12-16T08:09:17Z  gs://{バケット名}/test-bg-iceberg/flights/data/1fb05088-1ade-4368-aff2-df75a37f65ce-3c572a0966cfc0a7-f-00000-of-00001.parquet
   7.98MiB  2024-12-16T08:00:38Z  gs://{バケット名}/test-bg-iceberg/flights/data/11bc4f7e-8486-4500-a722-10760457bf30-726ea64391cfcf2d-f-00000-of-00001.parquet

Update文で発生した差分が発生したのでdataディレクトリにparquetファイルev_ZL6xSW79yB3XdVY2sNxyp5J2tZAb3O88QeoE1McA3To-f102f5a02d26cfcb-f-00000-of-00001.parquetが作成されています。

Delete文

最後にDelete分でIcebergテーブルのデータを削除してみます。

> DELETE FROM `data_set_iceberg.flights` WHERE FL_DATE <= '2006-02-01';
$ gcloud storage ls -l -r --readable-sizes gs://{バケット名}/test-bg-iceberg/flights/data | sort -k2 -r
   1.36MiB  2024-12-16T08:49:25Z  gs://{バケット名}/test-bg-iceberg/flights/data/ev_MwvpbFILghO24WBnVttrAv3lk8iyE1Tc9Gl148oJv78-cfeb88971c2b9717-f-00001-of-00002.parquet
   1.97MiB  2024-12-16T08:49:25Z  gs://{バケット名}/test-bg-iceberg/flights/data/ev_MwvpbFILghO24WBnVttrAv3lk8iyE1Tc9Gl148oJv78-cfeb88971c2b9717-f-00000-of-00002.parquet
   2.65kiB  2024-12-16T08:46:18Z  gs://{バケット名}/test-bg-iceberg/flights/data/ev_ZL6xSW79yB3XdVY2sNxyp5J2tZAb3O88QeoE1McA3To-f102f5a02d26cfcb-f-00000-of-00001.parquet
   2.37kiB  2024-12-16T08:13:19Z  gs://{バケット名}/test-bg-iceberg/flights/data/40a3e2fb-9c1b-4d7f-b82c-e5cc024b9281-e1ba57e0976265a5-f-00000-of-00001.parquet
  34.69kiB  2024-12-16T08:09:17Z  gs://{バケット名}/test-bg-iceberg/flights/data/1fb05088-1ade-4368-aff2-df75a37f65ce-3c572a0966cfc0a7-f-00000-of-00001.parquet
   7.98MiB  2024-12-16T08:00:38Z  gs://{バケット名}/test-bg-iceberg/flights/data/11bc4f7e-8486-4500-a722-10760457bf30-726ea64391cfcf2d-f-00000-of-00001.parquet

こちらもDelete文で発生した差分が発生したのでdataディレクトリにparquetファイルev_MwvpbFILghO24WBnVttrAv3lk8iyE1Tc9Gl148oJv78-cfeb88971c2b9717-f-00001-of-00002.parquetが作成されています。

まとめ

Apache Iceberg用のBigQueryテーブル(Icebergテーブル)を試してみました。従来のBigQueryの管理機能やパフォーマンスを維持したまま、Apache IcebergフォーマットでデータをGoogle Cloud Storage(GCS)に保存できました。データガバナンスの要件を満たしながらBigQueryの分析機能を活用したい場合の有効な選択肢となります。今回は基本的な機能を検証しましたが、今後はより実践的なユースケースでの検証も進めていきたいです。

当アドベントカレンダー企画をお読み頂いた皆様、ありがとうございました!

Share this article

facebook logohatena logotwitter logo

© Classmethod, Inc. All rights reserved.