Работа с датами в SQL — полный гайд для аналитика
users(user_id) и orders(user_id, order_id)). Какой запрос посчитает правильно?Зачем аналитику разбираться в датах
Аналитик работает с датами в каждом втором запросе: группирует метрики по месяцам, фильтрует заказы за последние 30 дней, считает retention по неделям, определяет возраст пользователя с момента регистрации. Если вы не уверенно владеете функциями дат — на собеседовании по SQL это сразу заметно.
В этом гайде разберём всё, что нужно аналитику: типы данных, основные функции, арифметику дат, практические примеры и типичные ошибки. Фокус — на PostgreSQL, но отличия от MySQL тоже покажем.
Типы данных для дат
В PostgreSQL три основных типа:
- DATE — только дата:
2026-04-08 - TIMESTAMP — дата и время без часового пояса:
2026-04-08 14:30:00 - TIMESTAMPTZ — дата и время с часовым поясом:
2026-04-08 14:30:00+03
В продакшене почти всегда используется TIMESTAMPTZ — он хранит момент времени однозначно, без зависимости от настроек сервера. Если видите в таблице колонку created_at — скорее всего это TIMESTAMPTZ.
Текущая дата и время
SELECT CURRENT_DATE; -- 2026-04-08 (тип DATE)
SELECT CURRENT_TIMESTAMP; -- 2026-04-08 14:30:00+03 (TIMESTAMPTZ)
SELECT NOW(); -- то же самое, что CURRENT_TIMESTAMPNOW() и CURRENT_TIMESTAMP внутри одной транзакции возвращают одно и то же значение. Для аналитических запросов разницы нет — используйте то, что привычнее.
DATE_TRUNC — усечение даты
Самая частая функция для аналитика. Обрезает дату до нужной точности: дня, недели, месяца, квартала, года.
SELECT DATE_TRUNC('month', '2026-04-08 14:30:00'::TIMESTAMP);
-- 2026-04-01 00:00:00
SELECT DATE_TRUNC('week', '2026-04-08 14:30:00'::TIMESTAMP);
-- 2026-04-06 00:00:00 (понедельник этой недели)
SELECT DATE_TRUNC('year', '2026-04-08 14:30:00'::TIMESTAMP);
-- 2026-01-01 00:00:00Допустимые значения: microseconds, milliseconds, second, minute, hour, day, week, month, quarter, year.
Типичное использование — группировка по месяцам:
SELECT
DATE_TRUNC('month', created_at) AS month,
COUNT(*) AS new_users
FROM users
GROUP BY DATE_TRUNC('month', created_at)
ORDER BY monthEXTRACT и DATE_PART — извлечение компонента даты
Обе функции делают одно и то же — вытаскивают часть даты: год, месяц, день, день недели и т.д.
SELECT EXTRACT(YEAR FROM '2026-04-08'::DATE); -- 2026
SELECT EXTRACT(MONTH FROM '2026-04-08'::DATE); -- 4
SELECT EXTRACT(DOW FROM '2026-04-08'::DATE); -- 3 (среда, 0 = воскресенье)
SELECT EXTRACT(ISODOW FROM '2026-04-08'::DATE); -- 3 (среда, 1 = понедельник)
-- DATE_PART — аналог с другим синтаксисом
SELECT DATE_PART('month', '2026-04-08'::DATE); -- 4DOW считает воскресенье за 0, ISODOW — понедельник за 1. Для аналитики удобнее ISODOW — неделя начинается с понедельника.
Пример — распределение заказов по дням недели:
SELECT
EXTRACT(ISODOW FROM order_date) AS day_of_week,
COUNT(*) AS orders
FROM orders
GROUP BY 1
ORDER BY 1INTERVAL — арифметика дат
INTERVAL позволяет прибавлять или вычитать периоды времени:
SELECT CURRENT_DATE - INTERVAL '30 days'; -- 30 дней назад
SELECT CURRENT_DATE + INTERVAL '1 month'; -- через месяц
SELECT CURRENT_DATE - INTERVAL '1 year'; -- год назадФильтрация за последние 30 дней:
SELECT *
FROM orders
WHERE created_at >= NOW() - INTERVAL '30 days'Фильтрация за текущий месяц:
SELECT *
FROM orders
WHERE created_at >= DATE_TRUNC('month', CURRENT_DATE)Разница между датами
Вычитание дат
SELECT '2026-04-08'::DATE - '2026-03-01'::DATE;
-- 38 (целое число дней)Вычитание двух DATE возвращает integer — количество дней. Вычитание двух TIMESTAMP возвращает INTERVAL.
AGE — «возраст» между двумя датами
SELECT AGE('2026-04-08'::DATE, '2025-01-15'::DATE);
-- 1 year 2 mons 24 daysAGE удобна для человекочитаемого результата, но в аналитических запросах чаще используют вычитание дат и EXTRACT.
Практические примеры для аналитика
Группировка выручки по месяцам
SELECT
DATE_TRUNC('month', order_date) AS month,
SUM(amount) AS revenue,
COUNT(DISTINCT user_id) AS buyers
FROM orders
WHERE order_date >= DATE_TRUNC('year', CURRENT_DATE)
GROUP BY 1
ORDER BY 1Retention по неделям
SELECT
DATE_TRUNC('week', u.created_at) AS cohort_week,
EXTRACT(DAY FROM e.created_at - u.created_at) / 7 AS week_number,
COUNT(DISTINCT e.user_id) AS active_users
FROM users u
JOIN events e ON u.id = e.user_id
GROUP BY 1, 2
ORDER BY 1, 2Возраст пользователя с момента регистрации
SELECT
user_id,
CURRENT_DATE - created_at::DATE AS days_since_registration
FROM usersКонверсия за последние 7 дней
SELECT
COUNT(DISTINCT CASE WHEN purchased THEN user_id END)::float
/ COUNT(DISTINCT user_id) AS conversion_rate
FROM events
WHERE created_at >= NOW() - INTERVAL '7 days'PostgreSQL vs MySQL: ключевые отличия
| Функция | PostgreSQL | MySQL |
|---|---|---|
| Усечение даты | DATE_TRUNC('month', dt) |
DATE_FORMAT(dt, '%Y-%m-01') |
| Извлечение части | EXTRACT(MONTH FROM dt) |
MONTH(dt) или EXTRACT(MONTH FROM dt) |
| Разница в днях | date1 - date2 |
DATEDIFF(date1, date2) |
| Прибавить интервал | dt + INTERVAL '1 month' |
DATE_ADD(dt, INTERVAL 1 MONTH) |
| Возраст | AGE(date1, date2) |
Нет аналога |
На собеседованиях аналитика обычно используют PostgreSQL-синтаксис. Если пишете под MySQL — проверяйте документацию, функции отличаются.
Типичные ошибки
Сравнение DATE и TIMESTAMP без приведения типов.
WHERE created_at = '2026-04-08'может не вернуть ничего, еслиcreated_at— это TIMESTAMP. Запись'2026-04-08'приведётся к2026-04-08 00:00:00, а большинство записей создаются не ровно в полночь. Правильно:WHERE created_at >= '2026-04-08' AND created_at < '2026-04-09'илиWHERE created_at::date = '2026-04-08'.Игнорирование часовых поясов. Если в таблице TIMESTAMPTZ, а вы фильтруете по строке без часового пояса — PostgreSQL использует часовой пояс сессии. Результат может отличаться на разных серверах. Явно указывайте зону или приводите к DATE.
Использование EXTRACT вместо DATE_TRUNC для группировки.
GROUP BY EXTRACT(MONTH FROM created_at)смешает январь 2025 и январь 2026 в одну группу. Для группировки по календарным месяцам используйте DATE_TRUNC.
Вопросы с собеседований
— Как сгруппировать данные по месяцам в PostgreSQL?
— DATE_TRUNC('month', date_column) усекает дату до первого числа месяца. Используйте его в GROUP BY и SELECT. Не путайте с EXTRACT(MONTH FROM ...) — тот возвращает только номер месяца без года.
— Чем DATE_TRUNC отличается от EXTRACT? — DATE_TRUNC возвращает дату, усечённую до указанной точности (результат — timestamp). EXTRACT возвращает числовое значение одного компонента (год, месяц, день). Для группировки — DATE_TRUNC, для фильтрации по компоненту — EXTRACT.
— Как найти записи за последние N дней?
— WHERE created_at >= NOW() - INTERVAL 'N days'. Работает и с DATE, и с TIMESTAMP. Для полных календарных дней лучше: WHERE created_at >= CURRENT_DATE - N.
— Как посчитать количество дней между двумя датами?
— Если обе колонки — DATE: date1 - date2 возвращает integer. Если TIMESTAMP: EXTRACT(DAY FROM date1 - date2) или приведите к DATE: date1::date - date2::date.
— Что вернёт DATE_TRUNC('week', ...)? — Понедельник той недели, к которой относится дата. В PostgreSQL неделя начинается с понедельника (ISO 8601).
FAQ
Когда использовать DATE, а когда TIMESTAMP?
DATE — если вам важна только дата без времени (дата рождения, дата отчётного периода). TIMESTAMP / TIMESTAMPTZ — если важен момент времени (дата создания записи, время заказа). В аналитических таблицах событий почти всегда TIMESTAMPTZ.
Что лучше — EXTRACT или DATE_PART?
В PostgreSQL они идентичны по результату. EXTRACT — стандарт SQL, DATE_PART — специфика PostgreSQL. На собеседованиях можете использовать любую. В новом коде рекомендуют EXTRACT — он стандартнее.
Как PostgreSQL определяет начало недели?
DATE_TRUNC('week', ...) всегда возвращает понедельник (ISO 8601). EXTRACT(DOW FROM ...) считает воскресенье за 0, понедельник за 1. EXTRACT(ISODOW FROM ...) считает понедельник за 1, воскресенье за 7. Для аналитики удобнее ISODOW.