BigQuery Lakehouse カタログ Iceberg テーブルへの SQL 書き込みとクエリパフォーマンスを検証してみた。

BigQuery Lakehouse カタログ Iceberg テーブルへの SQL 書き込みとクエリパフォーマンスを検証してみた。

2026.06.10

こんにちは、みかみです。

ここのところ雨で犬の散歩に行けません。
犬の散歩と言いつつ私の運動不足解消の目的が強いので残念です。
散歩があまり好きではないらしい犬はへっちゃらなようです。

はじめに

BigQuery 管理の Iceberg テーブルを Snowflake から参照することは可能ですが、最新データを参照するためには、メタデータファイルパスを更新する必要があります。

https://dev.classmethod.jp/articles/bigquery-tables-for-apache-iceberg-to-snowflake-iceberg-table/

このメタデータファイルパスの更新処理なしに最新データを参照可能な Google Cloud BigLake メタストアカタログ統合が、Snowflake で先日 GA になりました。
なお、Google Cloud の BigLake は Lakehouse に名称変更されたので、Lakehouse と BigLake は同義です。

https://docs.snowflake.com/en/release-notes/2026/other/2026-06-02-iceberg-google-biglake-metastore-catalog-integration-ga

この BigLake メタストアカタログ統合では、Snowflake からのデータ書き込みが可能なので、Snowflake → BigQuery 方向のデータ連携でもメタデータファイルパスの更新なしで最新データを参照することが可能になります。
ですが、BigLake メタストアの Iceberg テーブルは、BigQuery の SQL から直接データを書き込みすることができません

https://dev.classmethod.jp/articles/snowflake-iceberg-rest-catalog-integration-google-cloud-lakehouse-runtime/

また、BigQuery の Lakehouse カタログ Iceberg テーブルの公式ドキュメントには、

クエリ速度は Cloud Storage からデータを読み取る処理と同等になります

との制限事項の記載があります。

https://docs.cloud.google.com/lakehouse/docs/about-lakehouse-catalogs?hl=ja#limitations

BigQuery の Lakehouse カタログ Iceberg テーブルに、SQL レイヤからデータ書き込みするにはどうすればよいか、また、他の種類のテーブルと比べてクエリ速度はどのくらいなのか、確認してみます。

やりたいこと

  • BigQuery から Snowflake に、メタデータファイルパスの更新なしに Iceberg テーブル経由でデータ連携したい
  • BigQuery からの連携データは SQL レイヤから書き込みしたい
  • BigQuery Lakehouse カタログ Iceberg テーブルのクエリ処理速度を確認したい

前提

Google Cloud SDK(gcloud コマンド)の実行環境は準備済みであるものとします。 本エントリでは、Cloud Shell を使用しました。

また、BigQuery や GCS など各サービス操作に必要な API の有効化と権限は付与済みです。
Snowflake 環境側でも、必要なユーザーの作成や権限付与は実施済みです。

なお、文中、Google Cloud プロジェクト ID など一部の文字は伏字に変更しています。

準備:BigQuery の Lakehouse カタログ Iceberg テーブルと、Snowflake の BigLake メタストアカタログ統合を作成

検証に使用する BigQuery の Lakehouse カタログ Iceberg テーブルと、Snowflake の BigLake メタストアカタログ統合の作成手順は、以下のブログをご参照ください。

https://dev.classmethod.jp/articles/snowflake-iceberg-rest-catalog-integration-google-cloud-lakehouse-runtime/

作成した BigQuery の Lakehouse カタログ Iceberg テーブルデータを、Snowflake と BigQuery 双方から参照できることが確認できました。

sf_lakehouse_iceberg

bq_lakehouse_iceberg

なお、BigQuery から Lakehouse カタログ Iceberg テーブルにクエリを実行する際には、FROM 句の テーブル名はプロジェクト ID 含めた完全修飾名で指定しないとエラーになります。

bq_lakehouse_iceberg_error

通常のテーブルは プロジェクト.データセット.テーブル の 3層構造なのに対して、Lakehouse カタログの Iceberg テーブルは プロジェクト.カタログ.グループ.テーブル の 4層で構成されているためです。

準備:BigQuery SQL からのデータ書き込みプロシージャを作成

BigQuery で Lakehouse カタログの Iceberg テーブルにデータを書き込むためのプロシージャを作成します。

