Как работать с датами и временем в SQL (на примере PostgreSQL)
Как работать с датами и временем в SQL (на примере PostgreSQL)
Рассказываем, как избежать ошибок при работе с датой и временем и какие форматы данных можно использовать в основных операциях.
Время в данных — это не просто «фильтр по дате». Это календарные правила, часовые пояса, особенности недель и переходов на летнее время, индексы и пр. Если их неправильно записывать, могут возникать неочевидные ошибки. Например, отчёт за «вчера» может незаметно начать учитывать 23-часовой или 25-часовой день, а за «январь» — включать события из другого месяца. В SQL есть три разные сущности, которые часто путают:
Время можно записывать по-разному в зависимости от вида события. Виды делят на событие в моменте и локальное время. Расскажем об этом подробнее.
Для момента можно использовать два подхода:
В PostgreSQL момент и локальное время записывают как timestamptz и timestamp:
Работать с данными и аналитикой на языке SQL учат на курсе «SQL для работы с данными и аналитики». За 3 месяца студенты осваивают навыки работы с PostgreSQL, чтобы применять их в других СУБД. Курс подходит для опытных специалистов и новичков в аналитике, а также для специалистов смежных профессий, например маркетологов и финансистов.
Форматы данных даты и времени на примере PostgreSQL
Типы данных времени на примере PostgreSQL
Операций и фильтраций по датам может быть много. Мы расскажем про основные шесть.
1. Нужно посчитать количество заказов. Допустим, нужно посчитать, сколько было заказов с 1 по 30 сентября:
Смотрим количество строчек с помощью COUNT(*), добавляем фильтрацию в WHERE с помощью диапазона дат.
2. Когда нужно перевести дату в одном часовом поясе:
Сначала устанавливаем время сессии в UTC. При попытке перевести дату и время с часовым поясом +3 только в формат даты получаем 2025-05-31, так как изначально «0:30 в часовом поясе +3». Но если мы находимся в часовом поясе Москвы, то получаем дату «2025-06-01». Чтобы получить стабильную дату в UTC, можно использовать:
Когда нужно «усечь» дату. Существует удобная функция — date_trunc (). Она позволяет «усечь» до определённого момента. Например, при усечении до дня из «2025-06-01 7:30» получим «2025-06-01 0:00».
Если использовать date_trunc ('day', timestamptz), то может показаться, что усечение происходит по UTC. Но это не так. Границы «00:00» берутся в текущем TIME ZONE.
timestamptz — это момент. Когда мы «усекаем до дня», Postgres берёт полночь в текущем часовом поясе сеанса и возвращает момент, соответствующий этой локальной полуночи. Если поменять TIME ZONE, изменится и результат.
bucket_utc и bucket_msk — это разные точки времени (бакеты). Если нужны дни по UTC независимо от настроек сеанса, явно переводите во временную зону UTC в запросе:
3. Когда нужно, чтобы 1 января не считалось прошлым годом. Например, неделя по ISO начинается в понедельник, «первая неделя» — та, где есть четверг января. Поэтому 1 января может относиться к неделе прошлого года.
Чтобы корректно и прозрачно сделать запрос, можно использовать ISO:
ISO 8601 — международный стандарт форматов даты/времени. Для недель он задаёт следующее:
● неделя начинается в понедельник;
● первая неделя года — та, в которой есть четверг (или (эквивалентно) которая содержит 4 января);
● часть дат в начале/конце календарного года может относиться к ISO-году соседнего года.
В PostgreSQL:
● EXTRACT (isoweek FROM ts) — номер ISO-недели (1–53);
● EXTRACT (isoyear FROM ts) — ISO-год (может отличаться от календарного);
● EXTRACT (isodow FROM ts) — день недели (понедельник = 1, воскресенье = 7).
Пример в коде:
4. Как нужно посчитать, сколько прошло секунд:
Получился тип interval, что не всегда удобно для дальнейшего использования. В результате такого запроса получилось чистое число секунд:
● EPOCH — это количество секунд с начала Unix-эпохи (1970-01-01 00:00:00+00).
● В PostgreSQL EXTRACT (EPOCH FROM…) всегда возвращает число в секундах (тип double precision).
● Для интервалов (как в нашем запросе) это просто «сколько секунд длился интервал». Он может быть отрицательным, если started_at > now ().
Для timestamptz epoch считает секунды от UTC-эпохи — отображение пояса не влияет на результат.
Если делать INTERVAL '1 month', то при переводе в секунды PostgreSQL берёт 30 дней на месяц (2 592 000 секунд) — это результат соглашения, а не календарная точность. Для разницы реальных дат лучше сначала вычесть даты/времена (получить интервал), а уже потом брать epoch.
Но в календаре месяцы разные: 28, 29, 30, 31 день. Поэтому «секунды в месяце» зависят от конкретных дат.
Пример, как делать не надо:
Пример правильного кода: делаем разницу конкретных дат, а уже её переводим в секунды:
INTERVAL '1 month' удобно добавлять к дате. Например, «через месяц от X».
Но переводить «месяц» сразу в секунды нельзя, если нужна календарная точность.
Всегда берём разницу конкретных дат/времён, а затем EXTRACT (EPOCH FROM …).
5. Когда важно получить реальное «сейчас». Есть удобная функция now (). Но она получает время в момент срабатывания и «замораживает» его. Простыми словами, функция хранит время после получения и до конца выполнения запроса. Чтобы время всегда было актуальным, можно использовать:
● now () ≡ transaction_timestamp () — время начала транзакции.
● statement_timestamp () — время старта текущего SQL-оператора.
● clock_timestamp () — часы в реальном времени (обновляется даже внутри запроса).
Представьте, что вы выполняете огромный список операций начисления бонусов пользователям в онлайн-игре. Для упрощения представим, что это выполняется посредством ручного, сложного запроса для всех игроков, и весь пул запросов будет идти целый час. Допустим, выполнять начали в 16:00. Тогда:
● при now () получится «время зачисления бонусов — 16:00»;
● при clock_timestamp () получится реальное время зачисления бонусов на каждое такое событие.
6. Неочевидный фильтр по дате. Допустим, нужно отсортировать все события по дате с 2025-09-01 до 2025-09-30. Сразу возникает желание использовать BETWEEN в WHERE-условии:
Но в таком случае получится событие только до 2025-09-30 0:00:00. То есть фактически «до 29 сентября включительно», хотя корректнее сказать «до 30 сентября не включительно».
Правильнее будет взять «до 1 октября не включительно», что соответствует всем событиям с начала до последних секунд сентября. Такой код вернёт корректный результат:
Либо используйте ключевое слово DATE — это позволит уйти от проблемы с «обрезанием по времени».
Памятка по основным функциям
Основные функции даты и времени в SQL
Рекомендации эксперта
Читать также: