SQL

БЛОК 5. Строки и даты — 16. Работа с датами

📚 22 вопросовПройти тест →
Лекция

БЛОК 5. Строки и даты — 16. Работа с датами

SQL

16. Работа с датами

🧭 Введение: почему даты ломают отчёты чаще, чем кажется

С датами и временем в SQL почти всегда возникают ошибки на границах:
«за сегодня» даёт не те строки, «за месяц» прыгает из-за времени,
а группировка «по дням» неожиданно раздроблена на тысячи значений.
Ключевая проблема в том, что дата и момент времени — это разные сущности.
Если их смешать без правил, отчёт может выглядеть правдоподобно, но быть неверным.
В этой лекции вы разберёте базовый набор, который нужен в реальной работе:
CURRENT_DATE, NOW(), INTERVAL, корректные сравнения диапазонов,
группировку по датам и выбор между DATE и TIMESTAMP.
💡 Совет: Для любого фильтра по времени сначала определяйте тип данных (DATE или TIMESTAMP),
потом фиксируйте границы диапазона, и только после этого пишите WHERE.
Вывод: Работа с датами — это не «синтаксис ради синтаксиса», а контроль точности аналитики и бизнес-логики.

⚠️ Проблема -> решение

Типичная проблема:
  1. сравнивают TIMESTAMP как обычную дату без учёта времени;
  2. используют BETWEEN там, где нужен полуинтервал;
  3. группируют по «сырым» timestamp-полям.
Последствия:
  1. пропуски и дубли на границах периода;
  2. неверные метрики по дням/месяцам;
  3. споры «почему отчёт не сходится с дашбордом».
Решение:
  1. явно выбирать DATE или TIMESTAMP под задачу;
  2. строить фильтры через полуинтервал (>= и <);
  3. использовать явную нормализацию времени для группировок (DATE_TRUNC, ::date).
🟢 Если совсем просто: Сначала выбираем правильный тип времени, потом задаём корректные границы, потом агрегируем.
🎯 Как понять, что этап прошёл успешно: Вы можете объяснить, почему запись на границе суток попала или не попала в результат.

🛠️ Чем помогает и как работает

Работа с датами нужна почти в каждом SQL-проекте:
  • отчёты «за сегодня / за неделю / за месяц»;
  • SLA и контроль сроков;
  • сегментация «активные за N дней»;
  • аналитика по дням и часам;
  • аудит событий в логах.
🟢 Если совсем просто: Функции даты/времени задают «когда произошло событие» и помогают правильно считать периоды.
🎯 Как понять, что этап прошёл успешно: Вы уверенно строите запросы вида «последние 7 дней», «сегодня», «по дням за месяц».
Чем помогает:
  • CURRENT_DATE даёт текущую календарную дату;
  • NOW() даёт текущий момент времени;
  • INTERVAL позволяет сдвигать временные границы;
  • сравнения >= и < задают стабильные диапазоны;
  • DATE_TRUNC и приведение к DATE позволяют корректно группировать.
Как это работает:
  • Шаг 1: определяем, что хранится в колонке (DATE или TIMESTAMP);
  • Шаг 2: задаём диапазон с точными границами;
  • Шаг 3: фильтруем данные по этому диапазону;
  • Шаг 4: при необходимости приводим время к нужной гранулярности (день/месяц);
  • Шаг 5: проверяем пограничные кейсы.
Вывод: Точная работа с датами строится на дисциплине границ и правильном выборе типа.

📚 Ключевые термины (простыми словами)

Перед практикой синхронизируем словарь.
🟢 Если совсем просто: Нужно чётко понимать разницу между «дата» и «момент времени».
🎯 Как понять, что этап прошёл успешно: Вы не путаете CURRENT_DATE, NOW(), DATE, TIMESTAMP, INTERVAL.
  • CURRENT_DATE — текущая календарная дата без времени.
  • NOW() — текущий момент времени.
  • INTERVAL — длительность ('1 day', '7 days', '1 month').
  • DATE — тип для календарной даты (без часов/минут/секунд).
  • TIMESTAMP — тип для даты и времени.
  • TIMESTAMPTZ — момент времени с учётом часового пояса в PostgreSQL.
  • Half-open interval — диапазон вида >= start AND < end.
  • DATE_TRUNC — округление timestamp до дня/месяца/часа.
Вывод: Эти термины закрывают минимальную базу для безопасной работы с датами.

📅 1. CURRENT_DATE и NOW(): дата против момента

