データエンジニアリング — BigQuery パーティション × クラスタリング最適化

2026-04-08 (Day 4) C: データエンジニアリング ★★★☆☆ BigQuery / dbt / パーティション / クラスタリング

概要

📂

パーティショニング

TIMESTAMPカラムをパーティションキーにすると、WHERE フィルタで該当パーティションのみスキャン(パーティションプルーニング)。

📊

クラスタリング

パーティション内でカラム値でブロックをソートして格納。GROUP BY campaign_idWHERE campaign_id = 'xxx' が高速化。

dbt incremental

config() ブロックでパーティション・クラスタリングを設定。既存テーブルへの適用は --full-refresh が必要。

💰

コスト削減

BigQueryは課金もスキャン量ベース。パーティションプルーニングで1.5億行→数百万行になり、費用と速度の両方が改善。

問題

毎朝10分以上かかるレポートクエリを改善してください。

遅いクエリ

-- レポートチームが書いたクエリ(BigQuery)
SELECT
    DATE(ordered_at) AS order_date,
    campaign_id,
    COUNT(*) AS order_count,
    SUM(sales_amount) AS total_sales,
    SUM(discount_amount) AS total_discount
FROM `project.mart.fct_orders`
WHERE ordered_at >= '2025-01-01'
  AND ordered_at < '2026-01-01'
GROUP BY 1, 2
ORDER BY 1, 2;

fct_orders の仕様

項目
行数約1.5億行(全期間)
ordered_atTIMESTAMP型、パーティションキー未設定
campaign_idSTRING型、カーディナリティ約500
sales_amount, discount_amountNUMERIC型
更新方式dbt incremental(unique_key = order_id)で毎日差分更新
期待する回答形式: 文章(問題点の特定)+ dbtモデルのコード(改善案)+ SQL(検証クエリ例)

ヒント(段階的開示)

ヒント1 — 方向性
BigQueryのパフォーマンス問題の大半は「フルスキャン」が原因。クエリを見てどのカラムがフィルタ・集計に使われているか確認してみよう。
ヒント2 — アプローチ

BigQueryはパーティショニングとクラスタリングが重要な最適化手段。

  • WHERE ordered_at >= ... AND ordered_at < ... というフィルタがあるとき、ordered_at カラムのテーブル設定を考えてみよう
  • GROUP BY campaign_id があるとき、クラスタリングカラムの候補は?
ヒント3 — dbt設定
{{
    config(
        materialized='incremental',
        unique_key='order_id',
        partition_by={
            "field": "ordered_at",
            "data_type": "timestamp",
            "granularity": "day"
        },
        cluster_by=["campaign_id"]
    )
}}
ただし既存テーブルへの追加は --full-refresh が必要になる。その運用上の注意点も考えてみよう。

問題点の特定

#問題点影響対策
1 パーティションなし(フルスキャン) WHERE ordered_at >= '2025-01-01' があっても1.5億行全行をスキャンする。課金もスキャン量ベースで増大 TIMESTAMP型の日次パーティションを設定
2 クラスタリングなし GROUP BY campaign_id の絞り込み最適化が効かない campaign_id でクラスタリング設定
3 TIMESTAMP型のパーティション未活用 DATE(ordered_at) でキャストしているがパーティション自体がないため意味がない パーティション設定後はプルーニングが自動で効く

ETLパイプライン図 — dbt Lineage

raw_orders Bronze Layer stg_orders Silver Layer int_orders_enriched +campaign / coupon情報 Silver/Gold Bridge fct_orders ★ partition_by: ordered_at (day) cluster_by: [campaign_id] 1.5億行 → パーティション365個 Source Staging Intermediate Mart (改善対象)

模範解答

{{
    config(
        materialized='incremental',
        unique_key='order_id',
        on_schema_change='fail',
        partition_by={
            "field": "ordered_at",
            "data_type": "timestamp",
            "granularity": "day"
        },
        cluster_by=["campaign_id"],
        require_partition_filter=False
    )
}}

SELECT
    order_id,
    ordered_at,
    campaign_id,
    sales_amount,
    discount_amount
FROM {{ ref('int_orders_enriched') }}

{% if is_incremental() %}
    -- 前日分のみ差分投入(バッファとして2日前から取得)
    WHERE ordered_at >= TIMESTAMP_SUB(
        TIMESTAMP_TRUNC(CURRENT_TIMESTAMP(), DAY),
        INTERVAL 2 DAY
    )
{% endif %}
# 既存テーブルにパーティション・クラスタリングを追加するにはfull-refreshが必要
# ※本番では夜間バッチ前に実施し、影響範囲を確認してから実行
dbt run --select fct_orders --full-refresh

# 運用手順:
# 1. 夜間バッチ(Argo Workflows)を停止
# 2. full-refresh 実行(テーブル再作成)
# 3. データ件数・サンプルデータを確認
# 4. バッチ再開
-- パーティションプルーニングが効くか確認
SELECT
    DATE(ordered_at) AS order_date,
    campaign_id,
    COUNT(*) AS order_count,
    SUM(sales_amount) AS total_sales,
    SUM(discount_amount) AS total_discount
FROM `project.mart.fct_orders`
WHERE ordered_at >= '2025-01-01'
  AND ordered_at < '2026-01-01'
GROUP BY 1, 2
ORDER BY 1, 2;

-- BigQuery上でプルーニングが効いているか確認:
-- → クエリ実行後「このクエリで処理されたバイト数」を確認
-- → パーティション数 × 1日分のデータ量と合うか検証
-- → Before: 1.5億行分のスキャン
-- → After:  約365パーティション × 1日分のスキャンのみ

ポイント解説

1 BigQueryパーティショニング
TIMESTAMPカラムをパーティションキーにすると、WHERE フィルタで該当パーティションのみスキャン(パーティションプルーニング)。1.5億行→約40万行(1年=365パーティション)になりうる。
2 クラスタリング
パーティション内でさらにカラム値でブロックをソートして格納。GROUP BY campaign_idWHERE campaign_id = 'xxx' が高速化。BigQueryはmax4カラムまで指定可能。
3 incrementalモデルのfull-refreshリスク
パーティション・クラスタリング設定変更はテーブル再作成が必要。本番では(1)夜間バッチ停止、(2)full-refresh実行、(3)バッチ再開という手順が必要。Argo Workflowsで事前hookとして組み込むと安全。

実務への応用

MOpsチームでの販促施策効果計測クエリ

「キャンペーンIDごとに先月の売上集計」は毎朝レポートチームが実行するケースが多く、フルスキャンが重なると費用と時間が両方膨らむ。fct_orders にパーティション+クラスタリングを設定するだけで、10分→30秒以下になることも多い。

また、dbt Mesh(cross-project refs)を使っている場合は、fct_orders を公開モデルとして定義し、downstream projectからはそのままクラスタリング恩恵を受けられる。

次のステップ

発展問題: fct_orders にMaterialized Viewを作成して、日次集計をあらかじめキャッシュする設計を考える。MVのリフレッシュタイミングとArgo Workflowsの実行順序をどう整合させるか?
  • 参考: BigQuery公式「パーティション分割テーブルの概要」
  • 参考: dbt docs「Incremental models - BigQuery configurations」

今日のまとめ

BigQueryの遅いクエリはまず「パーティションプルーニングが効いているか」を確認し、dbtの config() ブロックでパーティション+クラスタリングを設定するのが基本対処法。

既存テーブルへの適用は --full-refresh が必要なため、本番運用手順の設計も重要。夜間バッチとの協調を忘れずに。

自己評価

自分の回答

気づき・メモ