本文へスキップ
martechfarmDATA & TECHNOLOGY DOCS

すべての記事のタイトル・本文を検索

すべての記事

127 件

タイトルを選ぶと本文が開きます

連続購買をSQLで分析する方法|何ヶ月買い続けているかを判定するクエリ

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

Treasure Data SQLでN日前・N日後の日付を計算する方法

Treasure Data SQLで日付差分を計算する方法|日数・時間数の出し方

Treasure Data SQLで先月・今月の月初月末を取得する方法

Treasure Data SQLで今日の日付・現在時刻を取得する方法

Treasure Data SQLで日付文字列をUNIXタイムに変換する方法

Treasure Data SQLでUNIXタイムを日付文字列に変換する方法

Treasure Data SQLで整数8桁の日付をUNIXタイムに変換する方法

異常値・重複データをSQLで検知する方法|データ品質チェックの実務クエリ

クーポン施策の効果をSQLで検証する方法|割引の売上貢献を正しく測る

メルマガの購買転換率をSQLで計測する方法|配信効果を可視化するクエリ

前年同月比・前月比をSQLで計算する方法|売上推移を同時比較するクエリ

バスケット分析をSQLで行う方法|一緒に買われる商品を見つけるクエリ

ABC分析をSQLで実装する方法|売上上位80%の商品を抽出するクエリ

コホート分析をSQLで行う方法|月次リテンション率を可視化するクエリ

新規・既存・復活・休眠顧客をSQLで自動判定する方法

LTVをSQLで計算する方法|顧客生涯価値を正確に出す実務クエリ

RFM分析をSQLだけで実装する方法|顧客ランクを自動分類する実務クエリ

Treasure Data CJOでジャーニーが更新されない原因と確認ポイント

Treasure Data CJOの処理完了を確認する方法|ジョブ状態と実行結果の見方

ポケモン殿堂入りに学ぶ出口戦略|キャリアと事業のゴール設計

ポケモン交換に学ぶ市場原理|需要と供給・ネットワーク効果のビジネス論

ミュウに学ぶ希少性マーケティング|限定性が価値を生む経済学

本文
記事一覧へ ↑

新規・既存・復活・休眠顧客をSQLで自動判定する方法


「先月と比べて顧客はどう動いたか」を毎月追えているか

前回(Vol.2)でLTVを計算した。今回のテーマはもう少し時間軸を意識したものだ。

毎月の施策を振り返る会議で、こんな質問が飛んでくることがある。

「今月の新規顧客は何人?」 「復活購買した休眠顧客はどれくらい?」 「既存顧客のうち、今月購買しなかった人は?」

こういった質問に答えるためには、顧客を「今月の購買状況」と「過去の購買履歴」の2軸で分類するクエリが必要だ。

今回作るのは毎月の顧客ステータスを自動で分類するクエリだ。手動でExcelを集計する作業を完全に置き換えることができる。


4つのステータス定義

まずは用語を明確に定義しよう。定義が曖昧なままクエリを書くと、チームごとに数字が変わってしまい「どれが正しいのか」という不毛な議論が始まる。

今回はこの定義を使う。

ステータス定義
新規今月はじめて購買した顧客(今月が生涯初購買)
既存先月以前にも購買歴があり、今月も購買した顧客
復活過去に購買歴があるが、一定期間(今回は90日)購買がなかったあと、今月購買した顧客
休眠今月は購買しなかった顧客(直近90日以内に購買歴があるもの)

「復活」の定義にある「90日間購買なし」という閾値は自社のビジネスモデルに合わせて変えてほしい。定期購買型なら30日、年1〜2回のギフト需要が多いなら180日のほうが実態に合うこともある。


使用するテーブル

引き続き orders テーブルを使う。

-- orders テーブル
-- order_id     : 注文ID
-- customer_id  : 顧客ID
-- order_date   : 注文日(DATE型)
-- total_amount : 注文金額
-- status       : 'completed' / 'cancelled' など