Clous Shell から以下のコマンドを実行して、BigQuery から Spark ジョブを実行するための connection を作成して、Spark が Lakehouse カタログと GCS にアクセスするための権限を付与します。

export PROJECT_ID="[PROJECT_ID]"
export LOCATION="asia-northeast1"
export GCS_BUCKET="sb-iceberg-verify-mikami"

bq mk --connection \
  --location="$LOCATION" \
  --connection_type=SPARK \
  --project_id="$PROJECT_ID" \
  "spark-iceberg-verify"

# サービスアカウントを変数に設定
export SPARK_SA=$(bq show --connection --project_id="$PROJECT_ID" \
  --location="$LOCATION" --format=json spark-iceberg-verify \
  | jq -r '.cloudResource.serviceAccountId')

# Spark サービスアカウントへの権限付与
gcloud storage buckets add-iam-policy-binding "gs://$GCS_BUCKET" \
  --member="serviceAccount:$SPARK_SA" --role="roles/storage.objectAdmin"
gcloud projects add-iam-policy-binding "$PROJECT_ID" \
  --member="serviceAccount:$SPARK_SA" --role="roles/biglake.admin"
gcloud projects add-iam-policy-binding "$PROJECT_ID" \
  --member="serviceAccount:$SPARK_SA" --role="roles/serviceusage.serviceUsageConsumer"
gcloud projects add-iam-policy-binding "$PROJECT_ID" \
  --member="serviceAccount:$SPARK_SA" --role="roles/bigquery.dataViewer"
gcloud projects add-iam-policy-binding "$PROJECT_ID" \
  --member="serviceAccount:$SPARK_SA" --role="roles/bigquery.jobUser"

続いて BigQuery で以下の SQL を実行して、プロシージャで書き込むデータを格納したテーブルを作成します。
追加用データテーブルには、はじめに Lakehouse カタログに投入した payment_id 1001〜1004 の続き(1005〜1010)の6件のデータを格納します。

CREATE OR REPLACE TABLE
  iceberg_dataset_tokyo.new_payments
AS
SELECT
  payment_id,
  CASE MOD(payment_id, 3)
    WHEN 0 THEN 'STORE_A' WHEN 1 THEN 'STORE_B' ELSE 'STORE_C'
  END AS store_code,
  (MOD(payment_id, 5) + 1) * 2000 AS amount,
  DATE_ADD(DATE '2026-05-05', INTERVAL (payment_id - 1005) DAY) AS payment_date
FROM UNNEST(GENERATE_ARRAY(1005, 1010)) AS payment_id;

SELECT * FROM iceberg_dataset_tokyo.new_payments
ORDER BY payment_id;

source_1005-1010

追加データが準備できました。

以下の SQL で、追加データテーブルを読み込み、Lakehouse カタログの payment_log テーブルに書き込む Spark プロシージャを作成して実行します。

CREATE OR REPLACE PROCEDURE
  iceberg_dataset_tokyo.append_to_iceberg()
WITH CONNECTION `[PROJECT_ID].asia-northeast1.spark-iceberg-verify`
OPTIONS (
  engine = 'SPARK',
  runtime_version = '2.2'
)
LANGUAGE python AS R"""
import subprocess, sys
subprocess.run(
    [sys.executable, "-m", "pip", "install", "pyiceberg[gcsfs,pyarrow]", "--quiet"],
    check=True
)

from pyspark.sql import SparkSession
import google.auth
import google.auth.transport.requests
from pyiceberg.catalog.rest import RestCatalog
from google.cloud import bigquery as bq_lib

creds, _ = google.auth.default(
    scopes=["https://www.googleapis.com/auth/cloud-platform"]
)
creds.refresh(google.auth.transport.requests.Request())

spark = SparkSession.builder \
    .appName("append_to_iceberg") \
    .getOrCreate()

# BigQuery Python クライアントで読み込み(Storage Read API 不使用)
bq_client = bq_lib.Client(
    project="[PROJECT_ID]",
    credentials=creds
)
arrow_table = (
    bq_client
    .query(
        'SELECT '
        'CAST(payment_id   AS INT64)  AS payment_id, '
        'CAST(store_code   AS STRING) AS store_code, '
        'CAST(amount       AS INT64)  AS amount, '
        'CAST(payment_date AS DATE)   AS payment_date '
        'FROM `[PROJECT_ID].iceberg_dataset_tokyo.new_payments`'
    )
    .result()
    .to_arrow(create_bqstorage_client=False)
)
print(f"Source rows: {len(arrow_table)}")

catalog = RestCatalog(
    name="lakehouse",
    **{
        "uri": "https://biglake.googleapis.com/iceberg/v1/restcatalog",
        "warehouse": "gs://sb-iceberg-verify-mikami",
        "token": creds.token,
        "header.x-goog-user-project": "[PROJECT_ID]",
        "header.X-Iceberg-Access-Delegation": "vended-credentials",
    }
)

tbl = catalog.load_table("sb_verify.payment_log")
tbl.append(arrow_table)

result = tbl.scan().to_arrow()
print(result)
print(f"Total rows: {len(result)}")
""";
CALL iceberg_dataset_tokyo.append_to_iceberg();

