「同じ人」が何人いるか、本当に分かっているか
通販データベースを長年運用していると、必ずぶつかる問題がある。
「田中太郎さん」が
C0012、C0481、C1024の3つのIDで登録されている。
原因はさまざまだ。昔のシステムと新システムで別IDが採番された。退会後に再登録した。ゲスト購買と会員購買が別IDになっている。メールアドレスを変えて再登録した。
この状態のまま分析を続けると何が起きるか。田中さんは「3人の新規顧客」としてカウントされ、LTVは3分の1に分散し、RFMスコアはバラバラになり、同じ人に3通のメルマガが届く。
顧客名寄せ(名寄せ)とは、これらを同一人物として統合する処理のことだ。データ基盤の品質を根本から左右する、最も重要かつ最も難しい作業の一つだ。
この記事では、SQLで実装できる名寄せのアプローチを「完全一致」「部分一致」「確率的マッチング」の3段階で解説する。完全に自動化できない部分もあるが、どこまでSQLで対処できるかの全体像を理解してほしい。
名寄せの3段階アプローチ
| 段階 | 手法 | 精度 | 自動化 |
|---|---|---|---|
| 1 | 完全一致マッチング | 高(誤検知なし) | 完全自動 |
| 2 | 部分一致マッチング | 中(表記ゆれに対応) | 半自動 |
| 3 | 確率的マッチング | 高(複数属性の組み合わせ) | 半自動(要目視確認) |
最初に完全一致で確実に同一と言える組を統合し、残ったグレーゾーンを部分一致・確率的マッチングで絞り込んでいく。
使用するテーブル
-- customers テーブル
-- customer_id : 顧客ID(主キー)
-- email : メールアドレス
-- phone : 電話番号(ハイフンありなし混在)
-- last_name : 姓
-- first_name : 名
-- birth_date : 生年月日
-- postal_code : 郵便番号
-- registered_at : 登録日時
-- channel : 登録チャネル現実のデータには表記ゆれが山ほどある。
電話番号は 090-1234-5678 だったり 09012345678 だったり。姓名は全角・半角混在、スペースの有無。これを前処理してから照合するのが名寄せの基本だ。
STEP 1 ― 完全一致マッチング(メールアドレス)
最も確実な名寄せキーはメールアドレスだ。完全一致であれば「同一人物」と高い確信を持って判断できる。
ただしメールアドレスも大文字小文字の違いや、前後の空白が混入している場合がある。正規化してから照合する。
-- STEP1: メールアドレスの完全一致グループを特定する
WITH normalized_email AS (
SELECT
customer_id,
registered_at,
-- 小文字化・前後空白除去で正規化
LOWER(TRIM(email)) AS normalized_email
FROM customers
WHERE email IS NOT NULL
),
email_groups AS (
SELECT
normalized_email,
COUNT(customer_id) AS id_count,
-- グループ内で最も古い登録日のIDを「代表ID」とする
MIN(CASE WHEN registered_at = MIN(registered_at) OVER (PARTITION BY normalized_email)
THEN customer_id END)
OVER (PARTITION BY normalized_email) AS representative_id,
-- すべての重複IDをカンマ区切りで並べる
ARRAY_JOIN(
ARRAY_AGG(customer_id ORDER BY registered_at ASC)
OVER (PARTITION BY normalized_email)
, ',') AS all_ids
FROM normalized_email
)
SELECT DISTINCT
normalized_email,
id_count,
representative_id,
all_ids
FROM email_groups
WHERE id_count > 1 -- 2件以上のIDが同じメアドを持つグループのみ
ORDER BY id_count DESC, normalized_email;出力イメージ
| normalized_email | id_count | representative_id | all_ids |
|---|---|---|---|
| tanaka.taro@example.com | 3 | C0012 | C0012,C0481,C1024 |
| yamada.hanako@gmail.com | 2 | C0089 | C0089,C0312 |
| suzuki@company.co.jp | 2 | C0201 | C0201,C0445 |
representative_id が「名寄せ後に使う統合ID」の候補だ。最も古い登録日のIDを代表にするのが一般的だが、最も購買金額が多いIDを代表にする設計もある。自社のシステム都合に合わせて変える。
STEP 2 ― 電話番号の正規化と完全一致マッチング
電話番号はハイフンの有無・全角半角など表記ゆれが多い。正規化してから照合する。
-- STEP2: 電話番号を正規化して重複グループを特定する
WITH normalized_phone AS (
SELECT
customer_id,
registered_at,
phone,
-- 数字以外をすべて除去(ハイフン・スペース・括弧など)
REGEXP_REPLACE(phone, '[^0-9]', '') AS normalized_phone
FROM customers
WHERE phone IS NOT NULL
AND phone <> ''
),
phone_groups AS (
SELECT
normalized_phone,
COUNT(DISTINCT customer_id) AS id_count,
MIN(customer_id) AS representative_id,
ARRAY_JOIN(
ARRAY_AGG(customer_id ORDER BY registered_at ASC)
, ',') AS all_ids
FROM normalized_phone
-- 桁数チェック:日本の電話番号は10〜11桁
WHERE LENGTH(normalized_phone) BETWEEN 10 AND 11
GROUP BY normalized_phone
HAVING COUNT(DISTINCT customer_id) > 1
)
SELECT *
FROM phone_groups
ORDER BY id_count DESC;
REGEXP_REPLACEについて
Treasure DataのPrestoではREGEXP_REPLACE(string, pattern, replacement)が使える。[^0-9]は「数字以外のすべての文字」にマッチする正規表現だ。これで090-1234-5678も09012345678も09012345678(全角)も、すべて09012345678に統一される。
STEP 3 ― 部分一致マッチング(姓名+生年月日)
メールアドレスも電話番号も変わってしまった顧客には、「姓名+生年月日」の組み合わせで照合する。表記ゆれへの対策として、姓名の全角半角を統一し、スペースを除去してから照合する。
-- STEP3: 姓名+生年月日の一致グループを特定する
WITH normalized_name AS (
SELECT
customer_id,
registered_at,
birth_date,
-- 姓名を結合・全角スペース除去・半角スペース除去
REGEXP_REPLACE(
last_name || first_name,
'\s', -- スペース(全角・半角)を除去
''
) AS normalized_fullname
FROM customers
WHERE last_name IS NOT NULL
AND first_name IS NOT NULL
AND birth_date IS NOT NULL
),
name_birth_groups AS (
SELECT
normalized_fullname,
birth_date,
COUNT(DISTINCT customer_id) AS id_count,
MIN(customer_id) AS representative_id,
ARRAY_JOIN(
ARRAY_AGG(customer_id ORDER BY registered_at ASC)
, ',') AS all_ids
FROM normalized_name
GROUP BY normalized_fullname, birth_date
HAVING COUNT(DISTINCT customer_id) > 1
)
SELECT *
FROM name_birth_groups
ORDER BY id_count DESC;姓名+生年月日での照合は同姓同名(例:田中一郎が2人)の誤マッチリスクがある。これは自動統合せず、目視確認リストとして出力する運用が安全だ。
STEP 4 ― 確率的マッチング(複数キーのスコアリング)
メール・電話・姓名の一部だけ一致する「グレーゾーン」の組を確率的に評価する。複数の一致項目にスコアを付け、合計スコアが閾値を超えたペアを「同一人物の可能性が高い」として候補に挙げる。
-- STEP4: 複数属性スコアリングによる確率的マッチング
WITH normalized AS (
SELECT
customer_id,
LOWER(TRIM(email)) AS norm_email,
REGEXP_REPLACE(phone, '[^0-9]', '') AS norm_phone,
REGEXP_REPLACE(last_name || first_name, '\s', '') AS norm_name,
birth_date,
postal_code
FROM customers
),
-- 全顧客ペアを生成(計算コストが高いため件数が多い場合は要注意)
candidate_pairs AS (
SELECT
a.customer_id AS id_a,
b.customer_id AS id_b,
-- 各属性の一致スコア(一致で加点)
CASE WHEN a.norm_email = b.norm_email
AND a.norm_email IS NOT NULL THEN 40 ELSE 0 END AS email_score,
CASE WHEN a.norm_phone = b.norm_phone
AND a.norm_phone IS NOT NULL THEN 30 ELSE 0 END AS phone_score,
CASE WHEN a.norm_name = b.norm_name
AND a.norm_name IS NOT NULL THEN 20 ELSE 0 END AS name_score,
CASE WHEN a.birth_date = b.birth_date
AND a.birth_date IS NOT NULL THEN 20 ELSE 0 END AS birth_score,
CASE WHEN a.postal_code = b.postal_code
AND a.postal_code IS NOT NULL THEN 10 ELSE 0 END AS postal_score
FROM normalized a
JOIN normalized b
ON a.customer_id < b.customer_id -- 重複ペアを防ぐ
)
SELECT
id_a,
id_b,
email_score,
phone_score,
name_score,
birth_score,
postal_score,
-- 合計スコア
(email_score + phone_score + name_score + birth_score + postal_score) AS total_score,
-- 判定
CASE
WHEN (email_score + phone_score + name_score + birth_score + postal_score) >= 60
THEN '同一人物の可能性が高い(要確認)'
WHEN (email_score + phone_score + name_score + birth_score + postal_score) >= 40
THEN '同一人物の可能性あり(要目視)'
ELSE
'別人の可能性が高い'
END AS match_judgment
FROM candidate_pairs
WHERE (email_score + phone_score + name_score + birth_score + postal_score) >= 40
ORDER BY total_score DESC;
出力イメージ
| id_a | id_b | email_score | phone_score | name_score | birth_score | total_score | match_judgment |
|---|---|---|---|---|---|---|---|
| C0012 | C1024 | 40 | 30 | 20 | 20 | 110 | 同一人物の可能性が高い(要確認) |
| C0089 | C0312 | 40 | 0 | 20 | 20 | 80 | 同一人物の可能性が高い(要確認) |
| C0201 | C0598 | 0 | 30 | 20 | 10 | 60 | 同一人物の可能性が高い(要確認) |
| C0310 | C0720 | 0 | 0 | 20 | 20 | 40 | 同一人物の可能性あり(要目視) |
スコア60以上を「高確率で同一人物」として自動統合候補に、40〜59を「目視確認リスト」に振り分ける設計だ。スコアの配点と閾値は自社データで試しながら調整してほしい。
STEP 5 ― 名寄せ結果を統合マッピングテーブルとして管理する
名寄せが完了したら、「旧ID → 代表ID」のマッピングテーブルを作る。このテーブルが存在することで、既存の分析クエリを変えることなく名寄せ後の視点で集計できる。
-- STEP5: 名寄せマッピングテーブルの作成
CREATE TABLE customer_id_mapping AS
-- メール一致グループ
WITH email_match AS (
SELECT
customer_id AS old_id,
FIRST_VALUE(customer_id) OVER (
PARTITION BY LOWER(TRIM(email))
ORDER BY registered_at ASC
) AS representative_id,
'email_match' AS match_type
FROM customers
WHERE email IS NOT NULL
),
-- 電話一致グループ(メール一致で既に統合されたIDを除く)
phone_match AS (
SELECT
c.customer_id AS old_id,
FIRST_VALUE(c.customer_id) OVER (
PARTITION BY REGEXP_REPLACE(c.phone, '[^0-9]', '')
ORDER BY c.registered_at ASC
) AS representative_id,
'phone_match' AS match_type
FROM customers c
WHERE c.phone IS NOT NULL
AND c.customer_id NOT IN (
SELECT old_id FROM email_match WHERE old_id <> representative_id
)
)
-- マッピングを統合(old_id = representative_id のものは名寄せ不要)
SELECT
old_id,
representative_id,
match_type,
old_id <> representative_id AS is_merged, -- 統合されたかどうか
CURRENT_DATE AS created_at
FROM email_match
WHERE old_id <> representative_id -- 代表ID以外の重複IDのみ出力
UNION ALL
SELECT
old_id,
representative_id,
match_type,
old_id <> representative_id,
CURRENT_DATE
FROM phone_match
WHERE old_id <> representative_id;このマッピングテーブルを使って既存クエリを書き換えるのは簡単だ。
-- 名寄せ後のLTV計算(マッピングテーブルを介して代表IDに集約する)
SELECT
COALESCE(m.representative_id, o.customer_id) AS unified_customer_id,
SUM(o.total_amount) AS total_ltv
FROM orders o
LEFT JOIN customer_id_mapping m ON o.customer_id = m.old_id
WHERE o.status = 'completed'
GROUP BY COALESCE(m.representative_id, o.customer_id)
ORDER BY total_ltv DESC;COALESCE(m.representative_id, o.customer_id) がポイントだ。マッピングテーブルに存在するIDは代表IDに置き換え、存在しないID(元々ユニークな顧客)はそのままにする。
実務での運用ヒント
① 自動統合は「メール完全一致」だけにする
電話番号・姓名の一致は「同姓同名の別人」や「家族が同じ電話番号で登録」のケースがあり得る。自動統合するのはメールアドレスの完全一致のみに留め、それ以外は目視確認リストとして担当者が判断する運用が安全だ。
② 全顧客ペアの総当たりは避ける
STEP4の確率的マッチングで「全顧客 × 全顧客」のペアを作ると、顧客100万人の場合は5,000億件のペアが生成される。実際には「同じ苗字」「同じ郵便番号」など1つ以上の属性が一致する組だけを候補に絞ってからスコアリングするか、専用の名寄せツール(PrivacyPlex、Reltio等)との併用を検討する。
③ 名寄せは「一度やれば終わり」ではない
新規顧客が日々登録されるため、名寄せは継続的なプロセスだ。週次・月次でマッピングテーブルを更新し、新たな重複を検知する運用を組み込む必要がある。
④ マッピングテーブルは削除しない
名寄せの根拠(どのIDがどのIDと統合されたか、理由は何か)は必ず残しておく。後から「この統合は間違いだった」と判明したとき、元に戻せるようにするためだ。
まとめ
今回の名寄せアプローチを振り返る。
- メールアドレスの完全一致(正規化後)で確実な重複グループを特定する
- 電話番号の完全一致(数字のみ抽出・正規化後)で次の重複グループを特定する
- 姓名+生年月日の一致で目視確認候補を抽出する
- 複数属性のスコアリングでグレーゾーンの「同一人物の可能性が高いペア」を炙り出す
- 結果をcustomer_id_mapping テーブルにまとめ、既存の分析クエリと
COALESCEで統合する
名寄せは「分析の精度を根本から決める作業」だ。どれだけ精緻なRFMを計算しても、同一顧客が3つのIDに分散していれば「購買頻度が3分の1に見える優良顧客」が生まれる。顧客理解の出発点として、名寄せの整備は避けて通れない。