0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

dbtの構造と役割について、Pythonとの比較

Last updated at Posted at 2025-09-01

もっと早く知っておけばよかったdbtを使うメリット

現在取り組んでいるデータ基盤構築プロジェクトで、初めてdbtを導入しました。
本記事では、その経験を踏まえ、プロジェクト目線で感じたdbtの利点を簡単にご紹介します。

本記事の目次は以下のとおりになります。

  1. dbtとは何か?なぜ必要か?
  2. dbtの構造と役割
  3. なぜこの構造なのか?
    3.1. 責任の分離
    3.2. 依存関係の自動管理
    3.3. 自動テスト実行
  4. Pythonとの比較
  5. 具体例:staging層の価値
  6. 効率性の比較
  7. まとめ:なぜdbtなのか

dbtとは何か?なぜ必要か

dbtの定義としては、データウェアハウスにあるデータに対し、SQLを用いてデータ変換を行うためのツールです。

下記(1)と(2)のコードを比較してみまよう。

(1)
# Pythonでやろうとすると...
import pandas as pd
from google.cloud import bigquery

# データ取得
client = bigquery.Client()
query = """
SELECT symbol, ts, open, high, low, close, volume, ingested_at
FROM `project.raw_fin.raw_prices`
WHERE symbol IS NOT NULL AND close > 0
"""
df = client.query(query).to_dataframe()

# データクリーニング
df['data_quality_flag'] = 'valid'
df.loc[df['close'] <= 0, 'data_quality_flag'] = 'invalid_price'
# ... 100行以上のコード

# BigQueryに書き戻し
df.to_gbq('project.dwh_fin.stg_prices', if_exists='replace')
(2)
-- stg_prices.sql(たった30行)
{{ config(materialized='view') }}

SELECT symbol, ts, open, high, low, close, volume,
  CASE WHEN close <= 0 THEN 'invalid_price' ELSE 'valid' END as data_quality_flag
FROM {{ source('raw_fin','raw_prices') }}
WHERE symbol IS NOT NULL

上記の二つのコードからわかるように、SQLを用いるだけで、余分なコードを使わず、データの変換を簡潔かつきれいに行うことができます。

dbtの構造と役割

1. dbtにおいてSQLファイル (.sql) の役割

SQLの役割は、データ変換ロジックを定義することです。SELECT分やWITH句、CASEなどを使って、どの列を残すか、どのように変換・計算などをするか宣言的に記述します。つまり、データを「どう変換するか」直接書くのがSQLです。

{{ config(materialized='view') }}

WITH cleaned_data AS (
  SELECT 
    symbol,
    ts,
    close,
    -- ⭐ ビジネスロジックを宣言的に記述
    CASE 
      WHEN close <= 0 THEN 'invalid_price'
      WHEN high < low THEN 'invalid_ohlc'
      ELSE 'valid'
    END AS data_quality_flag
  FROM {{ source('raw_fin','raw_prices') }}
)
SELECT * FROM cleaned_data WHERE data_quality_flag = 'valid'

dbtにおいてYML (.yml) の役割

YMLは、データ変換ロジックの設計・管理するためのメタデータを定義する役割を持っています。具体的には以下のようなことを担います。
モデルの定義:どのSQLファイルがどんな意味を持つのか?
カラムの説明:ドキュメンテーション
テストの設定:not nullや範囲チェックなど
ソース定義:出どころ

つまり、変換の仕様や品質保証を記述するのがYMLです。

version: 2

models:
  - name: stg_prices
    description: "クリーンな価格データ"
    columns:
      - name: close
        description: "終値"
        tests:
          - not_null          # ⭐ 自動テスト
          - dbt_utils.accepted_range:
              min_value: 0

なぜこの構造なのか

なぜこの構造になっているか、以下の三つに分けて見ていきます。
1)責任の分離
2)依存関係の自動管理
3)自動テスト実行

責任の分離

ファイル 責任
.sql 何を作るか(ロジック) データ変換、集計、結合など
.yml 品質をどう保つか(メタデータ) テスト、ドキュメント、型定義

依存関係の自動管理

dbtでは、source()やraw()を使うと、テーブルの依存関係が自動的に決定されます。
例えば、以下のコードを見ると、stg_prices.sqlは「raw_pricesに依存している」と宣言しており、daily_calculate.sqlは、「stg_pricesに依存している」と宣言しています。

- models/staging/stg_prices.sql
FROM {{ source('raw_fin','raw_prices') }}  -- ⬅️ RAWデータに依存

-- models/marts/daily_calculate.sql  
FROM {{ ref('stg_prices') }}  -- ⬅️ stagingに依存

自動テスト実行

YMLファイルでカラムごとに品質ルールを宣言できます。

# schema.yml
tests:
  - not_null
  - unique  
  - accepted_values: ['valid', 'invalid']

以上のコードのように設定しておけば、dbt testを実行するだけで、
・NULLがあるか?
・重複があるか?
・値が指定したリストに収まっているか?
などの内容を自動的に検証してくれます。

Pythonとの比較

次に、Pythonと比較してみてみます。

以下のコードを見てわかるように、Pythonで実装すると、依存関係管理・データ品質チェック・保存処理・実行順序をすべて自分で制御しないといけません。したがって、ミスや抜け漏れが起こりやすくて、運用コストも高くなります。

# 手動でやらないといけないこと
def clean_prices():
    # 1. 依存関係を手動管理
    raw_df = get_raw_prices()  # この順序を間違えるとエラー
    
    # 2. データ品質チェックを手動実装
    if raw_df['close'].isnull().any():
        raise ValueError("NULL値があります")
    
    # 3. 変換ロジック
    cleaned_df = raw_df[raw_df['close'] > 0]
    
    # 4. 手動でBigQueryに保存
    cleaned_df.to_gbq('project.dwh_fin.stg_prices')
    
    # 5. 次の処理も手動で呼び出し
    create_daily_calculate(cleaned_df)

def create_daily_calculate(input_df):
    # また同じことを繰り返し...

一方、dbtでは処理を「宣言的」に記述できるため、依存関係の解決・品質チェック・保存・実行順序を自動で最適化してくれます。その結果、Pythonで毎回ゴリゴリと手続き的なコードを書く必要がなくなり、再現性と保守性の高いデータ基盤を構築できます。

SELECT symbol, close,
  CASE WHEN close > 0 THEN 'valid' ELSE 'invalid' END as flag
FROM {{ ref('stg_prices') }}

具体例:staging層の価値

staging層を例に、具体的に見ていきます。

例えば、金、銀、原油など複数の商品データを分析したいとします。
Pythonで直接 raw_prices を読み込むと、以下のコードのようになります。

# analyze_gold.py
df = pd.read_gbq("SELECT * FROM raw_prices WHERE symbol='GC=F'")
df = df[df['close'] > 0]   # クリーニング処理(重複)
df['daily_return'] = df['close'].pct_change()

# analyze_silver.py
df = pd.read_gbq("SELECT * FROM raw_prices WHERE symbol='SI=F'")
df = df[df['close'] > 0]   # また同じ処理をコピペ
df['daily_return'] = df['close'].pct_change()

以上のやり方の問題点として以下あげることができます。
・同じクリーニングロジックを何度もコピペ
・分析対象が増えるたびに「また同じ処理」を書く必要がある
・修正が入ったら、すべてのスクリプトに反映しなければならない

一方、dbtのstaging層を使う場合、staging層に「共通のクリーニング処理」を一度だけ定義します。

-- stg_prices.sql
SELECT symbol, ts, close,
       CASE WHEN close > 0 THEN 'valid' ELSE 'invalid' END AS flag
FROM {{ source('raw_fin','raw_prices') }}
WHERE flag = 'valid'

すると、後続の分析モデルでは「きれいにしたデータ」だけを呼び出せばOKです:

-- gold_analysis.sql
SELECT ts, close,
       LAG(close) OVER(...) - 1 AS daily_return
FROM {{ ref('stg_prices') }}
WHERE symbol = 'GC=F';

dbtを使うと以下のメリットをあげることができます。
・クリーニング処理を一度だけ書けばいい
・金でも銀でも石油でも、同じstg_pricesを参照するだけ
・修正があればstg_prices.sqlを直すだけで全体に反映される

効率の比較

以下の表に、効率性の比較をまとめました。

観点 Python dbt
コード量 多い(100行+) 少ない(20−30行)
再利用性 低い 高い(staging層で共通化)
テスト 手動実装 自動生成
依存関係 手動管理 自動管理
ドキュメント 別途作成 自動生成
バージョン管理 複雑 シンプル

まとめ:なぜdbtなのか

1)モジュール化:staging層で共通のクリーニングロジック
2)自動化:依存関係とテストの自動実行
3)宣言的:SQLで「何を作るか」だけ記述
4)品質保証:YMLでテストとドキュメントを定義
5)チーム開発:標準化された構造

Pythonでもできるけれども、dbtはSQL中心の「データパイプライン専用フレームワーク」として遥かに効率的であることを実感させられました。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?