Даты и время в SQL: шпаргалка для собеседования
Зачем аналитику даты в SQL
80% аналитических запросов привязаны ко времени: за день, за неделю, за месяц, YoY, MoM, Retention D7. Если даты путаются — вся аналитика рассыпается.
Даты в SQL — одна из самых частых тем на собеседовании для продуктовых аналитиков. Спрашивают редко глубоко, но ошибок делают много — и интервьюеры это знают.
Типы данных
| Тип | Что хранит | Пример |
|---|---|---|
| DATE | Только дата | '2026-04-15' |
| TIME | Только время | '14:30:00' |
| TIMESTAMP | Дата + время без timezone | '2026-04-15 14:30:00' |
| TIMESTAMPTZ | Дата + время с timezone | '2026-04-15 14:30:00+03' |
На собесе спрашивают: «Когда использовать TIMESTAMP vs TIMESTAMPTZ?» Ответ: всегда TIMESTAMPTZ в продакшн — он не теряет timezone информацию. TIMESTAMP без TZ интерпретируется относительно настройки сервера.
DATE_TRUNC — округление
Самая часто используемая функция. Срезает точность до нужного уровня:
SELECT
DATE_TRUNC('hour', created_at) AS hour_bucket,
DATE_TRUNC('day', created_at) AS day_bucket,
DATE_TRUNC('week', created_at) AS week_bucket,
DATE_TRUNC('month', created_at) AS month_bucket,
DATE_TRUNC('quarter', created_at) AS quarter_bucket,
DATE_TRUNC('year', created_at) AS year_bucket
FROM orders;Группировка по неделям:
SELECT DATE_TRUNC('week', created_at) AS week, SUM(amount)
FROM orders
GROUP BY 1
ORDER BY 1;Нюанс: в PostgreSQL неделя начинается с понедельника, в BigQuery — с воскресенья. Проверяйте.
EXTRACT — достать часть даты
SELECT
EXTRACT(YEAR FROM created_at) AS yr,
EXTRACT(MONTH FROM created_at) AS mo,
EXTRACT(DAY FROM created_at) AS dy,
EXTRACT(DOW FROM created_at) AS day_of_week, -- 0=воскресенье в PostgreSQL
EXTRACT(ISODOW FROM created_at) AS iso_dow, -- 1=понедельник
EXTRACT(HOUR FROM created_at) AS hr
FROM orders;В ClickHouse — функции toYear, toMonth, toDayOfWeek.
INTERVAL — арифметика дат
-- Дата 7 дней назад
SELECT CURRENT_DATE - INTERVAL '7 day';
-- Заказы за последние 30 дней
SELECT * FROM orders WHERE created_at >= CURRENT_DATE - INTERVAL '30 day';
-- Разница между датами
SELECT (end_date - start_date) AS days_between FROM events;
-- В часах/секундах
SELECT EXTRACT(EPOCH FROM (end_ts - start_ts)) / 3600 AS hours_between FROM sessions;Ловушка: в MySQL используется INTERVAL 7 DAY (без кавычек), в PostgreSQL INTERVAL '7 day' (с кавычками). Это разные диалекты, но путают их часто.
Если хочется сразу закрепить тему на практике — открой тренажёр в Telegram. 10 минут в день — и синтаксис в пальцах.
Форматирование даты в строку
-- PostgreSQL
SELECT TO_CHAR(created_at, 'YYYY-MM-DD') AS d,
TO_CHAR(created_at, 'Day') AS day_name;
-- MySQL
SELECT DATE_FORMAT(created_at, '%Y-%m-%d');Парсинг строки в дату
-- PostgreSQL
SELECT TO_DATE('15.04.2026', 'DD.MM.YYYY');
SELECT TO_TIMESTAMP('2026-04-15 14:30', 'YYYY-MM-DD HH24:MI');
-- Cast
SELECT '2026-04-15'::DATE;
SELECT CAST('2026-04-15' AS DATE);Timezone: главная ловушка
-- Конвертация timezone
SELECT
created_at AT TIME ZONE 'UTC' AT TIME ZONE 'Europe/Moscow' AS moscow_time,
created_at::timestamptz AT TIME ZONE 'Asia/Tokyo' AS tokyo_time
FROM events;Почему это частая ошибка:
- Аналитик считает DAU группировкой по day.
- У кого-то пользователь заходит в 23:30 UTC = 02:30 MSK следующего дня.
- Если не конвертировать в правильную TZ, пользователь «попадает» не в тот день.
На собесе: всегда спрашивайте, в какой timezone считать метрики. Базовое правило: хранить в UTC, при агрегации приводить к пользовательской TZ.
Первый и последний день периода
-- Первый день месяца
SELECT DATE_TRUNC('month', CURRENT_DATE) AS month_start;
-- Последний день месяца
SELECT (DATE_TRUNC('month', CURRENT_DATE) + INTERVAL '1 month' - INTERVAL '1 day')::DATE;
-- Первый понедельник месяца
SELECT DATE_TRUNC('week', DATE_TRUNC('month', CURRENT_DATE) + INTERVAL '6 day')::DATE;Разница в «рабочих днях»
Нестандартная задача, но спрашивают:
WITH cal AS (
SELECT generate_series(start_date, end_date, '1 day'::INTERVAL)::DATE AS day
)
SELECT COUNT(*) AS workdays
FROM cal
WHERE EXTRACT(ISODOW FROM day) NOT IN (6, 7); -- без субботы и воскресеньяЧтобы не только читать теорию, но и решать реальные задачи — загляните в бот Карьерника. Там по каждой теме подборка вопросов с разборами.
Генерация календаря
Нужно, когда надо LEFT JOIN-ить данные и включить дни без записей:
SELECT generate_series('2026-04-01'::DATE, '2026-04-30'::DATE, '1 day'::INTERVAL)::DATE AS day;Или рекурсивный CTE (если generate_series недоступен — ClickHouse, SQL Server):
WITH RECURSIVE cal AS (
SELECT DATE '2026-04-01' AS day
UNION ALL
SELECT day + INTERVAL '1 day' FROM cal WHERE day < DATE '2026-04-30'
)
SELECT * FROM cal;10 задач на даты
1. Выручка по дням за апрель 2026
SELECT created_at::DATE AS day, SUM(amount)
FROM orders
WHERE created_at >= '2026-04-01' AND created_at < '2026-05-01'
GROUP BY 1 ORDER BY 1;2. MoM growth
WITH monthly AS (
SELECT DATE_TRUNC('month', created_at) AS month, SUM(amount) AS rev
FROM orders GROUP BY 1
)
SELECT month, rev,
LAG(rev) OVER (ORDER BY month) AS prev,
ROUND(100.0*(rev-LAG(rev) OVER (ORDER BY month))/LAG(rev) OVER (ORDER BY month), 1) AS growth
FROM monthly;3. Retention D7
SELECT
COUNT(DISTINCT u.user_id) AS cohort,
COUNT(DISTINCT e.user_id) AS returned_d7
FROM users u
LEFT JOIN events e ON e.user_id = u.user_id
AND e.event_time::DATE = u.registered_at::DATE + 7;4. Возраст пользователей в днях
SELECT user_id, CURRENT_DATE - registered_at::DATE AS days_since_signup
FROM users;5. Время между первым и последним заказом
SELECT user_id, MAX(created_at) - MIN(created_at) AS lifespan
FROM orders GROUP BY user_id;6. Пользователи, зашедшие сегодня
SELECT DISTINCT user_id
FROM events
WHERE event_time >= CURRENT_DATE AND event_time < CURRENT_DATE + INTERVAL '1 day';7. WAU (Weekly Active Users) по неделям
SELECT DATE_TRUNC('week', event_time) AS week, COUNT(DISTINCT user_id)
FROM events
GROUP BY 1 ORDER BY 1;8. Доля заказов в выходные
SELECT
COUNT(*) FILTER (WHERE EXTRACT(ISODOW FROM created_at) IN (6,7)) * 100.0 / COUNT(*) AS weekend_pct
FROM orders;9. Средний чек по часам
SELECT EXTRACT(HOUR FROM created_at) AS hr, AVG(amount)
FROM orders GROUP BY 1 ORDER BY 1;10. Пользователи с перерывом больше 30 дней
WITH sessions AS (
SELECT user_id, event_time,
LAG(event_time) OVER (PARTITION BY user_id ORDER BY event_time) AS prev
FROM events
)
SELECT DISTINCT user_id
FROM sessions
WHERE event_time - prev > INTERVAL '30 day';Как тренироваться
Работа с датами в SQL учится на реальных аналитических задачах: MoM, retention, когорты, расчёты «за последние N дней». Теория без практики забывается за неделю.
Тренажёр Карьерник содержит блок задач на даты и временные функции — от базового DATE_TRUNC до таймзон и рабочих дней. 10 минут в день, и блок проходится без запинок.
Совет: на собесе всегда уточняйте timezone. Это не ваша занудство — это признак зрелости. Интервьюеры знают, что 80% багов в аналитике идёт из-за несогласованных TZ.
Читайте также
- Работа с датами в SQL
- GROUP BY SQL: шпаргалка
- Оконные функции SQL: шпаргалка
- Подготовка к собеседованию аналитика
FAQ
TIMESTAMP vs TIMESTAMPTZ — что выбрать?
TIMESTAMPTZ (с timezone). Он не теряет timezone информацию. TIMESTAMP без TZ интерпретируется относительно серверной настройки — это источник багов. Единственное исключение: локальные бизнес-даты (день рождения, дата события в локальной TZ), где timezone не нужна.
Как посчитать разницу в месяцах между датами?
В PostgreSQL: EXTRACT(YEAR FROM AGE(d1, d2)) * 12 + EXTRACT(MONTH FROM AGE(d1, d2)). В MySQL: TIMESTAMPDIFF(MONTH, d2, d1). Помните, что «месяцев» не всегда 30 дней — арифметика зависит от конкретных дат.
Как сгруппировать по неделям, начиная с понедельника?
DATE_TRUNC('week', date) в PostgreSQL возвращает понедельник (ISO-неделя). В BigQuery: DATE_TRUNC(date, WEEK(MONDAY)). В ClickHouse: toMonday(date).
Что делать, если нужно работать с часовыми поясами?
Всегда храните в UTC. При агрегации конвертируйте в пользовательскую TZ через AT TIME ZONE. Если у вас многорегиональный продукт — фиксируйте основную TZ для репортинга (обычно локальная для бизнеса) и документируйте это.