SQL-фильтр за месяц: как сохранить вечер последнего дня и не захватить следующий
Отчёт за октябрь сходится почти полностью, но покупка 31 октября в 23:47 не попала в результат. В запросе стоит created_at <= '2026-10-31'. Когда значение поля содержит ещё и время, запись позднее полуночи 31 октября оказывается за такой верхней границей. Добавлять 23:59:59 неудобно: событие с дробной частью секунды снова выпадет.
Граница следующего месяца проще последней секунды
Для октября задайте интервал с 1 октября включительно до 1 ноября исключительно: created_at >= '2026-10-01 00:00:00' AND created_at < '2026-11-01 00:00:00'. Нижняя граница принадлежит октябрю, верхняя уже принадлежит ноябрю. Следующий отчёт начнётся ровно там, где закончился первый. Одна и та же запись не окажется сразу в двух отчётах, а момент прямо перед полуночью не потеряется при любой точности дробных секунд.
Вот самодостаточная проверка для SQLite. Здесь created_at хранится как текст в едином формате YYYY-MM-DD HH:MM:SS.SSS; это важно для обычного строкового сравнения:
WITH orders(id, created_at) AS (
VALUES
(1, '2026-10-01 00:00:00.000'),
(2, '2026-10-31 23:47:00.000'),
(3, '2026-10-31 23:59:59.999'),
(4, '2026-11-01 00:00:00.000')
)
SELECT id, created_at
FROM orders
WHERE created_at >= '2026-10-01 00:00:00.000'
AND created_at < '2026-11-01 00:00:00.000'
ORDER BY id;
Результат — строки 1, 2 и 3. Строка 4 уйдёт в ноябрьский отчёт. Если заменить верхнее условие на <= '2026-10-31', вечерние строки исчезнут: у текстовых значений после даты идёт ещё время, а строка '2026-10-31' короче. Именно такой маленький набор у границ полезно держать рядом с рабочим отчётом.
Сначала определите, что означает «октябрь»
В примере все значения уже записаны в одной временной шкале. В реальной системе «октябрь по Москве» и «октябрь в UTC» задают разные моменты начала и конца. Если события хранятся в UTC, сначала вычислите UTC-моменты, соответствующие локальной полуночи 1 октября и 1 ноября, и только затем подставьте их в запрос. При переходах на летнее время не прибавляйте к началу фиксированное число часов или секунд за весь месяц: вычисляйте обе календарные границы отдельно в нужной зоне.
У SQLite нет отдельного типа даты и времени: значения могут лежать текстом, числом дней или Unix-временем. Приведённое сравнение текста корректно только при одном и том же нормализованном формате и одной временной шкале. Если в столбце смешаны 2026-10-31 23:47:00, 2026-10-31T23:47:00Z и значения с разными смещениями, сначала приведите хранение или условие к общему представлению. Для числового Unix-времени сравнивайте числовые границы.
Как проверить рабочий отчёт
Сверьте записи ровно на нижней границе, поздно вечером последнего дня и ровно на верхней границе. Затем выполните два соседних отчёта и убедитесь, что ни один id не появился в обоих и не пропал из их общего периода. Если для created_at есть индекс, сравнение самого столбца с двумя границами также оставляет оптимизатору возможность использовать диапазонный поиск. Оборачивание столбца в date(created_at) меняет выражение условия; план такого запроса следует проверять отдельно через EXPLAIN QUERY PLAN.
Та же схема работает для дня, недели или отчётного квартала: берите начало нужного периода и начало следующего. Это снимает необходимость угадывать последнюю представимую долю секунды. Для событий с NULL в поле времени отдельно определите правило отчёта: обычное сравнение границ такие строки не включит.
Если отчёт стал сложнее одного фильтра
В платном курсе «Продвинутый SQL: сложные запросы и задачи для собеседований» вы продолжите работать с календарными рядами, пропущенными датами, интервалами и сверкой отчётов. Практика проходит в SQLite: после разобранных примеров вы пишете запросы с автоматической проверкой на разных наборах данных, а в итоговом проекте собираете аналитическую витрину интернет-магазина. Откройте урок «AS-OF JOIN: выбор исторического значения» и посмотрите программу курса: в ней есть отдельные темы о временных рядах и сверке результатов.
Продолжить работу с отчётами помогут разборы аналитических SQL-запросов; для фильтра по индексированному столбцу пригодится проверка составного индекса.