Частая ошибка — считать, что CURRENT_DATE и NOW() взаимозаменяемы.
На деле это разные по смыслу значения, и выбор влияет на результат фильтрации.
🟢 Если совсем просто: CURRENT_DATE — «сегодня как дата», NOW() — «сейчас как точное время».
🎯 Как понять, что этап прошёл успешно: Вы выбираете функцию под задачу и понимаете, почему это влияет на выборку.
Назначение: Получать текущую дату или текущий момент времени в зависимости от бизнес-требования.
Простыми словами: Если нужна календарная граница суток — берём CURRENT_DATE;
если нужен «живой» момент — берём NOW().
Для новичка: Для фильтра «за сегодня» чаще всего удобнее опираться на CURRENT_DATE и полуинтервал. CURRENT_DATE берётся в TimeZone текущей сессии; для фиксированной бизнес-TZ задавайте её явно.
Аналогия: CURRENT_DATE — это «лист календаря», NOW() — это «время на часах прямо сейчас».
Пример:
SELECT  CURRENT_DATE AS today_date,  NOW() AS current_moment;
🔎 Как это происходит на практике:
  • Контекст: дашборд «события за сегодня».
  • Действия: границы берут от CURRENT_DATE, а не от «текущей секунды».
  • Результат: выборка стабильно покрывает весь календарный день.
Характеристики:
  • CURRENT_DATE удобен для дневных отчётов;
  • NOW() полезен для онлайн-метрик и SLA;
  • CURRENT_DATE зависит от TZ сессии;
  • путаница между ними часто даёт ошибки на границах периода.
Когда использовать: Когда нужно строго разделять календарные даты и точные временные отметки.
Вывод: CURRENT_DATE и NOW() не конкуренты, а инструменты для разных задач.

⏳ 2. INTERVAL: сдвигаем временные границы

INTERVAL нужен, когда период задаётся относительно текущего момента или даты:
«последние 7 дней», «за 24 часа», «на месяц назад».
🟢 Если совсем просто: INTERVAL — это «добавить/вычесть длительность».
🎯 Как понять, что этап прошёл успешно: Вы уверенно строите запросы с временными окнами без ручных расчётов дат.
Назначение: Динамически формировать диапазоны времени в SQL.
Простыми словами: Вместо фиксированной даты используем «сегодня минус N дней».
Для новичка: Проверяйте, что период выражен тем же типом времени, что и поле фильтра. INTERVAL '30 days' и INTERVAL '1 month' — не одно и то же.
Аналогия: Это как сказать «встречаемся через 2 часа» вместо «в 16:37:12».
Пример:
SELECT  o.id,  o.created_atFROM orders oWHERE o.created_at >= NOW() - INTERVAL '7 days';
Ловушка 1 month vs 30 days:
SELECT  o.id,  o.created_atFROM orders oWHERE o.created_at >= NOW() - INTERVAL '30 days'; SELECT  o.id,  o.created_atFROM orders oWHERE o.created_at >= DATE_TRUNC('month', CURRENT_DATE) - INTERVAL '1 month'  AND o.created_at < DATE_TRUNC('month', CURRENT_DATE);
🔎 Как это происходит на практике:
  • Контекст: мониторинг активности за последнее окно времени.
  • Действия: от текущего момента отнимают нужный интервал.
  • Результат: автоматически обновляемая выборка без ручного редактирования даты.
Характеристики:
  • гибко для rolling-окон;
  • удобно для автоматических отчётов;
  • требует понимания, окно календарное или «скользящее по секундам».
Когда использовать: Когда период задаётся относительно «сейчас» или «сегодня».
Вывод: INTERVAL делает временные фильтры динамическими и поддерживаемыми.

🧪 3. Сравнение дат и времени: фильтруем без ловушек

