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

Проверь себя · 1/3разбор после ответа
Есть payments(user_id) и refunds(user_id). Нужно получить пользователей, у которых был платёж, но не было возврата. Какой вариант корректнее всего описывает задачу?

Зачем аналитику считать ARPU в SQL

ARPU — одна из базовых метрик продукта с монетизацией: от него зависит, что мы пишем в юнит-экономике, как выглядит LTV и что показываем инвесторам. В SQL формула простая, но ошибиться в знаменателе — привычное дело: кого именно считать «пользователем» (зарегистрированных, активных, платящих) и за какой период.

В этой статье — формулы, готовые запросы и декомпозиция через paying rate. Дальше — разрезы по сегментам, динамика и частые ошибки.

Формулы ARPU и ARPPU

ARPU (Average Revenue Per User) — выручка на пользователя:

ARPU = Revenue / Total Users

ARPPU (Average Revenue Per Paying User) — выручка на платящего:

ARPPU = Revenue / Paying Users

Связь двух метрик:

ARPU = Paying Rate × ARPPU

Полезно держать в голове: ARPU падает либо из-за снижения доли платящих, либо из-за падения среднего чека у платящих. Декомпозиция позволяет понять, где именно просела выручка.

Схема данных

В примерах используем три таблицы:

users         (user_id, registered_at, channel, platform)
orders        (user_id, amount, status, created_at)
events        (user_id, event, event_at)

1. ARPU за месяц

SELECT
    SUM(o.amount)::NUMERIC                                         AS revenue,
    COUNT(DISTINCT u.user_id)                                      AS total_users,
    SUM(o.amount)::NUMERIC / NULLIF(COUNT(DISTINCT u.user_id), 0)  AS arpu
FROM users u
LEFT JOIN orders o
       ON o.user_id = u.user_id
      AND o.status  = 'paid'
      AND o.created_at >= '2026-04-01'
      AND o.created_at <  '2026-05-01'
WHERE u.registered_at < '2026-05-01';

Два важных момента:

  • LEFT JOIN, чтобы пользователи без заказов остались в знаменателе.
  • NULLIF(..., 0) — чтобы запрос не падал, если в периоде нет пользователей.

2. ARPPU за месяц

Знаменатель — только платящие:

SELECT
    SUM(amount)::NUMERIC                                   AS revenue,
    COUNT(DISTINCT user_id)                                AS paying_users,
    SUM(amount)::NUMERIC / NULLIF(COUNT(DISTINCT user_id), 0) AS arppu
FROM orders
WHERE status = 'paid'
  AND created_at >= '2026-04-01'
  AND created_at <  '2026-05-01';

ARPPU почти всегда значительно выше ARPU — он измеряет средний чек платящих и не «размывается» нулями от бесплатной аудитории.

3. ARPU и ARPPU вместе — декомпозиция

Одним запросом видно обе метрики и paying rate:

WITH monthly AS (
    SELECT
        DATE_TRUNC('month', created_at)::DATE            AS month,
        COUNT(DISTINCT user_id)                           AS total_users,
        COUNT(DISTINCT user_id) FILTER (WHERE status = 'paid') AS paying_users,
        SUM(amount) FILTER (WHERE status = 'paid')        AS revenue
    FROM orders
    GROUP BY 1
)
SELECT
    month,
    revenue,
    total_users,
    paying_users,
    revenue::NUMERIC / NULLIF(total_users, 0)            AS arpu,
    paying_users::NUMERIC / NULLIF(total_users, 0)       AS paying_rate,
    revenue::NUMERIC / NULLIF(paying_users, 0)           AS arppu
FROM monthly
ORDER BY month;

Если ARPU упал, достаточно посмотреть на paying rate и ARPPU, чтобы понять, какая из двух сил потянула метрику вниз.

4. ARPU по каналам

SELECT
    u.channel,
    SUM(o.amount)::NUMERIC / NULLIF(COUNT(DISTINCT u.user_id), 0) AS arpu
FROM users u
LEFT JOIN orders o
       ON o.user_id = u.user_id
      AND o.status  = 'paid'
      AND o.created_at >= '2026-04-01'
      AND o.created_at <  '2026-05-01'
WHERE u.registered_at < '2026-04-01'
GROUP BY u.channel
ORDER BY arpu DESC;

Два типичных наблюдения: органика часто даёт более высокий ARPU, чем платный трафик, а длинные когорты ведут себя лучше свежих — поэтому при сравнении каналов полезно зафиксировать срок регистрации.

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

5. ARPU по платформам

SELECT
    u.platform,
    SUM(o.amount)::NUMERIC / NULLIF(COUNT(DISTINCT u.user_id), 0) AS arpu