今回のクエリは「2024年12月を分析月」として設定する。実運用では変数や DATE_TRUNC('month', CURRENT_DATE) に置き換えると自動化できる。


STEP 1 ― 今月購買した顧客を抽出する

まずは「今月(2024年12月)に購買した顧客」のリストを作る。

-- STEP1: 今月購買顧客の抽出

WITH this_month_buyers AS (
    SELECT DISTINCT
        customer_id
    FROM orders
    WHERE
        status = 'completed'
        AND order_date >= DATE '2024-12-01'
        AND order_date <  DATE '2025-01-01'  -- 翌月1日未満にすることで月末を正確に含む
)
SELECT *
FROM this_month_buyers;

DISTINCT を使っているのは、同一顧客が今月に複数回購買していても1行にまとめるためだ。ステータス分類では「購買したかしていないか」だけが重要で、購買回数は問わない。

月末の扱いに注意
order_date <= '2024-12-31' と書いても多くの場合は動くが、DATE型のカラムに時刻情報が含まれる場合(TIMESTAMP型など)に意図しない除外が発生することがある。< '2025-01-01' と書くほうが安全で明確だ。


STEP 2 ― 各顧客の「今月より前の購買履歴」を整理する

次に、今月より前の購買履歴をまとめる。具体的には「過去の購買があるか」と「最終購買日はいつか」の2点だ。

-- STEP2: 今月以前の購買履歴を集計

WITH past_purchase_history AS (
    SELECT
        customer_id,
        MAX(order_date)  AS last_purchase_date  -- 今月より前の最終購買日
    FROM orders
    WHERE
        status = 'completed'
        AND order_date < DATE '2024-12-01'  -- 今月より前のみ
    GROUP BY customer_id
)
SELECT *
FROM past_purchase_history;

このCTEで「今月前に一度でも買ったことがある顧客」と「その最終購買日」が分かる。逆に言えば、このCTEに存在しない顧客は「今月が生涯初購買の新規顧客候補」だ。


STEP 3 ― 2つを組み合わせてステータスを判定する

STEP1(今月購買者)とSTEP2(過去購買履歴)をLEFT JOINで結合し、CASE WHENでステータスを振り分ける。

LEFT JOINを使うのは「今月購買した顧客の中に、過去購買がない人(=新規)」が存在するためだ。INNER JOINにすると新規顧客が消えてしまう。

-- STEP3: 今月購買顧客のステータス判定

WITH this_month_buyers AS (
    SELECT DISTINCT
        customer_id
    FROM orders
    WHERE
        status = 'completed'
        AND order_date >= DATE '2024-12-01'
        AND order_date <  DATE '2025-01-01'
),
past_purchase_history AS (
    SELECT
        customer_id,
        MAX(order_date)  AS last_purchase_date
    FROM orders
    WHERE
        status = 'completed'
        AND order_date < DATE '2024-12-01'
    GROUP BY customer_id
),
buyer_status AS (
    SELECT
        t.customer_id,
        p.last_purchase_date,

        -- ステータス判定
        CASE
            -- 過去購買なし → 新規
            WHEN p.last_purchase_date IS NULL
                THEN '新規'
            -- 過去購買あり、かつ最終購買が90日以上前 → 復活
            WHEN DATE_DIFF('day', p.last_purchase_date, DATE '2024-12-01') >= 90
                THEN '復活'
            -- 過去購買あり、最終購買が90日以内 → 既存
            ELSE
                '既存'
        END  AS customer_status
    FROM this_month_buyers  t
    LEFT JOIN past_purchase_history  p  ON t.customer_id = p.customer_id
)
SELECT *
FROM buyer_status
ORDER BY customer_status;

出力イメージ

customer_idlast_purchase_datecustomer_status
C0012024-10-15既存
C008NULL新規
C0192024-07-03復活
C0312024-11-28既存
C045NULL新規
………

STEP 4 ― 「今月購買しなかった顧客」=休眠フラグを追加する

