本文へ移動

ホーム / Data / 第3部 データパイプライン

第3部 データパイプライン・第7章

変換(ELT・dbt・冪等性)

★★★ 必須9トピック・面接 5問

集めたデータを使える形に変える処理を、やり直せて、速く、間違えにくく作る方法を押さえる。実行の管理は次の章で扱う。

別のページで開く →

DBラボを読み込んでいます…

7.1ETL と ELT

用語★★★ 必須
ひとことで

DWH に入れる前に変換する ETL と、まず生のまま入れてから DWH の中で SQL で変換する ELT。

昔は、DWH に入れる前に専用のサーバーで変換する ETL(Extract → Transform → Load)が主流でした。クラウド DWH が安く速くなった今は、まず生のまま DWH に入れて、DWH の中で SQL で変換する ELT が主流です。生のデータが残るのでやり直しがきき、SQL が書ける人なら誰でも変換に参加できます。

変換するのは DWH に入れる前か、後かETL元データE変換サーバーT:ここで整えるLDWH(完成品だけ)ELT元データE・LDWH生データ(raw)T:SQL・dbt整えたテーブル生データが残るので、ロジックを直したら作り直せる
ETL と ELT の違い
ETLELT
変換する場所DWH に入れる前の専用のサーバーDWH の中(SQL)
生のデータ残らないことが多いDWH に残るので、やり直せる
変換を書く人専用の道具を使えるエンジニアSQL を書ける人なら誰でも
向くもの入れる前に個人情報を落とす必要がある、DWH の計算が高いクラウド DWH を使う今の多くの基盤

dbt で変換をコードとして管理する

面接ではこう答える

ETL は DWH に入れる前に専用のサーバーで変換する方法、ELT はまず生のまま DWH に入れてから DWH の中で SQL で変換する方法です。クラウド DWH が安く速くなったので今は ELT が主流で、生のデータが残るので変換を直してやり直せ、SQL を書ける人なら誰でも変換に参加できます。ただし個人情報を DWH に入れる前に落とす必要がある場合などは、入れる前の変換も組み合わせます。

ひとことで

SELECT 文を1つのモデルとして書き、ref() で依存関係をつなぎ、テーブルやビューの作成とテストを順番に実行する変換の道具。

dbt は ELT の「T」を担う道具です。1つの SELECT 文を1つのモデル(テーブルやビュー)として書き、ref() で他のモデルを参照すると、依存関係の順に実行してくれます。SQL を Git で管理し、レビュー・テスト・ドキュメント作りができるようになります。

dbt は ref() から依存関係を読み取り、順番に作るsource取り込んだ生の表source()stg_ordersref で参照ref('stg_orders')int_…途中の部品ref(…)fct_orders利用者向けテーブルとして作る
SQL の SELECT だけを書けば、CREATE や実行の順番は dbt が決める
-- models/staging/stg_shop__orders.sql
SELECT
  id                          AS order_id,
  user_id                     AS customer_id,
  CAST(total AS NUMERIC)      AS amount,
  created_at                  AS ordered_at
FROM {{ source('shop', 'orders') }}
WHERE _deleted IS NOT TRUE
マテリアライズ作られるもの向くもの
viewビュー(毎回元から計算)軽い staging。データが小さい
table毎回作り直すテーブル中くらいまでの mart
incremental差分だけを足す・MERGE するテーブル大きなファクト
ephemeral作らず、使う側の SQL に CTE として埋め込む小さな部品

source() で元のテーブルを、ref() で他のモデルを参照します。テーブルの名前を直接書かないことで、依存関係が分かり、開発用と本番用でスキーマを切り替えられます。

面接ではこう答える

dbt は ELT の変換を担う道具で、1つの SELECT 文を1つのモデルとして書きます。ref() で他のモデルを参照すると依存関係が分かるので、dbt が順番に CREATE してくれます。テーブルにするかビューにするか、差分だけ足すかはマテリアライズの設定で選びます。SQL を Git で管理して、レビュー・テスト・ドキュメント作りができるのが一番の利点です。

ひとことで

元のテーブルを整える staging、途中の部品の intermediate、利用者向けの mart に分ける。レイクハウスでは bronze・silver・gold と呼ぶ。

dbt のモデルは、役割ごとに層に分けます。層を分けると、どこに何を書くかが決まり、変更の影響が読みやすくなります。

層役割例
staging(stg_)元のテーブル1つにつき1モデル。列名・型をそろえ、重複を除くだけ。業務ロジックは入れないstg_shop__orders
intermediate(int_)つなぐ・集める途中の部品。利用者には見せないint_orders_with_payments
mart(fct_・dim_)利用者が使う完成品。スター型スキーマやワイドテーブルfct_orders・dim_customers

