Как посчитать DAU в SQL
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'ом и кладётся в отдельную таблицу.
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')::DATE3. События вместо пользователей
COUNT(*) ≠ COUNT(DISTINCT user_id). Для DAU нужен DISTINCT user_id.
4. Боты и тестовые аккаунты
Их обязательно фильтровать — иначе DAU раздувается и ломает стиковость.
5. «Активный» ≠ «тот, кто что-то сделал»
Определение активности должно быть одинаковым на всех уровнях: и в алертах, и в отчётах, и в доклaде CEO. Иначе два графика с одинаковой подписью начнут «противоречить» друг другу.
Интерпретация движений
- DAU растёт — продукт здоровый.
- DAU стабилен, а MAU растёт — новые приходят, но не возвращаются, есть retention-проблема.
- DAU падает при стабильной MAU — слабеет «ядро», теряется вовлечение.
- DAU выше на выходных — продукт consumer.
- DAU выше в будни — скорее всего, B2B.
Связанные темы
- DAU простыми словами
- Как посчитать MAU в SQL
- Как посчитать stickiness в SQL
- Как считать retention
- Кейс: DAU упал
FAQ
Что считать активностью в DAU?
Для соцсети — просмотр ленты, для маркетплейса — открытие товара, для мобильной игры — запуск сессии. Конкретное определение согласуется с PM и фиксируется в документации.
Как фильтровать ботов?
По IP, user-agent, частоте событий и другим сигналам. Для крупных продуктов обычно используют отдельную модель детекции аномалий.
WAU или средний DAU за 7 дней?
Это разные метрики. WAU — уникальные пользователи за неделю. Средний DAU — среднее за 7 дней. В WAU один пользователь считается один раз, даже если заходил каждый день. Смотрите обе.
Почему нельзя просто COUNT(DISTINCT) в оконной функции?
В большинстве СУБД (в том числе Postgres) COUNT(DISTINCT ...) не поддерживается как оконная функция. Для скользящего MAU используйте либо LATERAL / подзапрос, либо предварительно посчитанную таблицу активности по дням.