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

Проверь себя · 1/3разбор после ответа
У пользователя price = 100 и discount = NULL. Что вернёт выражение SELECT price + discount FROM products для этой строки?

Что такое DAU

DAU (Daily Active Users) — число уникальных пользователей, совершивших целевое действие за один календарный день.

«Целевое действие» зависит от продукта:

  • соцсеть — открыл ленту или хотя бы одно сообщение;
  • маркетплейс — просмотрел товар;
  • мобильная игра — запустил сессию.

На собесе стоит явно уточнять: «что считаем активностью?». В реальных проектах именно это определение решает, какие цифры попадут в отчёт.

Формула

DAU = COUNT(DISTINCT user_id) WHERE действие совершено в этот день

Схема данных

events (user_id, event_name, platform, created_at)

1. DAU по дням

SELECT
    created_at::DATE        AS day,
    COUNT(DISTINCT user_id) AS dau
FROM events
WHERE event_name = 'app_open'
GROUP BY 1
ORDER BY 1;

Простая форма. Для мониторинга её обычно сохраняют в materialized view или daily-snapshot-таблицу.

2. DAU по платформам

SELECT
    created_at::DATE        AS day,
    platform,
    COUNT(DISTINCT user_id) AS dau
FROM events
WHERE event_name = 'app_open'
GROUP BY 1, 2
ORDER BY 1, 2;

3. DAU за вчера

SELECT COUNT(DISTINCT user_id) AS yesterday_dau
FROM events
WHERE event_name = 'app_open'
  AND created_at >= CURRENT_DATE - INTERVAL '1 day'
  AND created_at <  CURRENT_DATE;

4. Средний DAU за 7 дней

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
)
SELECT AVG(dau) AS avg_7d_dau
FROM daily
WHERE day >= CURRENT_DATE - INTERVAL '7 day';

5. Скользящее среднее (7-day MA)

Сглаживает колебания выходных и праздников:

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
)
SELECT
    day,
    dau,
    AVG(dau) OVER (
        ORDER BY day
        ROWS BETWEEN 6 PRECEDING AND CURRENT ROW
    ) AS dau_7d_ma
FROM daily
ORDER BY day;

6. Rolling MAU по дням

В Postgres (и во многих других СУБД) COUNT(DISTINCT ...) нельзя использовать как оконную функцию, поэтому считаем скользящий MAU через self-join или LATERAL:

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.created_at >= d.day - INTERVAL '29 day'
          AND e.created_at <  d.day + INTERVAL '1 day'
          AND e.event_name = 'app_open'
    ) AS mau_30d
FROM days d
ORDER BY d.day;

Такой запрос тяжёлый, поэтому в продовом пайплайне MAU обычно пересчитывается ежедневно batch-job'ом и кладётся в отдельную таблицу.

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

7. 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
),
mau AS (
    SELECT
        d.day,
        (
            SELECT COUNT(DISTINCT e.user_id)
            FROM events e
            WHERE e.created_at >= d.day - INTERVAL '29 day'
              AND e.created_at <  d.day + INTERVAL '1 day'
              AND e.event_name = 'app_open'
        ) AS mau_30d
    FROM daily d
)
SELECT
    d.day,
    d.dau,
    m.mau_30d,
    ROUND(d.dau::NUMERIC / NULLIF(m.mau_30d, 0), 3) AS stickiness
FROM daily d
JOIN mau   m USING (day)
ORDER BY d.day;

Ориентиры:

  • 50% — ежедневный продукт (мессенджер, основной feed);

  • 20–40% — регулярный (соцсеть, e-commerce);
  • < 20% — редкий (страховка, госуслуги, travel).

Это примерные ориентиры, которые зависят от продукта и аудитории.

8. WAU (Weekly Active Users)

SELECT
    DATE_TRUNC('week', created_at)::DATE AS week,
    COUNT(DISTINCT user_id)              AS wau
