Задачи на даты SQL на собеседовании
Карьерник — квиз-тренажёр в Telegram с 1500+ задач и вопросов для собесов аналитика. SQL, Python, A/B, метрики. Бесплатно.
Зачем проверяют работу с датами
Аналитик постоянно работает с датами: отчёты по дням/неделям/месяцам, когорты, retention, когортный анализ. Частые подводные камни: таймзоны, NULL в датах, конец месяца.
Задачи с разборами
Задача 1. Выручка по месяцам
SELECT
DATE_TRUNC('month', created_at) AS month,
SUM(total) AS revenue,
COUNT(*) AS orders
FROM orders
GROUP BY 1
ORDER BY 1;Задача 2. Заказы за последние 7 дней
SELECT *
FROM orders
WHERE created_at >= CURRENT_DATE - INTERVAL '7 days';Альтернативы:
- MySQL:
DATE_SUB(NOW(), INTERVAL 7 DAY) - ClickHouse:
now() - INTERVAL 7 DAY
Задача 3. Пользователи, зарегистрировавшиеся на этой неделе
SELECT *
FROM users
WHERE DATE_TRUNC('week', created_at) = DATE_TRUNC('week', CURRENT_DATE);Задача 4. Разница в днях между двумя датами
SELECT
user_id,
(MAX(created_at) - MIN(created_at))::INTERVAL AS activity_span,
EXTRACT(DAY FROM (MAX(created_at) - MIN(created_at))) AS days_between
FROM orders
GROUP BY user_id;В ClickHouse: dateDiff('day', min, max).
Задача 5. Возраст пользователей
SELECT
user_id,
EXTRACT(YEAR FROM AGE(birth_date)) AS age
FROM users;MySQL:
TIMESTAMPDIFF(YEAR, birth_date, CURDATE())Задача 6. Заказы в выходные
SELECT *
FROM orders
WHERE EXTRACT(DOW FROM created_at) IN (0, 6); -- 0=Sunday, 6=SaturdayВнимание: разные СУБД считают DOW по-разному:
- Postgres: 0=Sunday, 6=Saturday
- MySQL: 1=Sunday, 7=Saturday (с DAYOFWEEK)
- MySQL: 0=Monday, 6=Sunday (с WEEKDAY)
Задача 7. Последний день месяца
SELECT
DATE_TRUNC('month', created_at) + INTERVAL '1 month' - INTERVAL '1 day' AS last_day
FROM orders;Или в Postgres:
SELECT (DATE_TRUNC('month', created_at) + INTERVAL '1 month - 1 day')::DATE;Задача 8. Первое и последнее событие пользователя
SELECT
user_id,
MIN(created_at) AS first_event,
MAX(created_at) AS last_event,
MAX(created_at) - MIN(created_at) AS activity_duration
FROM events
GROUP BY user_id;Задача 9. Клиенты, не совершавшие покупок 30+ дней
SELECT
u.user_id,
MAX(o.created_at) AS last_order,
CURRENT_DATE - MAX(o.created_at)::DATE AS days_inactive
FROM users u
LEFT JOIN orders o ON o.user_id = u.user_id
GROUP BY u.user_id
HAVING MAX(o.created_at) IS NULL
OR CURRENT_DATE - MAX(o.created_at)::DATE > 30;Задача 10. Количество дней активности каждого пользователя
SELECT
user_id,
COUNT(DISTINCT DATE(event_at)) AS active_days
FROM events
GROUP BY user_id;Задача 11. Когорты по месяцу регистрации + retention
WITH first_order AS (
SELECT
user_id,
DATE_TRUNC('month', MIN(created_at)) AS cohort_month
FROM orders GROUP BY user_id
),
activity AS (
SELECT DISTINCT
user_id,
DATE_TRUNC('month', created_at) AS activity_month
FROM orders
)
SELECT
f.cohort_month,
a.activity_month,
EXTRACT(MONTH FROM AGE(a.activity_month, f.cohort_month)) AS months_since_signup,
COUNT(DISTINCT a.user_id) AS active_users
FROM first_order f
JOIN activity a ON a.user_id = f.user_id
GROUP BY 1, 2
ORDER BY 1, 2;Задача 12. Генерация календаря дат
Postgres:
SELECT generate_series('2026-01-01'::DATE, '2026-12-31'::DATE, '1 day')::DATE AS day;MySQL:
WITH RECURSIVE days AS (
SELECT DATE('2026-01-01') AS day
UNION ALL
SELECT day + INTERVAL 1 DAY FROM days WHERE day < '2026-12-31'
)
SELECT * FROM days;Задача 13. Разница в рабочих часах
Количество рабочих дней между двумя датами — сложная задача. Приблизительно:
SELECT
order_id,
COUNT(*) AS workdays_between
FROM orders o
JOIN generate_series(
o.created_at::DATE,
o.delivered_at::DATE,
INTERVAL '1 day'
) day
WHERE EXTRACT(DOW FROM day) NOT IN (0, 6)
GROUP BY o.order_id;Задача 14. Форматирование даты
Postgres:
TO_CHAR(created_at, 'DD.MM.YYYY') -- '21.04.2026'
TO_CHAR(created_at, 'YYYY-MM-DD HH24:MI') -- '2026-04-21 10:30'
TO_CHAR(created_at, 'Mon') -- 'Apr'MySQL:
DATE_FORMAT(created_at, '%d.%m.%Y')Задача 15. Время до первой покупки
SELECT
u.user_id,
u.created_at AS signup,
MIN(o.created_at) AS first_order,
EXTRACT(EPOCH FROM (MIN(o.created_at) - u.created_at)) / 86400 AS days_to_first_order
FROM users u
LEFT JOIN orders o ON o.user_id = u.user_id
GROUP BY u.user_id, u.created_at;Частые ошибки
Ошибка 1. Таймзоны
-- если сервер UTC, а Метрика в МСК — сдвиг 3 часа
-- аналитический день может быть разным
DATE_TRUNC('day', created_at) -- UTC
DATE_TRUNC('day', created_at AT TIME ZONE 'Europe/Moscow') -- МСКВсегда уточняйте таймзону при агрегации по дням.
Ошибка 2. Месяц — переменная длина
«Предыдущий месяц» нельзя тупо CURRENT_DATE - INTERVAL '30 days'. Правильно:
DATE_TRUNC('month', CURRENT_DATE - INTERVAL '1 month')Ошибка 3. BETWEEN с датами
-- исключает часть дня
WHERE created_at BETWEEN '2026-01-01' AND '2026-01-31'
-- 2026-01-31 23:00 не попадёт
-- лучше
WHERE created_at >= '2026-01-01'
AND created_at < '2026-02-01'Ошибка 4. NULL в датах
-- NULL = любое значение = UNKNOWN
WHERE last_login < CURRENT_DATE - INTERVAL '30 days'
-- пропустит пользователей, у которых last_login IS NULL
-- лучше
WHERE last_login IS NULL
OR last_login < CURRENT_DATE - INTERVAL '30 days'Ошибка 5. Неделя как DOW
Начало недели зависит от страны:
- США: воскресенье
- Россия/Европа: понедельник
-- Postgres ISO (понедельник)
DATE_TRUNC('week', created_at)Связанные темы
FAQ
Какая функция округляет дату до месяца?
Postgres/Snowflake: DATE_TRUNC('month', date). MySQL: DATE_FORMAT(date, '%Y-%m-01').
Как посчитать разницу в днях?
Postgres: (date1 - date2)::INTEGER. MySQL: DATEDIFF(date1, date2). ClickHouse: dateDiff('day', d1, d2).
Как учитывать таймзоны?
Храните в UTC, конвертируйте при выборке: created_at AT TIME ZONE 'Europe/Moscow'.
DATE_TRUNC или EXTRACT?
DATE_TRUNC — для группировки (DATE_TRUNC('month', d) → '2026-04-01'). EXTRACT — для конкретной части (EXTRACT(MONTH FROM d) → 4).
Тренируйте SQL — откройте тренажёр с 200+ задачами и 1500+ вопросами для собесов аналитиков.