レイクハウスでは同じ考え方をメダリオン(bronze=生データ、silver=整えたデータ、gold=利用者向け)と呼ぶことが多く、名前は違っても「層ごとに役割を分ける」点は同じです。

層ごとに役割を分けるsource取り込んだ生データstaging列名・型・重複intermediateつなぐ・集める部品mart利用者が使う完成品BI・分析メダリオン(レイクハウスでの呼び方)bronzesilvergold下流ほど「業務の意味」が入り、利用者に近くなる
dbt のモデルの層と、メダリオン(bronze・silver・gold)のおおよその対応

よくある失敗:staging に業務ロジック(「売上はキャンセルを除く」)を入れて、元の値が見られなくなる。mart から mart を何段も参照して、1つの変更の影響が読めなくなる。テストを書かずにモデルだけ増やす。最低でも、各モデルの主キーに unique と not_null を入れます。

面接ではこう答える

dbt のモデルは、元のテーブル1つにつき1つで列名や型をそろえるだけの staging、つなぐ・集める途中の部品の intermediate、利用者が使うファクトやディメンションの mart に分けます。業務ロジックは staging に入れず、mart から mart を何段も参照しないのがコツです。レイクハウスでは同じ考え方を bronze・silver・gold のメダリオンと呼びます。

dbt のテストは、モデルの YAML に書きます。よく使う4つは unique・not_null・accepted_values(決まった値だけか)・relationships(参照先に存在するか)です。

models:
  - name: fct_orders
    columns:
      - name: order_id
        tests: [unique, not_null]
      - name: status
        tests:
          - accepted_values:
              values: ['paid', 'shipped', 'cancelled']
      - name: customer_id
        tests:
          - relationships:
              to: ref('dim_customers')
              field: customer_id
dbt build はテストが落ちたら下流を作らないstg_orders作るテストunique・not_nullfct_orders作らない(SKIP)ダッシュボード前日の正しい値のまま
壊れたデータが利用者に届く前に止まる

dbt build はモデルとテストを依存の順に実行し、テストが失敗するとその下流のモデルは作りません。壊れたデータがダッシュボードまで届くのを止められます。

テストは「最低でも各モデルの主キーに unique と not_null」から始めます。テストの考え方や置き場所はデータのテストで詳しく扱います。

ひとことで

何回やり直しても結果が同じになるように書く。MERGE やパーティションの入れ替えを使う。

冪等(べきとう)とは、同じ処理を何回やっても結果が同じになることです。パイプラインは必ずどこかで失敗し、やり直されます。「途中まで INSERT してから落ちた処理をもう一度流したら、半分が2倍になった」という事故を防ぐには、最初から冪等に書きます。INSERT INTO … SELECT を足していくだけの処理は冪等ではありません。

方法やり方向くもの
MERGE(upsert)一意キーで照合し、あれば更新・なければ追加注文・顧客など、あとから更新される行
パーティションの入れ替え対象の日のパーティションを丸ごと作り直して置き換えるログ・イベントなど、日付で区切れるデータ
DELETE + INSERT対象の範囲を消してから入れる(1つのトランザクションで)MERGE やパーティションの入れ替えがない DB
全件の作り直し毎回テーブルを丸ごと作る小さいテーブル
MERGE INTO fct_orders AS t
USING stg_orders_delta AS s       -- 今回の差分(キーごとに1行に重複を除いておく)
  ON t.order_id = s.order_id
WHEN MATCHED THEN
  UPDATE SET status = s.status, amount = s.amount, updated_at = s.updated_at
WHEN NOT MATCHED THEN
  INSERT (order_id, status, amount, updated_at)
  VALUES (s.order_id, s.status, s.amount, s.updated_at);

MERGE の元(USING)に同じキーが2行あると、エラーになるか結果が不定になります。元を必ずROW_NUMBER で1行にしてから使います。

面接ではこう答える

冪等とは、同じ処理を何回やっても結果が同じになることです。パイプラインは必ず失敗してやり直されるので、INSERT を足していくだけの処理だと、途中で落ちてやり直したときに重複します。あとから更新される行は一意キーで MERGE し、日付で区切れるデータは対象の日のパーティションを丸ごと作り直して入れ替えます。MERGE の元はキーごとに1行に重複を除いておきます。

別のページで開く →

DBラボを読み込んでいます…

7.6増分処理(incremental)

用語★★★ 必須
ひとことで

毎回全件を作り直す代わりに、新しく来た分・変わった分だけを処理してテーブルに足す・MERGE する。

増分処理は、毎回全件を作り直す代わりに、新しく来た分・変わった分だけを処理する方法です。データが大きくなるほど、時間と料金の差が開きます。dbt では materialized='incremental' と unique_key を指定し、is_incremental() の中に「前回より新しい分」の条件を書きます。

