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

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


RFMを「なんとなく知っている」から「自分で計算できる」へ

RFM分析という言葉は、マーケティングに携わる人なら一度は耳にしたことがあるはず。

でも「RFMって何ですか?」と聞かれたとき、こう答えられる人がどれだけいるだろう。

「Recency(最終購買日)、Frequency(購買頻度)、Monetary(購買金額)の3軸で顧客をスコアリングし、ランク別に分けてアプローチを変える手法です。SQLのウィンドウ関数と NTILE を使えば、外部ツールなしで計算できます」

この記事を最後まで読めば、上の一文を自分の言葉で説明できるようになる。

そして何より、自分のデータベースに対してそのまま実行できるクエリを手に入れられる。


RFMとは何か ― 3つの軸の意味をきちんと理解する

RFMは3つの英単語の頭文字。

指標英語意味高いほど
RRecency最終購買からの経過日数最近買っている(≒ Rの値は小さいほど良い)
FFrequency一定期間内の購買回数よく買っている
MMonetary一定期間内の購買総額たくさん使っている

「最近(R)、何度も(F)、たくさん(M)買っている顧客が優良顧客」というのが基本的な考え方だ。

なぜRFMが重要なのか?

全顧客に同じメルマガを送る時代は終わった。

コスト効率とエンゲージメント率を両立するには、顧客を正しくセグメントして適切なメッセージを届ける必要がある。

RFMを使うと、たとえばこんな分類ができる。

  • R高・F高・M高 → ロイヤル顧客。VIP特典・先行案内を届ける
  • R低・F高・M高 → 離脱危機の優良顧客。ウィンバック施策を最優先で打つ
  • R高・F低・M低 → 新規顧客。2回目購買を促すステップメールを送る
  • R低・F低・M低 → 休眠顧客。大規模割引か、コスト考慮してリストから外す

このセグメントを毎月自動で更新できるのが、SQLによるRFM計算の最大の価値だ。


使用するテーブル

今回は2つのテーブルを使う。

-- 注文テーブル(orders)
-- order_id     : 注文ID(主キー)
-- customer_id  : 顧客ID
-- order_date   : 注文日(DATE型)
-- total_amount : 注文金額(税抜)
-- status       : 注文ステータス('completed' / 'cancelled' など)
order_idcustomer_idorder_datetotal_amountstatus
1001C0012024-09-105800completed
1002C0022024-10-1512400completed
1003C0012024-11-023200completed
1004C0032023-12-258800completed
1005C0022024-11-204600completed
1006C0012024-12-019200completed

実務では status = 'completed' の行だけを対象にする。キャンセル注文を含めて集計すると、F・Mの数値が実態より高くなってしまうためだ。これは見落としがちなポイント。


STEP 1 ― 顧客ごとにR・F・Mの生の値を計算する

まずは「スコアリング」の前に、各顧客のR・F・Mを実数で出す。

-- STEP1: 顧客別R・F・M実値の計算
-- 分析基準日:2024-12-31 とする(実務では CURRENT_DATE に置き換える)

WITH rfm_raw AS (
    SELECT
        customer_id,

        -- R: 最終購買日から基準日までの経過日数(小さいほど最近)
        DATE_DIFF('day',
            MAX(order_date),
            DATE '2024-12-31'
        )                                   AS recency_days,

        -- F: 購買回数
        COUNT(order_id)                     AS frequency,

        -- M: 購買総額
        SUM(total_amount)                   AS monetary
    FROM orders
    WHERE
        status = 'completed'
        AND order_date >= DATE '2024-01-01'  -- 集計対象期間(直近1年)
        AND order_date <= DATE '2024-12-31'
    GROUP BY customer_id
)
SELECT *
FROM rfm_raw
ORDER BY monetary DESC;

DATE_DIFF について Treasure Data(Presto)では date_diff('day', start, end) の形式を使う。MySQLなら DATEDIFF(end, start)、BigQueryなら DATE_DIFF(end, start, DAY) と書き方が変わる。

出力イメージ

customer_idrecency_daysfrequencymonetary
C00130318,200
C00241217,000
C00337118,800
C00415538,400

C003は371日前にしか買っていない(Rが悪い)、C004は5回買っている(Fが良い)というのが読み取れる。


