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

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


「施策の効果」を本当に測れているか

「先月のメルマガ施策、効果ありましたか?」

この質問に対して、「今月の売上が先月より増えました」と答えるのは正確ではない。なぜなら季節変動や新規顧客の流入など、施策以外の要因が売上を動かしているかもしれないからだ。

施策の本当の効果を測るために必要なのがコホート分析だ。

コホートとは「同じ時期に同じ経験をした人のグループ」のことだ。マーケティングでは「同じ月に初めて購買した顧客グループ」をコホートとして定義することが多い。

このコホートを月ごとに追跡することで、「2024年1月に入会した顧客は3ヶ月後の購買継続率が何%か」「2024年7月入会コホートと比べて高いか低いか」が分かる。これがリテンション率の月次追跡だ。


コホート分析で何が分かるのか

具体的なイメージとして、完成した出力を先に見てもらおう。

コホート別リテンション率テーブル(完成イメージ)

初回購買月人数M+0M+1M+2M+3M+4M+5
2024-01312100%38%29%24%21%19%
2024-02284100%41%31%27%23%
2024-03298100%35%28%24%
2024-04341100%44%33%
2024-05276100%42%
2024-06309100%

M+0は初回購買月(定義上100%)、M+1はその翌月、M+2は2ヶ月後の購買継続率だ。

このテーブルから読み取れることがある。2024年4月コホートのM+1リテンションが44%と他の月より高い。もし4月に何か施策を打っていたなら、その施策が2回目購買の押し上げに効いた可能性がある。逆に2024年3月コホートのM+1が35%と低いなら、3月入会者への初期フォローに課題があったと仮説が立てられる。

これが「施策の効果を本当に測る」ということだ。


使用するテーブル

今回も orders テーブル1本で完結する。

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

STEP 1 ― 各顧客の「初回購買月」を特定する

コホート分析の起点は「その顧客がいつ初めて買ったか」だ。

-- STEP1: 顧客ごとの初回購買月を特定

WITH first_purchase AS (
    SELECT
        customer_id,
        -- 初回購買日
        MIN(order_date)  AS first_order_date,
        -- 初回購買月(年月だけ取り出す)
        DATE_TRUNC('month', MIN(order_date))  AS cohort_month
    FROM orders
    WHERE status = 'completed'
    GROUP BY customer_id
)
SELECT *
FROM first_purchase
ORDER BY cohort_month, customer_id;

出力イメージ

customer_idfirst_order_datecohort_month
C0012024-01-082024-01-01
C0022024-01-152024-01-01
C0082024-02-032024-02-01
C0122024-02-192024-02-01

DATE_TRUNC('month', date) は日付を月の初日に丸める関数だ。2024-01-082024-01-312024-01-01 になる。これで「2024年1月コホート」のグループが作れる。

Treasure Dataでの注意
Treasure DataのPrestoでは DATE_TRUNC('month', CAST(order_date AS DATE)) のようにCASTが必要な場合がある。order_date の型がTIMESTAMP型で格納されているケースでは、DATE型に変換してから使おう。


STEP 2 ― 各購買記録に「初回購買月からの経過月数」を付与する

次に、すべての購買記録に対して「初回購買月から何ヶ月後の購買か」を計算する。

-- STEP2: 購買記録に経過月数を付与

WITH first_purchase AS (
    SELECT
        customer_id,
        DATE_TRUNC('month', MIN(order_date))  AS cohort_month
    FROM orders
    WHERE status = 'completed'
    GROUP BY customer_id
),
all_purchases AS (
    SELECT
        o.customer_id,
        DATE_TRUNC('month', o.order_date)  AS purchase_month
    FROM orders  o
    WHERE o.status = 'completed'
),
cohort_data AS (
    SELECT
        f.cohort_month,
        a.purchase_month,
        a.customer_id,
        -- 経過月数: 購買月 - 初回購買月(月単位)
        DATE_DIFF('month', f.cohort_month, a.purchase_month)  AS month_number
    FROM all_purchases   a
    JOIN first_purchase  f  ON a.customer_id = f.customer_id
)
SELECT *
FROM cohort_data
ORDER BY cohort_month, customer_id, month_number;

出力イメージ

cohort_monthpurchase_monthcustomer_idmonth_number
2024-01-012024-01-01C0010
2024-01-012024-02-01C0011
2024-01-012024-04-01C0013
2024-01-012024-01-01C0020
2024-01-012024-06-01C0025

