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-запросов; для фильтра по индексированному столбцу пригодится проверка составного индекса.