STEP 2 ― NTILEでスコアリングする

生の数値をそのまま使っても良いが、RFM分析では一般的に1〜5のスコアに変換してから使う。

スコア変換に使うのが NTILE() 関数だ。

NTILE(5) OVER (ORDER BY ...) は「全顧客を5等分して1〜5の番号を振る」という意味だ。

注意点が1つある。Rだけは「小さい値=良い」なので、ORDER BY recency_days DESC(降順)にしてスコアを振る必要がある。F・Mは大きい値が良いので昇順でよい。

-- STEP2: R・F・MスコアをNTILEで付与

WITH rfm_raw AS (
    SELECT
        customer_id,
        DATE_DIFF('day', MAX(order_date), DATE '2024-12-31')  AS recency_days,
        COUNT(order_id)                                        AS frequency,
        SUM(total_amount)                                      AS monetary
    FROM orders
    WHERE
        status = 'completed'
        AND order_date BETWEEN DATE '2024-01-01' AND DATE '2024-12-31'
    GROUP BY customer_id
),
rfm_scored AS (
    SELECT
        customer_id,
        recency_days,
        frequency,
        monetary,

        -- Rスコア:経過日数が少ない(最近買った)ほど高スコア
        NTILE(5) OVER (ORDER BY recency_days DESC)   AS r_score,

        -- Fスコア:購買回数が多いほど高スコア
        NTILE(5) OVER (ORDER BY frequency ASC)       AS f_score,

        -- Mスコア:購買金額が高いほど高スコア
        NTILE(5) OVER (ORDER BY monetary ASC)        AS m_score
    FROM rfm_raw
)
SELECT *
FROM rfm_scored
ORDER BY r_score DESC, f_score DESC, m_score DESC;

出力イメージ

customer_idrecency_daysfrequencymonetaryr_scoref_scorem_score
C00415538,400555
C00130318,200444
C00241217,000433
C00337118,800112

C004はすべて5点満点のロイヤル顧客だ。C003はRもFも最低スコアで、明らかに休眠状態にある。


STEP 3 ― スコアからセグメント名をつける

スコアの組み合わせに意味のある名前をつける。ここは自社のビジネス定義に合わせてカスタマイズしてほしい。今回は代表的な6セグメントを定義する。

-- STEP3: セグメント名の付与(完成版)

WITH rfm_raw AS (
    SELECT
        customer_id,
        DATE_DIFF('day', MAX(order_date), DATE '2024-12-31')  AS recency_days,
        COUNT(order_id)                                        AS frequency,
        SUM(total_amount)                                      AS monetary
    FROM orders
    WHERE
        status = 'completed'
        AND order_date BETWEEN DATE '2024-01-01' AND DATE '2024-12-31'
    GROUP BY customer_id
),
rfm_scored AS (
    SELECT
        customer_id,
        recency_days,
        frequency,
        monetary,
        NTILE(5) OVER (ORDER BY recency_days DESC)  AS r_score,
        NTILE(5) OVER (ORDER BY frequency ASC)      AS f_score,
        NTILE(5) OVER (ORDER BY monetary ASC)       AS m_score
    FROM rfm_raw
),
rfm_segmented AS (
    SELECT
        customer_id,
        recency_days,
        frequency,
        monetary,
        r_score,
        f_score,
        m_score,
        CASE
            WHEN r_score >= 4 AND f_score >= 4 AND m_score >= 4
                THEN 'ロイヤル顧客'
            WHEN r_score >= 4 AND f_score >= 3
                THEN '優良顧客'
            WHEN r_score >= 3 AND f_score <= 2
                THEN '新規・見込み顧客'
            WHEN r_score <= 2 AND f_score >= 4
                THEN '離脱危機優良顧客'
            WHEN r_score <= 2 AND f_score >= 2
                THEN '休眠顧客'
            ELSE
                'その他'
        END  AS segment
    FROM rfm_scored
)
SELECT *
FROM rfm_segmented
ORDER BY r_score DESC, f_score DESC, m_score DESC;

STEP 4 ― セグメント別のサマリを出す

最後に、経営会議やレポートで使えるセグメント別集計も付け加えよう。