毎日の処理で読む量(1年分のファクトの例)全件の作り直し365日分を毎日読む増分(直近3日を作り直す)3日分だけ
データが大きくなるほど、時間と料金の差が開く
{{ config(materialized='incremental', unique_key='order_id') }}

SELECT order_id, status, amount, updated_at
FROM {{ ref('stg_orders') }}
{% if is_incremental() %}
  -- 前回の最大より少し前から取り、重なりは unique_key の MERGE で吸収する
  WHERE updated_at >= (SELECT MAX(updated_at) FROM {{ this }}) - INTERVAL '1 day'
{% endif %}
気をつけること理由
ロジックを変えたら全件で作り直す(--full-refresh)過去の行は古いロジックのまま残る
少し重ねて取る境目や遅れて届いた行の取りこぼしを防ぐ
定期的に全件と突き合わせる小さなずれがたまっていないか確かめる

面接ではこう答える

増分処理は、毎回テーブルを全件作り直す代わりに、前回より新しい分や変わった分だけを処理して足すか MERGE する方法で、データが大きくなるほど時間と料金の差が開きます。dbt では incremental のマテリアライズと unique_key を使います。境目や遅れて届いた行のために少し重ねて取り、ロジックを変えたときは全件で作り直すのが注意点です。

ひとことで

起きてから何日も遅れて届くイベントを取りこぼさないよう、毎回直近の数日分を作り直す。

「昨日だけ」では、遅れて届いた分が漏れる11/01パーティション11/02パーティション11/03パーティション11/04パーティション11/05パーティション11/5 の実行:直近3日を作り直す(MERGE・入れ替え)11/5 に届いた、11/3 に起きたイベント「11/4 だけ」作る方式だと、このイベントは どこにも入らない
遅れて届くデータとルックバック(11/5 の実行の例)

遅れて届くデータへの備えは、イベントを扱う集計では欠かせません。スマホのアプリは電波がない間イベントをためて、あとでまとめて送ります。「昨日のパーティションだけ処理する」だと、今日届いた昨日の分が漏れます。対策は、毎回直近の数日分(例:3日)を作り直すことです(ルックバック)。「前回の最大の時刻より後」で絞るときも、その時刻から少し重ねて取り、重なった分は MERGE で吸収します。イベントが起きた時刻と届いた時刻の両方を持っておくと、どのくらい遅れて届くかを測って、さかのぼる日数を決められます。

方法やり方注意
ルックバック毎回、直近の数日分を作り直す何日さかのぼるかを、遅れの分布から決める
届いた時刻で区切る取り込みは届いた時刻のパーティション、集計は起きた時刻で同じイベントが2つの日に入らないよう、集計側で起きた時刻を使う
確定の日を決める「3日前までの数字は確定」と利用者と決めるそれより遅れたものは別に記録する

面接ではこう答える

スマホのアプリなどは電波がない間イベントをためて、あとでまとめて送るので、昨日のパーティションだけを処理すると今日届いた昨日の分が漏れます。対策は、毎回直近の数日分を作り直すルックバックです。起きた時刻と届いた時刻の両方を持っておけば遅れの分布を測れるので、さかのぼる日数を根拠を持って決め、「何日前までは確定」と利用者と合意します。

変換の SQL もアプリのコードと同じように、開発環境 → レビュー → CI → 本番の流れで出します。本番のテーブルを手で直接直すと、何が変わったか誰にも分からなくなります。

変換のコードも、レビューと CI を通して本番に出す開発自分用のスキーマプルリクエストレビューCI変更分と下流をテスト本番マージ後に反映
変更したモデルとその下流だけを CI で作ると、速く安く確かめられる
仕組みやり方
開発用のスキーマ人ごとに dbt_自分の名前 のようなスキーマに作る(dbt の target で切り替える)
変更分だけの CIdbt build --select state:modified+ で、変更したモデルと下流だけを作ってテストする
上流は本番を参照--defer で、変更していない上流は本番のテーブルを使う
差分の確認本番と CI の結果の件数・主な指標を比べて、レビューに貼る

DWH の中の変換は SQL を基本にし、SQL では難しいことだけを Python などで書くのが、保守しやすい分け方です。

SQL が向くPython などが向く
JOIN・集計・ウィンドウ関数・重複の除去API からの取り込み、ファイルの解析
DWH の計算資源で大量のデータを処理外部のライブラリが要る処理(形態素解析・地理の計算・ML)
アナリストもレビュー・修正できる複雑な分岐や繰り返し、テストしたい関数

Python で DWH から全件を読み出してメモリで処理すると、データが増えたときに必ず詰まります。大きな処理は SQL か Spark に寄せ、Python は「指示を出す」「SQL ではできない部分だけ」に絞ります。dbt や DWH には Python のモデルを DWH の中で動かす仕組みもあります。

