SQL даты и время в аналитике: периоды, недели и возраст пользователя
Как работать с датами в SQL: строить полные периоды, группировать события по дням и неделям, считать возраст пользователя и не ошибаться на границах интервалов.

Запрос “покажи метрику за июнь” звучит просто, пока не выясняется, что сегодня 30 июня, данные приходят в UTC, а текущий день ещё не закончился. Ошибка в датах редко вызывает падение SQL: запрос успешно выполнится и покажет убедительную, но несопоставимую цифру. Разберём безопасные границы периода и базовые операции с датами.
Коротко
Для периода используй левую включённую и правую исключённую границу: `date >= start` и `date < next_start`. Так запрос не зависит от количества часов, не дублирует события между соседними периодами и не смешивает полный день с текущим.
- `date_trunc` округляет дату до начала дня, недели или месяца.
- Для июня границы — `2026-06-01` включительно и `2026-07-01` исключительно.
- Таймзона должна быть частью определения метрики.
- Неполный текущий день сравнивай отдельно или исключай из истории.
- Возраст пользователя — разница между событием и датой старта.
Полный месяц через две границы
Для июня не нужно вычислять “последнюю секунду месяца”. Следующий месяц уже является естественной правой границей. Это работает одинаково для DATE и TIMESTAMP и не создаёт проблем с миллисекундами.
| Период | Левая граница | Правая граница |
|---|---|---|
| Июнь | 2026-06-01 включительно | 2026-07-01 исключительно |
| Вторая неделя июня | 2026-06-08 включительно | 2026-06-15 исключительно |
| Последние 30 дней | current_date - interval '30 days' | current_date исключительно |
select\n count(*) as events_count,\n count(distinct user_id) as active_users\nfrom events\nwhere event_date >= date '2026-06-01'\n and event_date < date '2026-07-01';date_trunc: собрать данные по календарному периоду
`date_trunc` приводит timestamp к началу выбранного периода. Сначала выбери таймзону продукта, потом округляй дату и группируй результат. Если данные уже имеют тип DATE, достаточно группировать по самой колонке.
select\n date_trunc('week', event_date)::date as week_start,\n count(distinct user_id) as active_users\nfrom events\nwhere event_name = 'core_action'\n and event_date >= date '2026-06-01'\n and event_date < date '2026-07-01'\ngroup by week_start\norder by week_start;Проверь, с какого дня начинается неделя в вашей BI-системе и совпадает ли это с SQL-движком. Календарная неделя и последние семь дней — разные окна.
Календарный период и rolling window
Календарный месяц отвечает на вопрос “что произошло в июне”. Rolling window отвечает на вопрос “что происходило за последние 30 дней на каждую дату”. Нельзя подменять один подход другим: rolling-график сглаживает границы месяцев, но может скрыть сезонный перелом.
| Окно | Пример вопроса | Риск |
|---|---|---|
| Календарный месяц | Как прошёл июнь? | неполный текущий месяц |
| Календарная неделя | Как меняется недельный ритм? | разная граница недели |
| Последние 30 дней | Какой свежий тренд? | смешение сезонов и кампаний |
Возраст пользователя для когорт
Для retention, activation и LTV нужно знать, сколько дней прошло от старта пользователя до события. В учебной схеме стартом будет `signup_date`, а событием — `core_action`. Разница дат создаёт возраст когорты, который потом можно использовать в D1, D7 или D30.
select\n u.user_id,\n u.signup_date,\n e.event_date,\n e.event_date - u.signup_date as days_since_signup\nfrom users as u\njoin events as e using (user_id)\nwhere e.event_name = 'core_action'\n and e.event_date >= u.signup_date\norder by u.user_id, e.event_date;- D0 — событие в день старта.
- D1 — событие на следующий календарный день.
- D7 — событие через семь календарных дней.
- Rolling 24 hours может отличаться от календарного D1.
Если в данных есть timestamp и таймзона
Событие в 23:30 UTC может быть уже следующим днём для пользователя в Москве. Поэтому сначала переведи timestamp в бизнесовую таймзону, а только потом применяй `date_trunc` или приводи значение к DATE. Таймзона — часть определения метрики, а не косметика формата.
select\n (occurred_at at time zone 'Europe/Moscow')::date as business_day,\n count(distinct user_id) as active_users\nfrom events_with_timestamp\nwhere occurred_at >= timestamp '2026-06-01 00:00:00+03'\n and occurred_at < timestamp '2026-07-01 00:00:00+03'\ngroup by business_day\norder by business_day;Храни исходное время и отдельно фиксируй, в какой зоне строятся отчёты. Если один дашборд считает день по UTC, а другой по Москве, их DAU не обязан совпадать.
Частые ошибки с датами
Ошибки в датах обычно выглядят как маленькие расхождения, поэтому их легко списать на округление. На практике они могут менять когорты, зрелость retention и сравнение релизов.
- Использовать `<= last_day 23:59:59` и потерять события с миллисекундами.
- Включить сегодняшний неполный день в сравнение с полными днями.
- Считать неделю как последние семь дней, хотя отчёт построен по календарным неделям.
- Применить таймзону после группировки, а не до неё.
- Сравнить когорты, которым доступны разные возраста D7 или D30.
- Фильтровать timestamp строковым значением без явного типа и границы.
Мини-практика и следующий шаг
Посчитай DAU за полный июнь, затем сгруппируй его по неделям. Добавь days_since_signup и проверь D1 для пользователей из каждой когорты. В конце выпиши, какая таймзона и какое окно должны быть указаны рядом с метрикой в дашборде.
Материалы по теме
SQL-проверки качества данных: дубли, пропуски и скачки метрик
Практический чеклист SQL-проверок перед дашбордом: найти дубли, пропущенные ключи, события без пользователей и неожиданные скачки дневного объёма.
Читать материал
SQL CASE, COALESCE и NULL: как не сломать сегменты и метрики
Разбираем NULL, CASE и COALESCE на задачах аналитика: как группировать пустые значения, создавать сегменты и показывать ноль вместо пропущенных данных.
Читать материал
Когорты и retention в SQL: как считать возвращаемость по датам
Разбираем когортный retention в SQL: как определить дату старта, посчитать D1 и D7, собрать матрицу возвращаемости и сравнить каналы без самообмана.
Читать материал