Databricks Certified Data Engineer Associate 教科書
第3章 データ変換とモデリング(Data Transformation and Modeling, 22%)
🎯 この節の学習目標
MERGE を使った再実行に強いシルバー層更新を設計できるメダリオンアーキテクチャ(Medallion Architecture)は、レイクハウス上でデータを段階的に品質向上させていく設計パターンです。データをメダル(ブロンズ→シルバー→ゴールド)にたとえ、下流の層へ進むほど「よりきれいで、より使いやすい」データになります。この章(試験の22%を占めるデータ変換とモデリング)の土台となる考え方であり、第2章で取り込んだ生データがどこに着地し、どう加工されていくかの全体地図でもあります。
図:メダリオンアーキテクチャの3層構造とデータの流れ
| 層 | データの状態 | 主な処理 | 主な利用者 |
|---|---|---|---|
| ブロンズ | ソースの生データそのまま(raw)。取り込み時刻やソースファイル名などのメタデータを付与することが多い | 取り込み(追記中心)。変換は最小限 | データエンジニア(再処理・監査の起点) |
| シルバー | クリーニング済み。null 処理・型統一・重複排除・命名の標準化が済んだ状態 | フィルタ、クレンジング、結合、正規化 | データエンジニア、データサイエンティスト |
| ゴールド | ビジネス単位で集計・整形された提供用データ | 集計、ディメンショナルモデリング、KPI 算出 | BI/分析チーム、業務ユーザー |
📝 試験のポイント
「なぜブロンズに生のまま残すのか」が問われます。答えは再処理可能性(replayability)です。変換ロジックに誤りが見つかっても、ブロンズに生データが残っていれば、ソースに再アクセスせずシルバー以降を作り直せます。「ブロンズで積極的にクリーニングして容量を節約する」という選択肢は誤りです。また、各層の説明の対応付け(「クリーニング済みで分析の土台になる層はどれか」→シルバー)も定番です。
ここからは、ECサイトを模した架空の注文データ bronze.orders_raw を例に、ブロンズ→シルバーの変換を実装していきます。ブロンズには次のような「汚れ」が典型的に含まれます。
"1980"、"2026-08-01")" Tokyo "、"TOKYO")まずブロンズを読み込みます。PySpark と SQL のどちらでも同じテーブルを操作できます。
# PySpark:ブロンズテーブルを DataFrame として読む
df_bronze = spark.read.table("bronze.orders_raw")
df_bronze.printSchema() # スキーマを確認して「汚れ」を把握する
-- SQL:同じテーブルをクエリで確認
SELECT * FROM bronze.orders_raw LIMIT 10;
DESCRIBE bronze.orders_raw;
null の扱いは「捨てる」「埋める」「代替値で補う」の3択です。どの列の null がビジネス上許容できないかを先に決めてから選びます。
| 手法 | PySpark | SQL 相当 | 使いどころ |
|---|---|---|---|
| 捨てる | df.dropna(subset=[...]) | WHERE col IS NOT NULL | キー列(注文IDなど)が null の行は分析に使えないので除外 |
| 固定値で埋める | df.fillna(値, subset=[...]) | COALESCE(col, 既定値) / IFNULL | 数量の null を 0、区分の null を "unknown" にするなど |
| 他列で補う | coalesce(col1, col2, ...)(関数) | COALESCE(col1, col2, ...) | 「配送先がなければ請求先を使う」のような優先順位付きの補完 |
💡 具体例:null 処理の3パターン
from pyspark.sql import functions as F
df = spark.read.table("bronze.orders_raw")
# (1) キー列が null の行は削除(subset で対象列を限定するのが実務の基本)
df = df.dropna(subset=["order_id", "customer_id"])
# (2) 固定値で埋める:数量の null は 0、地域の null は "unknown"
df = df.fillna({"quantity": 0, "region": "unknown"})
# (3) coalesce:配送先住所がなければ請求先住所で補完
df = df.withColumn(
"ship_address",
F.coalesce(F.col("ship_address"), F.col("billing_address"))
)
-- SQL で同じことを行う場合
SELECT
order_id,
customer_id,
COALESCE(quantity, 0) AS quantity,
COALESCE(region, 'unknown') AS region,
COALESCE(ship_address, billing_address) AS ship_address
FROM bronze.orders_raw
WHERE order_id IS NOT NULL
AND customer_id IS NOT NULL;
df.dropna() を引数なしで呼ぶと「いずれかの列が null の行をすべて削除」になり、意図せず大量の行が消えることがあります。subset で対象列を明示するのが安全です。
ブロンズでは JSON や CSV 由来の列が文字列型のまま入っていることが多く、シルバーでは正しい型に揃えるのが重要な仕事です。型が揃っていないと、比較・集計・結合のすべてで事故が起きます(例:文字列の "9" は "10" より大きいと判定される)。
💡 具体例:型変換の基本セット
from pyspark.sql import functions as F
df = (df
# 文字列 → 数値(try_cast:変換できない値は null になる)
.withColumn("amount", F.col("amount").try_cast("double"))
.withColumn("quantity", F.col("quantity").try_cast("int"))
# 文字列 → 日付/タイムスタンプ(フォーマットを明示)
.withColumn("order_date", F.to_date(F.col("order_date"), "yyyy-MM-dd"))
.withColumn("updated_at", F.to_timestamp(F.col("updated_at"),
"yyyy-MM-dd HH:mm:ss"))
)
-- SQL 版:TRY_CAST と to_date / to_timestamp
SELECT
TRY_CAST(amount AS DOUBLE) AS amount,
TRY_CAST(quantity AS INT) AS quantity,
to_date(order_date, 'yyyy-MM-dd') AS order_date,
to_timestamp(updated_at, 'yyyy-MM-dd HH:mm:ss') AS updated_at
FROM bronze.orders_raw;
Databricks Runtime 17.0 以降では ANSI モードが既定で有効になっており、cast は変換できない値(例:"abc" を数値へ)に対してエラー(CAST_INVALID_INPUT)で失敗します。不正値を null に落として処理を続けたい場合は try_cast(SQL では TRY_CAST(amount AS DOUBLE)、PySpark では F.col("amount").try_cast("double") や F.expr("try_cast(amount AS double)"))を使います。その場合も「try_cast 後に null が増えていないか」を確認する習慣が品質を守ります(検証クエリのパターンは 3-6 で扱います)。
📝 試験のポイント
to_date は日付(DATE 型)、to_timestamp は日時(TIMESTAMP 型)を返す、という対応を押さえてください。また「文字列の日付列を日付型に変換する最も適切な方法」を問う問題では、フォーマット文字列を指定した to_date(col, 'yyyy-MM-dd') のような選択肢が正解側になります。substring で切り出して連結し直すような選択肢は、動いたとしても標準的な方法ではありません。
文字列の表記ゆれは、結合キーの不一致や重複判定の失敗を招きます。シルバーに入れる前に代表的な整形を済ませます。
from pyspark.sql import functions as F
df = (df
.withColumn("city", F.trim(F.col("city"))) # 前後の空白を除去
.withColumn("city", F.lower(F.col("city"))) # 小文字に統一
.withColumn("email", F.trim(F.lower(F.col("email"))))
# 電話番号からハイフン等の記号を除去(正規表現)
.withColumn("phone", F.regexp_replace(F.col("phone"), "[^0-9]", ""))
)
-- SQL 版
SELECT
lower(trim(city)) AS city,
lower(trim(email)) AS email,
regexp_replace(phone, '[^0-9]', '') AS phone
FROM bronze.orders_raw;
ほかに upper(大文字化)、initcap(先頭大文字化)、lpad/rpad(桁揃え)なども整形でよく使います。
変換した DataFrame をシルバーへ書き込みます。最も単純なのは上書き・追記ですが、ジョブは失敗して再実行されるものという前提に立つと、「何回実行しても結果が同じになる」冪等な書き込みが品質と再現性の要になります。単純な append は再実行のたびに同じデータが二重に積まれてしまいます。
| 書き込み方式 | コード | 再実行したら |
|---|---|---|
| 追記(append) | df.write.mode("append").saveAsTable(...) | 重複が発生(冪等でない) |
| 全上書き(overwrite) | df.write.mode("overwrite").saveAsTable(...) | 結果は同じ(冪等)だが、全件書き直しでコスト大 |
| MERGE(upsert) | MERGE INTO ... WHEN MATCHED / NOT MATCHED | キー一致なら更新、なければ挿入。再実行しても重複しない(冪等) |
💡 具体例:MERGE による冪等なシルバー更新
-- 変換結果を一時ビューとして登録(PySpark 側)
-- df_clean.createOrReplaceTempView("orders_clean")
MERGE INTO silver.orders AS t
USING orders_clean AS s
ON t.order_id = s.order_id
WHEN MATCHED THEN
UPDATE SET *
WHEN NOT MATCHED THEN
INSERT *;
# PySpark(Delta API)で同じ MERGE を書く場合
from delta.tables import DeltaTable
silver = DeltaTable.forName(spark, "silver.orders")
(silver.alias("t")
.merge(df_clean.alias("s"), "t.order_id = s.order_id")
.whenMatchedUpdateAll()
.whenNotMatchedInsertAll()
.execute())
キー(ここでは order_id)が一致する行は最新の内容で更新され、存在しない行だけが挿入されます。同じ入力で何度実行してもテーブルの最終状態は変わらないため、リトライやバックフィル(過去分の再処理)に安全に対応できます。
📝 試験のポイント
「パイプラインが途中失敗して再実行したら行が重複した。どう設計すべきだったか」→ MERGE による upsert で冪等にするが正解の型です。なお MERGE では、ソース側の1行がターゲットの複数行に一致するのは問題ありませんが、ターゲットの1行にソースの複数行が一致するとエラーになります。MERGE の前にソース側の重複排除(3-4)が必要になる、という接続も意識しておきましょう。
✅ この節のまとめ
dropna(捨てる)、fillna(埋める)、coalesce(優先順位付き補完)を使い分ける。cast / try_cast、日付系は to_date / to_timestamp(フォーマット明示)。ANSI モード既定(DBR 17+)では cast の失敗はエラーになり、null に落としたい場合は try_cast を使う。trim・lower・regexp_replace などで表記ゆれを潰してからシルバーへ。MERGE INTO ... WHEN MATCHED / NOT MATCHED による upsert。問1. メダリオンアーキテクチャにおけるブロンズ層の説明として最も適切なものはどれか。
正解:B
ブロンズは生データの保存層で、変換ロジックの誤りが後で見つかってもソースに再アクセスせずやり直せる「再処理可能性」を担います。Aはゴールド層、Cはシルバー層の説明です。Dのようにブロンズ段階で列を削り込むと再処理の起点としての価値が失われるため、設計として不適切です。
問2. ブロンズテーブルの order_date 列は "2026-08-01" 形式の文字列である。シルバー層で DATE 型に標準化する方法として最も適切なものはどれか。
F.to_date(F.col("order_date"), "yyyy-MM-dd") で変換するF.to_timestamp(F.col("order_date")) で変換すれば DATE 型になるF.substring(F.col("order_date"), 1, 4) で年だけ取り出して保存する正解:A
フォーマットを明示した to_date が DATE 型への標準的な変換です。Bの to_timestamp が返すのは TIMESTAMP 型であり、DATE 型にはなりません。Cは情報を失うだけで型の標準化になっていません。Dは "yyyy-MM-dd" 形式なら偶然正しく並ぶ場合もありますが、日付演算(月の加算など)ができず、型を揃えるというシルバー層の目的に反します。
問3. 注文データの ship_address が null の場合は billing_address の値を使い、どちらも null なら "unknown" としたい。最も適切な式はどれか。
F.coalesce(F.col("ship_address"), F.col("billing_address"), F.lit("unknown"))df.dropna(subset=["ship_address"])df.fillna("unknown")F.when(F.col("ship_address") == "", F.col("billing_address"))正解:A
coalesce は左から順に評価し、最初の非 null 値を返すため、「優先順位付きの補完」をそのまま表現できます。Bは補完ではなく行の削除です。Cは全列の null を "unknown" で埋めてしまい、billing_address による補完という要件を満たしません。Dは空文字と null を混同しているうえ、otherwise がないため条件を満たさない行が null になってしまいます。
問4. ブロンズ→シルバーの日次ジョブが途中で失敗し、再実行したところシルバーテーブルに同じ注文が2行ずつ存在するようになった。今後の再発を防ぐ最も適切な設計はどれか。
mode("append") のまま、失敗時は手動で重複行を削除する運用にするMERGE INTO ... WHEN MATCHED THEN UPDATE / WHEN NOT MATCHED THEN INSERT に変更し、書き込みを冪等にするappend する正解:B
キーに基づく MERGE(upsert)なら、同じ入力で何度実行しても既存行は更新・新規行は挿入となり、重複は発生しません(冪等)。Aは手作業に依存し根本解決になりません。Cはジョブが失敗する現実を無視した運用であり、リトライできないパイプラインはかえって脆弱です。Dは実質的な全上書きを遠回りに行うもので、DROP と append の間に読み取りが入ると空のテーブルが見えるなど、新たな問題を生みます。