ここまでは「今月購買した顧客」の分類だった。マーケティング施策では「今月購買しなかったが、直近90日以内に購買歴がある顧客(=今まさに休眠しかけている顧客)」の把握も同じくらい重要だ。

-- STEP4: 休眠顧客の抽出(完成版・全ステータス統合)

WITH this_month_buyers AS (
    SELECT DISTINCT
        customer_id
    FROM orders
    WHERE
        status = 'completed'
        AND order_date >= DATE '2024-12-01'
        AND order_date <  DATE '2025-01-01'
),
past_purchase_history AS (
    SELECT
        customer_id,
        MAX(order_date)  AS last_purchase_date
    FROM orders
    WHERE
        status = 'completed'
        AND order_date < DATE '2024-12-01'
    GROUP BY customer_id
),

-- 今月購買者のステータス
buyer_status AS (
    SELECT
        t.customer_id,
        p.last_purchase_date,
        CASE
            WHEN p.last_purchase_date IS NULL                                              THEN '新規'
            WHEN DATE_DIFF('day', p.last_purchase_date, DATE '2024-12-01') >= 90          THEN '復活'
            ELSE                                                                                '既存'
        END  AS customer_status
    FROM this_month_buyers  t
    LEFT JOIN past_purchase_history  p  ON t.customer_id = p.customer_id
),

-- 今月購買していない顧客の中で、直近90日以内に購買歴がある顧客=休眠
dormant_customers AS (
    SELECT
        p.customer_id,
        p.last_purchase_date,
        '休眠'  AS customer_status
    FROM past_purchase_history  p
    -- 今月購買者でないこと
    WHERE p.customer_id NOT IN (SELECT customer_id FROM this_month_buyers)
    -- 直近90日以内に購買歴があること(古すぎる顧客は対象外)
    AND DATE_DIFF('day', p.last_purchase_date, DATE '2024-12-01') < 90
)

-- 全ステータスをUNION ALLで統合
SELECT customer_id, last_purchase_date, customer_status FROM buyer_status
UNION ALL
SELECT customer_id, last_purchase_date, customer_status FROM dormant_customers
ORDER BY customer_status, last_purchase_date DESC;

NOT IN の落とし穴 NOT IN (サブクエリ) は、サブクエリの結果にNULLが1件でも含まれると全件がFALSEになるという有名な罠がある。this_month_buyers の customer_id はNOT NULLのはずだが、念のため NOT EXISTS や LEFT JOIN ... WHERE IS NULL への書き換えも知っておくと安全だ。


STEP 5 ― ステータス別サマリで月次レポートを仕上げる

最後に、経営会議やレポートに貼り付けられる月次サマリを出す。

-- STEP5: 月次ステータス別サマリ

WITH this_month_buyers AS (
    SELECT DISTINCT customer_id
    FROM orders
    WHERE status = 'completed'
    AND order_date >= DATE '2024-12-01'
    AND order_date <  DATE '2025-01-01'
),
this_month_sales AS (
    SELECT
        customer_id,
        SUM(total_amount)  AS monthly_amount
    FROM orders
    WHERE status = 'completed'
    AND order_date >= DATE '2024-12-01'
    AND order_date <  DATE '2025-01-01'
    GROUP BY customer_id
),
past_purchase_history AS (
    SELECT
        customer_id,
        MAX(order_date)  AS last_purchase_date
    FROM orders
    WHERE status = 'completed'
    AND order_date < DATE '2024-12-01'
    GROUP BY customer_id
),
buyer_status AS (
    SELECT
        t.customer_id,
        p.last_purchase_date,
        CASE
            WHEN p.last_purchase_date IS NULL                                          THEN '新規'
            WHEN DATE_DIFF('day', p.last_purchase_date, DATE '2024-12-01') >= 90      THEN '復活'
            ELSE                                                                            '既存'
        END  AS customer_status
    FROM this_month_buyers  t
    LEFT JOIN past_purchase_history  p  ON t.customer_id = p.customer_id
),
dormant_customers AS (
    SELECT
        p.customer_id,
        p.last_purchase_date,
        '休眠'  AS customer_status
    FROM past_purchase_history  p
    WHERE p.customer_id NOT IN (SELECT customer_id FROM this_month_buyers)
    AND DATE_DIFF('day', p.last_purchase_date, DATE '2024-12-01') < 90
),
all_status AS (
    SELECT customer_id, customer_status FROM buyer_status
    UNION ALL
    SELECT customer_id, customer_status FROM dormant_customers
)
SELECT
    a.customer_status,
    COUNT(a.customer_id)                AS customer_count,
    COALESCE(SUM(s.monthly_amount), 0)  AS total_sales,
    COALESCE(
        ROUND(AVG(s.monthly_amount), 0)
    , 0)                                AS avg_sales_per_customer
