DAU, WAU и MAU: как считать активных пользователей в SQL
DAU, WAU и MAU — уникальные активные пользователи за день, неделю и месяц. Запросы для PostgreSQL и pandas: календарные и скользящие окна, новые и вернувшиеся, ловушки с числами.
Содержание статьи
DAU, WAU и MAU — это число уникальных пользователей, которые совершили целевое действие за день, неделю и месяц (daily, weekly, monthly active users). В SQL все три считает одна конструкция — COUNT(DISTINCT user_id) с группировкой по дню, неделе или месяцу. Отчёты расходятся из-за другого: какое действие засчитано, где проходит граница периода и закрыт ли он. Ниже — запросы для PostgreSQL и DuckDB и те же расчёты в pandas на учебной базе сервиса «Маяк»: 4 613 пользователей и 35 341 событие с 1 июня по 30 августа 2026 года. Средний DAU августа в ней равен 486,9 — или 478,1, или 470,9, смотря что считать.
Что такое DAU, WAU и MAU и как выглядит запрос?
DAU — сколько разных людей были активны за день, WAU — за неделю, MAU — за месяц. Человек, который зашёл пять раз за день, добавляет к DAU единицу; заходивший каждый день недели — единицу к WAU.
Таблица events учебной базы хранит одно действие в строке: кто (user_id), когда (event_time) и что сделал (event_name). DAU — число разных user_id в каждом дне. Для WAU и MAU в запросе меняется одно выражение: вместо CAST(event_time AS date) стоит date_trunc('week', event_time) или date_trunc('month', event_time).
За последние шесть полных дней выгрузки запрос возвращает 484, 529, 568, 556, 586 и 530 человек. 30 августа в фильтр не попало намеренно: данные за этот день обрываются в 12:59. В рабочем запросе правая граница — event_time < CURRENT_DATE: сегодняшний день не закрыт.
| Метрика | Окно | Группировка | На учебной базе |
|---|---|---|---|
| DAU | день | CAST(event_time AS date) | 484–586 за 24–29 августа |
| WAU | неделя с понедельника | date_trunc('week', event_time) | 2 045 за 17–23 августа |
| MAU | календарный месяц | date_trunc('month', event_time) | 2 688 за июль |
SELECT CAST(event_time AS date) AS day, COUNT(DISTINCT user_id) AS dau
FROM events
WHERE event_time >= DATE '2026-08-24' AND event_time < DATE '2026-08-30'
GROUP BY 1 ORDER BY 1;Кого считать активным: любое событие или целевое действие?
Если засчитывать любое событие, в метрику попадут и автологин, и фоновая синхронизация — всё, что пишется в ту же таблицу. Целевое действие (active action, core action) — то, после которого человек получил от продукта результат. Проверка: оно повторяется в обычном сценарии, не создаётся автоматически, и его трекинг не меняется от релиза к релизу.
В учебной базе пять типов событий: открытие приложения app_open и четыре содержательных — создано рабочее пространство, создан отчёт, отправлено приглашение, выполнен экспорт. Средний DAU августа по любому событию — 486,9, по открытиям — 470,9, по содержательным действиям — 68,3. MAU по тем же трём определениям: 4 109, 4 109 и 1 329.
Разрыв в семь раз — свойство этих данных: каждое содержательное событие случается у пользователя один раз и только в первые три дня после регистрации, это шаги активации. Поэтому 68,3 — поток новичков (новых в августе 65,3 в день), а не ядро аудитории. В живом продукте основное действие повторяется, и разрыв будет другим. Определение выбирают вместе с продуктовой командой и пишут в подписи к графику; как искать такое действие — в статье про activation rate. Единственное повторяющееся действие базы — app_open; ниже активностью считается любое событие, от счёта по открытиям оно отличается на 3%.
| Продукт | Засчитывают | Не засчитывают |
|---|---|---|
| Мобильная игра | начало сессии, пройденный уровень | полученный пуш |
| SaaS | создал проект, изменил задачу, открыл рабочий отчёт | вход, обновление токена |
| Маркетплейс | поиск, просмотр товара, сообщение продавцу, заказ | открытое письмо, показ уведомления |
| BI и аналитика | открыл дашборд, выполнил запрос, поделился отчётом | главная страница |
WITH daily AS (
SELECT
CAST(event_time AS date) AS day,
COUNT(DISTINCT user_id) AS dau_any,
COUNT(DISTINCT user_id) FILTER (WHERE event_name = 'app_open') AS dau_open,
COUNT(DISTINCT user_id) FILTER (WHERE event_name <> 'app_open') AS dau_core
FROM events
WHERE event_time >= DATE '2026-08-01' AND event_time < DATE '2026-08-30'
GROUP BY 1
)
SELECT
COUNT(*) AS full_days,
ROUND(AVG(dau_any), 1) AS avg_dau_any,
ROUND(AVG(dau_open), 1) AS avg_dau_open,
ROUND(AVG(dau_core), 1) AS avg_dau_core
FROM daily;Как посчитать DAU в SQL: почему пользователи, а не события?
COUNT(*) считает строки, то есть действия. 28 августа в таблице 690 событий от 586 человек: COUNT(*) завышает DAU в 1,18 раза. Это мало, потому что в учебной базе открытие приложения записано не чаще раза в день на человека; в рабочих логах событий на человека больше. Чем длиннее окно, тем больше ошибка: за неделю 17–23 августа 4 057 событий приходится на 2 045 человек (в 2,0 раза), за август — 16 571 на 4 109 (в 4,0 раза).
COUNT(DISTINCT user_id) защищает и от дублей: событие, доставленное дважды, не добавит второго пользователя. В учебной базе точных дублей нет; в рабочих данных их убирают отдельно — см. как найти и убрать дубликаты событий. Пустой user_id в COUNT(DISTINCT …) не попадает: анонимные события выпадут из метрики без предупреждения.
Запрос ниже считает средний DAU по месяцам только по полным дням: 153,5 в июне, 349,7 в июле, 486,9 в августе. Последний столбец — среднее число событий в день; в августе это 563,6, и на дашборде его легко принять за DAU.
WITH daily AS (
SELECT
CAST(event_time AS date) AS day,
COUNT(DISTINCT user_id) AS dau,
COUNT(*) AS events
FROM events
WHERE event_time < DATE '2026-08-30' -- 30 августа неполное
GROUP BY 1
)
SELECT
CAST(date_trunc('month', day) AS date) AS month,
COUNT(*) AS full_days,
ROUND(AVG(dau), 1) AS avg_dau,
MIN(dau) AS min_dau,
MAX(dau) AS max_dau,
ROUND(AVG(events), 1) AS avg_events
FROM daily
GROUP BY 1 ORDER BY 1;WAU в SQL: календарная неделя или скользящие 7 дней?
Календарный WAU — уникальные пользователи с понедельника по воскресенье. date_trunc('week', …) и в PostgreSQL, и в DuckDB возвращает понедельник. За пять недель с 27 июля: 1 754, 1 851, 1 965, 2 045 и 2 118. Последняя неделя не закрыта: столбец last_event показывает 30 августа 12:59. Без этого воскресенья в ней 2 037 человек.
Пока календарная неделя идёт, число неполное: в среду 19 августа WAU «с начала недели» равен 1 193 и к воскресенью дорастает до 2 045, а закрытое значение появляется раз в семь дней. Скользящий WAU считает уникальных пользователей за последние семь дней на каждую дату: на ту же среду — 2 050.
Напрямую оконной функцией в PostgreSQL это не посчитать: COUNT(DISTINCT user_id) OVER (ORDER BY CAST(event_time AS date) RANGE BETWEEN INTERVAL '6 days' PRECEDING AND CURRENT ROW) завершается ошибкой DISTINCT is not implemented for window functions (проверено в PostgreSQL 14; та же проверка стоит в исходниках версии 18). В DuckDB это документированная возможность, версии 1.4 и 1.5 запрос выполняют. Переносимый способ — соединить ряд дат с событиями по условию «событие попало в семь дней, которые кончаются этой датой» и посчитать уникальных в каждой группе. Первые шесть дат от начала данных — неполные окна, их из ряда убирают. LEFT JOIN сохраняет даты без событий нулём. Каждое событие попадает в семь окон, поэтому на больших таблицах соединяют заранее собранную таблицу «пользователь — день».
В воскресенье оба числа совпадают: 2 045. В остальные дни скользящее окно захватывает хвост прошлой недели, поэтому в отчёте пишут, какой WAU показан. Рамки ROWS и RANGE разобраны в статье про рамку окна, тот же приём с интервалами — в статье про INTERVAL.
SELECT
CAST(date_trunc('week', event_time) AS date) AS week_start,
COUNT(DISTINCT user_id) AS wau,
COUNT(*) AS events,
MAX(event_time) AS last_event
FROM events
WHERE event_time >= DATE '2026-07-27'
GROUP BY 1 ORDER BY 1;SELECT
CAST(d.day AS date) AS day,
COUNT(DISTINCT e.user_id) AS wau_rolling_7d,
COUNT(DISTINCT e.user_id)
FILTER (WHERE e.event_time >= date_trunc('week', d.day)) AS wau_week_to_date
FROM generate_series(TIMESTAMP '2026-08-17', TIMESTAMP '2026-08-23', INTERVAL '1 day') AS d(day)
LEFT JOIN events AS e
ON e.event_time >= d.day - INTERVAL '6 days'
AND e.event_time < d.day + INTERVAL '1 day'
GROUP BY d.day
ORDER BY d.day;MAU в SQL: календарный месяц или скользящие 28 и 30 дней?
Календарный MAU — тот же COUNT(DISTINCT user_id) с группировкой по date_trunc('month', event_time): 1 042 в июне, 2 688 в июле, 4 109 в августе. Август в выгрузке не закрыт; по 29 полным дням MAU равен 4 102.
15 августа MAU «с начала месяца» — 2 856, и рядом с июльскими 2 688 он выглядит как рост на 6%, хотя прошло полмесяца. Скользящее окно в 30 дней на ту же дату даёт 3 495.
Окно в 28 дней берут ради сопоставимости: в нём ровно четыре недели и по четыре каждого дня недели, а в календарных месяцах от 28 до 31 дня и разное число выходных. На 29 августа получается 4 102 с начала месяца, 4 048 за 28 дней и 4 151 за 30 дней. Все три числа верны, пока рядом написано, какое окно взято. В таблице ниже последний столбец — отношение среднего DAU к MAU, о нём следующий раздел.
| Месяц | Полных дней | Средний DAU | WAU последней полной недели | MAU | DAU/MAU |
|---|---|---|---|---|---|
| Июнь | 30 | 153,5 | 747 | 1 042 | 14,7% |
| Июль | 31 | 349,7 | 1 573 | 2 688 | 13,0% |
| Август | 29 | 486,9 | 2 045 | 4 102 | 11,9% |
SELECT
CAST(date_trunc('month', event_time) AS date) AS month,
COUNT(DISTINCT user_id) AS mau,
COUNT(DISTINCT CAST(event_time AS date)) AS days_with_data
FROM events
GROUP BY 1 ORDER BY 1;SELECT
CAST(d.day AS date) AS as_of,
COUNT(DISTINCT e.user_id)
FILTER (WHERE e.event_time >= date_trunc('month', d.day)) AS mau_month_to_date,
COUNT(DISTINCT e.user_id)
FILTER (WHERE e.event_time >= d.day - INTERVAL '27 days') AS mau_28d,
COUNT(DISTINCT e.user_id)
FILTER (WHERE e.event_time >= d.day - INTERVAL '29 days') AS mau_30d
FROM (VALUES (TIMESTAMP '2026-07-31'), (TIMESTAMP '2026-08-15'),
(TIMESTAMP '2026-08-23'), (TIMESTAMP '2026-08-29')) AS d(day)
JOIN events AS e
ON e.event_time >= d.day - INTERVAL '30 days'
AND e.event_time < d.day + INTERVAL '1 day'
GROUP BY d.day
ORDER BY d.day;Почему WAU меньше суммы DAU, а MAU — суммы WAU?
Уникальных пользователей нельзя складывать по дням: человек, активный в понедельник и в среду, попадает в два дневных значения и в одно недельное. За неделю 17–23 августа сумма семи DAU равна 3 501, а WAU — 2 045.
Отношение этих чисел полезно само по себе: 3 501 / 2 045 = 1,71 — столько дней в неделю активен средний недельный пользователь. С месяцем то же: WAU четырёх недель с 27 июля по 23 августа в сумме дают 7 615, а уникальных за эти 28 дней — 3 780. Поэтому WAU и MAU считают от событий, из готовой таблицы дневных DAU их не получить.
Отношение среднего DAU к MAU называют stickiness, или липкостью: какая доля месячной аудитории активна в обычный день. В августе это 11,9%. Много это или мало, зависит от ритма продукта — об этом отдельная статья про DAU/MAU и stickiness.
В pandas уникальных считает nunique. Блок pandas в конце раздела работает не на учебной базе: он строит по формуле условный набор событий (527 строк, 77 пользователей, две недели). На нём сумма DAU по неделям — 82 и 178, WAU — 47 и 75. Скользящее окно собрано через таблицу «день × пользователь» — приём для небольших данных; dau.rolling(7).sum() дал бы сумму DAU.
DAU/MAU = средний DAU за период / MAU за тот же периодАвгуст, 29 полных дней: 486,9 / 4 102 = 11,9%. То же число — активные дни на человека, делённые на длину периода: 3,44 / 29.
WITH user_days AS (
SELECT DISTINCT user_id, CAST(event_time AS date) AS day
FROM events
WHERE event_time >= DATE '2026-08-17' AND event_time < DATE '2026-08-24'
)
SELECT
COUNT(*) AS sum_of_dau, -- пар «пользователь — день»
COUNT(DISTINCT user_id) AS wau,
ROUND(1.0 * COUNT(*) / COUNT(DISTINCT user_id), 2) AS active_days_per_user
FROM user_days;import pandas as pd
# условные события, заданные формулой (не учебная база): 80 номеров пользователей,
# с событиями — 77; две недели с понедельника 10 августа 2026 года
start = pd.Timestamp('2026-08-10')
rows = [(u, start + pd.Timedelta(days=d, hours=9 + (u + d + k) % 10))
for u in range(1, 81)
for d in range(u % 9, 14) # пользователь u появляется на день u % 9
if (u * 37 + d * 11 + u * d * 13) % 97 < 30 # и заходит примерно в трёх днях из десяти
for k in range(1 + (u + d) % 3)] # от одного до трёх событий за день
ev = pd.DataFrame(rows, columns=['user_id', 'event_time'])
ev['day'] = ev['event_time'].dt.normalize()
dau = ev.groupby('day')['user_id'].nunique() # DAU
wau = ev.groupby(ev['day'].dt.to_period('W'))['user_id'].nunique() # неделя пн–вс
active = pd.crosstab(ev['day'], ev['user_id']).gt(0) # день × пользователь
wau_7d = active.rolling(7).max().sum(axis=1).iloc[6:].astype(int) # скользящие 7 дней
print(len(ev), ev['user_id'].nunique()) # 527 77
print(dau.iloc[-7:].tolist()) # [23, 24, 25, 25, 25, 30, 26]
print(dau.resample('W').sum().tolist(), wau.tolist()) # [82, 178] [47, 75]
print(wau_7d.tolist()) # [47, 54, 63, 67, 69, 71, 73, 75]Из кого состоят DAU и MAU: новые и вернувшиеся?
DAU складывается из людей, которые пришли сегодня, и тех, кто вернулся. В учебной базе каждый пользователь активен в день регистрации, поэтому новый — тот, у кого signup_date совпадает с днём события. Если даты регистрации в данных нет, новым считают день первого события. В августе из 486,9 среднего DAU новых 65,3, вернувшихся 421,6. Доля новых — 13,4% против 22,6% в июне. Средний DAU августа выше июльского на 39% (486,9 против 349,7): 92% прироста дали вернувшиеся, 8% — новые.
С MAU картина резче. Августовские 4 109 — это 1 895 зарегистрированных в августе (46,1%) и 2 214 пришедших раньше. MAU вырос к июлю на 53%, а из июньской когорты (зарегистрированные в июне) в августе активны 643 из 1 042 против 1 012 в июле. Читать это как отток нельзя: в учебной базе события пользователя записаны только до 59-го дня после регистрации. У пришедших 1–2 июня августовских событий быть не может, а из пришедших 21–30 июня в августе активны 345 из 392. Среди вернувшихся есть и «воскресшие»: 16 человек из июньской когорты пропустили июль и появились в августе.
Пока регистрации идут, MAU растёт, даже когда прежние пользователи перестают появляться — ушли они или, как здесь, закончилась запись их событий. Поэтому рядом с MAU показывают долю новых, а удержание смотрят по когортам — см. retention по когортам и каналам.
Двенадцать полных недель с 1 июня по 23 августа, значения округлены до целых. Доля новых упала с 38,3% в первую неделю до 13,7% в последнюю.
WITH user_days AS (
SELECT DISTINCT CAST(e.event_time AS date) AS day, e.user_id, u.signup_date
FROM events AS e
JOIN users AS u ON u.user_id = e.user_id
WHERE e.event_time < DATE '2026-08-30'
),
daily AS (
SELECT
day,
COUNT(*) AS dau,
COUNT(*) FILTER (WHERE signup_date = day) AS new_users,
COUNT(*) FILTER (WHERE signup_date < day) AS returning_users
FROM user_days
GROUP BY day
)
SELECT
CAST(date_trunc('month', day) AS date) AS month,
ROUND(AVG(dau), 1) AS avg_dau,
ROUND(AVG(new_users), 1) AS avg_new,
ROUND(AVG(returning_users), 1) AS avg_returning,
ROUND(100.0 * SUM(new_users) / SUM(dau), 1) AS new_share_pct
FROM daily
GROUP BY 1 ORDER BY 1;WITH monthly AS (
SELECT DISTINCT
CAST(date_trunc('month', e.event_time) AS date) AS month,
e.user_id,
CAST(date_trunc('month', u.signup_date) AS date) AS signup_month
FROM events AS e
JOIN users AS u ON u.user_id = e.user_id
)
SELECT
month,
COUNT(*) AS mau,
COUNT(*) FILTER (WHERE signup_month = month) AS new_users,
COUNT(*) FILTER (WHERE signup_month < month) AS returning_users,
ROUND(100.0 * COUNT(*) FILTER (WHERE signup_month = month) / COUNT(*), 1) AS new_share_pct
FROM monthly
GROUP BY month ORDER BY month;DAU по сегментам: почему каналы складываются, а события нет?
28 августа DAU равен 586. По каналам привлечения: organic 216, paid_search 117, partner 91, referral 162 — в сумме 586. По устройствам 309 и 277 — снова 586. Канал и устройство в этой базе записаны в таблице users: это свойства человека, каждый попадает ровно в одну группу, и группы складываются.
С типами событий так не выйдет. Приложение открывали 567 человек, пространство создали 48, отчёт — 32, приглашение отправили 22, экспорт сделал 21. В сумме 690: один человек совершает несколько действий. То же случится с любым признаком события: страницей, версией приложения, устройством, если человек заходит и с телефона, и с ноутбука.
Правило проверки: сумма сегментов равна общему DAU, только когда каждый пользователь в этот день принадлежит одной группе. Иначе строку «всего» считают отдельным запросом. Пустой признак — тоже группа: у 55 человек в учебной базе не заполнена страна, и фильтр по списку стран их потеряет.
SELECT 'канал: ' || u.channel AS segment, COUNT(DISTINCT e.user_id) AS dau
FROM events AS e
JOIN users AS u ON u.user_id = e.user_id
WHERE e.event_time >= DATE '2026-08-28' AND e.event_time < DATE '2026-08-29'
GROUP BY u.channel
UNION ALL
SELECT 'событие: ' || event_name, COUNT(DISTINCT user_id)
FROM events
WHERE event_time >= DATE '2026-08-28' AND event_time < DATE '2026-08-29'
GROUP BY event_name
ORDER BY segment;Как часовой пояс сдвигает границу дня?
День в DAU — это сутки в каком-то часовом поясе. В учебной базе event_time записан по московскому времени, и CAST(event_time AS date) даёт московский день. Если бы те же события хранились в UTC и их привели к дате без перевода, «день» начинался бы в 03:00 по Москве. Для команды в поясе UTC+6 он начинается в 21:00 предыдущего дня.
Для 28 августа три границы дают 586, 586 и 590 пользователей. Сдвиг на три часа вперёд ничего не изменил: между полуночью и тремя часами ночи событий в базе нет. При сдвиге назад число выросло на четыре — немного, потому что после 21:00 приходит 281 событие из 35 341, то есть 0,8%. Чем больше вечерней и ночной активности, тем больше людей переедет через границу.
Перевод зависит от типа столбца. timestamptz переводят одним шагом: CAST(event_time AT TIME ZONE 'Europe/Moscow' AS date). Если в столбце timestamp без пояса лежит UTC, шагов два: CAST(event_time AT TIME ZONE 'UTC' AT TIME ZONE 'Europe/Moscow' AS date) — первый объявляет, что время записано в UTC, второй переводит его в московское. Одношаговая запись на таком столбце даст день по поясу сессии. Пояс записывают в определение метрики рядом с целевым действием. О типах времени — в статье про функции даты в SQL.
SELECT
COUNT(DISTINCT user_id) FILTER (
WHERE event_time >= TIMESTAMP '2026-08-28 00:00'
AND event_time < TIMESTAMP '2026-08-29 00:00') AS dau_moscow_day,
COUNT(DISTINCT user_id) FILTER (
WHERE event_time >= TIMESTAMP '2026-08-28 03:00'
AND event_time < TIMESTAMP '2026-08-29 03:00') AS dau_utc_day,
COUNT(DISTINCT user_id) FILTER (
WHERE event_time >= TIMESTAMP '2026-08-27 21:00'
AND event_time < TIMESTAMP '2026-08-28 21:00') AS dau_utc_plus_6_day
FROM events;Где DAU, WAU и MAU обманывают: ловушки с числами
Неполный период. 30 августа DAU равен 223 против 530 накануне: минус 58% из-за обрыва выгрузки. С этим днём средний DAU августа — 478,1 вместо 486,9. Последняя неделя с понедельника по субботу выросла к тем же дням предыдущей на 8,2% (542,2 против 501,0), а среднее по всем семи дням — 496,6 против 500,1 — показывает падение на 0,7%.
Среднее при тренде. Средний DAU июня — 153,5, но в первую неделю он был 73,1, а в последнюю полную — 220,3, втрое выше. В августе то же: 486,9 за месяц и 535,4 за последние семь дней. План, построенный от месячного среднего, отстаёт от текущего уровня.
Месяцы разной длины. Тридцать первый день июля добавил к MAU 70 человек (2 618 → 2 688) — столько зарегистрировалось 31 июля — и 480 из 10 840 пар «пользователь — день», 4,4%.
Дни без событий. GROUP BY не создаёт строк для дней, когда ничего не происходило. Платный поиск запущен 1 июля: за неделю с 28 июня по 4 июля запрос по этому каналу вернёт четыре строки, и среднее по ним — 23,8. С рядом дат из generate_series и LEFT JOIN строк семь, три нулевые, среднее — 13,6. Подробнее — в статье про ряд дат без пропусков.
Блок pandas ниже повторяет три числа: 486,9, 535,4 и сумму DAU недели 3 501.
WITH daily AS (
SELECT CAST(e.event_time AS date) AS day, COUNT(DISTINCT e.user_id) AS dau
FROM events AS e
JOIN users AS u ON u.user_id = e.user_id
WHERE u.channel = 'paid_search'
AND e.event_time >= DATE '2026-06-28' AND e.event_time < DATE '2026-07-05'
GROUP BY 1
)
SELECT
CAST(c.day AS date) AS day,
COALESCE(d.dau, 0) AS dau
FROM generate_series(TIMESTAMP '2026-06-28', TIMESTAMP '2026-07-04', INTERVAL '1 day') AS c(day)
LEFT JOIN daily AS d ON d.day = CAST(c.day AS date)
ORDER BY c.day;import pandas as pd
# DAU по дням 1–29 августа: результат запроса «DAU по дням» на учебной базе
dau = pd.Series(
[428, 406, 443, 419, 485, 499, 446, 457, 441, 444, 454, 494, 503, 498, 491,
459, 505, 467, 531, 502, 519, 482, 495, 484, 529, 568, 556, 586, 530],
index=pd.date_range('2026-08-01', periods=29, freq='D'),
)
print(round(dau.mean(), 1)) # 486.9 — средний DAU за 29 дней
print(dau.rolling(7).mean().round(1).iloc[-1]) # 535.4 — среднее за 23–29 августа
week = dau.resample('W').agg(['sum', 'mean', 'count']) # недели пн–вс, метка — воскресенье
print(week.round(1).loc['2026-08-23'].tolist()) # [3501.0, 500.1, 7.0] — сумма DAU, а WAU 2045Если DAU резко изменился, сначала исключите технические причины: релиз трекинга, новое событие в определении активности, боты, внутренние и тестовые аккаунты, пуш-кампания, склейка анонимных и вошедших пользователей. Продуктовое объяснение — после.
Частые вопросы
Что такое DAU простыми словами?
Число разных людей, которые за день сделали в продукте засчитываемое действие. Один человек считается один раз, сколько бы раз он ни заходил.
В чём разница между DAU и MAU?
В окне: день и месяц. MAU не равен сумме DAU за месяц. В августе на учебной базе это 4 102 против 14 121: средний пользователь активен 3,4 дня.
Как посчитать MAU в SQL одним запросом?
SELECT date_trunc('month', event_time) AS month, COUNT(DISTINCT user_id) AS mau FROM events GROUP BY 1. Перед выводом проверьте, что последний месяц закрыт.
Что такое WAU?
Weekly active users — уникальные активные пользователи за неделю: календарную, с понедельника по воскресенье, или за скользящие семь дней. Числа разные, поэтому окно подписывают.
Какой DAU считается хорошим?
Общей нормы нет: число зависит от размера аудитории и от того, как часто продукт нужен человеку. Смотрят на динамику, долю вернувшихся и DAU/MAU в сравнении с собственным прошлым.
Что проверить перед отчётом и что читать дальше
Когда метрика изменилась, идите от общего к частному: граница периода и трекинг, новые и вернувшиеся, канал и устройство, доля целевого действия.
Упражнение. Посчитайте скользящий WAU на 29 августа (окно 23–29 августа) и WAU по одним открытиям приложения за неделю 10–16 августа. Ответы: 2 244 и 1 964. Все запросы можно выполнить в песочнице симулятора на этой же базе. Тот же расчёт с проверкой ответа — в бесплатной главе симулятора о DAU: там активностью считается app_open.
Перед тем как отдать число в отчёт, проверьте шесть вещей.
- Действие: какое событие считается активностью и одинаково ли оно для DAU, WAU и MAU.
- Единица:
COUNT(DISTINCT user_id), без тестовых и внутренних аккаунтов. - Окно: календарное или скользящее, с какого дня начинается неделя, какой часовой пояс.
- Период закрыт: последний день, неделя и месяц не обрываются посередине.
- Состав: новые и вернувшиеся, сегменты с отдельно посчитанной строкой «всего».
- Соседняя метрика: retention, активация или выручка показывают, чего стоит рост аудитории.
Материалы по теме

DAU/MAU и stickiness: когда простая метрика начинает врать
Как читать DAU/MAU, WAU/MAU и stickiness без самообмана: когда метрика показывает привычку, а когда скрывает сезонность, платный трафик или редкий сценарий продукта.

CTE в SQL: WITH, несколько шагов, рекурсивный запрос и проверка каждого слоя
CTE и оператор WITH на задачах аналитика: как разложить расчёт конверсии на именованные шаги, проверить каждый контрольным числом, собрать календарь без пропусков через WITH RECURSIVE и понять, когда CTE материализуется, а когда его пора превратить во view.

Когортный анализ: что это, как построить и читать таблицу
Когортный анализ простыми словами: как построить когортную таблицу в SQL, pandas и Excel, читать её по строкам, столбцам и диагоналям и что делать с незрелыми когортами.