BETWEEN в SQL: как выбрать даты между двумя датами

BETWEEN включает обе границы, поэтому для столбца DATE запрос с 1 по 31 января отработает правильно.

Если в столбце хранится время, заказы позже полуночи последнего дня пропадут.

Показываем полуинтервал, формат дат, NULL и индексы на одной таблице в PostgreSQL, MySQL и SQLite.

Статью написал:
Ваня Буявец, продюсер, основатель Checkroi
Ваня Буявец
Основатель Checkroi, продюсер, эксперт в выборе онлайн-курсов
Все 3221 статья автора Подписаться на Телеграм-канал
Одобрено экспертом:
Наташа Буявец, основатель Checkroi, эксперт по онлайн-курсам
Наташа Буявец
Основательница Checkroi, продюсер Youtube-каналов, эксперт по онлайн-курсам
Все 3875 экспертных мнений Подписаться на Телеграм-канал
Обложка: BETWEEN в SQL: как выбрать даты между двумя датами

Чтобы выбрать строки с датой между двумя датами, поставьте в WHERE оператор BETWEEN: SELECT * FROM orders WHERE order_date BETWEEN '2026-01-01' AND '2026-01-31';. Обе границы входят в диапазон, то есть запрос вернёт и 1, и 31 января.

Так работает для столбца типа DATE. Если в столбце хранится ещё и время, BETWEEN молча потеряет почти весь последний день. Для такого столбца пишите условие created_at >= '2026-01-01' AND created_at < '2026-02-01'. Ниже оба случая разобраны на одной таблице в PostgreSQL, MySQL и SQLite. Если запросы SELECT пока в новинку, начните с разбора команды SELECT в MySQL или с рассказа о том, как устроен язык SQL.

CheckroiCheckroiПодборка курсов по SQL423 курса • 41 школаСравните цены, школы, программу и найдите выгодные предложения по обучениюСравнить→

Синтаксис BETWEEN и почему обе границы входят в диапазон

Рой пишет SQL-запрос с BETWEEN за ноутбуком

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

SELECT столбцы
FROM таблица
WHERE столбец BETWEEN начало AND конец;

Запись a BETWEEN x AND y значит ровно то же, что a >= x AND a <= y. Документация PostgreSQL говорит прямо: «BETWEEN treats the endpoint values as included in the range». То же определение дают справка MySQL и справка SQL Server.

Запись Что проверяет Границы
x BETWEEN 1000 AND 3000 x от 1000 до 3000 включены
x NOT BETWEEN 1000 AND 3000 x меньше 1000 или больше 3000 не входят в результат
x >= 1000 AND x < 3000 x от 1000 и строго меньше 3000 правая исключена

Все примеры ниже идут по одной таблице заказов. В ней есть дата заказа без времени и момент создания со временем:

CREATE TABLE orders (
  id         integer PRIMARY KEY,
  customer   varchar(50),
  order_date date,
  created_at timestamp,   -- в MySQL datetime
  amount     integer
);

INSERT INTO orders VALUES
  (1, 'Анна',  '2026-01-30', '2026-01-30 09:15:00', 2400),
  (2, 'Борис', '2026-01-31', '2026-01-31 00:00:00', 1800),
  (3, 'Вера',  '2026-01-31', '2026-01-31 18:42:00', 5200),
  (4, 'Глеб',  '2026-02-01', '2026-02-01 00:00:00', NULL),
  (5, 'Дина',  '2026-02-14', '2026-02-14 12:30:00', 3100),
  (6, 'Егор',  '2026-03-02', '2026-03-02 08:05:00', 760);

Вывод ниже показан из PostgreSQL. Где MySQL или SQLite на тех же данных ведут себя иначе, это сказано рядом с запросом.

Как выбрать даты между двумя датами

Самый частый случай: нужны заказы за январь, а дата лежит в столбце типа DATE. Границы пишите в формате ГГГГ-ММ-ДД, его понимают все СУБД:

SELECT id, customer, order_date
FROM orders
WHERE order_date BETWEEN '2026-01-01' AND '2026-01-31';

 id | customer | order_date
