RFM — самый практичный метод сегментации клиентов в e-commerce / SaaS. Делишь юзеров на 3-5 групп по каждой из трёх метрик: Recency (как давно покупал), Frequency (как часто), Monetary (сколько денег принёс). Получаешь матрицу 3×3×3 = 27 сегментов, и для каждого — свою маркетинговую стратегию.
Этот гайд — пошаговый разбор как посчитать RFM в чистом SQL. Без Python, без ML — только агрегации и оконные функции. Готовый запрос можно копировать.
Зачем RFM-сегментация
«Лояльные клиенты» и «неактивные» — слишком грубо. RFM даёт operational segments:
- Champions — RFM 555 (недавно купили, часто покупают, много тратят) → программа лояльности
- Sleeping Champions — RFM 1-2/4-5/4-5 (давно не покупали, но раньше часто и много) → реактивация
- At Risk — высокий M, средний F, R падает → срочно вернуть
- Lost — низкий R/F → возможно не возвращать (CAC > LTV)
Каждому сегменту — свой email/push/скидка. Эффективность маркетинга растёт в 2-3 раза.
Шаг 1: подготовь данные
Тебе нужна таблица заказов с user_id, order_date, amount.
-- Пример таблицы
CREATE TABLE orders (
order_id INT,
user_id INT,
order_date DATE,
amount NUMERIC(10,2)
);
Шаг 2: посчитай R, F, M для каждого юзера
WITH user_rfm AS (
SELECT
user_id,
MAX(order_date) AS last_order_date,
CURRENT_DATE - MAX(order_date) AS recency_days,
COUNT(*) AS frequency,
SUM(amount) AS monetary
FROM orders
WHERE order_date >= CURRENT_DATE - INTERVAL '365 days'
GROUP BY user_id
)
SELECT * FROM user_rfm LIMIT 5;
Получишь таблицу:
| user_id | last_order_date | recency_days | frequency | monetary |
|---|---|---|---|---|
| 1 | 2026-04-20 | 5 | 12 | 45000 |
| 2 | 2026-02-15 | 69 | 3 | 8200 |
| 3 | 2026-04-22 | 3 | 1 | 2500 |
Шаг 3: разбей на квинтили (5 групп)
Используй NTILE(5) чтобы каждый юзер получил оценку 1-5 по каждой метрике:
WITH user_rfm AS (...),
scored AS (
SELECT
user_id,
recency_days,
frequency,
monetary,
-- R: меньше дней = лучше → инвертируем (5 = свежий, 1 = давно)
6 - NTILE(5) OVER (ORDER BY recency_days ASC) AS r_score,
NTILE(5) OVER (ORDER BY frequency ASC) AS f_score,
NTILE(5) OVER (ORDER BY monetary ASC) AS m_score
FROM user_rfm
)
SELECT * FROM scored LIMIT 5;
| user_id | r_score | f_score | m_score |
|---|---|---|---|
| 1 | 5 | 5 | 5 |
| 2 | 2 | 3 | 3 |
| 3 | 5 | 1 | 2 |
Шаг 4: собери RFM-код и сегмент
WITH user_rfm AS (...),
scored AS (...)
SELECT
user_id,
r_score, f_score, m_score,
CONCAT(r_score::text, f_score::text, m_score::text) AS rfm_code,
CASE
-- Champions: топ-сегмент
WHEN r_score >= 4 AND f_score >= 4 AND m_score >= 4 THEN 'Champions'
WHEN r_score >= 4 AND f_score >= 3 THEN 'Loyal'
WHEN r_score >= 4 AND f_score <= 2 THEN 'New'
WHEN r_score <= 2 AND f_score >= 4 AND m_score >= 4 THEN 'At Risk'
WHEN r_score <= 2 AND f_score >= 3 AND m_score >= 3 THEN 'Sleeping'
WHEN r_score = 3 AND f_score = 3 THEN 'Need Attention'
WHEN r_score <= 2 AND f_score <= 2 THEN 'Lost'
ELSE 'Other'
END AS segment
FROM scored;
Готовый полный запрос
WITH user_rfm AS (
SELECT
user_id,
CURRENT_DATE - MAX(order_date) AS recency_days,
COUNT(*) AS frequency,
SUM(amount) AS monetary
FROM orders
WHERE order_date >= CURRENT_DATE - INTERVAL '365 days'
GROUP BY user_id
),
scored AS (
SELECT
user_id,
recency_days, frequency, monetary,
6 - NTILE(5) OVER (ORDER BY recency_days ASC) AS r_score,
NTILE(5) OVER (ORDER BY frequency ASC) AS f_score,
NTILE(5) OVER (ORDER BY monetary ASC) AS m_score
FROM user_rfm
)
SELECT
user_id,
recency_days,
frequency,
monetary,
CONCAT(r_score::text, f_score::text, m_score::text) AS rfm_code,
CASE
WHEN r_score >= 4 AND f_score >= 4 AND m_score >= 4 THEN 'Champions'
WHEN r_score >= 4 AND f_score >= 3 THEN 'Loyal'
WHEN r_score >= 4 AND f_score <= 2 THEN 'New'
WHEN r_score <= 2 AND f_score >= 4 AND m_score >= 4 THEN 'At Risk'
WHEN r_score <= 2 AND f_score >= 3 AND m_score >= 3 THEN 'Sleeping'
WHEN r_score = 3 AND f_score = 3 THEN 'Need Attention'
WHEN r_score <= 2 AND f_score <= 2 THEN 'Lost'
ELSE 'Other'
END AS segment
FROM scored
ORDER BY rfm_code DESC;
ClickHouse-вариант
В CH NTILE есть, но нет INTERVAL. Замени:
WHERE order_date >= today() - 365
И r_score:
6 - ntile(5) OVER (ORDER BY recency_days ASC) AS r_score
Остальное идентично.
Стратегии работы с сегментами
| Сегмент | % юзеров | Стратегия |
|---|---|---|
| Champions (~5%) | топ-юзеры | Программа лояльности, эксклюзивные офферы, ранний доступ к новинкам |
| Loyal (~10%) | стабильные | Up-selling (более дорогие товары), cross-sell |
| New (~15%) | недавно пришли | Welcome-серия, second-purchase incentive |
| At Risk (~10%) | падает R | Срочно реактивировать: персональная скидка, опрос «что не так» |
| Sleeping (~15%) | давно нет | Реактивационная кампания: «мы скучали», большая скидка |
| Need Attention (~10%) | средние | Engagement-контент, образовательные емейлы |
| Lost (~25%) | в прошлом | Один последний оффер, потом — исключить из активного маркетинга (CAC > LTV) |
| Other (~10%) | смешанные | По ситуации |
Подводные камни
Малая выборка
Если у тебя 200 юзеров — NTILE(5) даст по 40 в каждом квинтиле. Для статистически значимых сегментов нужно минимум 5000 юзеров на сегмент.
Bot-traffic / тестовые юзеры
Часто Champions — это тестовый юзер компании или бот-сборщик данных. Фильтруй до RFM:
WHERE user_id NOT IN (SELECT user_id FROM internal_users)
Сильно skewed monetary
1% юзеров приносят 50% выручки → NTILE даст странную картину. Используй log(monetary) вместо raw, или вместо квинтилей — фиксированные пороги (50K / 200K / 1M).
RFM не измеряет engagement
Если юзер каждый день заходит, но ничего не покупает — RFM покажет его как «Lost». А он — твой лучший pre-buyer. Дополняй RFM behavioural метриками.
Связанные материалы
- SQL-тренажёр: задачи на агрегацию — 325 задач с проверкой
- Когортный анализ retention в SQL — другой важный метод
- Оконные функции SQL: гайд — без NTILE/ROW_NUMBER не сделать RFM
- Метрики продукта — формулы LTV, CAC, ARPU
- Продуктовые кейсы — кейсы с RFM-сегментацией
Открой SQL-тренажёр, найди задачу с тегом «сегментация» или «retention» — попробуй применить RFM. Через 2 часа практики научишься делать это в любой схеме данных.