Аналитика24 октября 2026 г.

Как отбирать события за месяц в SQL без ошибки на границе периода

Автор
Ненашев А.
На чтение
6 мин
Дата обновления
24.10.2026
Старт:В любой момент
Срок обучения:2 месяца
SQL для анализа данных
Курс для тех, кому нужно работать с базами данных. Вы освоите язык запросов SQL – и с его помощью сможете самостоятельно получать нужные данные, сопоставлять и анализировать их.
Подробнее
50 828 ₽92 414 ₽
#COURSE##INNER#

Как отбирать события за месяц в SQL без ошибки на границе периода

Почему ошибка появляется именно на границе месяца

Проблема обычно начинается не в середине периода, а ровно на стыке двух месяцев. Если написать фильтр так, будто конец периода включен, например через BETWEEN или условие <= для первого дня следующего месяца, запись с временем 2024-03-01 00:00:00 попадет и в февральскую, и в мартовскую выборку. Ошибка незаметна, пока вы просто смотрите строки, но быстро всплывает в отчетах: суммы по месяцам перестают сходиться, события дублируются, а проверка «по дням все верно» только путает.

Для месячной выборки безопаснее думать не категориями «с начала по конец включительно», а категориями границ. Тогда период февраля — это все, что не раньше 2024-02-01 00:00:00 и строго раньше 2024-03-01 00:00:00. Такой формат называют полуоткрытым интервалом: левая граница включена, правая исключена. Он удобен тем, что соседние периоды не пересекаются и не оставляют дыр.

Отдельная причина ошибок — попытка описать «последний момент месяца» вручную. В PostgreSQL время хранится с точностью до микросекунд, поэтому идея вроде 23:59:59 не покрывает записи с дробной частью секунд. Гораздо надежнее не угадывать конец месяца, а взять начало следующего и сравнивать через <.

Какой тип времени выбрать для такого фильтра

В этом примере я сознательно выбираю поле события типа timestamptz, то есть timestamp with time zone в PostgreSQL, и фиксирую часовой пояс сессии как UTC. Такой выбор полезен, когда событие означает конкретный момент во времени: клик, оплата, отправка письма, создание заказа. PostgreSQL хранит значения timestamptz внутренне относительно UTC, а при выводе показывает их в текущем часовом поясе сессии. Поэтому один и тот же момент можно сравнивать последовательно, если вы заранее договорились, в каком часовом поясе строите границы периода.

Почему не брать timestamp without time zone? Потому что у него есть неприятная ловушка: если передать строку с часовым смещением, PostgreSQL для такого типа его проигнорирует. На практике это означает, что запись может выглядеть «правильно», но уже на этапе загрузки потеряет смысл исходного часового пояса. Для учебного примера лучше не оставлять это место двусмысленным.

Важно и другое: тип столбца и тип границ должны совпадать по смыслу. Если события хранятся как timestamptz, то и границы месяца стоит задавать как timestamptz. Иначе часть преобразований выполнится неявно, а на границе суток вы получите результат, который сложно объяснить спустя неделю.

Учебная выборка: пять событий и две границы месяца

Учебный пример. Ниже — минимальная таблица для PostgreSQL, где есть пять событий: одно перед февралем, одно ровно в начале февраля, одно в середине месяца, одно в последний микромомент февраля и одно ровно в начале марта. Часовой пояс сессии явно установлен в UTC, чтобы результат у всех читателей был одинаковым.

SET TIME ZONE 'UTC';

CREATE TEMP TABLE events (
    event_id integer PRIMARY KEY,
    happened_at timestamptz NOT NULL
);

INSERT INTO events (event_id, happened_at) VALUES
    (1, TIMESTAMPTZ '2024-01-31 23:59:59+00'),
    (2, TIMESTAMPTZ '2024-02-01 00:00:00+00'),
    (3, TIMESTAMPTZ '2024-02-15 12:30:00+00'),
    (4, TIMESTAMPTZ '2024-02-29 23:59:59.999999+00'),
    (5, TIMESTAMPTZ '2024-03-01 00:00:00+00');

Теперь покажу два варианта запроса. Первый выглядит правдоподобно, но на границе периода ошибается. Второй использует полуоткрытый интервал и работает так, как нужно для отчетности и повторяемых выгрузок.

Вариант с ошибкой на правой границе

