dbt の実行時間は DAG のクリティカルパス、マテリアライゼーション選択、増分戦略、セレクション、並列度、そして DWH 固有の最適化に強く依存します。闇雲な最適化より、因数分解して対処するのが近道です。
本稿は dbt 公式ドキュメントの安定機能を前提に、試験で問われやすい原則と現場で効く設定の両方を詰め込みました。まずはボトルネックの見える化、その後にマテリアライゼーションとセレクションで作業量を削り、最後に並列度と DWH 機能で底上げします。
最適化の前に、どこに時間がかかっているかをデータで把握します。dbt は run_results.json と manifest.json を出力し、各ノードの開始・終了時刻、依存関係を確認できます。クリティカルパス上の重いノードを短縮すると、全体の壁時計時間が最も縮みます。
DAG の形状を意識しましょう。扇形のうち細長い系列がクリティカルパスになりやすく、そこにある巨大な結合・ソート・再集計が主犯です。まずは対象を絞ってから素材(マテリアライゼーション)を替えます。
DAG とクリティカルパスのイメージ
[src] ----> stg_orders ----> dim_customer ----> fct_sales ----> rpt_sales
\ \-> dim_product ----/
\-> stg_events ---------------------------> fct_engagement
クリティカルパス: src -> stg_orders -> dim_customer -> fct_sales -> rpt_salesrun_results.json から上位ボトルネックを抽出(例)
jq -r '.results[] | {node: .unique_id, time: (.execution_time|tonumber)} | select(.time!=null) | [.time, .node] | @tsv' target/run_results.json \
| sort -nr \
| head -n 10最も効くレバーはマテリアライゼーションです。全再計算が発生する view と table の選択、そして初回以降の作業量を削れる incremental の適用可否が実行時間を大きく左右します。増分は unique_key と戦略の整合が前提です(MERGE、DELETE+INSERT、INSERT_OVERWRITE などはアダプタ依存)。
巨大なファクトは incremental、広く参照されるディメンションは table、小規模で頻繁に変わる整形は view か ephemeral が基本線。試験でもこの原則が問われます。
| マテリアライゼーション | 実行時間の傾向 | 適したケース |
|---|---|---|
| view | ビルドは最速(実体なし)。下流クエリ時に都度計算 | 軽量な整形、頻繁に更新されるロジック、参照回数が少ない |
| table | ビルドは再作成で中。下流は高速かつ安定 | 多くの下流から参照、再計算コスト許容、サイズ中程度 |
| incremental | 初回は重いが以降高速。差分のみ適用 | 大規模ファクトや CDC、日次更新。unique_key を前提に戦略を明示 |
増分モデルの安全なパターン(アダプタ別戦略の振り分け例)
{{
config(
materialized='incremental',
unique_key='order_id',
tags=['fact','critical'],
**(
{'incremental_strategy': 'merge'} if target.adapter in ['snowflake','databricks','spark'] else {}
)
)
}}
with src as (
select * from {{ ref('stg_orders') }}
{% if is_incremental() %}
where _ingested_at >= dateadd(day, -2, current_timestamp)
{% endif %}
)
select
order_id,
customer_id,
order_date,
total_amount
from src毎回フル DAG を回すのは避け、変更波及の最小範囲だけを選択します。selectors.yml に基準を定義し、state:modified と親子のプラス指定を組み合わせるのが定番です。--defer と --state を併用すると、本番アーティファクトに依存しながら差分だけを開発環境でビルドできます。
dbt build はモデル・シード・スナップショット・テストを一括実行します。短縮が目的なら、変更時のみテストを実行する、重いデータテストは夜間ジョブに回す、などの運用分離が有効です。
selectors.yml と CLI 例
# selectors.yml
selectors:
- name: modified_and_downstream
definition:
union:
- method: state
value: modified
children: true
- method: tag
value: critical
# 差分のみビルド(本番をデファー)
dbt build --select selector:modified_and_downstream --defer --state target
# 開発ループをさらに短縮(テスト除外)
dbt build --select selector:modified_and_downstream --exclude resource_type:test --defer --state targetthreads は高ければ速いとは限りません。DWH の同時実行枠・I/O 帯域・ロック競合で頭打ちになります。まずは中程度の並列度から計測し、クリティカルパスを分割できるよう DAG の層を意識します。
中間テーブルの書き込みがボトルネックなら ephemeral に置き換え、書き込みを削減します。逆に重いサブクエリが複数下流で再利用されるなら table 化して再計算を避けます。
profiles.yml とモデルごとの並列制御例
# profiles.yml(例)
my_project:
target: dev
outputs:
dev:
type: snowflake
account: ...
user: ...
password: ...
role: ...
warehouse: XSMALL
database: ...
schema: ...
threads: 8
-- タグで重いモデルを別ジョブに
# dbt Cloud では Step1: tag:heavy、Step2: tag:not heavy などに分割
# ローカル例
dbt run --select tag:heavy && dbt run --select tag:lightDWH の機能は計測しながら慎重に使います。Snowflake ならマイクロパーティションの自動クラスタを前提に、必要な場合のみクラスタリングキーを設定。結果キャッシュとウェアハウスサイズのバランスも重要です。Databricks なら Delta Lake の OPTIMIZE や Z-ORDER をポストフックで実行してスキャン量を削減できます。
いずれも効果はデータ分布とクエリパターン次第です。全テーブルに機械的に適用せず、対象列の選定と実測で判断します。
ポストフックで DWH 最適化(条件付き)
{{
config(
materialized='table',
tags=['large','read_heavy'],
post_hook=(
[
"OPTIMIZE {{ this }} ZORDER BY (order_date)",
] if target.adapter == 'databricks' else []
)
)
}}
select * from {{ ref('fct_sales') }}品質は落とさず、実行回数と重さを最適化します。重いデータテストは夜間、開発ループではスキーマテスト中心に。state:modified に紐づくモデルだけテストを走らせ、全体回帰は日次に分離します。
スナップショットやシードのフルリロードも高コストです。頻度を最小化し、必要なときだけ --full-refresh を使います。
テスト選択の例と誤爆防止
# 変更分のみテスト(generic のみ)
dbt test --select state:modified+ test_type:generic --defer --state target
# フルリフレッシュは専用ジョブに限定
dbt build --full-refresh --select tag:allow_full_refreshAnalytics Engineer
問題 1
巨大なファクトテーブル fct_events を日次で更新している。実行時間が長く、下流レポートの待ちが発生している。上流は日次で新規行が追加され、一部の既存行が訂正される可能性もある。最も現実的に実行時間を短縮できる構成はどれか。
正解: A
追加と一部更新があるため incremental + unique_key + MERGE 戦略が適合する。さらに state:modified+ で変更波及範囲のみをビルドすれば総作業量が最も減る。他選択肢は再計算量が多すぎるか、問題の規模に対し効果が限定的。
incremental と table はどちらを選べばよいですか?
更新頻度とサイズで判断します。巨大で日次差分が明確なら incremental。参照が多くサイズ中程度で完全再計算のコストが許容できる、あるいは更新ロジックが複雑で差分が組みにくい場合は table。
threads を増やしても速くなりません。どうすれば?
DAG のクリティカルパスが支配している可能性があります。長い系列の重いモデルを優先最適化し、I/O 競合するモデルをタグでグループ化して実行順を分けます。DWH の同時実行枠やクラスターサイズも見直してください。
--full-refresh の安全な運用方法は?
専用のジョブに限定し、タグで対象を明示します。依存関係の多い基盤テーブルはメンテナンス時間帯に実施。誤爆防止に通常ジョブから --full-refresh を外し、必要なときだけ手動承認フローを設けます。
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)、設定優先度...