Как посчитать stickiness (DAU/MAU) в SQL
Карьерник — квиз-тренажёр в Telegram с 1500+ вопросами для собесов аналитика. SQL, Python, A/B, метрики. Бесплатно.
Что такое stickiness
Stickiness = DAU / MAU — доля месячных пользователей, которую мы видим в среднем за день.
Stickiness = Average DAU / MAUБенчмарки:
- Соцсети топ: 50–60%
- Стриминг: 15–25%
- B2B SaaS: 20–40%
- E-commerce: 5–15%
Схема данных
Предположим таблицу событий:
events (user_id, event_at)1. Stickiness за конкретный месяц
WITH daily_active AS (
SELECT
DATE(event_at) AS day,
COUNT(DISTINCT user_id) AS dau
FROM events
WHERE event_at >= '2026-04-01'
AND event_at < '2026-05-01'
GROUP BY 1
),
mau AS (
SELECT COUNT(DISTINCT user_id) AS mau
FROM events
WHERE event_at >= '2026-04-01'
AND event_at < '2026-05-01'
),
avg_dau AS (
SELECT AVG(dau) AS avg_dau FROM daily_active
)
SELECT
avg_dau.avg_dau,
mau.mau,
avg_dau.avg_dau / mau.mau AS stickiness
FROM avg_dau, mau;2. Stickiness по дням (скользящая)
Каждый день — свой расчёт DAU / MAU (где MAU — скользящие 30 дней).
WITH daily AS (
SELECT
DATE(event_at) AS day,
COUNT(DISTINCT user_id) AS dau
FROM events
GROUP BY 1
),
rolling_mau AS (
SELECT
d.day,
COUNT(DISTINCT e.user_id) AS mau_30d
FROM daily d
LEFT JOIN events e
ON e.event_at >= d.day - INTERVAL '29 days'
AND e.event_at < d.day + INTERVAL '1 day'
GROUP BY d.day
)
SELECT
d.day,
d.dau,
rm.mau_30d,
d.dau::FLOAT / NULLIF(rm.mau_30d, 0) AS stickiness
FROM daily d
JOIN rolling_mau rm USING (day)
ORDER BY d.day;Внимание: этот запрос тяжёлый на больших данных. Для production лучше materialized view или daily snapshot table.
3. Stickiness по месяцам
Для каждого месяца — среднее DAU и MAU этого месяца.
WITH daily_per_month AS (
SELECT
DATE_TRUNC('month', event_at) AS month,
DATE(event_at) AS day,
COUNT(DISTINCT user_id) AS dau
FROM events
GROUP BY 1, 2
),
monthly AS (
SELECT
DATE_TRUNC('month', event_at) AS month,
COUNT(DISTINCT user_id) AS mau
FROM events
GROUP BY 1
)
SELECT
m.month,
AVG(dpm.dau) AS avg_dau,
m.mau,
AVG(dpm.dau) / m.mau AS stickiness
FROM daily_per_month dpm
JOIN monthly m USING (month)
GROUP BY m.month, m.mau
ORDER BY m.month;4. Stickiness по сегментам
По платформе:
WITH daily AS (
SELECT
platform,
DATE(event_at) AS day,
COUNT(DISTINCT user_id) AS dau
FROM events
WHERE event_at >= '2026-04-01'
AND event_at < '2026-05-01'
GROUP BY 1, 2
),
mau AS (
SELECT
platform,
COUNT(DISTINCT user_id) AS mau
FROM events
WHERE event_at >= '2026-04-01'
AND event_at < '2026-05-01'
GROUP BY platform
)
SELECT
m.platform,
AVG(d.dau) AS avg_dau,
m.mau,
AVG(d.dau) / m.mau AS stickiness
FROM daily d
JOIN mau m USING (platform)
GROUP BY m.platform, m.mau;5. DAU/WAU (альтернатива)
Для быстрых продуктов — недельный ratio чище:
WITH daily AS (
SELECT DATE(event_at) AS day, COUNT(DISTINCT user_id) AS dau
FROM events WHERE event_at >= NOW() - INTERVAL '7 days'
GROUP BY 1
),
wau AS (
SELECT COUNT(DISTINCT user_id) AS wau
FROM events WHERE event_at >= NOW() - INTERVAL '7 days'
)
SELECT AVG(d.dau) / w.wau AS dau_wau_stickiness
FROM daily d, wau w
GROUP BY w.wau;6. L-метрика (Lxx = доля пользователей, которые были активны X дней из последних 28)
Альтернативная метрика engagement вместо stickiness:
WITH user_days AS (
SELECT
user_id,
COUNT(DISTINCT DATE(event_at)) AS active_days_28
FROM events
WHERE event_at >= NOW() - INTERVAL '28 days'
GROUP BY user_id
)
SELECT
AVG(CASE WHEN active_days_28 >= 28 THEN 1.0 ELSE 0 END) AS l28,
AVG(CASE WHEN active_days_28 >= 21 THEN 1.0 ELSE 0 END) AS l21,
AVG(CASE WHEN active_days_28 >= 14 THEN 1.0 ELSE 0 END) AS l14,
AVG(CASE WHEN active_days_28 >= 7 THEN 1.0 ELSE 0 END) AS l7,
AVG(CASE WHEN active_days_28 >= 1 THEN 1.0 ELSE 0 END) AS l1
FROM user_days;L-метрика распределение даёт больше информации, чем одно число stickiness.
7. Stickiness по когортам
Как stickiness меняется для пользователей в зависимости от срока использования:
WITH user_age AS (
SELECT
user_id,
DATE_TRUNC('month', MIN(event_at)) AS cohort_month
FROM events
GROUP BY user_id
),
daily AS (
SELECT
ua.cohort_month,
DATE(e.event_at) AS day,
COUNT(DISTINCT e.user_id) AS dau
FROM events e
JOIN user_age ua ON ua.user_id = e.user_id
WHERE e.event_at >= '2026-04-01'
AND e.event_at < '2026-05-01'
GROUP BY 1, 2
),
mau AS (
SELECT
ua.cohort_month,
COUNT(DISTINCT e.user_id) AS mau
FROM events e
JOIN user_age ua ON ua.user_id = e.user_id
WHERE e.event_at >= '2026-04-01'
AND e.event_at < '2026-05-01'
GROUP BY ua.cohort_month
)
SELECT
m.cohort_month,
AVG(d.dau) AS avg_dau,
m.mau,
AVG(d.dau) / m.mau AS stickiness
FROM daily d
JOIN mau m USING (cohort_month)
GROUP BY m.cohort_month, m.mau
ORDER BY m.cohort_month;Обычно старые когорты имеют более высокий stickiness (лояльнее).
Частые ошибки
Ошибка 1. Определение «активности»
Что считается activity event? Любой event / ключевой event / engagement > threshold?
Разное определение → разные цифры. Договоритесь внутри команды.
Ошибка 2. Timezone
DAU — за «день» по какому времени? UTC или local? Особенно важно для международных продуктов.
Ошибка 3. MAU скользящий vs календарный
Скользящие 30 дней ≠ календарный месяц. Документируйте, какой.
Ошибка 4. DISTINCT по user_id
Если есть анонимные события, нужно COALESCE(user_id, device_id) или разделять «identified» и «anonymous».
Ошибка 5. Боты в числителе
Если не фильтруете ботов, они могут искусственно раздувать DAU. Бот-фильтр обязателен.
Связанные темы
- Как считать DAU в SQL
- Как считать MAU в SQL
- Stickiness DAU/MAU гайд
- Метрики продукта
- Кейс: stickiness упала
FAQ
Какая нормальная stickiness?
Соцсети топ: 50–60%. Большинство продуктов: 10–30%. Всё зависит от use case.
DAU/MAU или DAU/WAU?
Стандарт — DAU/MAU. DAU/WAU для продуктов с более высокой частотой.
Нужно ли вычитать собственных сотрудников из DAU?
Да, обязательно — иначе искажение на малых выборках.
Лучше stickiness или L28?
Stickiness — одно число, удобно для дашборда. L28 — распределение, даёт больше инсайта. Используйте оба.
Тренируйте SQL — откройте тренажёр с 1500+ вопросами для собесов.