Самая частая прод-ошибка с датами — неверные границы диапазона.
Для TIMESTAMP безопасный базовый шаблон: >= start AND < end.
🟢 Если совсем просто: Для времени используйте полуинтервал, а не «обе границы включительно».
🎯 Как понять, что этап прошёл успешно: Записи на границе суток не теряются и не дублируются.
Назначение: Фильтровать временные данные без двусмысленностей на границах.
Простыми словами: BETWEEN может быть ловушкой для TIMESTAMP,
потому что '2026-03-05' обычно значит начало суток. BETWEEN start AND end включает обе границы, поэтому на стыках периодов возможен двойной учёт.
Для новичка: Для «за день» используйте: created_at >= DATE '2026-03-05' AND created_at < DATE '2026-03-06'.
Аналогия: Это как правило входа: «с 00:00 включительно до 00:00 следующего дня не включительно».
Пример:
SELECT  o.id,  o.created_atFROM orders oWHERE o.created_at >= DATE '2026-03-05'  AND o.created_at < DATE '2026-03-06';
Антипример (может ухудшить работу индекса):
SELECT  o.id,  o.created_atFROM orders oWHERE DATE(o.created_at) = CURRENT_DATE;
Исправление:
SELECT  o.id,  o.created_atFROM orders oWHERE o.created_at >= CURRENT_DATE  AND o.created_at < CURRENT_DATE + INTERVAL '1 day';
🔎 Как это происходит на практике:
  • Контекст: отчёт «за конкретный день».
  • Действия: задают полуинтервал в точных границах.
  • Результат: нет пропусков записей в конце дня.
Характеристики:
  • шаблон универсален для аналитики и API;
  • снижает ошибки на границах;
  • лучше дружит с индексами, чем DATE(created_at) = ...;
  • легко переносится на недели/месяцы.
Когда использовать: Когда фильтруете TIMESTAMP по календарным периодам.
Вывод: Полуинтервал — базовый стандарт безопасной фильтрации времени.

📊 4. Группировка по датам: день, месяц, час

Для агрегатов важно привести время к нужной гранулярности,
иначе группировка по TIMESTAMP даст почти уникальную строку на каждую запись.
🟢 Если совсем просто: Перед GROUP BY округляйте время до дня/месяца/часа.
🎯 Как понять, что этап прошёл успешно: В отчёте одна строка = один день (или месяц), а не одна строка = один timestamp.
Назначение: Собирать корректные временные агрегаты для отчётов и дашбордов.
Простыми словами: DATE_TRUNC('day', created_at) превращает «точное время» в «корзину дня».
Для новичка: Группировка по форматированной строке (TO_CHAR) обычно хуже по читаемости и поддержке, чем по дате.
Аналогия: Это как сортировать чеки по папкам «день», а не хранить каждый по секунде покупки.
Пример:
SELECT  DATE_TRUNC('day', o.created_at) AS day_bucket,  COUNT(*) AS orders_count,  SUM(o.total_amount) AS revenueFROM orders oWHERE o.created_at >= CURRENT_DATE - INTERVAL '30 days'  AND o.created_at < CURRENT_DATE + INTERVAL '1 day'GROUP BY 1ORDER BY 1;
Важно про TZ и определение «дня»: Сначала фиксируем, в какой зоне бизнес считает день, и только потом строим границы и группировку.
SELECT  DATE_TRUNC('day', o.created_at AT TIME ZONE 'UTC') AS day_utc,  DATE_TRUNC('day', o.created_at AT TIME ZONE 'Europe/London') AS day_londonFROM orders o;
🔎 Как это происходит на практике:
  • Контекст: график заказов по дням.
  • Действия: timestamp приводят к дню и агрегируют.
  • Результат: корректные дневные точки для визуализации.
Характеристики:
  • даёт правильную гранулярность;
  • упрощает построение BI-отчётов;
  • требует единых правил по часовому поясу.
Когда использовать: Когда считаете метрики по периодам (день/неделя/месяц/час).
Вывод: Группировка по датам работает корректно только после явного приведения гранулярности.

🗂️ 5. DATE и TIMESTAMP: как выбрать тип под задачу

DATE и TIMESTAMP выглядят похожими, но несут разный бизнес-смысл.
Неправильный выбор типа усложняет фильтры, индексы и отчёты.
🟢 Если совсем просто: DATE — когда важен только день, TIMESTAMP — когда важен точный момент.
🎯 Как понять, что этап прошёл успешно: Каждое поле времени в схеме отвечает на конкретный вопрос бизнеса.
Назначение: Хранить временные данные в типе, соответствующем реальному смыслу поля.
Простыми словами:
  • birth_date: нужен только день рождения -> DATE;
  • created_at: важна точная секунда создания -> TIMESTAMP/TIMESTAMPTZ.
  • planned_delivery_date: план на день -> DATE;
  • delivered_at: факт момента доставки -> TIMESTAMPTZ.
