Работа с датами в SQL — полный гайд для аналитика
Зачем аналитику разбираться в датах
Аналитик работает с датами в каждом втором запросе: группирует метрики по месяцам, фильтрует заказы за последние 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.
Тренируйтесь решать задачи с датами в SQL — откройте тренажёр с 1500+ вопросами. Больше примеров — на странице с вопросами.