Как посчитать churn в SQL

Проверь себя · 1/3разбор после ответа
Как поведёт себя фильтр WHERE country IN ('RU', 'KZ') для строки, где country = NULL?

Что такое churn

Churn rate — доля пользователей, переставших быть активными (в подписке — отменивших её) за период:

Churn = ушедшие за период / активные в начале периода × 100%

Связан с удержанием напрямую: Churn + Retention = 100%.

В статье — готовые SQL-запросы:

  • базовый monthly customer churn;
  • behavioral churn для продуктов без подписки;
  • voluntary vs involuntary;
  • когортный churn;
  • churn по тарифам;
  • dollar churn;
  • ошибки и ориентиры.

1. Monthly customer churn для подписки

WITH active_start AS (
    SELECT COUNT(*) AS active
    FROM subscriptions
    WHERE started_at <= '2026-03-01'
      AND (ended_at IS NULL OR ended_at > '2026-03-01')
),
churned AS (
    SELECT COUNT(*) AS cnt
    FROM subscriptions
    WHERE ended_at >= '2026-03-01'
      AND ended_at <  '2026-04-01'
)
SELECT
    c.cnt::NUMERIC / NULLIF(a.active, 0) AS churn_rate
FROM active_start a, churned c;

Знаменатель — «активные в начале периода», не «все, кто когда-либо был». Это принципиально: смешав эти две базы, получите сильно заниженный churn.

2. Behavioral churn (для продуктов без подписки)

Пользователь считается ушедшим, если давно не заходил:

SELECT COUNT(DISTINCT u.user_id) AS churned
FROM users u
WHERE EXISTS (
    SELECT 1 FROM events e WHERE e.user_id = u.user_id
)
AND NOT EXISTS (
    SELECT 1
    FROM events e
    WHERE e.user_id  = u.user_id
      AND e.event_at >= CURRENT_DATE - INTERVAL '30 day'
);

Окно «30 дней» — пример. Конкретный срок зависит от продукта: для ежедневного сервиса 30 дней это уже долго, для сезонного продукта — может быть нормально.

3. Voluntary vs involuntary

-- voluntary: пользователь сам отменил подписку
SELECT COUNT(*) AS voluntary
FROM subscriptions
WHERE ended_at >= '2026-04-01'
  AND ended_at <  '2026-05-01'
  AND cancel_reason = 'user_cancel';

-- involuntary: не прошла оплата, карта истекла и т.п.
SELECT COUNT(*) AS involuntary
FROM subscriptions
WHERE ended_at >= '2026-04-01'
  AND ended_at <  '2026-05-01'
  AND cancel_reason IN ('payment_failed', 'card_expired');

Это разные проблемы: voluntary обычно решается продуктом и коммуникацией, involuntary — технически: retry-логика, обновление карт, SMS-напоминания.

4. Когортный churn

WITH cohort AS (
    SELECT
        user_id,
        DATE_TRUNC('month', started_at)::DATE AS cohort_month
    FROM subscriptions
),
ended AS (
    SELECT
        user_id,
        DATE_TRUNC('month', ended_at)::DATE AS end_month
    FROM subscriptions
    WHERE ended_at IS NOT NULL
)
SELECT
    c.cohort_month,
    COUNT(*) AS cohort_size,
    COUNT(e.user_id) FILTER (
        WHERE e.end_month <= c.cohort_month + INTERVAL '3 month'
    ) AS churned_in_3m,
    COUNT(e.user_id) FILTER (
        WHERE e.end_month <= c.cohort_month + INTERVAL '3 month'
    )::NUMERIC
    / NULLIF(COUNT(*), 0) AS churn_3m
FROM cohort c
LEFT JOIN ended e USING (user_id)
GROUP BY c.cohort_month
ORDER BY c.cohort_month;

Когортный разрез нужен, чтобы увидеть, улучшается ли retention в свежих когортах. Если у новых когорт churn ниже — значит, продукт становится лучше.

5. Churn по тарифам

