JOIN は事故の温床。本記事では、JOIN キー設計の作法と、行数チェックでの検証パターンを扱います。
JOIN の事前確認 4 点
- 1カーディナリティ:1:1 / 1:N / N:N のどれか
- 2主キー検証:JOIN キーが各テーブルで unique か
- 3NULL の扱い:JOIN キーに NULL があると LEFT JOIN で欠損
- 4型の一致:string vs int の混在で JOIN がヒットしない
での自動検証
schema.yml で JOIN 結果を検証
YAML
models: - name: stg_orders_with_customers columns: - name: order_id tests: - unique # 重複防止(N:N JOIN事故検知) - not_null tests: - dbt_utils.equal_rowcount: compare_model: ref('stg_orders') # JOIN 前後で行数同じSCD Type 2 との関係
Slowly Changing Dimension の Type 2 で valid_from / valid_to を持つテーブルとの JOIN は、`event_date BETWEEN valid_from AND valid_to` で時点合わせが必須。
Python での JOIN 検証
JOIN 前に行数とカーディナリティを検証
Python
import pandas as pd
def validate_join_keys(left: pd.DataFrame, right: pd.DataFrame, on: str) -> dict: """JOIN 前のヘルスチェック""" report = {}
# 1. キーの NULL 率 report["left_key_null_pct"] = left[on].isna().mean() report["right_key_null_pct"] = right[on].isna().mean()
# 2. キーの一意性(主キーかどうか) report["left_unique"] = left[on].is_unique report["right_unique"] = right[on].is_unique
# 3. 型が一致するか report["dtype_match"] = left[on].dtype == right[on].dtype report["left_dtype"] = str(left[on].dtype) report["right_dtype"] = str(right[on].dtype)
# 4. 共通キー数(カーディナリティ) common = set(left[on].dropna()) & set(right[on].dropna()) report["common_keys"] = len(common) report["left_only"] = len(set(left[on].dropna()) - common) report["right_only"] = len(set(right[on].dropna()) - common)
return report
report = validate_join_keys(orders, customers, "user_id")for k, v in report.items(): print(f"{k:25}: {v}")
assert report["left_unique"] or report["right_unique"], "両方が N:N → 危険"assert report["dtype_match"], f"型不一致: {report['left_dtype']} vs {report['right_dtype']}"assert report["left_key_null_pct"] < 0.01, "left の JOIN キーに NULL 多い"JOIN 後の検証(行数の予期しない膨張)
Python
import pandas as pd
def safe_join(left: pd.DataFrame, right: pd.DataFrame, on: str, how: str = "left") -> pd.DataFrame: """JOIN 後に行数チェックする安全 JOIN""" n_before = len(left) result = left.merge(right, on=on, how=how) n_after = len(result)
# LEFT JOIN なら行数は変わらないはず if how == "left" and n_after != n_before: ratio = n_after / n_before if ratio > 1.5: raise ValueError( f"LEFT JOIN で行数が {n_before} → {n_after} ({ratio:.2f}x) " f"→ right テーブルのキーが unique でない可能性" ) return result
# 使用例merged = safe_join(orders, customers, on="user_id", how="left")print(f"OK: 行数 {len(merged):,}")SCD Type 2 との時点 JOIN
Python
import pandas as pd
# customers_history: user_id, name, valid_from, valid_to の SCD Type 2# orders: user_id, order_date
def temporal_join(orders, customers_history, key="user_id", date_col="order_date"): """注文時点での顧客情報を取得""" merged = orders.merge(customers_history, on=key, how="left") mask = ( (merged[date_col] >= merged["valid_from"]) & ((merged["valid_to"].isna()) | (merged[date_col] < merged["valid_to"])) ) return merged[mask]
result = temporal_join(orders, customers_history)# 各注文に「その時点の顧客名」を付与ふくふくの進め方
「JOIN で結果がおかしくなる」のご相談には、現状診断 → キー設計レビュー → dbt テスト整備を 1 週間で。
次回予告
EP.11 はカテゴリ変数のエンコーディング。One-Hot / Target / Hashing の使い分け。
この記事の感想を教えてください
あなたの 1 クリックで、本当にこの記事は更新されます。「もっと詳しく」「続編希望」が一定数集まった記事は、 ふくふくが 実際に内容を拡充したり続編記事を公開 します。 送信したリアクションはお使いのブラウザに記録され、再カウントされません。