Для новичка: В PostgreSQL TIMESTAMPTZ хранит момент времени; часовой пояс влияет на ввод/вывод.
Аналогия: Дата рождения — это «страница календаря», событие оплаты — «точка на временной оси».
Пример:
CREATE TABLE users (  id BIGSERIAL PRIMARY KEY,  birth_date DATE,  created_at TIMESTAMPTZ NOT NULL DEFAULT NOW());
Кейс план vs факт:
CREATE TABLE shipments (  id BIGSERIAL PRIMARY KEY,  planned_delivery_date DATE,  delivered_at TIMESTAMPTZ);
🔎 Как это происходит на практике:
  • Контекст: проектирование таблиц пользователей.
  • Действия: отдельно выбирают тип для «даты» и для «момента».
  • Результат: меньше костылей в фильтрах и меньше логических ошибок.
Характеристики:
  • DATE проще для календарных сценариев;
  • TIMESTAMP обязателен для событий;
  • правильный тип снижает сложность SQL в долгую.
Когда использовать: Когда проектируете или ревьюите схему времени в таблицах.
Вывод: Выбор типа времени — это архитектурное решение, а не косметика.

⚡ 6. Комбинируем всё в реальном запросе

В production обычно нужна комбинация:
динамический период + корректные границы + группировка по дням.
🟢 Если совсем просто: Сначала строим окно времени, потом режем данные, потом агрегируем по дню.
🎯 Как понять, что этап прошёл успешно: Запрос стабильно считает дневные метрики за скользящий период.
Назначение: Собрать production-шаблон для отчёта «дневная выручка за 14 дней».
Простыми словами: CURRENT_DATE задаёт календарные границы, INTERVAL двигает окно, DATE_TRUNC агрегирует.
Для новичка: Храните границы периода в CTE, чтобы не дублировать выражения по запросу.
Аналогия: Это как сначала выставить рамку периода, а потом раскладывать события по коробкам-дням.
Пример:
WITH bounds AS (  SELECT    CURRENT_DATE - INTERVAL '14 days' AS date_from,    CURRENT_DATE + INTERVAL '1 day' AS date_to)SELECT  DATE_TRUNC('day', o.created_at) AS day_bucket,  COUNT(*) FILTER (WHERE o.status = 'paid') AS paid_orders,  SUM(o.total_amount) FILTER (WHERE o.status = 'paid') AS paid_revenueFROM orders oCROSS JOIN bounds bWHERE o.created_at >= b.date_from  AND o.created_at < b.date_toGROUP BY DATE_TRUNC('day', o.created_at)ORDER BY day_bucket;
🔎 Как это происходит на практике:
  • Контекст: дашборд по продажам за последние две недели.
  • Действия: задают окно через CTE и агрегируют по дням.
  • Результат: стабильный SQL-шаблон для API и BI.
Характеристики:
  • уменьшает ошибки на границах;
  • легко масштабируется на другие периоды;
  • хорошо читается на ревью.
Когда использовать: Когда делаете регулярные отчёты по времени.
Вывод: Комбинация CURRENT_DATE + INTERVAL + DATE_TRUNC закрывает базовую аналитическую работу с датами.

🆚 Сравнение: DATE vs TIMESTAMP

ТипЧто хранитКогда использоватьРиск неверного выбора
DATEТолько календарный деньДень рождения, дата документа, плановая датаПотеря точности, если нужно время события
TIMESTAMPДата + времяЛоги, заказы, события, статусыСложные фильтры, если нужна только дата
TIMESTAMPTZМомент времени с учётом TZ (PostgreSQL)Межзонные системы, API-событияПутаница, если не зафиксированы TZ-правила
Вывод: Выбирайте тип по смыслу поля, а не «на всякий случай».

🧠 Must-Know (запомнить)

  • CURRENT_DATE и NOW() решают разные задачи: дата против момента.
  • CURRENT_DATE считается в TZ текущей сессии.
  • «День» всегда считается в конкретной TZ (обычно бизнес-TZ), это правило нужно фиксировать явно.
  • Для TIMESTAMP-фильтров базовый безопасный шаблон — >= start AND < end.
  • BETWEEN start AND end включает обе границы; для TIMESTAMP это риск двойного учёта на стыках.
  • INTERVAL удобно использовать для динамических окон (последние N дней).
  • INTERVAL '30 days' — это ровно 30 суток, INTERVAL '1 month' — календарный месяц.
  • Для агрегатов по периодам сначала приводите время (DATE_TRUNC, ::date).
  • Избегайте DATE(created_at) = CURRENT_DATE в фильтрах, используйте диапазон.
  • DATE и TIMESTAMP нельзя выбирать «по привычке» — только по бизнес-смыслу.
  • В PostgreSQL TIMESTAMPTZ хранит момент времени; формат вывода зависит от TZ-сессии.
