Работа с датами в SQL — полный гайд для аналитика

Проверь себя · 1/3разбор после ответа
Нужно посчитать число пользователей, которые сделали хотя бы 1 заказ (таблицы 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_TIMESTAMP

NOW() и 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 month

EXTRACT и 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);    -- 4

DOW считает воскресенье за 0, ISODOW — понедельник за 1. Для аналитики удобнее ISODOW — неделя начинается с понедельника.

Пример — распределение заказов по дням недели:

SELECT
    EXTRACT(ISODOW FROM order_date) AS day_of_week,
    COUNT(*) AS orders
FROM orders
GROUP BY 1
ORDER BY 1

INTERVAL — арифметика дат

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)
Прокачай SQL для собеса
500+ задач по SQL: оконные функции, JOIN, CTE — с разбором каждой
Тренировать SQL в Telegram

Разница между датами

Вычитание дат

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 days

AGE удобна для человекочитаемого результата, но в аналитических запросах чаще используют вычитание дат и 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 1

Retention по неделям

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 — проверяйте документацию, функции отличаются.

Типичные ошибки

  1. Сравнение 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'.

  2. Игнорирование часовых поясов. Если в таблице TIMESTAMPTZ, а вы фильтруете по строке без часового пояса — PostgreSQL использует часовой пояс сессии. Результат может отличаться на разных серверах. Явно указывайте зону или приводите к DATE.

  3. Использование 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.