C001さんは1月(M+0)、2月(M+1)、4月(M+3)に購買している。3月(M+2)は購買しなかったが、4月には戻ってきている。こういう「飛び飛びの購買」も正確に記録できている。


STEP 3 ― コホート×月ごとのユニーク購買者数を集計する

同じ月に同じ顧客が複数回購買していても「その月に購買した」かどうかだけを見たいので、COUNT(DISTINCT customer_id) を使う。

-- STEP3: コホート×経過月ごとの購買者数

WITH first_purchase AS (
    SELECT
        customer_id,
        DATE_TRUNC('month', MIN(order_date))  AS cohort_month
    FROM orders
    WHERE status = 'completed'
    GROUP BY customer_id
),
all_purchases AS (
    SELECT
        o.customer_id,
        DATE_TRUNC('month', o.order_date)  AS purchase_month
    FROM orders  o
    WHERE o.status = 'completed'
),
cohort_data AS (
    SELECT DISTINCT  -- 同月内複数購買の重複を排除
        f.cohort_month,
        a.purchase_month,
        a.customer_id,
        DATE_DIFF('month', f.cohort_month, a.purchase_month)  AS month_number
    FROM all_purchases   a
    JOIN first_purchase  f  ON a.customer_id = f.customer_id
),
cohort_size AS (
    -- 各コホートの初月人数(M+0の人数)
    SELECT
        cohort_month,
        COUNT(DISTINCT customer_id)  AS cohort_customer_count
    FROM first_purchase
    GROUP BY cohort_month
),
cohort_monthly_buyers AS (
    -- コホート×経過月ごとの購買者数
    SELECT
        cohort_month,
        month_number,
        COUNT(DISTINCT customer_id)  AS buyers
    FROM cohort_data
    GROUP BY cohort_month, month_number
)
SELECT
    b.cohort_month,
    s.cohort_customer_count,
    b.month_number,
    b.buyers,
    -- リテンション率 = その月の購買者数 ÷ 初月の購買者数
    ROUND(b.buyers * 100.0 / s.cohort_customer_count, 1)  AS retention_rate
FROM cohort_monthly_buyers  b
JOIN cohort_size             s  ON b.cohort_month = s.cohort_month
ORDER BY b.cohort_month, b.month_number;

出力イメージ(縦長形式)

cohort_monthcohort_customer_countmonth_numberbuyersretention_rate
2024-01-013120312100.0
2024-01-01312111938.1
2024-01-0131229028.8
2024-01-0131237524.0
2024-02-012840284100.0
2024-02-01284111640.8

これで必要なデータはすべて揃った。縦長(ロング形式)になっているので、BIツール(Looker、Tableau、Metabaseなど)にそのまま食わせる場合はこの形式のままでよい。


STEP 4 ― ピボット形式に変換して「人間が読めるテーブル」にする

冒頭のイメージのような横並びの表にするには、CASE WHEN を使ったピボット(横展開)が必要だ。

-- STEP4: ピボット形式のコホートテーブル(完成版)

WITH first_purchase AS (
    SELECT
        customer_id,
        DATE_TRUNC('month', MIN(order_date))  AS cohort_month
    FROM orders
    WHERE status = 'completed'
    GROUP BY customer_id
),
all_purchases AS (
    SELECT DISTINCT
        o.customer_id,
        DATE_TRUNC('month', o.order_date)  AS purchase_month
    FROM orders  o
    WHERE o.status = 'completed'
),
cohort_data AS (
    SELECT
        f.cohort_month,
        a.customer_id,
        DATE_DIFF('month', f.cohort_month, a.purchase_month)  AS month_number
    FROM all_purchases   a
    JOIN first_purchase  f  ON a.customer_id = f.customer_id
),
cohort_size AS (
    SELECT
        cohort_month,
        COUNT(DISTINCT customer_id)  AS cohort_count
    FROM first_purchase
    GROUP BY cohort_month
),
cohort_monthly_buyers AS (
    SELECT
        cohort_month,
        month_number,
        COUNT(DISTINCT customer_id)  AS buyers
    FROM cohort_data
    GROUP BY cohort_month, month_number
),
retention AS (
    SELECT
        b.cohort_month,
        s.cohort_count,
        b.month_number,
        ROUND(b.buyers * 100.0 / s.cohort_count, 1)  AS retention_rate
    FROM cohort_monthly_buyers  b
    JOIN cohort_size             s  ON b.cohort_month = s.cohort_month
)
-- ピボット(M+0〜M+5まで横展開)
SELECT
    cohort_month,
    MAX(cohort_count)                                                            AS cohort_size,
    MAX(CASE WHEN month_number = 0 THEN retention_rate END)  AS "M+0",
    MAX(CASE WHEN month_number = 1 THEN retention_rate END)  AS "M+1",
    MAX(CASE WHEN month_number = 2 THEN retention_rate END)  AS "M+2",
    MAX(CASE WHEN month_number = 3 THEN retention_rate END)  AS "M+3",
    MAX(CASE WHEN month_number = 4 THEN retention_rate END)  AS "M+4",
    MAX(CASE WHEN month_number = 5 THEN retention_rate END)  AS "M+5"
