Даты — одна из самых частых тем в аналитических SQL-запросах. Метрики по дням/месяцам, возраст пользователя, дни до дедлайна — всё это требует работы с датами. В этой статье — всё что нужно знать для работы с датами в PostgreSQL.
Типы данных для дат
-- DATE — только дата (год, месяц, день)
SELECT '2026-03-15'::DATE;
-- TIMESTAMP — дата и время без часового пояса
SELECT '2026-03-15 14:30:00'::TIMESTAMP;
-- TIMESTAMPTZ — дата и время с часовым поясом (рекомендуется)
SELECT NOW(); -- текущее время с TZ
-- INTERVAL — промежуток времени
SELECT INTERVAL '7 days';
SELECT INTERVAL '1 month 15 days';
Текущая дата и время
-- Текущая дата (без времени)
SELECT CURRENT_DATE; -- 2026-03-18
SELECT CURRENT_DATE::DATE; -- то же самое
-- Текущее дата+время
SELECT NOW(); -- 2026-03-18 14:30:00+03
SELECT CURRENT_TIMESTAMP; -- аналог NOW()
-- Только время
SELECT CURRENT_TIME;
-- Начало дня (сегодня в 00:00)
SELECT DATE_TRUNC('day', NOW());
DATE_TRUNC: обрезка до периода
Самая полезная функция для аналитики. Обрезает дату до нужной точности.
SELECT DATE_TRUNC('year', '2026-03-15 14:30'::TIMESTAMP); -- 2026-01-01 00:00
SELECT DATE_TRUNC('quarter', '2026-03-15 14:30'::TIMESTAMP); -- 2026-01-01 00:00
SELECT DATE_TRUNC('month', '2026-03-15 14:30'::TIMESTAMP); -- 2026-03-01 00:00
SELECT DATE_TRUNC('week', '2026-03-15 14:30'::TIMESTAMP); -- 2026-03-09 00:00 (пн)
SELECT DATE_TRUNC('day', '2026-03-15 14:30'::TIMESTAMP); -- 2026-03-15 00:00
SELECT DATE_TRUNC('hour', '2026-03-15 14:30'::TIMESTAMP); -- 2026-03-15 14:00
Классическое применение — метрики по периодам:
-- Выручка по месяцам
SELECT
DATE_TRUNC('month', created_at)::DATE AS month,
COUNT(*) AS orders,
SUM(amount) AS revenue
FROM orders
WHERE status = 'paid'
GROUP BY DATE_TRUNC('month', created_at)
ORDER BY month;
-- Активные пользователи по неделям (WAU)
SELECT
DATE_TRUNC('week', event_date)::DATE AS week_start,
COUNT(DISTINCT user_id) AS wau
FROM events
GROUP BY DATE_TRUNC('week', event_date)
ORDER BY week_start;
EXTRACT: извлечение части даты
SELECT EXTRACT(year FROM NOW()); -- 2026
SELECT EXTRACT(month FROM NOW()); -- 3
SELECT EXTRACT(day FROM NOW()); -- 18
SELECT EXTRACT(hour FROM NOW()); -- 14
SELECT EXTRACT(dow FROM NOW()); -- 0=воскресенье, 1=понедельник...6=суббота
SELECT EXTRACT(isodow FROM NOW()); -- 1=пн...7=вс (ISO)
SELECT EXTRACT(week FROM NOW()); -- номер недели ISO (1-53)
SELECT EXTRACT(quarter FROM NOW()); -- квартал (1-4)
SELECT EXTRACT(epoch FROM NOW()); -- Unix timestamp (секунды с 1970-01-01)
Применение:
-- Продажи по дням недели (0=вс, 1=пн, ...)
SELECT
EXTRACT(dow FROM created_at)::INT AS day_of_week,
TO_CHAR(created_at, 'Day') AS day_name,
COUNT(*) AS orders,
SUM(amount) AS revenue
FROM orders
GROUP BY EXTRACT(dow FROM created_at), TO_CHAR(created_at, 'Day')
ORDER BY day_of_week;
-- Только заказы в рабочее время (9-18)
SELECT * FROM orders
WHERE EXTRACT(hour FROM created_at) BETWEEN 9 AND 17
AND EXTRACT(isodow FROM created_at) BETWEEN 1 AND 5;
INTERVAL: арифметика с датами
-- Прибавить/вычесть период
SELECT NOW() + INTERVAL '7 days';
SELECT NOW() - INTERVAL '1 month';
SELECT NOW() + INTERVAL '1 year 3 months 15 days';
-- Пользователи, зарегистрированные за последние 30 дней
SELECT * FROM users
WHERE created_at >= NOW() - INTERVAL '30 days';
-- Заказы за прошлый месяц
SELECT * FROM orders
WHERE created_at >= DATE_TRUNC('month', NOW()) - INTERVAL '1 month'
AND created_at < DATE_TRUNC('month', NOW());
-- Пользователи, не заходившие 90+ дней (риск оттока)
SELECT user_id, last_login
FROM users
WHERE last_login < NOW() - INTERVAL '90 days'
AND is_active = true;
AGE: разница между датами
-- Возраст пользователя
SELECT
user_id,
birth_date,
AGE(NOW(), birth_date::TIMESTAMP) AS age,
EXTRACT(year FROM AGE(NOW(), birth_date::TIMESTAMP)) AS age_years
FROM users;
-- Сколько прошло с регистрации
SELECT
user_id,
created_at,
AGE(NOW(), created_at) AS account_age
FROM users;
Разница через вычитание (проще для дней):
-- Количество дней с регистрации
SELECT
user_id,
CURRENT_DATE - created_at::DATE AS days_since_registration
FROM users;
-- Дней до конца подписки
SELECT
user_id,
subscription_end::DATE - CURRENT_DATE AS days_left
FROM subscriptions
WHERE subscription_end > NOW();
TO_CHAR: форматирование даты
SELECT TO_CHAR(NOW(), 'DD.MM.YYYY'); -- 18.03.2026
SELECT TO_CHAR(NOW(), 'DD Month YYYY'); -- 18 March 2026
SELECT TO_CHAR(NOW(), 'YYYY-MM-DD HH24:MI'); -- 2026-03-18 14:30
SELECT TO_CHAR(NOW(), 'Day, DD Mon YYYY'); -- Wednesday, 18 Mar 2026
SELECT TO_CHAR(NOW(), 'Q'); -- 1 (квартал)
SELECT TO_CHAR(NOW(), 'IW'); -- 12 (неделя ISO)
TO_DATE / TO_TIMESTAMP: парсинг строк
-- Строка в дату
SELECT TO_DATE('15.03.2026', 'DD.MM.YYYY');
SELECT TO_DATE('March 15, 2026', 'Month DD, YYYY');
-- Строка в timestamp
SELECT TO_TIMESTAMP('15.03.2026 14:30', 'DD.MM.YYYY HH24:MI');
Типичные задачи в аналитике
Когортный анализ: когда зарегистрировался и когда сделал первый заказ
SELECT
DATE_TRUNC('month', u.created_at)::DATE AS cohort,
DATE_TRUNC('month', MIN(o.created_at))::DATE AS first_order_month,
EXTRACT(month FROM AGE(
DATE_TRUNC('month', MIN(o.created_at)),
DATE_TRUNC('month', u.created_at)
))::INT AS months_to_convert,
COUNT(DISTINCT u.id) AS users
FROM users u
JOIN orders o ON o.user_id = u.id
GROUP BY DATE_TRUNC('month', u.created_at), u.id
ORDER BY cohort;
Заполнить пропущенные даты нулями
-- Генерация ряда дат через generate_series
SELECT
d::DATE AS date,
COALESCE(r.revenue, 0) AS revenue
FROM generate_series(
'2026-01-01'::DATE,
'2026-03-18'::DATE,
'1 day'::INTERVAL
) AS d
LEFT JOIN (
SELECT created_at::DATE AS date, SUM(amount) AS revenue
FROM orders
GROUP BY created_at::DATE
) r ON r.date = d::DATE
ORDER BY d;
Скользящее среднее за 7 дней
SELECT
order_date,
revenue,
AVG(revenue) OVER (
ORDER BY order_date
ROWS BETWEEN 6 PRECEDING AND CURRENT ROW
) AS rolling_7d_avg
FROM daily_revenue
ORDER BY order_date;
Попробуй сам: все три запроса выше можно написать и проверить прямо в браузере — в тренажёре SQL Lab есть датасеты с реальными полями
created_atиorder_date. Никакой установки, работает сразу.
Шпаргалка по функциям дат
| Задача | Функция / Синтаксис |
|---|---|
| Сегодня | CURRENT_DATE |
| Сейчас | NOW() |
| Начало месяца | DATE_TRUNC('month', NOW()) |
| Прошлый месяц | DATE_TRUNC('month', NOW()) - INTERVAL '1 month' |
| Год из даты | EXTRACT(year FROM date_col) |
| Разница в днях | date1::DATE - date2::DATE |
| Форматировать | TO_CHAR(date_col, 'DD.MM.YYYY') |
| Ряд дат | generate_series(start, end, '1 day') |
| Возраст в годах | EXTRACT(year FROM AGE(NOW(), birth_date)) |
Что дальше
Функции дат — один из самых частых запросов на SQL-собеседованиях. Чтобы запомнить их не зазубриванием, а через практику:
Отработай на реальных задачах:
- Тренажёр SQL — задачи с датасетами
orders,users,events. Можно сразу писатьDATE_TRUNC,EXTRACT,AGEи видеть результат. - Курсы SQL — если хочешь пройти темы системно, от основ до оконных функций.
- Подготовка к собеседованию — вопросы по датам встречаются в блоке «Аналитические функции».
Попробуй написать запрос прямо сейчас — открыть тренажёр →