本文へ移動
顧客分析SQL 更新日: 2026年5月2日 約22分で読めます

顧客名寄せをSQLで行う方法|重複IDを統合する実務クエリ


「同じ人」が何人いるか、本当に分かっているか

通販データベースを長年運用していると、必ずぶつかる問題がある。

「田中太郎さん」が C0012C0481C1024 の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_emailid_countrepresentative_idall_ids
tanaka.taro@example.com3C0012C0012,C0481,C1024
yamada.hanako@gmail.com2C0089C0089,C0312
suzuki@company.co.jp2C0201C0201,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-56780901234567809012345678(全角)も、すべて 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_aid_bemail_scorephone_scorename_scorebirth_scoretotal_scorematch_judgment
C0012C102440302020110同一人物の可能性が高い(要確認)
C0089C0312400202080同一人物の可能性が高い(要確認)
C0201C0598030201060同一人物の可能性が高い(要確認)
C0310C072000202040同一人物の可能性あり(要目視)

スコア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と統合されたか、理由は何か)は必ず残しておく。後から「この統合は間違いだった」と判明したとき、元に戻せるようにするためだ。


まとめ

今回の名寄せアプローチを振り返る。

  1. メールアドレスの完全一致(正規化後)で確実な重複グループを特定する
  2. 電話番号の完全一致(数字のみ抽出・正規化後)で次の重複グループを特定する
  3. 姓名+生年月日の一致で目視確認候補を抽出する
  4. 複数属性のスコアリングでグレーゾーンの「同一人物の可能性が高いペア」を炙り出す
  5. 結果をcustomer_id_mapping テーブルにまとめ、既存の分析クエリと COALESCE で統合する

名寄せは「分析の精度を根本から決める作業」だ。どれだけ精緻なRFMを計算しても、同一顧客が3つのIDに分散していれば「購買頻度が3分の1に見える優良顧客」が生まれる。顧客理解の出発点として、名寄せの整備は避けて通れない。


次のアクション

SQLやデータ活用を、手元で試しながら理解する

記事で読んだ考え方を、SQL練習場や関連カテゴリの記事でさらに深掘りできます。相談やご依頼もお問い合わせページから受け付けています。

SQL練習場で試す お問い合わせ

この記事を書いた人:martechfarm

Treasure Data Top Lapidarist Award受賞。

SQL / Digdag / Python / CDP設計 / CRM設計を横断し、企業のデータ活用を支援。

実績・支援内容を見る →

MarTech Farmをもっと見る

今すぐ購読し、続きを読んで、すべてのアーカイブにアクセスしましょう。

続きを読む