「先月と比べて顧客はどう動いたか」を毎月追えているか
前回(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_id | last_purchase_date | customer_status |
|---|---|---|
| C001 | 2024-10-15 | 既存 |
| C008 | NULL | 新規 |
| C019 | 2024-07-03 | 復活 |
| C031 | 2024-11-28 | 既存 |
| C045 | NULL | 新規 |
| … | … | … |
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_status | customer_count | total_sales | avg_sales_per_customer |
|---|---|---|---|
| 新規 | 284 | 3,412,800 | 12,017 |
| 既存 | 891 | 18,234,000 | 20,465 |
| 復活 | 103 | 1,876,400 | 18,218 |
| 休眠 | 412 | 0 | 0 |
このサマリから読み取れることは多い。
- 既存顧客の平均購買額(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:コホート分析)につながる話でもある。
まとめ
今回のクエリの骨格は非常にシンプルだ。
- 「今月買った人」のリストと「過去の購買履歴」を別々に作る
- LEFT JOINと
last_purchase_date IS NULLの組み合わせで「新規」を特定する - 最終購買からの日数で「復活」と「既存」を区別する
- 今月不在かつ直近90日以内の購買がある人を「休眠」として別途抽出する
- UNION ALLで統合してサマリを出す
このクエリをBIツールに繋いで月次ダッシュボードにすれば、毎月の顧客状態変化が自動で可視化される。手でExcelを集計する作業を完全に置き換えられる。