----+----------+------------
  1 | Анна     | 2026-01-30
  2 | Борис    | 2026-01-31
  3 | Вера     | 2026-01-31
(3 rows)

Оба заказа от 31 января на месте, потому что правая граница входит в диапазон. MySQL и SQLite вернули те же три строки.

Конец месяца можно задать первым днём следующего месяца и строгим «меньше». Тогда не нужно помнить, сколько дней в феврале:

SELECT id, customer, order_date
FROM orders
WHERE order_date >= DATE '2026-02-01'
  AND order_date <  DATE '2026-02-01' + INTERVAL '1 month';

 id | customer | order_date
----+----------+------------
  4 | Глеб     | 2026-02-01
  5 | Дина     | 2026-02-14
(2 rows)

Если нужно сгруппировать заказы по месяцам, а не выбрать один, вытащите месяц из даты. Как это сделать, мы разобрали в статье про извлечение года и месяца в PostgreSQL.

Канал основателя Checkroi Вани Буявца3 800 человек читают мой Телеграм-канал про нейросетиСобрал промпты для Claude Code и ChatGPT, разборы ИИ-инструментов и лайфхаки по продвижению бизнеса в одном местеПрисоединиться

Ловушка со временем: куда пропадает последний день

Рой нашёл пропавший заказ за последний день месяца

Теперь тот же январский запрос, но по столбцу created_at, где рядом с датой хранится время:

SELECT id, customer, created_at
FROM orders
WHERE created_at BETWEEN '2026-01-01' AND '2026-01-31';

 id | customer |     created_at
----+----------+---------------------
  1 | Анна     | 2026-01-30 09:15:00
  2 | Борис    | 2026-01-31 00:00:00
(2 rows)

Заказ Веры от 31 января в 18:42 пропал. Строку '2026-01-31' СУБД превращает в 31 января 00:00:00, и всё, что случилось позже полуночи, оказывается за правой границей. Борис попал в выборку только потому, что его заказ создан ровно в полночь. MySQL вернул те же две строки.

SQLite вернул одну Анну. Время там хранится текстом, а строка «2026-01-31 00:00:00» длиннее «2026-01-31» и при сравнении оказывается больше, так что за границу выпал даже полуночный заказ.

Microsoft описывает то же поведение в примере D своей справки: «Если часть времени не указана, по умолчанию используется значение 12:00 A.M.» Строки с временем после полуночи последнего дня запрос не возвращает.

Решение: правую границу сделать строгой и поставить на начало следующего дня.

SELECT id, customer, created_at
FROM orders
WHERE created_at >= '2026-01-01'
  AND created_at <  '2026-02-01';

 id | customer |     created_at
----+----------+---------------------
  1 | Анна     | 2026-01-30 09:15:00
  2 | Борис    | 2026-01-31 00:00:00
  3 | Вера     | 2026-01-31 18:42:00
(3 rows)

Такое условие называют полуинтервалом. Оно одинаково работает для DATE, DATETIME и TIMESTAMP, и его не нужно переписывать, если тип столбца поменяется. Чем эти типы различаются в разных СУБД, собрано в таблице типов данных SQL.

Почему не 23:59:59

Популярный совет дописать к правой границе 23:59:59 на нашей таблице сработает: запрос с BETWEEN '2026-01-01' AND '2026-01-31 23:59:59' вернул всех троих. Но в PostgreSQL TIMESTAMP хранит доли секунды, и событие в 23:59:59.7 снова выпадет:

SELECT TIMESTAMP '2026-01-31 23:59:59.7'
       BETWEEN '2026-01-01' AND '2026-01-31 23:59:59' AS popal;

 popal
-------
 f
(1 row)

С полуинтервалом таких дыр нет ни при какой точности времени.

Выборка за один день

Та же ошибка встречается в запросе «все заказы за 31 января». created_at BETWEEN '2026-01-31' AND '2026-01-31' вернул в PostgreSQL и MySQL только Бориса, потому что диапазон сжался до одной точки, полуночи. SQLite не вернул ни одной строки. Правильный вариант даёт обе строки во всех трёх:

