АналитикаАнализ данныхАналитик данных

В отчёте за март нужно выбрать все события с временной меткой. Какой дефект граничного условия искажает выб...

В отчёте за март нужно выбрать все события с временной меткой. Какой дефект граничного условия искажает выборку?

SELECT event_id, occurred_at
FROM events
WHERE occurred_at BETWEEN '2024-03-01 00:00:00'
                       AND '2024-03-31 00:00:00';
Проходите собеседования с ИИ помощником Hintsage

Краткий ответ

Запрос включает события с 1 марта до полуночи 31 марта, но исключает почти весь последний день месяца. Причина — правая граница BETWEEN равна ровно 2024-03-31 00:00:00, а не концу 31 марта.

Надёжнее использовать полуоткрытый интервал: occurred_at >= начало и occurred_at < начало следующего месяца.

Исторический контекст

Полуоткрытые интервалы вида [начало, конец) широко применяются в работе со временем, потому что они однозначно задают границы соседних периодов. Для марта это интервал от 1 марта включительно до 1 апреля не включительно.

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

Постановка проблемы

В исходном запросе событие 2024-03-31 00:00:00 будет включено, но событие 2024-03-31 00:00:01 уже не попадёт в результат. Поэтому отчёт недосчитает события почти за весь последний день марта.

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

Подробное решение

Корректный фильтр задаёт левую границу включительно, а правую — исключительно:

SELECT event_id, occurred_at FROM events WHERE occurred_at >= '2024-03-01 00:00:00' AND occurred_at < '2024-04-01 00:00:00';

Так в выборку попадут все события марта, включая весь день 31 марта. Одновременно событие ровно в 2024-04-01 00:00:00 будет исключено и попадёт уже в апрельский отчёт.

Вариант с <= '2024-03-31 23:59:59' хуже: он может пропустить события с долями секунды после этой отметки. Приведение временной метки к типу даты иногда упрощает условие, но может ухудшить использование индекса и скрыть проблемы с часовым поясом.

Границы должны быть согласованы с часовым поясом данных. Если occurred_at хранится в UTC, границы периода следует сначала определить в нужной бизнес-зоне, затем корректно преобразовать в UTC; иначе ошибки возникнут даже при правильном полуоткрытом интервале.

Ситуация из практики

Аналитик строил месячный отчёт по заказам и использовал BETWEEN с последним календарным днём в полночь. В марте заказы после начала 31 марта не учитывались, поэтому выручка месяца оказалась ниже фактической.

Рассматривались три варианта. Можно было указать конец месяца с максимальным временем, но это зависело бы от точности хранения timestamp. Можно было преобразовывать значение к дате, но это могло увеличить объём сканирования и усложнить работу с часовыми поясами.

Выбрали полуоткрытый интервал до 1 апреля не включительно. Он не зависит от точности timestamp, естественно стыкует соседние месяцы и позволяет сохранить простой индексируемый предикат по исходному столбцу.

Что кандидаты часто упускают

  1. Вопрос: Что произойдёт, если граница месяца задана как 2024-04-01 00:00:00 с условием <=?

    Ответ: Событие ровно в полночь 1 апреля будет ошибочно включено в март. Это создаст пересечение периодов, если апрельский запрос использует обычную включительную нижнюю границу. При повторной агрегации одна запись может попасть в оба месяца.

  2. Вопрос: Почему замена timestamp на дату не всегда является лучшим исправлением?

    Ответ: Фильтрация вида DATE(occurred_at) = '2024-03-31' применяет функцию к столбцу. Во многих системах это мешает эффективному использованию индекса или партиционирования по исходной временной метке, хотя конкретное поведение зависит от СУБД и её оптимизатора. Полуоткрытый диапазон обычно сохраняет возможность отфильтровать исходный столбец по диапазону.

  3. Вопрос: Как проверить, что исправленный фильтр не создаёт пропусков или пересечений между соседними периодами?

    Ответ: Нужно проверить граничные значения: событие ровно в начале периода должно попасть только в этот период, а событие ровно в начале следующего периода — только в следующий. Практически полезно сравнить сумму событий за несколько соседних периодов с количеством событий за весь объединённый диапазон и отдельно протестировать timestamps на обеих границах.