Чтобы выбрать строки с датой между двумя датами, поставьте в 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.
Синтаксис 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.
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 |
|---|---|---|---|---|---|
| Нейросети для рабочих задач Перейти на сайт курса | 31 290 ₽ | - | 1 месяц | Обзор курса | |
| SQL для анализа данных Перейти на сайт курса | 50 865 ₽ | 6203 ₽/мес. | 2 месяца | Обзор курса | |
| SQL-разработчик: тариф PRO Перейти на сайт курса | 89 900 ₽ | 7491 ₽/мес. | 2 месяца | Обзор курса | |
| SQL-разработчик: тариф Базовый Перейти на сайт курса | 79 900 ₽ | 3329 ₽/мес. | 3 месяца | Обзор курса | |
| Data-Driven подход в разработке продукта: SQL и Yandex DataLens Перейти на сайт курса | 29 990 ₽ | 2999 ₽/мес. | 1 месяц | Обзор курса |
Больше программ — в полном каталоге курсов по SQL
Все программы по базам данных собраны в каталоге курсов SQL, там их можно сравнить по цене и длительности.