FROM users u
LEFT JOIN orders o
       ON o.user_id = u.user_id
      AND o.status  = 'paid'
      AND o.created_at >= '2026-04-01'
      AND o.created_at <  '2026-05-01'
GROUP BY u.platform
ORDER BY arpu DESC;

6. ARPU по когорте

WITH cohort AS (
    SELECT
        user_id,
        DATE_TRUNC('month', MIN(created_at))::DATE AS cohort_month
    FROM orders
    WHERE status = 'paid'
    GROUP BY user_id
)
SELECT
    c.cohort_month,
    SUM(o.amount)::NUMERIC / NULLIF(COUNT(DISTINCT c.user_id), 0) AS arpu_cohort
FROM cohort c
LEFT JOIN orders o
       ON o.user_id = c.user_id
      AND o.status  = 'paid'
GROUP BY c.cohort_month
ORDER BY c.cohort_month;

Когортный ARPU показывает, как меняется выручка на «старого» пользователя: в здоровом продукте более старые когорты постепенно накапливают большую сумму, чем свежие.

7. MoM-динамика ARPU

WITH monthly_arpu AS (
    SELECT
        DATE_TRUNC('month', o.created_at)::DATE AS month,
        SUM(o.amount)::NUMERIC / NULLIF(COUNT(DISTINCT u.user_id), 0) AS arpu
    FROM orders o
    JOIN users u USING (user_id)
    WHERE o.status = 'paid'
    GROUP BY 1
)
SELECT
    month,
    arpu,
    LAG(arpu) OVER (ORDER BY month)                                AS prev_arpu,
    ROUND(100.0 * (arpu - LAG(arpu) OVER (ORDER BY month))
                / NULLIF(LAG(arpu) OVER (ORDER BY month), 0), 2)   AS mom_pct
FROM monthly_arpu
ORDER BY month;

Быстрая диагностика «упал / вырос на сколько» без необходимости переключаться в BI.

8. Net ARPU с учётом возвратов

SELECT
    DATE_TRUNC('month', o.created_at)::DATE AS month,
    SUM(o.amount - COALESCE(o.refund_amount, 0))::NUMERIC
    / NULLIF(COUNT(DISTINCT u.user_id), 0) AS net_arpu
FROM orders o
JOIN users u USING (user_id)
WHERE o.status = 'paid'
GROUP BY 1
ORDER BY 1;

Если в отчёте не вычесть возвраты, ARPU будет систематически завышен — особенно в категориях с высоким refund rate.

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

1. Целочисленное деление

В Postgres SUM(int) / COUNT(int) может сработать как целочисленное деление. Приводите одну из сторон к NUMERIC (::NUMERIC или 1.0 * ...).

2. Разный знаменатель — разная метрика

«Все зарегистрированные» ≠ «активные в месяце» ≠ «платящие». Один и тот же бизнес на одних и тех же данных даст три разных ARPU. Решите заранее, какой знаменатель входит в определение метрики, и зафиксируйте его в документации.

3. Mix-эффект

ARPU может упасть не потому, что что-то ухудшилось, а потому, что изменился состав трафика — например, пришёл поток новых пользователей с низким ARPU. Полезно смотреть ARPU в разрезе каналов и когорт, а не только общую цифру.

4. Возвраты и скидки

Если считать SUM(amount) по всем paid-заказам, возвраты в расчёт не попадают и метрика завышается. Используйте SUM(amount - refund_amount) или добавляйте исключение возвращённых заказов.

5. ARPU — не LTV

ARPU — выручка на пользователя за период (обычно месяц). LTV — выручка за всё время жизни. Для подписочных продуктов приблизительно LTV ≈ ARPU / churn_rate, но это именно грубая оценка — см. как посчитать LTV в SQL.

Python-аналог

Быстрая проверка цифр из ноутбука:

revenue = orders.loc[orders['status'] == 'paid', 'amount'].sum()
total_users = users['user_id'].nunique()
arpu = revenue / max(total_users, 1)

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

FAQ

ARPU или ARPPU — что показывать PM?

Обычно обе. ARPU — для общей картины и отчётности, ARPPU — для оценки продажной части и тарифной политики.

Как считать ARPU с разовыми покупками?

Revenue за период делим на пользователей за тот же период. Пользователь с несколькими покупками учитывается в знаменателе один раз (поэтому COUNT(DISTINCT user_id)).

ARPU растёт, а orders падают — это хорошо?

Не всегда. Если активные пользователи платят больше — да. Если просто ушла аудитория с небольшими чеками, total revenue мог упасть сильнее, чем вырос средний. Смотрите и ARPU, и total revenue одновременно.

Нужно ли чистить ARPU от скидок?

Да, разумно хранить и gross, и net ARPU. В gross видно «ценник», в net — фактическое поступление денег.