概要
パーティショニング
TIMESTAMPカラムをパーティションキーにすると、WHERE フィルタで該当パーティションのみスキャン(パーティションプルーニング)。
クラスタリング
パーティション内でカラム値でブロックをソートして格納。GROUP BY campaign_id や WHERE 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_at | TIMESTAMP型、パーティションキー未設定 |
| campaign_id | STRING型、カーディナリティ約500 |
| sales_amount, discount_amount | NUMERIC型 |
| 更新方式 | 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
模範解答
{{
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カラムをパーティションキーにすると、
TIMESTAMPカラムをパーティションキーにすると、
WHERE フィルタで該当パーティションのみスキャン(パーティションプルーニング)。1.5億行→約40万行(1年=365パーティション)になりうる。
2
クラスタリング
パーティション内でさらにカラム値でブロックをソートして格納。
パーティション内でさらにカラム値でブロックをソートして格納。
GROUP BY campaign_id や WHERE campaign_id = 'xxx' が高速化。BigQueryはmax4カラムまで指定可能。
3
incrementalモデルのfull-refreshリスク
パーティション・クラスタリング設定変更はテーブル再作成が必要。本番では(1)夜間バッチ停止、(2)full-refresh実行、(3)バッチ再開という手順が必要。Argo Workflowsで事前hookとして組み込むと安全。
パーティション・クラスタリング設定変更はテーブル再作成が必要。本番では(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 が必要なため、本番運用手順の設計も重要。夜間バッチとの協調を忘れずに。