Вывод: Эти правила закрывают основную практику по датам для junior/middle SQL-задач.

❌ Частые мифы

Миф: CURRENT_DATE и NOW() всегда одинаковы. ✅ Как правильно: CURRENT_DATE — дата, NOW() — дата и время. 📎 Почему это важно: неверный выбор функции ломает диапазоны «за сегодня».
Миф: BETWEEN всегда лучший вариант для диапазона по времени. ✅ Как правильно: для TIMESTAMP обычно безопаснее полуинтервал >= и <. 📎 Почему это важно: уменьшается риск ошибок на последней секунде периода.
Миф: можно хранить все временные поля в одном типе. ✅ Как правильно: DATE и TIMESTAMP выбираются по смыслу конкретного поля. 📎 Почему это важно: правильный тип упрощает запросы и снижает баги.
Миф: группировка по «сырому» timestamp даст дневной отчёт. ✅ Как правильно: сначала приводите timestamp к дню (DATE_TRUNC/::date), потом группируйте. 📎 Почему это важно: иначе получите шум вместо аналитики.

🎤 Часто спрашивают на собеседованиях

Вопрос: В чём разница между CURRENT_DATE и NOW()? ✅ Ответ: CURRENT_DATE возвращает только дату, NOW() — точный момент времени.
Вопрос: Почему для TIMESTAMP часто рекомендуют >= start AND < end? ✅ Ответ: такой полуинтервал надёжно работает на границах и не теряет записи в конце периода.
Вопрос: Когда лучше использовать DATE, а когда TIMESTAMP? ✅ Ответ: DATE — когда важен только день, TIMESTAMP — когда важен точный момент события.
Вопрос: Как правильно сделать отчёт «за сегодня» по timestamp-полю? ✅ Ответ: через диапазон от CURRENT_DATE до CURRENT_DATE + INTERVAL '1 day' с >= и <.
Вопрос: Зачем нужен INTERVAL? ✅ Ответ: чтобы задавать динамические временные окна без ручной подстановки дат.
Вопрос: Как сгруппировать события по дням? ✅ Ответ: привести поле времени к дню (DATE_TRUNC('day', ...) или ::date) и использовать это выражение в GROUP BY.

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

  • Сравнивать timestamp с одной датой без верхней границы периода.
  • Считать «день» без явного TZ-правила в системе с несколькими часовыми поясами.
  • Использовать BETWEEN для точного времени, не проверив включённость границ.
  • Писать WHERE DATE(created_at) = ... и терять производительность на индексах.
  • Группировать по created_at без округления до дня/месяца.
  • Смешивать DATE и TIMESTAMP без явного понимания бизнес-смысла.
  • Писать отчёт «за N дней» через фиксированные даты вручную вместо INTERVAL.
  • Игнорировать часовой пояс в межсервисных системах.
  • Дублировать расчёт границ периода в нескольких местах запроса.
Вывод: Большинство ошибок с датами исчезают, если дисциплинированно задавать тип и границы периода.

✅ Best Practices

  • Для календарных отчётов используйте CURRENT_DATE, для онлайн-сценариев — NOW().
  • Явно фиксируйте TZ, в которой бизнес считает «день», и используйте её в границах/группировке.
  • Для TIMESTAMP-периодов применяйте полуинтервалы (>= и <).
  • Для TIMESTAMP обычно избегайте BETWEEN, особенно на стыках периодов.
  • Нормализуйте гранулярность перед GROUP BY (DATE_TRUNC, ::date).
  • Для равномерных окон используйте 30 days, для календарных месяцев — границы месяца.
  • Выносите временные границы в CTE (bounds), чтобы избежать копипаста.
  • Проверяйте пограничные случаи: начало суток, конец месяца, переход между днями.
  • Выбирайте DATE/TIMESTAMP ещё на этапе проектирования схемы.
  • Для отчётов с бизнес-часовым поясом фиксируйте правило TZ в одном месте.
Вывод: Сильный SQL по датам — это предсказуемые диапазоны, корректная гранулярность и понятные правила времени.

🏁 Заключение

Работа с датами — обязательный фундамент SQL-практики.
Если вы уверенно различаете дату и момент времени,
правильно задаёте интервалы и умеете агрегировать по дням,
то отчёты становятся точными, а код — поддерживаемым.
Вывод: Ключ к датам в SQL: правильный тип, правильные границы, правильная гранулярность.
🎯

Проверьте знания

Закрепите материал — пройдите тест по теме «БЛОК 5. Строки и даты — 16. Работа с датами»

Пройти тест →