Spark を起動する必要があるため、完了まで3分ほどがかかりました。
正常に完了したようなので、結果を確認してみます。

add_1005-1010

BigQuery の SQL レイヤから、payment_id 1005〜1010のデータが、Lakehouse カタログの Iceberg テーブルに追加されたことが確認できました。

Snowflake からも追加データが参照できるか確認してみます。

sf_add_1005-1010

メタデータファイルパスの更新なしで、BigQuery の SQL レイヤで追加した最新データが Snowflake から参照できることが確認できました。

Lakehouse カタログ Iceberg テーブル のクエリパフォーマンスを確認

公式ドキュメントの以下の記載を検証してみます。

BigQuery エンジンの Lakehouse ランタイム カタログのテーブルに対するクエリのパフォーマンスは、標準的な BigQuery テーブルのデータに対するクエリよりも低速になる可能性があります。一般的に、クエリ速度は Cloud Storage からデータを読み取る処理と同等になります。

以下4種類の BigQuery テーブルに対してクエリを実行して、平均処理時間を確認します。

  1. Native テーブル
  2. 外部テーブル(Parquet ファイル参照)
  3. Lakehouse カタログの Iceberg テーブル
  4. BigQuery 管理の Iceberg テーブル

準備:Nativeテーブルを作成

BigQuery で以下の SQL を実行して、payment_id = 1〜1000000 の 100万件のデータを格納した Native テーブルを作成します。

CREATE OR REPLACE TABLE
  iceberg_dataset_tokyo.payment_log_std
AS
SELECT
  payment_id,
  CONCAT('STORE_', LPAD(CAST(MOD(payment_id, 50) + 1 AS STRING), 3, '0')) AS store_code,
  CASE MOD(payment_id, 3) WHEN 0 THEN 'CREDIT' WHEN 1 THEN 'DEBIT' ELSE 'QR' END
    AS payment_method,
  CAST(100 + RAND() * 49900 AS INT64) AS amount,
  DATE_ADD(DATE '2022-01-01', INTERVAL MOD(payment_id, 2000) DAY) AS payment_date
FROM UNNEST(GENERATE_ARRAY(1, 1000000)) AS payment_id;

テーブルデータを確認します。

SELECT COUNT(*) as count, MIN(payment_id) as min_id, MAX(payment_id) as max_id FROM iceberg_dataset_tokyo.payment_log_std;

test_data

検証用データを格納した、Native テーブルが作成できました。

準備:外部テーブルを作成

以下の SQL で、先ほど作成した Native テーブルデータを Parquet ファイルで GCS にエクスポートして、ファイルを参照する外部テーブルを作成しました。

-- データエクスポート
EXPORT DATA OPTIONS (
  uri = 'gs://sb-iceberg-verify-mikami/perf_test/parquet/*.parquet',
  format = 'PARQUET', overwrite = true
)
AS SELECT * FROM `[PROJECT_ID].iceberg_dataset_tokyo.payment_log_std`;

-- 外部テーブルを作成
CREATE OR REPLACE EXTERNAL TABLE
  `[PROJECT_ID].iceberg_dataset_tokyo.payment_log_ext`
OPTIONS (
  format = 'PARQUET',
  uris   = ['gs://sb-iceberg-verify-mikami/perf_test/parquet/*.parquet']
);

準備:Lakehouse カタログ Iceberg テーブルを作成

続いて、今回の検証対象となる、Lakehouse カタログ Iceberg テーブルです。
Cloud Shell で以下を実行して、Lakehouse カタログ の Iceberg テーブルを作成し、外部テーブル用に出力した Parquet ファイルを取り込みました。