-- STEP4: セグメント別サマリ

WITH rfm_raw AS (
    SELECT
        customer_id,
        DATE_DIFF('day', MAX(order_date), DATE '2024-12-31')  AS recency_days,
        COUNT(order_id)                                        AS frequency,
        SUM(total_amount)                                      AS monetary
    FROM orders
    WHERE
        status = 'completed'
        AND order_date BETWEEN DATE '2024-01-01' AND DATE '2024-12-31'
    GROUP BY customer_id
),
rfm_scored AS (
    SELECT
        customer_id,
        recency_days,
        frequency,
        monetary,
        NTILE(5) OVER (ORDER BY recency_days DESC)  AS r_score,
        NTILE(5) OVER (ORDER BY frequency ASC)      AS f_score,
        NTILE(5) OVER (ORDER BY monetary ASC)       AS m_score
    FROM rfm_raw
),
rfm_segmented AS (
    SELECT
        customer_id,
        monetary,
        CASE
            WHEN r_score >= 4 AND f_score >= 4 AND m_score >= 4 THEN 'ロイヤル顧客'
            WHEN r_score >= 4 AND f_score >= 3                  THEN '優良顧客'
            WHEN r_score >= 3 AND f_score <= 2                  THEN '新規・見込み顧客'
            WHEN r_score <= 2 AND f_score >= 4                  THEN '離脱危機優良顧客'
            WHEN r_score <= 2 AND f_score >= 2                  THEN '休眠顧客'
            ELSE                                                      'その他'
        END  AS segment
    FROM rfm_scored
)
SELECT
    segment,
    COUNT(customer_id)          AS customer_count,
    ROUND(
        COUNT(customer_id) * 100.0 / SUM(COUNT(customer_id)) OVER ()
    , 1)                        AS customer_ratio,
    SUM(monetary)               AS total_monetary,
    ROUND(AVG(monetary), 0)     AS avg_monetary
FROM rfm_segmented
GROUP BY segment
ORDER BY total_monetary DESC;

出力イメージ

segmentcustomer_countcustomer_ratiototal_monetaryavg_monetary
ロイヤル顧客3128.2%48,230,000154,583
優良顧客89123.4%29,180,00032,749
新規・見込み顧客74319.5%8,920,00012,008
離脱危機優良顧客2245.9%19,640,00087,678
休眠顧客1,28433.7%9,870,0007,688
その他3549.3%3,210,0009,068

このサマリから、たとえばこんな洞察が生まれる。

  • 「離脱危機優良顧客」は全体の5.9%しかいないのに、1人あたりの平均購買額が87,678円と突出して高い。ここへのウィンバック投資対効果は大きい
  • 「休眠顧客」が33.7%と最多。全員にアプローチするよりも、M値が高い休眠顧客に絞って掘り起こすのが現実的だ

実務での運用ヒント

① 基準日は CURRENT_DATE にする

本番運用では DATE '2024-12-31'CURRENT_DATE(または TD_SCHEDULED_TIME() など環境固有の関数)に置き換えることで、毎月実行しても自動的に最新の日付を基準にできる。

② 集計期間の定義を統一する

「直近1年」「直近2年」など、チーム内でどの期間を使うかを先に決めておかないと、人によって出す数字がズレる。クエリのコメントに明記しておくのが鉄則だ。

③ Treasure DataでNTILEを使う際の注意

Treasure DataのPrestoエンジンでは NTILE() はそのまま動く。ただし顧客数が少ない場合、NTILEで5等分すると1スコアあたりの人数が偏る。顧客数が500人以下の場合は3分割(NTILE(3))を検討するとよい。

④ セグメント定義は聖域ではない

今回の CASE WHEN のセグメント定義はあくまで一例だ。自社のビジネスモデルや商材特性によって「Rが2週間以内なら高スコア」「年4回以上購買がF高」など、閾値をチューニングすることが重要だ。まずこのクエリで出してみて、実際の顧客リストを見ながら定義を調整していくのが現場の流儀だ。


次のアクション

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

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

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

この記事を書いた人:martechfarm

Treasure Data Top Lapidarist Award受賞。

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

実績・支援内容を見る →

MarTech Farmをもっと見る

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

続きを読む