dbtのマテリアライゼーションを整理したい
IT技術
はじめに
1月よりデータ推進室に配属になりました。
毎日業務でdbtを触っているのですが、マテリアライゼーションについて一度自分の中で整理する意味も含めて今回記事にまとめます。
マテリアライゼーションとは
dbtにおけるマテリアライゼーションとは、dbtモデルの変換結果をデータウェアハウス内にどのように保存するか決める戦略のことです。
どのマテリアライゼーションを選択するかによって、以下の要素に大きく影響します。
- ビルド時間(
dbt runの実行にかかる時間) - クエリ性能(エンドユーザーがデータを取得する速度)
- データの鮮度(データソースの変更がどれだけ早く反映されるか)
- ストレージ/スキャンコスト(BigQueryのオンデマンド料金等)
dbtには、標準で5つのマテリアライゼーションが組み込まれています。
| 種類 | BigQuery上のオブジェクト | データの保存有無 |
| view | ビュー | なし(SQLのみ) |
| table | テーブル | あり |
| incremental | テーブル(増分更新) | あり |
| ephemeral | なし | なし |
| materialized_view | マテリアライズどビュー | あり |
マテリアライゼーションの種類
1. view
データは保存せず、クエリ時に毎回SQLが実行されます。CREATE VIEW ASでビューを再作成します。
また、dbtのデフォルト設定であるため、何も設定しなければ自動的にビューとして作成されます。
- 特徴
- ビルド時間:一瞬(データの保存ではなく、SQLの保存のみのため)
- クエリ性能:遅い(クエリの度に変換処理を再実行するため)
- データ鮮度:常に最新(クエリ時にソースから直接読み取るため)
- コスト:ストレージは不要だが、クエリごとにスキャンコストが発生する
- メリット:
追加ストレージが不要であり、常にソースの最新状態を参照できる - デメリット:
頻繁にアクセスされるとスキャンコストが増大する - 最適なユースケース:
リネーム・キャストなどの軽量な変換や、staging モデルに最適
2. table
変換結果を静的データとして保存します。CREATE TABLE ASでテーブルをフルビルドします。
- 特徴
- ビルド時間:データ量に比例して増加(毎回全データを再計算するため)
- クエリ性能:早い(計算済みのデータを返すだけのため)
- データ鮮度:run時点のデータまで(次のrunまでソースの最新状態は反映されない)
- コスト:ストレージ費用が発生。ビルド時に全スキャンするが、クエリ時は読み取りのみ
- メリット:
クエリが常に高速。partition_byやcluster_byと組み合わせることで最適化が可能。 - デメリット:
データ量が多いとビルドに時間がかかる。ソースの変更がリアルタイムに反映されない。 - 最適なユースケース:
ダッシュボード向けのmart層モデルや、多くの下流モデルから反復参照される重い変換を持つモデルに適している
3. incremental
初回は全データでテーブルを作成し、2 回目以降はis_incremental()でフィルタされた差分データのみを追加・更新します。
特にデータ量の多いテーブルにおいては、tableよりも大幅なコスト削減が見込めます。
- 特徴
- ビルド時間:差分の処理のみで劇的に短縮(初回はテーブルと同等)
- クエリ性能:tableと同等(実態はテーブルであるため)
- データ鮮度:run時点のデータまで。
- コスト:ストレージはtableと同等、スキャン対象は差分データのみ
- メリット: ビルド時間とスキャンコストが大幅に短縮できる
- デメリット: 設定がやや複雑になる。条件を誤ると、過去データの変更を取りこぼしたり重複取得するリスクもある
- 最適なユースケース: イベントログなどの時系列データ。tableではビルドが遅延する場合の移行先となる
BigQueryで利用可能なIncremental Strategy
- merge
unique_keyで行を照合し、新規ならばINSERT、既存ならUPDATEを行う(unique_keyの設定が必須) - insert_overwrite
行単位の比較は行わず、パーティションを丸ごと上書きする(partition_byの設定が必須)
4. ephemeral
BigQuery 上にオブジェクトを作成せず、参照元モデルの SQL 内に CTE(共通テーブル式) として展開されます。
- 特徴
- ビルド時間: なし(BigQuery にオブジェクトを作成しないため)
- クエリ性能: 展開先のモデルの性能に依存する。
- データ鮮度: 常に最新(展開先モデルの実行時にソースから読み取られる)
- コスト: ストレージ不要。スキャンコストは展開先のクエリに含まれる
- メリット: BigQuery上のオブジェクトを増やさずにロジックを再利用できる
- デメリット: 直接SELECTできないためデバッグが困難。多用するとコンパイル後のSQLが巨大化してパフォーマンスに悪影響
- 最適なユースケース: 1〜2個下流のモデルからのみ参照される軽量な中間ロジック
5. materialized_view
BigQuery のマテリアライズドビュー機能を利用します。ベーステーブルの変更から、デフォルトで5分以内に自動リフレッシュ(最短30分間隔)されます。
- 特徴
- ビルド時間:
dbt runはコードデプロイのみ(データ更新は BigQuery が自動実行) - クエリ性能: table に近い(データを物理的に保存するため)
- データ鮮度: ほぼリアルタイム(BigQueryが自動で増分更新)
- コスト: ストレージ費用は発生するが、増分更新により全スキャンを回避できる
- ビルド時間:
- メリット: dbt のバッチ実行なしでもデータが更新される
- デメリット: SQL に制約がある。SQL 変更時は-full-refreshでの再構築が必要となる
- 最適なユースケース: incremental で十分要件を満たせるが、リフレッシュ管理を BigQuery に任せたい場合に適している
マテリアライゼーションの設定方法
設定には3つのレベルがあり、優先順位が高い設定が上位の設定をオーバーライドします。
設定の優先順位
モデルファイルのconfigブロック > properties.yml > dbt_project.yml
dbtでは、dbt_project.ymlでディレクトリ(層)単位の基本方針を定め、例外的なモデルのみconfig()で上書きする運用が推奨されています。
フォルダレベル: dbt_project.ymlモデルの層単位でデフォルトのマテリアライゼーションを設定
1models:
2 my_project:
3 staging:
4 +materialized: view
5 intermediate:
6 +materialized: ephemeral
7 marts:
8 +materialized: tableプロパティファイル:properties.yml
1models:
2 - name: user_orders
3 config:
4 materialized: tableモデルファイル内のconfigブロック
1{{
2 config(
3 materialized='table',
4 partition_by={
5 "field": "created_at",
6 "data_type": "timestamp",
7 "granularity": "day"
8 }
9 )
10}}
11select *
12from staging_model個人的に今後気をつけたいポイント
普段の実務では、view、table、incrementalのモデルに触れる機会が多くあります。
特にincrementalのモデルの設定時に特に注意が必要であることを実務を通して学びました。
以下は、レビューでの指摘や業務の中で得た具体的な気づきです。
①ソーステーブルにパーティションの設定がされていなければ、incrementalの効果は得られない。
一般的に、incrementalの設定をすると増分のみを更新するため、スキャン量が大幅に削減されるメリットがあります。
しかし、これはincrementalモデルが参照するソーステーブルにパーティションの設定がされていることを前提としています。
BigQueryは仕様上、データの挿入順序を保存しないため、データはランダムに配置されます。
従って、ソースにパーティション設定がない場合、日付等で増分指定をしても対象データがテーブル内に散らばっているため、BigQueryはフルスキャンを実行してしまいます。
結果的に、incrementalを設定してもコスト削減や高速化の効果を得ることができません。
対策としては、incrementalの設定をする場合は、必ず参照するソーステーブルがパーティション分割されていることを確認することが重要です。
② unique_keyに一意性が保証されていないとレコードの重複が発生する
merge戦略ではunique_keyの設定が必須ですが、このunique_keyが実データにおいて一意でなかった場合、出力テーブルに意図しない重複や上書きが発生します。
例えば、ユーザーが1日に複数回引いたガチャの回数を管理するモデルで、誤って unique_key='user_id' のみを指定したケースを考えます。
ここに、以下の新規データ(増分)をマージしようとします。
1── 新規データ(source) ────────────────────────────
2user_id | date | gacha_cnt
3------------+------------+----------
4user_001 | 2026-01-03 | 2 ← 新しい日付のデータ
5user_002 | 2026-01-01 | 8 ← 同日データの更新すると、user_001 の新規データが、既存の2つのレコード両方にマッチしてしまい、どちらも同じ値で上書きされるというデータの破損が起きてしまいます。
1── 誤ったMERGE 後のテーブル ─────────────────────────────
2user_id | date | gacha_cnt
3------------+------------+----------
4user_001 | 2026-01-03 | 2 ← 元の 2026-01-01 行が上書きされる
5user_001 | 2026-01-03 | 2 ← 元の 2026-01-02 行も同じ値で上書きされ、データが重複
6user_002 | 2026-01-01 | 8 ← 正常に更新このような時系列データでは、unique_key=['user_id', 'date'] のように複合キーを設定することで、日次情報を一意に特定する必要があります。
正しく設定すれば、期待通りに更新・追加が行われます。
1── 正しいMERGE 後のテーブル(unique_key=["user_id", "date"])─────────────
2user_id | date | gasha_cnt
3------------+------------+----------
4user_001 | 2026-01-01 | 3 ← マッチせずそのまま残る
5user_001 | 2026-01-02 | 1 ← マッチせずそのまま残る
6user_001 | 2026-01-03 | 2 ← マッチせず INSERT される
7user_002 | 2026-01-02 | 4 ← マッチして UPDATE される
さらに、この複合キーが一意性を本当に保証しているかを、ymlファイルの data_tests で事前に検証しておくことが望ましいです。
これにより、一意性制約の違反によるincrementalの失敗やデータ不整合を未然に防ぐことができます。
1models:
2 - name: xxx_table
3 data_tests:
4 - dbt_utils.unique_combination_of_columns:
5 arguments:
6 combination_of_columns:
7 - user_id
8 - dateまた、設定自体が正しくても、モデルのロジックによってはかえってコストが増大することもあります。
実装の際は、必ず実装前の全量更新と実装後を比較し、「データが正しく更新されているか」と「実際にクエリコストが削減されているか」の両方を検証することが不可欠です。
まとめ
今回は、dbtにおける各マテリアライゼーションの特徴や実装のパターンについて整理し、実務の中で気づいた注意点についてまとめました。
マテリアライゼーションはデータの性質に合わせて適切な手法を選択すれば、パフォーマンス向上やコスト削減に大きく貢献できます。実務でも、あるモデルをtableからincrementalに変更した結果、クエリコストを9割以上削減でき、その効果の大きさを実感しました。
まだまだ学ぶことは多いですが、少しでも早くチームに貢献できるよう知見を深めていきたいと思います。
ライトコードでは、エンジニアを積極採用中!
ライトコードでは、エンジニアを積極採用しています!カジュアル面談等もございますので、くわしくは採用情報をご確認ください。
採用情報へ