cat > create_perf_table.py << 'EOF'
import os, time
import google.auth, google.auth.transport.requests
import pyarrow as pa, pyarrow.parquet as pq
from google.cloud import storage
from pyiceberg.catalog.rest import RestCatalog

creds, _ = google.auth.default(scopes=["https://www.googleapis.com/auth/cloud-platform"])
creds.refresh(google.auth.transport.requests.Request())

catalog = RestCatalog(
    name="lakehouse",
    **{
        "uri": "https://biglake.googleapis.com/iceberg/v1/restcatalog",
        "warehouse": f"gs://{os.environ['GCS_BUCKET']}",
        "token": creds.token,
        "header.x-goog-user-project": os.environ["PROJECT_ID"],
        "header.X-Iceberg-Access-Delegation": "vended-credentials",
    },
)

# GCS の Parquet ファイルを読み込んで Iceberg テーブルに書き込む
client = storage.Client(project=os.environ["PROJECT_ID"])
blobs = list(client.bucket(os.environ["GCS_BUCKET"]).list_blobs(prefix="perf_test/parquet/"))
print(f"Parquet files: {len(blobs)}")

combined = pa.concat_tables([
    pq.read_table(f"gs://{os.environ['GCS_BUCKET']}/{b.name}") for b in blobs
])
print(f"Total rows: {len(combined)}")

t = catalog.create_table_if_not_exists("sb_verify.payment_log_perf", schema=combined.schema)
t0 = time.perf_counter()
t.append(combined)
print(f"Write elapsed: {time.perf_counter()-t0:.1f}s")
EOF
python3 create_perf_table.py

準備:BigQuery 管理 Iceberg テーブルを作成

最後に、BigQuery 管理の Iceberg テーブルです。
Cloud Shell で以下を実行して、BigQuery コネクションを作成して GCS バケットへのアクセス権限を付与します。

export PROJECT_ID="[PROJECT_ID]"
export LOCATION="asia-northeast1"
export GCS_BUCKET="sb-iceberg-verify-mikami"

bq mk --connection \
  --location="$LOCATION" \
  --connection_type=CLOUD_RESOURCE \
  --project_id="$PROJECT_ID" \
  bq-iceberg-resource

export RESOURCE_SA=$(bq show --connection --project_id="$PROJECT_ID" \
  --location="$LOCATION" --format=json bq-iceberg-resource \
  | jq -r '.cloudResource.serviceAccountId')

gcloud storage buckets add-iam-policy-binding "gs://$GCS_BUCKET" \
  --member="serviceAccount:$RESOURCE_SA" \
  --role="roles/storage.objectAdmin"

続いて以下の SQL で、検証用データ格納した BigQuery 管理の Iceberg テーブルを作成しました。

CREATE OR REPLACE TABLE
  iceberg_dataset_tokyo.payment_log_bq_iceberg
WITH CONNECTION `[PROJECT_ID].asia-northeast1.bq-iceberg-resource`
OPTIONS (
  table_format = 'ICEBERG',
  storage_uri = 'gs://sb-iceberg-verify-mikami/bq_iceberg/'
)
AS SELECT * FROM
  iceberg_dataset_tokyo.payment_log_std;

各テーブルのクエリ実行速度を確認

キャッシュ無効の指定をして、各テーブルに SELECT クエリを実行します。
Cloud Shell から以下のコマンドで、各クエリ5〜6回ずつ実行しました。

