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

Что такое DAU

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

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

  • Для соцсети — открыл ленту.
  • Для маркетплейса — просмотрел товар.
  • Для игры — запустил сессию.

На собесе всегда уточняйте: «что считаем активностью?»

Формула DAU

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

SQL:

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;

Разрез по платформе

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;

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;

Средний 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';

Больше таких примеров с разборами — в Telegram-тренажёре. Короткие сессии, прогресс по темам, объяснения после каждого ответа.

Скользящее среднее DAU

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

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;

DAU vs MAU

MAU (Monthly Active Users)

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

Rolling MAU по дням

SELECT day,
    COUNT(DISTINCT user_id) OVER (
        ORDER BY day
        RANGE BETWEEN INTERVAL '29 day' PRECEDING AND CURRENT ROW
    ) AS mau_30d
FROM (SELECT created_at::DATE AS day, user_id FROM events) d;

Stickiness = DAU / MAU

Показывает «липкость» продукта:

WITH dau AS (
    SELECT created_at::DATE AS day,
        COUNT(DISTINCT user_id) AS dau
    FROM events GROUP BY 1
),
mau AS (
    SELECT day,
        COUNT(DISTINCT user_id) OVER (
            ORDER BY day
            RANGE BETWEEN INTERVAL '29 day' PRECEDING AND CURRENT ROW
        ) AS mau
    FROM (SELECT DISTINCT created_at::DATE AS day, user_id FROM events) d
)
SELECT d.day, d.dau, m.mau,
    ROUND(d.dau * 1.0 / m.mau, 3) AS stickiness
FROM dau d JOIN mau m USING (day)
ORDER BY d.day;

Нормы:

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

WAU (Weekly Active Users)

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

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)
)
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;

DAU по событиям

Разные DAU для разных действий:

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

Если готовишься к собесу — бот @kariernik_bot закрывает 80% технических вопросов. SQL, Python, A/B, продуктовые метрики — всё в одном месте.

DAU в pandas

import pandas as pd

events = pd.read_sql("SELECT created_at, user_id FROM events", conn)
events['date'] = pd.to_datetime(events['created_at']).dt.date
dau = events.groupby('date')['user_id'].nunique().reset_index(name='dau')

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

1. Не указать event-фильтр

-- Все события, включая серверные, боты
COUNT(DISTINCT user_id)

-- Лучше — конкретное событие
COUNT(DISTINCT user_id) WHERE event = 'app_open'

2. Timezone

-- Считаем в 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.

4. Смешивать тестовых и реальных пользователей

Фильтруйте QA-аккаунты, ботов.

5. Игнорировать сессии

Если определение «активный» — по сессии, нужна логика sessions:

-- Сессия через 30-минутный таймаут
-- См. шпаргалку на оконные функции

Визуализация

import matplotlib.pyplot as plt

fig, ax = plt.subplots(figsize=(12, 5))
ax.plot(dau['date'], dau['dau'])
ax.set_title('DAU trend')
ax.set_xlabel('Date')
ax.set_ylabel('DAU')
plt.show()

Интерпретация

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

Читайте также

FAQ

DAU считать по «open» или по «first event»?

Зависит от продукта. Для соцсети — просмотр ленты. Для маркетплейса — open_product_page. Уточняйте с PM.

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

По IP, user-agent, скорости событий. Либо через ML-модель детекции аномалий.

WAU или DAU/7?

Это разные вещи. WAU — уникальные пользователи за неделю. DAU/7 — среднее DAU по 7 дням. Разные цифры.

DAU = активные пользователи?

«Активный» — зависит от определения. Активный по engagement (посмотрел ленту) ≠ активный по действию (купил). Фиксируйте одно на проекте.