FROM retention
GROUP BY cohort_month
ORDER BY cohort_month;

完成した出力

cohort_monthcohort_sizeM+0M+1M+2M+3M+4M+5
2024-01-01312100.038.128.824.021.219.1
2024-02-01284100.040.831.026.822.9NULL
2024-03-01298100.035.228.224.2NULLNULL
2024-04-01341100.044.033.1NULLNULLNULL
2024-05-01276100.042.0NULLNULLNULLNULL
2024-06-01309100.0NULLNULLNULLNULLNULL

NULLになっているセルは「まだその月が来ていないため測定不能」という意味だ。基準日が2024年7月末であれば、6月コホートのM+1はまだ集計できない。


このテーブルの読み方と分析の視点

① 列(縦)で読む:経過月ごとのリテンション推移

M+1列だけを縦に見ると、コホートごとの2回目購買率が並ぶ。4月コホートの44%が他の月と比べて明らかに高ければ、4月に行った施策(入会特典の変更、ステップメールの改善など)が効いた可能性が高い。

② 行(横)で読む:特定コホートの継続率の減衰カーブ

1月コホートの行を横に見ると 100% → 38% → 29% → 24% → 21% → 19% と減衰していく様子が見える。M+0からM+1にかけての落ち込みが最大(62ポイント減)で、その後は緩やかになっている。この「最初の1ヶ月で6割が離脱する」という事実は、初回購買後のフォローアップ施策の重要性を強く示唆している。

③ 斜め(対角線)で読む:同じ時期の購買者数の変化

M+1の列を斜め方向に見ると、ほぼ同じカレンダー月における継続率がコホートをまたいで比較できる。たとえば「2024年2月時点」のリテンションは、1月コホートのM+1(38%)に相当するが、これが翌年の2月に下がっていれば季節性ではなく顧客質の変化が起きている可能性がある。


実務での運用ヒント

① 追跡月数はビジネスモデルで決める

購買サイクルが短い消耗品通販なら M+6 〜 M+12 まで追うべきだ。逆に家電や家具のような高関与商材なら M+24 まで見ないと意味のある分析にならない。今回はM+5まで手書きしたが、追跡したい月数が増えた場合は縦長形式のままBIツールに渡すほうが現実的だ。

② 「購買有無」だけでなく「購買金額」のコホートも作れる

今回はリテンション率(購買した/しなかった)を測ったが、SUM(total_amount) を使えば「コホート別の月次売上貢献額」テーブルも同じ構造で作れる。リテンション率は高いが売上が伸びていないコホートは、アップセルの余地があることを示している。

③ Tableau・Looker Studio での可視化

縦長形式のSTEP3の出力を直接Tableauに接続し、行に cohort_month、列に month_number、色や値に retention_rate を置くだけでコホートヒートマップが完成する。Looker Studioでも同様に実現できる。ピボットはSQL側でやらなくてよい。


まとめ

今回のクエリの流れを振り返る。

  1. MIN(order_date)DATE_TRUNC('month', ...) で月単位に丸めて、各顧客のコホート月を特定する
  2. すべての購買記録にコホート月をJOINし、DATE_DIFF('month', ...)経過月数を計算する
  3. COUNT(DISTINCT customer_id) でコホート×経過月ごとの購買者数を集計し、初月人数で割ってリテンション率を出す
  4. CASE WHEN month_number = N で横展開(ピボット)して読みやすいテーブルにする

このテーブルが毎月自動更新される状態になれば、「先月と今月でリテンション率が何ポイント変わったか」「どのコホートから改善の兆しが見えるか」が定量的に語れるようになる。勘と経験だけのマーケティングから、データを起点とした意思決定へのシフトが、このクエリ1本から始まる。

次のアクション

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

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

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

この記事を書いた人:martechfarm

Treasure Data Top Lapidarist Award受賞。

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

実績・支援内容を見る →

MarTech Farmをもっと見る

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

続きを読む