経営会議の資料で、「取引先の数」が部署ごとに違っていました。営業のCRMでは12,400社、請求の基幹システムでは9,800社、ECサイトには別の会員IDがあります。データ担当のあなたは、半年かけて顧客の統合マスタを作りました。ところが翌期に営業の組織が変わり、顧客の担当区分が組み替えられます。ディメンションの表を作り直し、過去分を再計算すると、経営企画から「先月の資料と数字が違う。どちらが正しいのか」と聞かれました。業務が変わるたびに作り直しが降ってきて、そのたびに過去の数字まで動きます。

この「業務が変わるたびに統合の表を作り直す」状態から抜けるためのモデリング手法が、Data Vault 2.0です。業務の概念(顧客、商品、注文)と概念どうしの関係、属性の履歴を別々の表に持ち、項目や組織が変わっても既存の表は壊さずに表を足して対応します。提唱者はDan Linstedtで、Michael Olschimkeとの共著『Building a Scalable Data Warehouse with Data Vault 2.0』(Morgan Kaufmann、2015年)に体系がまとまっています。

Data Vaultが効く会社、効かない会社を4つの問いで分ける

表の数もJOINの数も増える手法なので、痛みがない会社が入れると手間だけが増えます。次の4つの問いに答えてみてください。

問い「はい」ならData Vaultの出番社内での確かめ方
同じ顧客や商品を、2つ以上のシステムが別々のIDで持っているか統合の軸(Hub)を作る価値があるCRM・基幹・ECのそれぞれで、顧客の件数とIDの体系を並べる
業務側の項目や区分の変更が、半年に1回以上あるか「表を足すだけ」の構造が効く過去1年のデータ基盤への改修依頼を数え、属性追加や区分変更が何件あったかを見る
過去の時点の値を、後から正確に再現する必要があるか(監査、規制対応、月次の締め直しなど)履歴を標準で持てることが効く経理や監査の担当に、過去の時点の数字を求められた回数を聞く
設計を学ぶ期間(目安として1〜2か月)を、データエンジニアが取れるか学習の負担を吸収できる担当者の本業の工数を、上長と一緒に確かめる

3つ以上が「はい」なら、統合層をData Vaultで作る価値があります。1〜2つなら、スタースキーマに履歴の仕組み(SCD Type 2)を足すほうが早く結果が出ます。0なら過剰です。4つ目だけが「いいえ」なら、対象を1業務に絞って学習の負担を小さくします。

3種類の表に分けて持つ

データは3種類の表に分けて持ちます。役割を混ぜないことが、変更に強い理由そのものです。

表の種類持つもの持たないもの例(小売)
Hub業務の概念を識別するキー(ビジネスキー)だけ属性HUB_CUSTOMER(顧客番号)、HUB_PRODUCT(商品コード)
LinkHubどうしの関係だけ属性LINK_ORDER_LINE(注文×商品)、LINK_ORDER_CUSTOMER(注文×顧客)
SatelliteHubやLinkにひもづく属性と、その変化の履歴キーの定義SAT_CUSTOMER_PROFILE(氏名・住所の変遷)、SAT_PRODUCT_PRICE(価格の変遷)

冒頭の場面に当てはめると、「顧客の担当区分」はSatelliteに入ります。区分が組み替わっても、Satelliteに新しい行が足されるだけです。顧客のHubも、過去の区分の行も残ります。「先月の資料の時点ではこの区分だった」という数字を、後からそのまま再現できます。

項目が1つ増えたとき、作業がどう変わるか

顧客に「業種区分」という項目が増えた場面で、作業を比べます。

作業スタースキーマで作っている場合Data Vaultで作っている場合
表の変更顧客ディメンションに列を足す業種区分のSatelliteを1つ足す。既存の表は触らない
過去の行の扱い過去の行の値をどうするか(空にするか、遡って埋めるか)を決める新しいSatelliteは、取り込んだ時点から履歴が始まる
影響の確認このディメンションを使う集計表(マート)をすべて確かめるこのSatelliteを使うマートだけを足す
過去の数字作り直し方によっては、過去の集計値が動く既存のSatelliteは変わらないので、過去の数字は動かない