SELECT
    tariff,
    COUNT(*)                                          AS cohort_size,
    COUNT(*) FILTER (WHERE ended_at IS NOT NULL)      AS churned,
    COUNT(*) FILTER (WHERE ended_at IS NOT NULL)::NUMERIC
        / NULLIF(COUNT(*), 0)                         AS overall_churn
FROM subscriptions
WHERE started_at >= CURRENT_DATE - INTERVAL '6 month'
GROUP BY tariff
ORDER BY overall_churn DESC;

Часто у бюджетных тарифов churn в разы выше, чем у дорогих — это надо знать до того, как принимать решения о скидках.

Закрепи формулу churn в Карьернике
Запомнить надолго — 5 коротких сессий с задачами на эту тему. Бесплатно
Тренировать churn в Telegram

6. Dollar churn (revenue churn)

Потерянная выручка, а не пользователи:

WITH mrr_start AS (
    SELECT SUM(monthly_price) AS mrr_start
    FROM subscriptions
    WHERE started_at <= '2026-04-01'
      AND (ended_at IS NULL OR ended_at > '2026-04-01')
),
mrr_churned AS (
    SELECT SUM(monthly_price) AS mrr_lost
    FROM subscriptions
    WHERE ended_at >= '2026-04-01'
      AND ended_at <  '2026-05-01'
)
SELECT
    mrr_churned.mrr_lost,
    mrr_start.mrr_start,
    mrr_churned.mrr_lost::NUMERIC / NULLIF(mrr_start.mrr_start, 0) AS dollar_churn_rate
FROM mrr_start, mrr_churned;

Если уходят в основном дорогие тарифы, dollar churn существенно выше пользовательского — и именно он определяет финансовый эффект.

7. Причины отмены

SELECT
    cancel_reason,
    COUNT(*)                                               AS cnt,
    ROUND(100.0 * COUNT(*) / SUM(COUNT(*)) OVER (), 2)     AS pct
FROM subscriptions
WHERE ended_at >= CURRENT_DATE - INTERVAL '30 day'
GROUP BY cancel_reason
ORDER BY cnt DESC;

Если причины собираются через exit-анкету, отчёт сразу показывает, куда бить в первую очередь: цены, UX, продуктовая ценность или технические проблемы.

Частые ошибки

1. Неправильная база знаменателя

ушедшие / всех когда-либо занижает churn. Правильная база — активные в начале периода.

2. Смешивать voluntary и involuntary

Это разные процессы и разные команды должны ими заниматься. Разделяйте.

3. Не учитывать триалы

Пользователи, не сконвертировавшиеся из trial, часто попадают в «ушедших». Это искажает картину paid churn — их стоит выделять отдельно.

4. Annual churn ≠ 12 × monthly

Правильная формула пересчёта при условии одинакового monthly churn:

annual_churn = 1 − (1 − monthly_churn)^12

Например, monthly 5% — это annual ≈ 46%, а не 60%.

Churn в Python

subs = pd.read_sql('SELECT * FROM subscriptions', conn)

active_start = (
    (subs['started_at'] <= '2026-04-01') &
    ((subs['ended_at'].isna()) | (subs['ended_at'] > '2026-04-01'))
).sum()

churned = (
    (subs['ended_at'] >= '2026-04-01') &
    (subs['ended_at'] <  '2026-05-01')
).sum()

churn_rate = churned / max(active_start, 1)

Связанные темы

FAQ

Customer или revenue churn?

Customer — доля ушедших пользователей. Revenue — доля потерянной выручки. В отчёте лучше показывать оба: customer даёт понимание людей, revenue — финансовый эффект.

Что важнее — retention или churn?

Одна и та же метрика в разных проекциях. Выбирайте ту, с которой удобнее команде: retention звучит как позитивная метрика (её растят), churn — как проблема (её снижают).

Когда пользователь считается churned в продукте без подписки?

Зависит от продукта. Для приложения с ежедневным использованием — 14–30 дней без активности. Для редко используемых сервисов срок может быть 90 дней и больше. Главное — зафиксировать определение и не менять его задним числом.

Как снижать churn?

Короткий чек-лист: retry-логика для involuntary, улучшение onboarding и ценности продукта для voluntary, коммуникации «при риске» на основе модели предсказания, retention-акции для ценных клиентов.