SELECT id, customer, created_at
FROM orders
WHERE created_at >= '2026-01-31'
  AND created_at <  '2026-02-01';

 id | customer |     created_at
----+----------+---------------------
  2 | Борис    | 2026-01-31 00:00:00
  3 | Вера     | 2026-01-31 18:42:00
(2 rows)

Если нужен не список, а число заказов за день, замените список столбцов на функцию COUNT: SELECT count(*) с тем же условием вернёт 2.

Даты за последние N дней в разных СУБД

Отчёт «за последнюю неделю» строится от текущей даты, а функции для неё у каждой СУБД свои. Диапазон в примерах берёт семь дней, включая сегодняшний:

СУБД Условие
PostgreSQL order_date BETWEEN CURRENT_DATE - 6 AND CURRENT_DATE
MySQL order_date BETWEEN CURDATE() - INTERVAL 6 DAY AND CURDATE()
SQLite order_date BETWEEN date('now', '-6 days') AND date('now')
SQL Server order_date BETWEEN DATEADD(day, -6, CAST(GETDATE() AS date)) AND CAST(GETDATE() AS date)

Эти условия годятся для столбца DATE. Для столбца со временем снова нужен полуинтервал: в PostgreSQL это created_at >= CURRENT_DATE - 6 AND created_at < CURRENT_DATE + 1. Без него пропадут сегодняшние записи после полуночи. Как сдвигать дату в SQL Server, описано в справке функции DATEADD.

Формат даты в запросе: где BETWEEN врёт без ошибки

Привычная запись «01.01.2026» в SQL не работает, и каждая СУБД реагирует на неё по-своему.

SELECT id, customer, order_date
FROM orders
WHERE order_date BETWEEN '01.01.2026' AND '31.01.2026';
  • PostgreSQL падает с ошибкой: ERROR: date/time field value out of range: "31.01.2026" и подсказкой сменить настройку datestyle.
  • MySQL возвращает Empty set, 12 warnings. В предупреждениях 1292 написано Incorrect date value: '01.01.2026', но если их не смотреть, кажется, что заказов просто нет.
  • SQLite возвращает все шесть строк, включая февраль и март.

С SQLite хуже всего, потому что ошибки нет совсем. Отдельного типа для дат в нём нет, и по документации SQLite даты хранятся текстом в формате «YYYY-MM-DD HH:MM:SS.SSS». BETWEEN сравнивает такие строки посимвольно, а «2026-02-14» больше «01.01.2026» и меньше «31.01.2026», потому что первый символ «2» лежит между «0» и «3». Запрос проходит, и отчёт тихо врёт.

По той же причине в SQLite ломаются даты без ведущих нулей: SELECT '2026-1-5' BETWEEN '2026-01-01' AND '2026-01-31' вернул 0, хотя 5 января в январь попадает. Решение одно: и в таблице, и в запросе использовать формат ГГГГ-ММ-ДД с нулями.

Справка MySQL советует для дат явное приведение типа: «For best results when using BETWEEN with date or time values, use CAST()». На практике это BETWEEN CAST('2026-01-01' AS DATE) AND CAST('2026-01-31' AS DATE).

Числа, строки и обратный порядок границ

BETWEEN работает с любым типом, который можно сравнивать. Заказы на сумму от 1000 до 3000 ₽:

SELECT id, customer, amount
FROM orders
WHERE amount BETWEEN 1000 AND 3000;

 id | customer | amount
----+----------+--------
  1 | Анна     |   2400
  2 | Борис    |   1800
(2 rows)

Границы перепутаны

Если написать amount BETWEEN 3000 AND 1000, запрос вернёт ноль строк во всех трёх СУБД и не выдаст ни ошибки, ни предупреждения. Меньшая граница всегда идёт первой.

В PostgreSQL есть вариант BETWEEN SYMMETRIC: он сам меняет границы местами, и amount BETWEEN SYMMETRIC 3000 AND 1000 вернул тех же Анну и Бориса. В MySQL и SQLite такого слова нет.

