ストーリー
田
田中VPoE
データレイクの最大の課題を知っているか?
あなた
「データスワンプ(データの沼)」ですよね。何でもかんでもデータレイクに投入した結果、何がどこにあるかわからなくなる
あ
田
田中VPoE
その通り。Medallionアーキテクチャは、データを品質レベルに応じてBronze、Silver、Goldの3層に分けることで、この問題を解決する。Databricksが提唱したパターンで、レイクハウスの標準的な設計手法になりつつある
あなた
金属の品質になぞらえているんですね。Bronze(銅)が生データ、Gold(金)がビジネスで使える高品質データ
あ
田
田中VPoE
そうだ。各層の責務を明確にすることで、データの信頼性とトレーサビリティを確保する。データエンジニアにとっての「Clean Architecture」だと思ってくれ
Medallionアーキテクチャの3層
全体構造
Medallion Architecture:
┌──────────────────────────────────────────────────────────┐
│ │
│ ┌────────────┐ ┌────────────┐ ┌────────────┐ │
│ │ Bronze │ │ Silver │ │ Gold │ │
│ │ (Raw) │───→│ (Cleaned) │───→│ (Business) │ │
│ │ │ │ │ │ │ │
│ │ 生データ │ │ クレンジング │ │ ビジネス │ │
│ │ そのまま保存 │ │ 正規化 │ │ ロジック │ │
│ │ 追記のみ │ │ 型変換 │ │ 集計 │ │
│ └────────────┘ └────────────┘ └────────────┘ │
│ │
│ データソース ───→ 取り込み ───→ 変換 ───→ 提供 │
└──────────────────────────────────────────────────────────┘
各層の詳細
| 層 | 目的 | データの状態 | スキーマ | 消費者 |
|---|
| Bronze | 生データの忠実な保存 | ソースシステムのまま(JSON, CSV等) | スキーマオンリード | データエンジニア |
| Silver | クレンジングと正規化 | 型変換済み、重複排除、NULL処理済み | 明示的スキーマ | データアナリスト、データサイエンティスト |
| Gold | ビジネスロジックの適用 | 集計済み、KPI計算済み、マート化 | ビジネス定義に基づくスキーマ | BIツール、経営層、アプリケーション |
Bronze層の設計
設計原則
| 原則 | 説明 | 理由 |
|---|
| 忠実な保存 | ソースデータをそのまま保存 | 後から変換ロジックを変更できる |
| 追記のみ(Append-only) | 既存データを更新しない | データの監査証跡を保持 |
| メタデータの付与 | 取り込み日時、ソース名、バッチIDを付加 | トレーサビリティの確保 |
| パーティショニング | 日付でパーティション分割 | クエリ効率とコスト最適化 |
Bronze層のディレクトリ構造:
s3://data-lake/bronze/
├── orders/
│ ├── year=2025/
│ │ ├── month=01/
│ │ │ ├── day=15/
│ │ │ │ ├── orders_20250115_001.parquet
│ │ │ │ └── _metadata.json
│ │ │ └── day=16/
│ │ └── month=02/
│ └── _schema_evolution/
│ └── schema_v1.json
├── customers/
├── products/
└── events/
Bronze層のdbtモデル例
-- models/bronze/brz_orders.sql
-- Bronze: 生データの取り込み(最小限の変換のみ)
{{ config(
materialized='incremental',
unique_key='_ingestion_id',
partition_by={'field': '_ingested_at', 'data_type': 'date'}
) }}
SELECT
*,
_FILE_NAME AS _source_file,
CURRENT_TIMESTAMP() AS _ingested_at,
{{ generate_surrogate_key(['order_id', '_FILE_NAME']) }} AS _ingestion_id
FROM {{ source('raw', 'orders') }}
{% if is_incremental() %}
WHERE _ingested_at > (SELECT MAX(_ingested_at) FROM {{ this }})
{% endif %}
Silver層の設計
変換ルール
| 変換カテゴリ | 具体的な処理 | 例 |
|---|
| 型変換 | 文字列→適切な型 | ”2025-01-15” → DATE型 |
| NULL処理 | デフォルト値の設定、フラグ付与 | NULL → “未設定” |
| 重複排除 | 同一レコードの除去 | order_idで重複排除 |
| 正規化 | 値の統一 | ”東京都”/“東京” → “東京都” |
| バリデーション | 異常値の検出・除外 | 負の金額を除外 |
| リレーション | テーブル間の結合 | orders + customers |
-- models/silver/slv_orders.sql
-- Silver: クレンジングと正規化
{{ config(
materialized='incremental',
unique_key='order_id',
partition_by={'field': 'order_date', 'data_type': 'date'}
) }}
WITH deduplicated AS (
SELECT
*,
ROW_NUMBER() OVER (
PARTITION BY order_id
ORDER BY _ingested_at DESC
) AS _row_num
FROM {{ ref('brz_orders') }}
{% if is_incremental() %}
WHERE _ingested_at > (SELECT MAX(_updated_at) FROM {{ this }})
{% endif %}
),
cleaned AS (
SELECT
CAST(order_id AS INT64) AS order_id,
CAST(customer_id AS INT64) AS customer_id,
CAST(order_date AS DATE) AS order_date,
CAST(total_amount AS NUMERIC) AS total_amount,
UPPER(TRIM(status)) AS status,
COALESCE(currency, 'JPY') AS currency,
_ingested_at AS _updated_at
FROM deduplicated
WHERE _row_num = 1
AND CAST(total_amount AS NUMERIC) >= 0 -- 負の金額を除外
AND order_id IS NOT NULL
)
SELECT * FROM cleaned
Gold層の設計
ビジネスロジックの適用
| Gold層のパターン | 説明 | 消費者 |
|---|
| ファクトテーブル | ビジネスイベントの記録(売上、注文等) | BIツール |
| ディメンションテーブル | マスターデータ(顧客、商品等) | BIツール |
| メトリクスマート | KPI計算済みの集計テーブル | ダッシュボード |
| ワイドテーブル | 分析用にファクトとディメンションを結合 | データサイエンティスト |
-- models/gold/gld_daily_revenue.sql
-- Gold: 日次売上サマリー
{{ config(
materialized='table',
partition_by={'field': 'revenue_date', 'data_type': 'date'}
) }}
SELECT
o.order_date AS revenue_date,
p.product_category,
c.customer_segment,
COUNT(DISTINCT o.order_id) AS order_count,
COUNT(DISTINCT o.customer_id) AS unique_customers,
SUM(o.total_amount) AS gross_revenue,
SUM(o.total_amount) - SUM(COALESCE(r.refund_amount, 0)) AS net_revenue,
AVG(o.total_amount) AS avg_order_value
FROM {{ ref('slv_orders') }} o
LEFT JOIN {{ ref('slv_customers') }} c ON o.customer_id = c.customer_id
LEFT JOIN {{ ref('slv_products') }} p ON o.product_id = p.product_id
LEFT JOIN {{ ref('slv_refunds') }} r ON o.order_id = r.order_id
WHERE o.status = 'COMPLETED'
GROUP BY 1, 2, 3
層間のデータ品質
各層の品質チェック
| 層 | チェック内容 | ツール |
|---|
| Bronze → Silver | スキーマ整合性、NULL率、レコード数の整合 | Great Expectations, dbt tests |
| Silver → Gold | ビジネスルール検証、集計値の整合、外部データとの突合 | dbt tests, custom assertions |
# models/silver/slv_orders.yml
# dbt テスト定義
models:
- name: slv_orders
description: "クレンジング済み注文データ"
columns:
- name: order_id
tests:
- not_null
- unique
- name: total_amount
tests:
- not_null
- dbt_utils.expression_is_true:
expression: ">= 0"
- name: status
tests:
- accepted_values:
values: ['PENDING', 'COMPLETED', 'CANCELLED', 'REFUNDED']
- name: order_date
tests:
- not_null
- dbt_utils.expression_is_true:
expression: "<= CURRENT_DATE()"
「Medallionアーキテクチャの本質は、データの品質を段階的に向上させることだ。Bronze層で生データを失わず、Silver層で信頼性を確保し、Gold層でビジネス価値を提供する」 — 田中VPoE
まとめ
| ポイント | 内容 |
|---|
| Medallionアーキテクチャ | Bronze/Silver/Goldの3層でデータ品質を段階的に向上 |
| Bronze層 | 生データの忠実な保存、追記のみ、メタデータ付与 |
| Silver層 | クレンジング、正規化、重複排除、バリデーション |
| Gold層 | ビジネスロジック適用、KPI計算、マート化 |
チェックリスト
次のステップへ
次は「演習:データ戦略ドキュメントを作成しよう」です。Step 1で学んだデータ戦略、アーキテクチャパターン、Data Mesh、Medallionアーキテクチャの知識を統合し、実践的なドキュメントを作成します。
推定読了時間: 30分