この章のまとめ

🖨 まとめシート(印刷・PDF)
  • ELT と dbt での変換、モデルの層とテスト
  • 冪等な処理と増分処理、遅れて届くデータ
  • 変換のコードのレビューと CI

この章のクイズ

面接ではこう聞かれる

まず答えを見ずに声に出して答え、そのあと開いて比べる。

Q1パイプラインを冪等にする方法は?

模範回答

基本は「追記ではなく、決まった単位で置き換える」ことです。更新がある注文のようなデータは、一意キー(注文 ID)で MERGE して、あれば更新・なければ追加にします。このとき元のデータは ROW_NUMBER でキーごとに1行にしておきます。ログのように日付で区切れるデータは、対象の日のパーティションを丸ごと作り直して入れ替えます。

加えて、処理の対象期間を引数で受け取り、処理の中で「今日」を使わないようにすると、過去分のやり直しも同じ結果になります。遅れて届くデータがあるので、直近の数日分を毎回作り直し、重なりは MERGE で吸収します。最後に主キーの unique テストを入れて、重複が出たら下流に流さないようにします。

深掘りされたら

  • MERGE が使えない DB ではどうしますか?
  • 外部の API に送る処理を冪等にするには?

見られているポイントMERGE・パーティション単位の上書き・一意キー

Q2ETL と ELT の違いと、ELT が主流になった理由を説明してください。

模範回答

ETL は DWH に入れる前に専用のサーバーで変換する方式、ELT はまず生のまま DWH に入れて、DWH の中で SQL で変換する方式です。ELT が主流になったのは、クラウド DWH の保存が安くなり、計算も必要なときに大きく使えるようになったからです。

ELT の利点は、生のデータが残るのでロジックを直したら作り直せること、SQL が書ける人なら誰でも変換に参加できること、dbt で変換をコードとして管理してテストできることです。一方で、生のデータに個人情報が入ったまま DWH に置かれるので権限の管理が大事になり、何でも DWH で計算すると料金が増えやすい点に注意します。

深掘りされたら

  • ELT でも、入れる前に変換したほうがよいものは?

見られているポイント方式の違いと、背景・トレードオフ

Q3dbt のモデルを staging・intermediate・mart に分けるのはなぜですか?

模範回答

役割を分けて、変更の影響を読みやすくするためです。staging は元のテーブル1つにつき1モデルで、列名・型・重複の除去だけをします。こうしておくと、上流の変更があってもここを直すだけで済みます。intermediate はつなぐ・集めるための途中の部品で、mart は利用者が使う完成品です。

業務のロジック(売上はキャンセルを除く等)を mart やその手前にまとめると、どこで数字の意味が決まるかがはっきりします。逆に staging にロジックを入れると元の値が見られなくなり、mart から mart を何段も参照すると、1つの変更がどこまで響くか分からなくなります。各層の主キーには unique と not_null のテストを入れています。

深掘りされたら

  • mart が増えすぎたら、どう整理しますか?

見られているポイント層ごとの責任と、保守のしやすさ

Q4スマホのアプリから、何日も遅れてイベントが届きます。集計をどう作りますか?

模範回答

イベントが起きた時刻と、届いた時刻の両方を持つようにして、集計は起きた時刻の日付で行います。そのうえで、毎回「昨日だけ」ではなく直近の数日分(例えば3日)のパーティションを作り直すルックバックにします。作り直しはパーティションの入れ替えや MERGE で行い、何度流しても同じ結果になるようにします。

何日さかのぼるかは、届いた時刻と起きた時刻の差の分布を測って決めます。それより遅れて届く分は、週に1回もっと長い期間を作り直すか、数字が変わり得ることを利用者に伝えておきます。確定した数字が必要な月次の報告などは、締めの日を決めて、それ以降は変えない運用にします。

深掘りされたら

  • ルックバックを長くしすぎると、何が困りますか?

見られているポイントイベント時刻と到着時刻の区別、ルックバック

Q5増分処理のモデルのロジックを変えたいとき、どう進めますか?

模範回答

増分処理では過去の行が古いロジックのまま残るので、全件で作り直す必要があるかをまず考えます。新しいロジックを開発用のスキーマで全件作り、本番の今のテーブルと件数・主な指標を比べて、差が説明できることを確かめます。

差を利用者に伝えてから本番で --full-refresh するか、大きすぎるなら期間を区切ってバックフィルします。テーブルが大きくて作り直しが重い場合は、新しいテーブルを横に作って検証し、切り替える方法も取ります。

深掘りされたら

  • 作り直しの間、利用者が古い数字を見ることはどう扱いますか?

見られているポイント過去分への影響と、検証してから切り替える手順