Как посчитать MAU в SQL
GROUP BY обрабатывает значения NULL в столбце группировки?Содержание:
Что такое MAU
MAU (Monthly Active Users) — число уникальных пользователей, совершивших целевое действие хотя бы раз за месяц.
«Целевое действие» зависит от продукта: запуск приложения, просмотр контента, действительно значимое событие (не просто пинг сервера). На собесе стоит явно уточнять определение активности перед тем, как писать SQL.
Два способа считать MAU
Calendar MAU
Самый простой — по календарному месяцу:
SELECT
DATE_TRUNC('month', created_at) AS month,
COUNT(DISTINCT user_id) AS mau
FROM events
WHERE event_name = 'app_open'
GROUP BY 1
ORDER BY 1;Минус очевидный: в феврале 28 дней, в марте — 31, MAU напрямую несравнимы.
Rolling MAU (28-day)
Каждый день считаем уникальных пользователей за предыдущие 28 дней. В Postgres COUNT(DISTINCT ...) нельзя использовать как оконную функцию, поэтому делаем через подзапрос:
WITH days AS (
SELECT DISTINCT created_at::DATE AS day FROM events
)
SELECT
d.day,
(
SELECT COUNT(DISTINCT e.user_id)
FROM events e
WHERE e.event_name = 'app_open'
AND e.created_at >= d.day - INTERVAL '27 day'
AND e.created_at < d.day + INTERVAL '1 day'
) AS mau_28d
FROM days d
ORDER BY d.day;28 дней — это ровно 4 недели, они убирают недельные паттерны и делают значения сравнимыми. Для продовых дашбордов такой расчёт обычно кэшируется в daily-snapshot-таблице, чтобы не гонять тяжёлый запрос каждый раз.
MAU за последние 30 дней
Короткая форма для ad-hoc:
SELECT COUNT(DISTINCT user_id) AS mau
FROM events
WHERE event_name = 'app_open'
AND created_at >= CURRENT_DATE - INTERVAL '30 day';MAU в разрезе
По платформе
SELECT
platform,
COUNT(DISTINCT user_id) AS mau
FROM events
WHERE event_name = 'app_open'
AND created_at >= CURRENT_DATE - INTERVAL '30 day'
GROUP BY platform;По каналу привлечения
SELECT
u.channel,
COUNT(DISTINCT e.user_id) AS mau
FROM events e
JOIN users u USING (user_id)
WHERE e.event_name = 'app_open'
AND e.created_at >= CURRENT_DATE - INTERVAL '30 day'
GROUP BY u.channel
ORDER BY mau DESC;DAU / MAU (stickiness)
Показывает долю MAU, которую мы видим в среднем за день:
WITH daily AS (
SELECT
created_at::DATE AS day,
COUNT(DISTINCT user_id) AS dau
FROM events
WHERE event_name = 'app_open'
GROUP BY 1
),
rolling_mau AS (
SELECT
d.day,
(
SELECT COUNT(DISTINCT e.user_id)
FROM events e
WHERE e.event_name = 'app_open'
AND e.created_at >= d.day - INTERVAL '27 day'
AND e.created_at < d.day + INTERVAL '1 day'
) AS mau_28d
FROM daily d
)
SELECT
d.day,
d.dau,
m.mau_28d,
ROUND(d.dau::NUMERIC / NULLIF(m.mau_28d, 0), 3) AS stickiness
FROM daily d
JOIN rolling_mau m USING (day)
ORDER BY d.day;Ориентиры:
50% — daily-продукт (мессенджер, основной feed);
- 20–40% — регулярный (соцсеть, e-commerce);
- < 20% — редкий (страховка, travel, госуслуги).
New vs returning MAU
WITH user_first AS (
SELECT user_id, MIN(created_at)::DATE AS first_seen
FROM events
GROUP BY user_id
),
last_30 AS (
SELECT DISTINCT user_id
FROM events
WHERE event_name = 'app_open'
AND created_at >= CURRENT_DATE - INTERVAL '30 day'
)
SELECT
COUNT(*) FILTER (WHERE first_seen >= CURRENT_DATE - INTERVAL '30 day') AS new_mau,
COUNT(*) FILTER (WHERE first_seen < CURRENT_DATE - INTERVAL '30 day') AS returning_mau
FROM last_30
JOIN user_first USING (user_id);Это полезный разрез: если new/returning соотношение резко меняется, тренд в MAU объясняется разным образом — либо приходят новые, либо возвращаются старые.
Частые ошибки
1. Month-to-date MAU вместо rolling
-- Month-to-date MAU: до конца месяца цифра заниженная
WHERE DATE_TRUNC('month', created_at) = DATE_TRUNC('month', CURRENT_DATE)
-- Rolling 30-day — сравнимое значение каждый день
WHERE created_at >= CURRENT_DATE - INTERVAL '30 day'2. Разные определения «active»
В одной компании бекенд может считать активностью открытие приложения, маркетинг — любое событие, а продукт — досмотр контента. MAU в трёх отчётах расходится, начинаются споры «у кого правильно». Определение метрики должно быть одно — и зафиксировано в документации.
3. Боты и тестовые аккаунты
Без фильтрации цифра раздута, stickiness кажется лучше, чем есть.
4. COUNT(DISTINCT ...) OVER
В большинстве СУБД это не работает. Для скользящего MAU используем подзапрос или LATERAL, как в шаблонах выше.
5. Отсутствие фильтра по событию
COUNT(DISTINCT user_id) без фильтра поднимает в MAU всех, у кого был любой event — включая технические пинги.
MAU vs WAU vs DAU
- DAU — за день.
- WAU — за неделю (7 дней).
- MAU — за месяц (28–30 дней).
Для B2B-продуктов WAU часто информативнее MAU: рабочая аудитория естественно приходит в будни. Для consumer-продуктов MAU — стандартная ориентировочная метрика.
Связанные темы
- Как посчитать DAU в SQL
- Как посчитать stickiness в SQL
- MAU простыми словами
- Stickiness DAU/MAU как интерпретировать
FAQ
Calendar или rolling MAU?
Для мониторинга — rolling (28 или 30 дней), потому что значения сравнимы каждый день. Для отчётности по финансам и для board-deck'ов обычно удобнее calendar.
Почему 28 дней, а не 30?
28 = 4 × 7. Такое окно убирает разницу в количестве выходных дней между месяцами и делает MAU ровно сравнимым во времени.
MAU выше DAU в 5 раз — это нормально?
Зависит от продукта. Для ежедневных продуктов MAU / DAU < 2,5x (stickiness > 40%). Для редких использований соотношение может быть в 10 раз и больше.
MAU падает — что делать?
Разделить на new и returning. Если падают новые — смотрите каналы привлечения и конверсию в регистрацию. Если падают возвращающиеся — смотрите retention, работу пушей, product issues после релизов.