FROM all_status           a
LEFT JOIN this_month_sales  s  ON a.customer_id = s.customer_id
GROUP BY a.customer_status
ORDER BY
    CASE a.customer_status
        WHEN '新規' THEN 1
        WHEN '既存' THEN 2
        WHEN '復活' THEN 3
        WHEN '休眠' THEN 4
    END;

出力イメージ

customer_statuscustomer_counttotal_salesavg_sales_per_customer
新規2843,412,80012,017
既存89118,234,00020,465
復活1031,876,40018,218
休眠41200

このサマリから読み取れることは多い。

  • 既存顧客の平均購買額(20,465円)が新規(12,017円)より7割近く高い。新規獲得より既存育成に投資する根拠になる
  • 復活顧客の平均購買額(18,218円)が新規より高い。「休眠からの復活」は費用対効果が良い施策であることが数字で示されている
  • 休眠顧客412人をこのまま放置すると、来月には90日を超えて「深い休眠」に移行する顧客が出てくる。今月中にウィンバック施策を打てるかが重要だ

実務での運用ヒント

① 分析月をパラメータ化して毎月使い回す

クエリの中に DATE '2024-12-01' と DATE '2025-01-01' が複数出てくる。これを毎月書き換えるのは手間だし、修正漏れのリスクもある。Treasure DataのdigdagワークフローならYAML変数として ${target_month} のように外出しにできる。

# digdag の例
_export:
  target_month: "2024-12-01"
  next_month: "2025-01-01"

② 「深い休眠」と「浅い休眠」を分ける

今回は90日未購買を「休眠」と定義したが、実務では180日超・365日超などさらに細分化して施策を変えるケースも多い。CASE WHENを拡張するだけで対応できる。

CASE
    WHEN DATE_DIFF('day', last_purchase_date, DATE '2024-12-01') < 90   THEN '休眠(浅)'
    WHEN DATE_DIFF('day', last_purchase_date, DATE '2024-12-01') < 365  THEN '休眠(中)'
    ELSE                                                                      '休眠(深)'
END

③ 先月のステータスと今月を比較する

「先月は新規だったが今月も購買した=2回目購買定着者」という追跡ができると、2回目購買率という重要KPIが計算できる。このクエリを月次でテーブルに書き出し、自己JOINすることで状態遷移の分析が可能になる。これは次のテーマ(Vol.4:コホート分析)につながる話でもある。


まとめ

今回のクエリの骨格は非常にシンプルだ。

  1. 「今月買った人」のリストと「過去の購買履歴」を別々に作る
  2. LEFT JOINと last_purchase_date IS NULL の組み合わせで「新規」を特定する
  3. 最終購買からの日数で「復活」と「既存」を区別する
  4. 今月不在かつ直近90日以内の購買がある人を「休眠」として別途抽出する
  5. UNION ALLで統合してサマリを出す

このクエリをBIツールに繋いで月次ダッシュボードにすれば、毎月の顧客状態変化が自動で可視化される。手でExcelを集計する作業を完全に置き換えられる。


MarTech Farmをもっと見る

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

続きを読む