1つのHubにSatelliteは複数持てるので、ソースシステム・更新頻度・機密度ごとに分けられます。公開してよい属性と本人確認データを別のSatelliteにすれば、アクセス権も別々にかけられます。

統合層はData Vault、分析用の表はスタースキーマの二段で組む

JOINが多いぶん、BIから直接読むと重くなります。統合と履歴はData Vaultで持ち、BIが読む表は別にスタースキーマや横長の表で用意します。データを層に分けて品質を上げていく3層の考え方(メダリオンアーキテクチャ)に当てはめると、次の分担になります。

層置くものData Vaultの役割
Bronzeソースの生データ(ソースの構造のまま)使わない
Silverクレンジングと統合Hub・Link・Satelliteで組む
Gold用途別の集計表(マート)使わない。Silverから切り出したスタースキーマや横長の表を置く

Gold層では、Data Vaultの履歴を「最新の値だけ」「過去すべて」「特定の時点」と用途に合わせて切り出します。BIの利用者からは、見慣れたスタースキーマが並んでいるようにしか見えません。層の分け方はメダリオンアーキテクチャ(Bronze/Silver/Gold)の解説に、dbtで層を組む方法はdbtの3層構造に書いています。

小さなECサイトで、モデリングを5つの手順で進める

会員登録した顧客が、商品コードで管理された商品を、1回の注文で複数買うECサイトを題材にします。配送先は注文ごとに指定でき、自宅以外も選べます。

手順1:業務の名詞を洗い出し、Hubにするかを決める

Hubの候補ビジネスキー判断
顧客顧客IDHubにする。業務の中で独立して存在する
商品商品コードHubにする
注文注文番号Hubにする
配送先住所住所ID迷う。複数の注文で同じ住所を参照する業務なら独立したHub、注文ごとに入力して使い回さないならSatellite

迷ったら、最初はSatelliteで作ります。使い回す場面が増えてからHubに昇格させるほうが、最初からHubを増やしすぎるより手戻りが少なくて済みます。

手順2:Hubどうしの関係をLinkで結ぶ

「注文×商品」の明細と「注文×顧客」をLinkにします。明細の数量や単価は、Linkには直接書きません。Linkにひもづく別のSatelliteに持たせます。

手順3:属性を、変わる頻度と機密度でSatelliteに分ける

Satellite持つ属性分けた理由
SAT_CUSTOMER_PROFILE氏名、メールアドレス基本情報で、変更は少ない
SAT_CUSTOMER_PREFERENCESメールマガジンの受信設定など頻繁に変わる
SAT_CUSTOMER_SENSITIVE本人確認のデータなどアクセス権を分けたい

手順4:ハッシュキーで表をつなぎ、差分だけを足す

2.0では、ビジネスキーをハッシュ関数にかけた値を、表どうしをつなぐキーにします。連番を採番する方式と違い、各表が他の表を待たずにキーを作れるので、並列にロードできます。

いちばん事故が多いのは、ハッシュにかける前の正規化の漏れです。同じ顧客IDが「ABC123」「abc123」「 ABC123 」と混ざると、別の顧客として扱われます。大文字への統一、前後の空白の除去、NULLの置き換えを、入口で必ずそろえます。Snowflakeの例です。

-- ビジネスキーを正規化してからハッシュにする
SELECT
  MD5_BINARY(UPPER(TRIM(COALESCE(customer_id, '')))) AS hub_customer_hk,
  customer_id AS customer_bk
FROM stg_customer;

SnowflakeのMD5_BINARYは16バイトの値を返します。公式ドキュメントは、MD5は暗号の用途にはもう使うべきでないが、破損の検出のような用途には使えるとしています。キーは識別のためのもので、暗号の強さは要りません。社内規程でMD5を禁じている場合は、SHA2_BINARYなどに替えます。

Satelliteは、属性をまとめてハッシュにした値(hash_diff)を前回の最新行と比べ、違うときだけ新しい行を足します。