Строки сравниваются по алфавиту

Запрос customer BETWEEN 'А' AND 'В' вернул Анну и Бориса, а Веру нет. Слово «Вера» длиннее одной буквы «В», поэтому в алфавитном порядке стоит после неё и выходит за границу. Порядок здесь тот же, в каком строки выстроит сортировка ORDER BY.

Чтобы взять всех на «А» и «Б», пишите customer >= 'А' AND customer < 'В'. Порядок букв задаёт сопоставление (collation) базы, поэтому на кириллице с буквой «Ё» результат стоит проверить на своих данных. Для поиска по началу слова чаще подходит оператор LIKE.

Ваня БуявецКанал основателя Checkroi Вани БуявцаЗабирайте промпты и обучение по нейросетям в моём Телеграм-каналеБольше 3 800 человек уже применяют Claude Code, ChatGPT и другие нейросети в работе, учёбе, бизнесе и жизниПерейти в канал

NOT BETWEEN и пустые значения

NOT BETWEEN выбирает всё, что лежит вне отрезка:

SELECT id, customer, amount
FROM orders
WHERE amount NOT BETWEEN 1000 AND 3000;

 id | customer | amount
----+----------+--------
  3 | Вера     |   5200
  5 | Дина     |   3100
  6 | Егор     |    760
(3 rows)

Заказ Глеба без суммы не попал ни сюда, ни в выборку BETWEEN выше. Сравнение с NULL даёт «неизвестно», а WHERE пропускает только «истину». В справке SQL Server это сформулировано так: если одна из частей ложна, выражение ложно, «в противном случае выражение оценивается как UNKNOWN».

Если строки без значения тоже нужны, добавьте их явно: WHERE amount NOT BETWEEN 1000 AND 3000 OR amount IS NULL. Подробнее о том, как ведут себя пустые значения, мы писали в статье про NULL в SQL.

Индексы: почему не стоит оборачивать столбец в функцию

Картотека с карточками по датам, одна секция выдвинута

Чтобы обойти ловушку со временем, часто приводят столбец к дате: WHERE CAST(created_at AS date) BETWEEN '2025-03-01' AND '2025-03-07'. Результат верный, но индекс по created_at перестаёт работать. Мы проверили это в PostgreSQL на таблице из 200 тысяч заказов с индексом по времени:

EXPLAIN SELECT count(*) FROM big_orders
WHERE created_at >= '2025-03-01' AND created_at < '2025-03-08';

 Aggregate  (cost=170.92..170.93 rows=1 width=8)
   ->  Index Only Scan using big_orders_created_at_idx on big_orders  (cost=0.42..160.64 rows=4111 width=0)
         Index Cond: ((created_at >= '2025-03-01 00:00:00'::timestamp without time zone) AND (created_at < '2025-03-08 00:00:00'::timestamp without time zone))

EXPLAIN SELECT count(*) FROM big_orders
WHERE CAST(created_at AS date) BETWEEN '2025-03-01' AND '2025-03-07';

 Finalize Aggregate  (cost=4634.52..4634.53 rows=1 width=8)
   ->  Gather  (cost=4634.41..4634.52 rows=1 width=8)
         Workers Planned: 1
         ->  Partial Aggregate  (cost=3634.41..3634.42 rows=1 width=8)
               ->  Parallel Seq Scan on big_orders  (cost=0.00..3632.94 rows=588 width=0)
                     Filter: (((created_at)::date >= '2025-03-01'::date) AND ((created_at)::date <= '2025-03-07'::date))

Оба запроса насчитали 3853 заказа. Первый прочитал только нужный кусок индекса (Index Only Scan), второй перебрал всю таблицу (Seq Scan), и оценка стоимости выросла с 171 до 4635, примерно в 27 раз. На миллионах строк это разница между мгновенным ответом и отчётом, который грузится минуту.

Функцию применяйте к границам диапазона, а столбец в условии оставляйте голым. Полуинтервал так и устроен, поэтому индекс с ним работает.

Частые ошибки и что с ними делать