# 1. Native テーブル
bq query --use_legacy_sql=false --nouse_cache \
"SELECT store_code, payment_method, SUM(amount) AS total, COUNT(*) AS cnt
 FROM \`[PROJECT_ID].iceberg_dataset_tokyo.payment_log_std\`
 GROUP BY store_code, payment_method ORDER BY total DESC"

# 2. 外部テーブル(Parquet)
bq query --use_legacy_sql=false --nouse_cache \
"SELECT store_code, payment_method, SUM(amount) AS total, COUNT(*) AS cnt
 FROM \`[PROJECT_ID].iceberg_dataset_tokyo.payment_log_ext\`
 GROUP BY store_code, payment_method ORDER BY total DESC"

# 3. Lakehouse カタログ Iceberg テーブル
bq query --use_legacy_sql=false --nouse_cache \
"SELECT store_code, payment_method, SUM(amount) AS total, COUNT(*) AS cnt
 FROM \`[PROJECT_ID].sb-iceberg-verify-mikami.sb_verify.payment_log_perf\`
 GROUP BY store_code, payment_method ORDER BY total DESC"

# 4. BigQuery 管理 Iceberg テーブル
bq query --use_legacy_sql=false --nouse_cache \
"SELECT store_code, payment_method, SUM(amount) AS total, COUNT(*) AS cnt
FROM \`[PROJECT_ID].iceberg_dataset_tokyo.payment_log_bq_iceberg\`
GROUP BY store_code, payment_method ORDER BY total DESC"

以下の SQL で、それぞれのテーブルに対するクエリ実行時間の平均値・最小値・最大値・スキャンデータ量を確認します。

SELECT
  CASE
    WHEN REGEXP_CONTAINS(query, r'payment_log_std')  THEN '1.Native テーブル'
    WHEN REGEXP_CONTAINS(query, r'payment_log_ext')  THEN '2.外部テーブル(Parquet)'
    WHEN REGEXP_CONTAINS(query, r'payment_log_perf') THEN '3.Lakehouse Iceberg テーブル'
    WHEN REGEXP_CONTAINS(query, r'payment_log_bq_iceberg') THEN '4.BigQuery 管理 Iceberg テーブル'
    ELSE 'その他'
  END                                                            AS target_table,
  ROUND(AVG(TIMESTAMP_DIFF(end_time, start_time, MILLISECOND)) / 1000.0, 2)
                                                                 AS avg_elapsed_sec,
  ROUND(MIN(TIMESTAMP_DIFF(end_time, start_time, MILLISECOND)) / 1000.0, 2)
                                                                 AS min_elapsed_sec,
  ROUND(MAX(TIMESTAMP_DIFF(end_time, start_time, MILLISECOND)) / 1000.0, 2)
                                                                 AS max_elapsed_sec,
  ROUND(AVG(total_bytes_processed) / POW(1024, 3), 3)           AS avg_processed_gb
FROM `[PROJECT_ID]`.`region-asia-northeast1`.INFORMATION_SCHEMA.JOBS
WHERE
  statement_type  = 'SELECT'
  AND cache_hit   = false
  AND creation_time >= TIMESTAMP_SUB(CURRENT_TIMESTAMP(), INTERVAL 24 HOUR)
  AND REGEXP_CONTAINS(query, r'payment_log_(std|perf|ext|bq_iceberg)')
GROUP BY target_table
ORDER BY target_table;

avg_elapsed_sec

外部テーブル(Parquet)と比較しても、Lakehouse カタログの Iceberg テーブルは処理時間が長くなる傾向が見られました。
BigQuery では 列指向フォーマットの Parquet ファイルの処理速度は速い傾向が見られるため、Parquet ファイルを参照する外部テーブルではそれほど目立った速度遅延は見られないようです。
一方、Lakehouse カタログ Iceberg テーブルもデータは Parquet 形式ですが、メタデータの管理が外部カタログ(Lakehouse)側にあるため、メタデータ解決処理が必要なために処理時間が長いのではないかと思われます。

また、BigQuery 管理の Iceberg テーブルは、Lakehouse カタログの Iceberg テーブルと比べてだいぶ速い結果となりました。
BigQuery 管理の Iceberg テーブルはメタデータを BigQuery が直接管理するため、外部カタログへのアクセスが不要となり、処理時間が大幅に短縮されたと考えられます。

まとめ(所感)

BigQuery Lakehouse カタログの Iceberg テーブルを経由して、BigQuery - Snowflake 間のデータ連携できることが確認できました。
しかし、処理速度や BigQuery SQL レイヤからのデータ書き込みなどにまだ課題が残る印象です。

マルチプラットフォーム間のデータ連携をシームレスに実現できるのは大きなメリットですが、データ書き込み方法やクエリ速度の制約事項を考慮すると、現時点では本番運用は難しい印象を受けました。
とはいえ、処理速度が許容できる場合や連携データ量が少ない場合など、ユースケースによっては十分に実用的な選択肢ではないかと思います。
マルチクラウド・マルチプラットフォーム間のデータ連携方法を実現できるアップデートに、今後も期待したいと思います。

参考


Snowflakeの導入支援はクラスメソッドに!

クラスメソッドでは Snowflake の導入を支援しております。
製品の詳細や支援の内容についてお気軽にお問い合わせください。

Snowflakeの詳細を見る

この記事をシェアする

関連記事