WITH source AS (
  SELECT
    MD5_BINARY(UPPER(TRIM(COALESCE(customer_id, '')))) AS hub_customer_hk,
    MD5_BINARY(CONCAT_WS('|',
      COALESCE(customer_name, ''),
      COALESCE(address, ''),
      COALESCE(email, '')
    )) AS hash_diff,
    customer_name, address, email,
    CURRENT_TIMESTAMP() AS load_date,
    'ec_main' AS record_source
  FROM stg_customer
),
latest AS (
  SELECT hub_customer_hk, hash_diff
  FROM sat_customer_profile
  QUALIFY ROW_NUMBER() OVER (PARTITION BY hub_customer_hk ORDER BY load_date DESC) = 1
)
SELECT s.*
FROM source s
LEFT JOIN latest l USING (hub_customer_hk)
WHERE l.hash_diff IS NULL OR l.hash_diff != s.hash_diff;

連結する列の順番を変えると、値が同じでもハッシュが変わります。列の順番は設計のときに決めて、変えないルールにします。

dbtで書く場合は、HubやSatelliteの定型を関数のように呼べるパッケージのAutomateDV(旧名dbtvault)が使えます。公式ドキュメントでは、Snowflake・BigQuery・SQL Server・Databricks・Postgresに対応しています。Hubは次のように書けます。

-- models/silver/hub_customer.sql
{{ config(materialized='incremental') }}
{{ automate_dv.hub(
    src_pk='hub_customer_hk',
    src_nk='customer_bk',
    src_ldts='load_date',
    src_source='record_source',
    source_model='stg_customer'
) }}

手順5:Gold層に、BIが読む表を別に作る

マートの例元にする表用途
FACT_DAILY_ORDERHubとLinkとSatelliteを結合日次の売上ダッシュボード
DIM_CUSTOMER_LATESTSAT_CUSTOMER_PROFILEの最新行顧客の今の属性を引く
DIM_CUSTOMER_ASOFSatelliteの履歴を月末時点で切り出す「先月の資料の時点」の数字を再現する

冒頭の「先月の資料と数字が違う」問いには、3行目の表で答えられます。

Snowflakeで増分ロードを自動化するときの注意

SnowflakeのStream(表の変更を追跡する仕組み)とTask(定期実行の仕組み)を組み合わせると、Bronzeに届いた新しい行だけをHubに足せます。

CREATE STREAM stream_bronze_customer_for_hub ON TABLE bronze.customer;

CREATE TASK task_load_hub_customer
  WAREHOUSE = wh_vault_load
  SCHEDULE = '15 MINUTES'
  WHEN SYSTEM$STREAM_HAS_DATA('stream_bronze_customer_for_hub')
AS
  INSERT INTO silver.hub_customer
  SELECT DISTINCT
    MD5_BINARY(UPPER(TRIM(COALESCE(s.customer_id, '')))),
    s.customer_id,
    CURRENT_TIMESTAMP(),
    'ec_main'
  FROM stream_bronze_customer_for_hub s
  WHERE s.METADATA$ACTION = 'INSERT'
    AND NOT EXISTS (
      SELECT 1 FROM silver.hub_customer h
      WHERE h.hub_customer_hk = MD5_BINARY(UPPER(TRIM(COALESCE(s.customer_id, ''))))
    );

-- 作成直後のTaskは停止状態なので、動かすには再開が要る
ALTER TASK task_load_hub_customer RESUME;

詰まりやすい点を、公式ドキュメントの仕様と合わせて挙げます。

詰まる点仕様対策
HubとSatelliteで同じStreamを読むと、片方に変更が届かないStreamの読み取り位置は、DMLの中で使われたときに進む読み手ごとにStreamを分ける
Streamが使えなくなる元の表のデータ保持期間を過ぎるまで読まないと、Streamは古くなり再作成が要るTaskを止めたまま長期間放置しない
Taskを作ったのに動かない作成直後のTaskは停止状態ALTER TASK … RESUMEで再開する
長期の履歴をTime Travelで代用しようとするTime Travelの既定の保持は1日、Enterprise Edition以上でも最大90日長期の履歴はSatelliteで持つ

