BigQuery のコストはスキャン量に直結します。dbt のモデルで partition_by と cluster_by を適切に指定できるかどうかは、パフォーマンスとコストの両面で決定的です。本稿では、dbt BigQuery アダプターの安定した基本機能をベースに、日付/タイムスタンプ・整数レンジのパーティション、クラスタの実務設計、増分戦略との組み合わせを解説します。
Analytics Engineer 試験でも、どの設定で何が起こるか(MERGE と INSERT OVERWRITE の使い分け、require_partition_filter の効果、プルーニングを阻害しない WHERE 句など)が問われやすい領域です。暗記ではなく、現場で通用する最小実装を押さえます。
dbt BigQuery アダプターでは、モデル単位で partition_by と cluster_by を宣言できます。partition はスキャン対象の大枠を物理的に絞り、cluster は同一パーティション内で近接配置によりさらに効率化を狙います。まずパーティションを正しく切ることが最優先で、クラスタは二段目の最適化です。
クラスタ列は最大4列まで、順序は重要(選択性の高い列を先頭)です。日付/タイムスタンプ列のパーティション粒度は実ワークロードに合わせて設定し、アドホック分析では require_partition_filter を有効化して無駄な全スキャンを防ぎます。
dbt ではモデル上部の config で宣言します。日付/タイムスタンプのパーティションに加え、必要に応じて require_partition_filter を有効化します。クラスタは頻出のフィルタ・結合キーを先頭に並べます。
タイムスタンプ/日時型のパーティションは granularity を指定できます(day が扱いやすい)。整数レンジ・パーティションもサポートされますが、利用する場合は使用バージョンの互換性とレンジ幅の設計に注意してください。
パーティションとクラスタの概念図
基本のモデル設定例(日付/タイムスタンプ・パーティション + クラスタ)
{{ config(
materialized='table',
partition_by={
'field': 'created_at',
'data_type': 'timestamp',
'granularity': 'day'
},
cluster_by=['user_id', 'event_type'],
require_partition_filter=true
) }}
select
user_id,
event_type,
created_at,
payload
from {{ ref('stg_events') }}
増分モデルは BigQuery でも基本は MERGE 戦略です。ユニークキーで同一行を更新し、パーティション列に範囲条件を入れて取り込み対象を絞ります。パーティションごとの全置換が適切な場合は INSERT OVERWRITE 戦略が有効です。該当パーティションだけを入れ替えられるため、再計算が頻繁なウィンドウ集計などで使いやすいです。
どちらの戦略でも、ソース側のクエリにパーティション列の範囲条件を明示して不要な再計算を避けます。INSERT OVERWRITE は触れたパーティションが丸ごと置き換わるため、WHERE 句で過去の影響範囲を明確に制限するのがポイントです。
| 戦略 | 主な用途 | 前提/制約 | コスト・特徴 |
|---|---|---|---|
| table(完全再作成) | バッチで全量が小さい/初回構築 | 特になし(時間がかかる) | 常に全スキャン・全書き込み |
| incremental(MERGE) | 遅延到着の修正、行単位の upsert | unique_key 必須、衝突解決ルール要 | 変更行のみ読み書き。パーティション条件でさらに最適化 |
| incremental(INSERT OVERWRITE) | 特定期間の再計算、ウィンドウ集計 | パーティション定義が必須。触れたパーティションは全置換 | 書き込みは対象パーティションに限定。粒度が細かいほど速い |
増分モデルの実装例(MERGE と INSERT OVERWRITE)
-- MERGE 戦略(遅延到着を想定し、直近N日だけ読む)
{{ config(
materialized='incremental',
unique_key='event_id',
partition_by={'field': 'created_at', 'data_type': 'timestamp', 'granularity': 'day'},
cluster_by=['user_id'],
require_partition_filter=true
) }}
with src as (
select * from {{ ref('stg_events') }}
{% if is_incremental() %}
where created_at >= timestamp_sub(current_timestamp(), interval 3 day)
{% endif %}
)
select * from src;
-- INSERT OVERWRITE 戦略(該当パーティションを丸ごと再生成)
{{ config(
materialized='incremental',
incremental_strategy='insert_overwrite',
partition_by={'field': 'created_at', 'data_type': 'timestamp', 'granularity': 'day'},
cluster_by=['user_id'],
require_partition_filter=true
) }}
select *
from {{ ref('agg_daily_metrics_source') }}
{% if is_incremental() %}
where created_at >= timestamp_sub(current_timestamp(), interval 7 day)
{% endif %};
プルーニングを効かせる鍵は、WHERE 句でパーティション列に対する範囲条件を直接書くことです。列を関数で包むとプルーニングが効きにくくなります。アドホック利用の多いテーブルは require_partition_filter を有効にして、明示的なパーティション条件なしの全スキャンを防ぎます。
クラスタは選択性の高い列を先頭に置くことで、同一パーティション内のスキャン量をさらに削れます。ただしクラスタは万能ではないため、まずパーティションで大枠を絞る前提を徹底します。
プルーニングを阻害しないフィルタの例
-- 推奨(列を関数で包まない)
where created_at >= timestamp('2026-04-10 00:00:00 UTC')
and created_at < timestamp('2026-04-17 00:00:00 UTC')
-- 非推奨(プルーニングが効きにくいことがある)
where date(created_at) between date('2026-04-10') and date('2026-04-16')
partition_by や cluster_by の定義を変更する場合、既存テーブルの物理定義に影響します。dbt の運用としては、定義変更を確実に反映するため該当モデルをフルリフレッシュするのが安全です。依存関係が大きい場合は下流も含めた計画的な実行が必要です。
クラスタ列の入れ替えは一部 DDL で更新できることもありますが、dbt のモデル定義に合わせて整合性を保つ観点では --full-refresh による再作成が明快です。
移行時のコマンド例
# 影響の大きいモデルを限定してフルリフレッシュ
$ dbt run --full-refresh -s fact_events
# 依存関係も含めて段階的に適用
$ dbt run --full-refresh -s +fact_events
dbt のテストでパーティション列の NOT NULL、増分キーの一意性を担保します。モデル定義とメタデータの整合性は、情報スキーマの照会で定期確認すると安心です。特に require_partition_filter の有効化とクラスタ列の順序は、定義ドリフトがないかを監視対象に含めます。
アドホックユーザー向けには、サンプルクエリにもパーティション条件を含めて配布し、意図せぬ全スキャンを減らします。
BigQuery メタデータの確認例(オプション)
-- テーブルオプション(require_partition_filter など)
select table_name, option_name, option_value
from `<PROJECT>.<REGION>.INFORMATION_SCHEMA.TABLE_OPTIONS`
where table_catalog = '<PROJECT>'
and table_schema = '<DATASET>'
and table_name = 'fact_events'
and option_name in ('require_partition_filter', 'partitioning_type', 'clustering_columns');
Analytics Engineer
問題 1
dbt BigQuery アダプターで、分析者のアドホック利用が多いイベントファクトのスキャン量を最小化したい。最も適切な組み合わせはどれか?
正解: A
パーティションで大枠のスキャンを削り、require_partition_filter で無条件全スキャンを防ぎ、SQL 側で created_at の範囲を明示するのが最も堅実。クラスタのみでは効果が限定的で、列を関数で包むとプルーニングが弱まる。期間制限なし増分や全期間再計算はコスト増につながる。
INT64 のレンジ・パーティションは使えますか?
dbt BigQuery アダプターは整数レンジ・パーティションの指定に対応しています。例: partition_by={'field': 'bucket', 'data_type': 'int64', 'range': {'start': 0, 'end': 1000, 'interval': 10}}。利用時はバージョン互換性とレンジ設計(ホットスポット回避)に注意してください。
クラスタ列はいくつまで?順序は重要ですか?
最大4列までです。順序は重要で、選択性(データを絞る力)が高い列を先頭に置くと効果が出やすくなります。不要な列を漫然と追加すると書き込みコストやメンテナンス性を損ねます。
既存モデルの partition_by/cluster_by を変えるには?
物理定義が変わるため、dbt の運用では対象モデルを --full-refresh で再作成するのが安全です。依存モデルがある場合は計画的にフルリフレッシュ範囲を広げるか、INSERT OVERWRITE で段階的にバックフィルします。
NicheeLab編集部
データエンジニアリング・クラウド資格の専門家。Databricks・Snowflake等の認定資格を保有し、実務経験に基づいた問題作成・解説を行っています。NicheeLab運営。
dbt Model の基礎: SQL で定義する変換の最小単位
Analytics Engineer 向けに、dbt Model の定義、マテリアライゼーション、依存関係、インクリメン...
dbt Analytics Engineer 試験ガイド: 出題範囲・配点・申込の実務視点
dbt Analytics Engineer 認定の出題範囲、配点の考え方、申込から受験までの流れを、公式ドキュメントの...
dbt Cloud と dbt Core の違いと選び方:Analytics Engineer 試験に効く要点
dbt Cloud と dbt Core の機能差を、実務と資格対策の両面から整理。スケジューリング、IDE、RBAC、...
dbt プロジェクト構造ガイド: models / seeds / macros の実務レイアウト
Analytics Engineer 向けに、dbt プロジェクトのディレクトリ構造と命名規約、dbt_project....
dbt_project.yml の読み方:主要設定と命名を最短で掴む
dbt_project.yml の必須キー、命名解決(database.schema.identifier)、設定優先度...