16. Работа с датами
🧭 Введение: почему даты ломают отчёты чаще, чем кажется
С датами и временем в SQL почти всегда возникают ошибки на границах:
«за сегодня» даёт не те строки, «за месяц» прыгает из-за времени,
а группировка «по дням» неожиданно раздроблена на тысячи значений.
«за сегодня» даёт не те строки, «за месяц» прыгает из-за времени,
а группировка «по дням» неожиданно раздроблена на тысячи значений.
Ключевая проблема в том, что дата и момент времени — это разные сущности.
Если их смешать без правил, отчёт может выглядеть правдоподобно, но быть неверным.
Если их смешать без правил, отчёт может выглядеть правдоподобно, но быть неверным.
В этой лекции вы разберёте базовый набор, который нужен в реальной работе:
группировку по датам и выбор между
CURRENT_DATE, NOW(), INTERVAL, корректные сравнения диапазонов,группировку по датам и выбор между
DATE и TIMESTAMP.💡 Совет:
Для любого фильтра по времени сначала определяйте тип данных (
потом фиксируйте границы диапазона, и только после этого пишите
DATE или TIMESTAMP),потом фиксируйте границы диапазона, и только после этого пишите
WHERE.✅ Вывод:
Работа с датами — это не «синтаксис ради синтаксиса», а контроль точности аналитики и бизнес-логики.
⚠️ Проблема -> решение
Типичная проблема:
- сравнивают
TIMESTAMPкак обычную дату без учёта времени; - используют
BETWEENтам, где нужен полуинтервал; - группируют по «сырым» timestamp-полям.
Последствия:
- пропуски и дубли на границах периода;
- неверные метрики по дням/месяцам;
- споры «почему отчёт не сходится с дашбордом».
Решение:
- явно выбирать
DATEилиTIMESTAMPпод задачу; - строить фильтры через полуинтервал (
>=и<); - использовать явную нормализацию времени для группировок (
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: правильный тип, правильные границы, правильная гранулярность.