Как посчитать 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. Бот-фильтр обязателен.

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

FAQ

Какая нормальная stickiness?

Соцсети топ: 50–60%. Большинство продуктов: 10–30%. Всё зависит от use case.

DAU/MAU или DAU/WAU?

Стандарт — DAU/MAU. DAU/WAU для продуктов с более высокой частотой.

Нужно ли вычитать собственных сотрудников из DAU?

Да, обязательно — иначе искажение на малых выборках.

Лучше stickiness или L28?

Stickiness — одно число, удобно для дашборда. L28 — распределение, даёт больше инсайта. Используйте оба.


Тренируйте SQL — откройте тренажёр с 1500+ вопросами для собесов.