Симптом Причина Что сделать
Пропали записи за последний день периода В столбце есть время, правая граница стоит на полуночи >= начало AND < следующий день
Запрос вернул ноль строк без ошибок Границы перепутаны местами Меньшую границу первой или BETWEEN SYMMETRIC в PostgreSQL
В SQLite в выборку попали чужие месяцы Дата записана как «ДД.ММ.ГГГГ» или без ведущих нулей Хранить и сравнивать только «ГГГГ-ММ-ДД»
В MySQL пусто и есть предупреждения 1292 Строка не распознана как дата SHOW WARNINGS, формат ГГГГ-ММ-ДД, CAST(... AS DATE)
Не видно строк с пустым значением NULL не проходит ни BETWEEN, ни NOT BETWEEN Добавить OR столбец IS NULL
Отчёт по датам тормозит Столбец обёрнут в функцию, индекс не работает Перенести функцию на границы диапазона

Когда период нужно не отфильтровать, а разложить по категориям вроде «январь», «февраль», «старше», пригодится оператор CASE.

Где освоить SQL системно

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

КурсШколаСтоимость со скидкойВ рассрочкуДлитель­ностьОбзор курса от Checkroi
Нейросети для рабочих задач
Перейти на сайт курса
SkillboxSkillbox31 290 ₽ - 1 месяцОбзор курса
SQL для анализа данных
Перейти на сайт курса
SkillboxSkillbox50 865 ₽6203 ₽/мес.2 месяцаОбзор курса
SQL-разработчик: тариф PRO
Перейти на сайт курса
Академия ЭдюсонЭдюсон89 900 ₽7491 ₽/мес.2 месяцаОбзор курса
SQL-разработчик: тариф Базовый
Перейти на сайт курса
Академия ЭдюсонЭдюсон79 900 ₽3329 ₽/мес.3 месяцаОбзор курса
Data-Driven подход в разработке продукта: SQL и Yandex DataLens
Перейти на сайт курса
ProductStarProductStar29 990 ₽2999 ₽/мес.1 месяцОбзор курса

Больше программ — в полном каталоге курсов по SQL

Все программы по базам данных собраны в каталоге курсов SQL, там их можно сравнить по цене и длительности.

Часто задаваемые вопросы

Включает ли BETWEEN границы диапазона?

Да, обе. Запись a BETWEEN x AND y равна a >= x AND a <= y, поэтому BETWEEN '2026-01-01' AND '2026-01-31' по столбцу DATE вернёт и 1, и 31 января.

Как выбрать записи между двумя датами, если в столбце есть время?

Поставьте строгую правую границу на начало следующего дня: created_at >= '2026-01-01' AND created_at < '2026-02-01'. С BETWEEN по такому столбцу пропадут записи последнего дня позже 00:00.

Как выбрать записи за последние 7 дней?

В PostgreSQL: order_date BETWEEN CURRENT_DATE - 6 AND CURRENT_DATE, в MySQL: order_date BETWEEN CURDATE() - INTERVAL 6 DAY AND CURDATE(), в SQLite: order_date BETWEEN date('now', '-6 days') AND date('now'). Для столбца со временем нужен полуинтервал до завтрашнего дня.

Можно ли писать дату в запросе как 01.01.2026?

Нет. PostgreSQL выдаст ошибку, MySQL вернёт пустой результат с предупреждениями 1292, а SQLite сравнит строки посимвольно и вернёт лишние строки. Пишите дату в формате ГГГГ-ММ-ДД.

Что быстрее: BETWEEN или два условия с >= и <=?

Разницы нет: в плане PostgreSQL условие BETWEEN разворачивается в те же >= и <= и использует индекс. Медленным запрос делает функция над столбцом, например CAST(created_at AS date), из-за неё индекс не работает.

Читайте Checkroi первым в Google
Добавьте Checkroi в избранные источники — и наши разборы курсов и обзоры школ будут показываться выше в вашей выдаче Google.
Оставить комментарий
0 комментариев
Форма комментария

Оставьте комментарий

Напишите, что думаете. Нам важно ваше мнение!