SELECT event_id, happened_at
FROM events
WHERE happened_at BETWEEN
      TIMESTAMPTZ '2024-02-01 00:00:00+00'
  AND TIMESTAMPTZ '2024-03-01 00:00:00+00'
ORDER BY happened_at;

Этот запрос включит событие 5, потому что BETWEEN включает обе границы. Для одиночной проверки это может показаться удобным, но уже при следующем запросе за март эта же запись снова окажется в результате.

Рабочий вариант

WITH bounds AS (
    SELECT
        TIMESTAMPTZ '2024-02-01 00:00:00+00' AS start_ts,
        TIMESTAMPTZ '2024-03-01 00:00:00+00' AS next_start_ts
)
SELECT e.event_id, e.happened_at
FROM events e
CROSS JOIN bounds b
WHERE e.happened_at >= b.start_ts
  AND e.happened_at < b.next_start_ts
ORDER BY e.happened_at;

Здесь правило читается буквально: взять все события с начала февраля включительно, но строго раньше начала марта. Для смежных периодов это удобно тем, что март можно описать так же: >= '2024-03-01 00:00:00+00' AND < '2024-04-01 00:00:00+00'. Ни пересечения, ни потери строки на стыке не будет.

Как проверить, что в выборке именно нужные события

Проверка должна быть не общей фразой «запрос вернул правильные строки», а сравнением каждого граничного случая. В нашем наборе ожидаемый результат легко расписать до выполнения запроса.

event_idhappened_atОжидание для февраля 2024 в UTCПочему
12024-01-31 23:59:59+00ИсключитьРаньше левой границы
22024-02-01 00:00:00+00ВключитьРовно равно началу периода
32024-02-15 12:30:00+00ВключитьНаходится внутри периода
42024-02-29 23:59:59.999999+00ВключитьВсе еще меньше начала марта
52024-03-01 00:00:00+00ИсключитьРовно равно правой границе, а она исключена

Если выполнить рабочий запрос, вы должны получить события 2, 3 и 4. Итого три строки. Это проверяемый результат: одна запись до периода отсеялась, одна запись на старте следующего месяца тоже отсеялась, а событие на последней микросекунде февраля сохранилось.

Полезно проверить и соседний месяц тем же правилом. Если запросить март как интервал от 2024-03-01 00:00:00+00 до 2024-04-01 00:00:00+00, в март попадет событие 5 и не попадут события 2, 3, 4. Это хороший тест на отсутствие дублей между периодами.

Что делать с часовым поясом и где заканчивается этот пример

В учебном примере выбран UTC не потому, что он обязателен всегда, а потому что он убирает лишнюю неоднозначность. Если ваша бизнес-логика считает месяц по часовому поясу компании, магазина или клиента, границы надо строить именно в этом часовом поясе. Иначе событие около полуночи может сменить месяц только из-за того, что вы сравнили его не с теми границами. Сама идея полуоткрытого интервала при этом не меняется: меняется только способ вычисления start и next_start.

Еще одно ограничение примера: здесь мы не обсуждаем генерацию границ из произвольной даты, календарей отчетности и переходов между разными часовыми поясами на уровне приложения. Задача уже решена на одном важном уровне: когда границы определены, фильтр для PostgreSQL должен быть >= start AND < next_start. Это короткое правило закрывает именно ошибку на границе периода.

Практический вывод можно свести к трем действиям. Первое: определите смысл поля времени и явно выберите тип, для момента события чаще всего подходит timestamptz. Второе: договоритесь о часовом поясе, в котором считается месяц. Третье: всегда фильтруйте месяцы через начало периода и начало следующего периода, а не через «последнюю секунду» месяца. Тогда граничные события будут вести себя предсказуемо и при разовых запросах, и в регулярной отчетности.

Если хотите отдельно потренировать фильтрацию строк и работу с датой и временем, во внутренней карточке каталога курса SQL для анализа данных в сохраненной программе перечислены темы по фильтрам SELECT и функциям даты и времени. Это как раз тот набор, который нужен для уверенной проверки подобных запросов на практике.

Курсы

3798
4,9
11 отзывов
SQL для анализа данных
  • В любой момент
  • 2 месяца
  • Онлайн
  • Диплом
50 828 ₽92 414 ₽
-45%
Перейти на сайт