dbt

dbt の性能チューニング: 実行時間短縮のコツ

2026-04-19
NicheeLab編集部

dbt の実行時間は DAG のクリティカルパス、マテリアライゼーション選択、増分戦略、セレクション、並列度、そして DWH 固有の最適化に強く依存します。闇雲な最適化より、因数分解して対処するのが近道です。

本稿は dbt 公式ドキュメントの安定機能を前提に、試験で問われやすい原則と現場で効く設定の両方を詰め込みました。まずはボトルネックの見える化、その後にマテリアライゼーションとセレクションで作業量を削り、最後に並列度と DWH 機能で底上げします。

実行計画の全体像とボトルネック特定

最適化の前に、どこに時間がかかっているかをデータで把握します。dbt は run_results.json と manifest.json を出力し、各ノードの開始・終了時刻、依存関係を確認できます。クリティカルパス上の重いノードを短縮すると、全体の壁時計時間が最も縮みます。

DAG の形状を意識しましょう。扇形のうち細長い系列がクリティカルパスになりやすく、そこにある巨大な結合・ソート・再集計が主犯です。まずは対象を絞ってから素材(マテリアライゼーション)を替えます。

  • run_results.json で各モデルの実行秒数とステータスを確認
  • クリティカルパスを優先最適化。扇状の枝の最長系列を短縮
  • 不要な再計算を除去。上流での過剰集計や無駄な DISTINCT を削減
  • ステージング層は軽量化。選択列の最小化と早期フィルタでデータ量削減

DAG とクリティカルパスのイメージ

 [src] ----> stg_orders ----> dim_customer ----> fct_sales ----> rpt_sales
    \                     \-> dim_product ----/
     \-> stg_events ---------------------------> fct_engagement

  クリティカルパス: src -> stg_orders -> dim_customer -> fct_sales -> rpt_sales

run_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 が基本線。試験でもこの原則が問われます。

  • 増分適用条件を明確化(append-only か、更新ありか、再計算境界はどこか)
  • unique_key と is_incremental 条件の整合。重複・欠損で MERGE が失敗しない設計
  • 依存する上流が増分でないと効果半減。上流から一貫して増分化
  • 小さな参照テーブルは 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 はモデル・シード・スナップショット・テストを一括実行します。短縮が目的なら、変更時のみテストを実行する、重いデータテストは夜間ジョブに回す、などの運用分離が有効です。

  • state:modified+ で変更とその下流のみを対象化
  • --defer --state で本番モデルを参照しつつ差分ビルド
  • 重いデータ品質テストはバッチに分離し、開発ループを短縮
  • セレクタで重要タグ(critical)を優先実行

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 target

並列度とジョブ構成の最適化

threads は高ければ速いとは限りません。DWH の同時実行枠・I/O 帯域・ロック競合で頭打ちになります。まずは中程度の並列度から計測し、クリティカルパスを分割できるよう DAG の層を意識します。

中間テーブルの書き込みがボトルネックなら ephemeral に置き換え、書き込みを削減します。逆に重いサブクエリが複数下流で再利用されるなら table 化して再計算を避けます。

  • threads は段階的にチューニング(例: 4 → 8 → 16)
  • I/O 競合の激しいモデルは並列実行から外す(タグで制御)
  • 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:light

DWH 固有の最適化を安全に取り込む

DWH の機能は計測しながら慎重に使います。Snowflake ならマイクロパーティションの自動クラスタを前提に、必要な場合のみクラスタリングキーを設定。結果キャッシュとウェアハウスサイズのバランスも重要です。Databricks なら Delta Lake の OPTIMIZE や Z-ORDER をポストフックで実行してスキャン量を削減できます。

いずれも効果はデータ分布とクエリパターン次第です。全テーブルに機械的に適用せず、対象列の選定と実測で判断します。

  • Snowflake: 結果キャッシュの活用。再実行が多い開発時は効果大
  • Snowflake: クラスタリングキーは更新パターンとフィルタ列に限定
  • Databricks: 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') }}

テストと CI を軽量化して反復を速くする

品質は落とさず、実行回数と重さを最適化します。重いデータテストは夜間、開発ループではスキーマテスト中心に。state:modified に紐づくモデルだけテストを走らせ、全体回帰は日次に分離します。

スナップショットやシードのフルリロードも高コストです。頻度を最小化し、必要なときだけ --full-refresh を使います。

  • 開発時: 変更モデルの generic テストのみ実行
  • 回帰時: 全テストとドキュメント生成をまとめて夜間実行
  • シードは差分ロード方針を検討(サイズと更新頻度次第)
  • --full-refresh は計画的に。誤用防止に専用ジョブを用意

テスト選択の例と誤爆防止

# 変更分のみテスト(generic のみ)
dbt test --select state:modified+ test_type:generic --defer --state target

# フルリフレッシュは専用ジョブに限定
dbt build --full-refresh --select tag:allow_full_refresh

問題で確認

Analytics Engineer

問題 1

巨大なファクトテーブル fct_events を日次で更新している。実行時間が長く、下流レポートの待ちが発生している。上流は日次で新規行が追加され、一部の既存行が訂正される可能性もある。最も現実的に実行時間を短縮できる構成はどれか。

  1. fct_events を incremental にし、unique_key を設定。アダプタが対応する場合は MERGE 戦略を選択。日次ジョブは dbt build --select state:modified+ を用い、変更波及のみをビルドする
  2. fct_events を view に変更し、下流をすべて再計算。threads を最大化して並列度で押し切る
  3. 上流のステージングをすべて ephemeral に変更し、中間書き込みをゼロにするだけで対応する
  4. 全モデルを常に --full-refresh で再作成し、結果キャッシュに頼って高速化する

正解: A

追加と一部更新があるため incremental + unique_key + MERGE 戦略が適合する。さらに state:modified+ で変更波及範囲のみをビルドすれば総作業量が最も減る。他選択肢は再計算量が多すぎるか、問題の規模に対し効果が限定的。

よくある質問

incremental と table はどちらを選べばよいですか?

更新頻度とサイズで判断します。巨大で日次差分が明確なら incremental。参照が多くサイズ中程度で完全再計算のコストが許容できる、あるいは更新ロジックが複雑で差分が組みにくい場合は table。

threads を増やしても速くなりません。どうすれば?

DAG のクリティカルパスが支配している可能性があります。長い系列の重いモデルを優先最適化し、I/O 競合するモデルをタグでグループ化して実行順を分けます。DWH の同時実行枠やクラスターサイズも見直してください。

--full-refresh の安全な運用方法は?

専用のジョブに限定し、タグで対象を明示します。依存関係の多い基盤テーブルはメンテナンス時間帯に実施。誤爆防止に通常ジョブから --full-refresh を外し、必要なときだけ手動承認フローを設けます。

この記事で学んだ内容を問題で確認しましょう

16,000問以上の問題で実力チェック

無料で問題を解いてみる
この記事の著者

NicheeLab編集部

データエンジニアリング・クラウド資格の専門家。Databricks・Snowflake等の認定資格を保有し、実務経験に基づいた問題作成・解説を行っています。NicheeLab運営。


関連記事
dbt

dbt Model の基礎: SQL で定義する変換の最小単位

Analytics Engineer 向けに、dbt Model の定義、マテリアライゼーション、依存関係、インクリメン...

dbt

dbt Analytics Engineer 試験ガイド: 出題範囲・配点・申込の実務視点

dbt Analytics Engineer 認定の出題範囲、配点の考え方、申込から受験までの流れを、公式ドキュメントの...

dbt

dbt Cloud と dbt Core の違いと選び方:Analytics Engineer 試験に効く要点

dbt Cloud と dbt Core の機能差を、実務と資格対策の両面から整理。スケジューリング、IDE、RBAC、...

dbt

dbt プロジェクト構造ガイド: models / seeds / macros の実務レイアウト

Analytics Engineer 向けに、dbt プロジェクトのディレクトリ構造と命名規約、dbt_project....

dbt

dbt_project.yml の読み方:主要設定と命名を最短で掴む

dbt_project.yml の必須キー、命名解決(database.schema.identifier)、設定優先度...

dbtの記事一覧 (100件)
© 2026 NicheeLab All rights reserved.