FROM events
WHERE event_name = 'app_open'
GROUP BY 1
ORDER BY 1;

9. New vs returning DAU

WITH user_first AS (
    SELECT user_id, MIN(created_at)::DATE AS first_seen
    FROM events
    GROUP BY user_id
),
daily AS (
    SELECT
        e.created_at::DATE AS day,
        e.user_id,
        uf.first_seen
    FROM events e
    JOIN user_first uf USING (user_id)
    WHERE e.event_name = 'app_open'
)
SELECT
    day,
    COUNT(DISTINCT user_id) FILTER (WHERE day = first_seen) AS new_users,
    COUNT(DISTINCT user_id) FILTER (WHERE day > first_seen) AS returning_users
FROM daily
GROUP BY day
ORDER BY day;

Разделение на новичков и возвращающихся помогает видеть, где именно растёт продукт — на приходящих или на удержании.

10. DAU по типам событий

SELECT
    created_at::DATE AS day,
    COUNT(DISTINCT user_id) FILTER (WHERE event_name = 'view')     AS view_dau,
    COUNT(DISTINCT user_id) FILTER (WHERE event_name = 'purchase') AS buy_dau,
    COUNT(DISTINCT user_id)                                        AS total_dau
FROM events
GROUP BY 1
ORDER BY 1;

Такая разбивка полезна, чтобы отличать «просмотровый» DAU от «платящего»: они могут двигаться в разные стороны.

DAU в pandas

events['date'] = pd.to_datetime(events['created_at']).dt.date
dau = (
    events.query('event_name == "app_open"')
          .groupby('date')['user_id']
          .nunique()
          .reset_index(name='dau')
)

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

1. Нет фильтра по событию

Без фильтра в DAU попадают серверные и технические события. Убедитесь, что берёте только «значимую» активность.

2. Таймзоны

-- UTC — пользователь из другой TZ попадает не в «свой» день
GROUP BY created_at::DATE

-- с учётом локальной зоны пользователя / продукта
GROUP BY (created_at AT TIME ZONE 'Europe/Moscow')::DATE

3. События вместо пользователей

COUNT(*) ≠ COUNT(DISTINCT user_id). Для DAU нужен DISTINCT user_id.

4. Боты и тестовые аккаунты

Их обязательно фильтровать — иначе DAU раздувается и ломает стиковость.

5. «Активный» ≠ «тот, кто что-то сделал»

Определение активности должно быть одинаковым на всех уровнях: и в алертах, и в отчётах, и в доклaде CEO. Иначе два графика с одинаковой подписью начнут «противоречить» друг другу.

Интерпретация движений

  • DAU растёт — продукт здоровый.
  • DAU стабилен, а MAU растёт — новые приходят, но не возвращаются, есть retention-проблема.
  • DAU падает при стабильной MAU — слабеет «ядро», теряется вовлечение.
  • DAU выше на выходных — продукт consumer.
  • DAU выше в будни — скорее всего, B2B.

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

FAQ

Что считать активностью в DAU?

Для соцсети — просмотр ленты, для маркетплейса — открытие товара, для мобильной игры — запуск сессии. Конкретное определение согласуется с PM и фиксируется в документации.

Как фильтровать ботов?

По IP, user-agent, частоте событий и другим сигналам. Для крупных продуктов обычно используют отдельную модель детекции аномалий.

WAU или средний DAU за 7 дней?

Это разные метрики. WAU — уникальные пользователи за неделю. Средний DAU — среднее за 7 дней. В WAU один пользователь считается один раз, даже если заходил каждый день. Смотрите обе.

Почему нельзя просто COUNT(DISTINCT) в оконной функции?

В большинстве СУБД (в том числе Postgres) COUNT(DISTINCT ...) не поддерживается как оконная функция. Для скользящего MAU используйте либо LATERAL / подзапрос, либо предварительно посчитанную таблицу активности по дням.