dbtやオーケストレーションツールで変換を管理している会社なら、同じ流れをそちらのジョブで組みます。ツールの選び方はオーケストレーションツールの選び方で扱っています。

つまずきやすい設計の誤り

誤り何が起きるか直し方
Hubのキーを連番で採番する並列ロードのたびに採番待ちが起きるビジネスキーのハッシュをキーにする
明細の数量や単価をLinkに直接書く関係と属性が混ざり、属性の変更でLinkを触ることになるLinkにひもづく別のSatelliteに分ける
全属性を1つのSatelliteに詰める変更の多い属性につられて行が増え、機密の分離もできない変わる頻度と機密度で分ける
Gold層を作らず、BIから直接読ませるJOINが多く、「Data Vaultは重い」と判断されて終わるBI向けのマートを必ず別に作る

既存のスタースキーマは残し、1つの業務から並走させる

動いているスタースキーマを全面的に作り替えると、移行のあいだ二重に管理することになります。変更が多く、履歴に困っている業務を1つ選び、既存の表の隣で並走させます。

月やること終わったと言える目安
1か月目冒頭の4つの問いで対象業務を1つ選び、HubとLinkの候補を表にする業務担当者と、Hubにする名詞の一覧に合意している
2か月目Hub・Link・Satelliteを作り、毎日ロードする既存の表と件数が一致している
3か月目Gold層に、既存と同じ形のマートをData Vaultから作るBIの数字が、既存のマートと一致している
4か月目業務側の変更を1件、Satelliteを足す形で取り込む既存の表を触らずに対応できた
5か月目以降次の業務に広げるかを決める変更1件あたりの作業時間が、以前より短くなっている

5か月目の判断には、作業時間の比較を使えます。たとえば月給50万円のエンジニアなら、会社の総コストは給与の約3倍で年1,800万円、年1,900時間で割ると時給は約9,500円です。属性の変更1件にスタースキーマで40時間かかっていたものが8時間になれば、1件あたり約30万円の差になります。時間はどちらも説明のための例です。自社の改修記録で置き換えて計算してください。

よくある質問(FAQ)

スタースキーマにSCD Type 2を入れれば、Data Vaultは要らないのでは?

ソースが1つで、履歴を持ちたい属性が限られているなら、SCD Type 2で足ります。ソースが複数あって統合が要る場合や、属性の追加が頻繁な場合、機密度ごとに履歴を分けたい場合は、SCD Type 2では手作業が増えます。Satelliteで履歴の持ち方がそろっていれば、要件が増えても同じ手順で対応できます。

Data Vaultを入れると、BIの表示は遅くなりませんか?

BIから直接Data Vaultを読むと、JOINが多いので遅くなります。BIにはGold層のマートを読ませ、Data Vaultは統合と履歴の元として使います。Gold層への変換が重くなってきたら、ある時点のSatelliteの行をまとめたPIT(Point-in-Time)テーブルや、よく使う関係をまとめたBridgeテーブルを、計測したうえで足します。

dbtでData Vaultを実装できますか?

できます。AutomateDVのようなパッケージを使えば、Hub・Link・Satelliteの定型を呼び出す形で書けます。同じ形の表を何十本も並べる手法なので、定型を部品にしておく効果は大きく出ます。

ハッシュキーはBINARYと文字列のどちらで持つべきですか?

Snowflakeでは、MD5_BINARYが返す16バイトのBINARYのまま持つと、文字列で持つより容量が小さくなります。人が目で確かめる場面だけ、16進の文字列に変換して表示します。

最初から全部の業務をData Vaultにするべきですか?

最初から全部を移すのは避けます。変更が多く、過去の数字の再現に困っている業務を1つ選び、既存のスタースキーマの隣で並走させます。業務側の変更を1件、Satelliteを足すだけで取り込めたことを確かめてから、次の業務に広げます。

出典

「先月の資料と数字が違う」が組織変更のたびに起きているなら、どの業務からData Vaultで並走させるかの切り出しから一緒に考えます。初回相談(30分・無料)で一緒に整理できます。