Den här artikeln finns för närvarande på ryska — en engelsk översättning är på gång.
OVERLAPS — это инфиксный оператор стандарта SQL, который отвечает на один вопрос: пересекаются ли два периода времени? Он избавляет от ручного сравнения четырёх дат и идеально подходит для поиска накладок в бронированиях, сменах и подписках.
Базовый синтаксис
Каждый период задаётся парой в скобках: (start, end). Выражение возвращает true, если интервалы имеют общую точку.
SELECT (DATE '2024-03-01', DATE '2024-03-10')
OVERLAPS (DATE '2024-03-08', DATE '2024-03-15') AS does_overlap;
SELECT (DATE '2024-03-01', DATE '2024-03-10')
OVERLAPS (DATE '2024-03-10', DATE '2024-03-20') AS does_overlap;
Второй пример возвращает false, и это главное, что нужно усвоить: границы трактуются как полуоткрытый интервал [start, end). Конец одного периода, совпадающий с началом другого, накладкой не считается — что удобно для встык идущих смен: смена с 09:00 до 17:00 не конфликтует со сменой с 17:00 до 01:00. Если бы граница была включающей, такие соседние смены ложно помечались бы как пересекающиеся.
- Каждый операнд — это пара
(start, end), то есть начало и конец.
- Поддерживаются
date, timestamp, time, а также пара «момент плюс interval».
- Если начало больше конца, PostgreSQL молча меняет их местами.
Поиск конфликтов в бронированиях
Представим, что у нас есть резервации, привязанные к заказам. Нужно найти заказ, чей период пересекается с новым запросом на 5–8 марта:
SELECT o.id, o.user_id
FROM orders o
WHERE (o.created_at, o.created_at + INTERVAL '3 days')
OVERLAPS (TIMESTAMP '2024-03-05', TIMESTAMP '2024-03-08');
Чтобы найти все пары пересекающихся периодов внутри одной таблицы, делаем самосоединение и условием o1.id < o2.id отсекаем дубли и сравнение строки с самой собой:
SELECT o1.id AS first_id, o2.id AS second_id
FROM orders o1
JOIN orders o2
ON o1.user_id = o2.user_id
AND o1.id < o2.id
AND (o1.created_at, o1.created_at + INTERVAL '1 hour')
OVERLAPS (o2.created_at, o2.created_at + INTERVAL '1 hour');
Эквивалент через сравнение дат
OVERLAPS — это синтаксический сахар над классическим условием. Два периода пересекаются тогда и только тогда, когда выполнено:
WHERE a_start < b_end
AND b_start < a_end
Эта форма незаменима, когда OVERLAPS неудобен или недоступен — например, в MySQL, где оператора нет вовсе. Тот же поиск накладок:
SELECT o1.id, o2.id
FROM orders o1
JOIN orders o2
ON o1.user_id = o2.user_id
AND o1.id < o2.id
AND o1.created_at < o2.created_at + INTERVAL '1 hour'
AND o2.created_at < o1.created_at + INTERVAL '1 hour';
Ловушка: распространённая ошибка — писать <= вместо <. Со строгим < смежные периоды (конец = начало) не считаются конфликтом, ровно как у OVERLAPS. Если же вам нужно считать касание накладкой (например, для непрерывного покрытия), берите <= — но тогда поведение разойдётся с OVERLAPS.
Range-типы как альтернатива
В PostgreSQL есть отдельные диапазонные типы (tsrange, daterange, int4range) и оператор пересечения &&. Это читабельнее и, в отличие от OVERLAPS, индексируется через GiST.
SELECT o.id
FROM orders o
WHERE tsrange(o.created_at, o.created_at + INTERVAL '3 days')
&& tsrange(TIMESTAMP '2024-03-05', TIMESTAMP '2024-03-08');
Главное преимущество — constraint исключения: база сама гарантирует, что периоды не пересекутся, без гонок на уровне приложения. Например, нельзя забронировать одного сотрудника на две смены сразу. Важная деталь: ограничение ниже сравнивает id оператором =, а штатный GiST-класс операторов знает только && для диапазонов. Чтобы включить в один GiST-индекс проверку равенства по скалярной колонке, нужно расширение btree_gist. Без него EXCLUDE USING gist (id WITH =, ...) упадёт с ошибкой «data type integer has no default operator class for access method gist». Поэтому сперва выполняем CREATE EXTENSION btree_gist;, и только затем создаём ограничение:
ALTER TABLE employees
ADD COLUMN shift tsrange;
ALTER TABLE employees
ADD CONSTRAINT no_shift_conflict
EXCLUDE USING gist (id WITH =, shift WITH &&);
Любая попытка вставить пересекающуюся смену для того же id упадёт с ошибкой на уровне БД. В ClickHouse ни OVERLAPS, ни range-типов нет — там используют ручное условие a_start < b_end AND b_start < a_end.
Итог: для разовой проверки в WHERE берите OVERLAPS, для переносимости — ручное сравнение, а для индексов и жёстких гарантий целостности — range-типы с EXCLUDE. Все три подхода описывают одно и то же пересечение полуоткрытых интервалов, поэтому переход между ними не меняет результат — меняется лишь читаемость, производительность и место, где живёт проверка.
OVERLAPS— это инфиксный оператор стандарта SQL, который отвечает на один вопрос: пересекаются ли два периода времени? Он избавляет от ручного сравнения четырёх дат и идеально подходит для поиска накладок в бронированиях, сменах и подписках.Базовый синтаксис
Каждый период задаётся парой в скобках:
(start, end). Выражение возвращаетtrue, если интервалы имеют общую точку.SELECT (DATE '2024-03-01', DATE '2024-03-10') OVERLAPS (DATE '2024-03-08', DATE '2024-03-15') AS does_overlap; -- true SELECT (DATE '2024-03-01', DATE '2024-03-10') OVERLAPS (DATE '2024-03-10', DATE '2024-03-20') AS does_overlap; -- falseВторой пример возвращает
false, и это главное, что нужно усвоить: границы трактуются как полуоткрытый интервал[start, end). Конец одного периода, совпадающий с началом другого, накладкой не считается — что удобно для встык идущих смен: смена с 09:00 до 17:00 не конфликтует со сменой с 17:00 до 01:00. Если бы граница была включающей, такие соседние смены ложно помечались бы как пересекающиеся.(start, end), то есть начало и конец.date,timestamp,time, а также пара «момент плюсinterval».Поиск конфликтов в бронированиях
Представим, что у нас есть резервации, привязанные к заказам. Нужно найти заказ, чей период пересекается с новым запросом на 5–8 марта:
SELECT o.id, o.user_id FROM orders o WHERE (o.created_at, o.created_at + INTERVAL '3 days') OVERLAPS (TIMESTAMP '2024-03-05', TIMESTAMP '2024-03-08');Чтобы найти все пары пересекающихся периодов внутри одной таблицы, делаем самосоединение и условием
o1.id < o2.idотсекаем дубли и сравнение строки с самой собой:SELECT o1.id AS first_id, o2.id AS second_id FROM orders o1 JOIN orders o2 ON o1.user_id = o2.user_id AND o1.id < o2.id AND (o1.created_at, o1.created_at + INTERVAL '1 hour') OVERLAPS (o2.created_at, o2.created_at + INTERVAL '1 hour');Эквивалент через сравнение дат
OVERLAPS— это синтаксический сахар над классическим условием. Два периода пересекаются тогда и только тогда, когда выполнено:-- a_start < b_end AND b_start < a_end WHERE a_start < b_end AND b_start < a_endЭта форма незаменима, когда
OVERLAPSнеудобен или недоступен — например, вMySQL, где оператора нет вовсе. Тот же поиск накладок:SELECT o1.id, o2.id FROM orders o1 JOIN orders o2 ON o1.user_id = o2.user_id AND o1.id < o2.id AND o1.created_at < o2.created_at + INTERVAL '1 hour' AND o2.created_at < o1.created_at + INTERVAL '1 hour';Range-типы как альтернатива
В PostgreSQL есть отдельные диапазонные типы (
tsrange,daterange,int4range) и оператор пересечения&&. Это читабельнее и, в отличие отOVERLAPS, индексируется через GiST.SELECT o.id FROM orders o WHERE tsrange(o.created_at, o.created_at + INTERVAL '3 days') && tsrange(TIMESTAMP '2024-03-05', TIMESTAMP '2024-03-08');Главное преимущество — constraint исключения: база сама гарантирует, что периоды не пересекутся, без гонок на уровне приложения. Например, нельзя забронировать одного сотрудника на две смены сразу. Важная деталь: ограничение ниже сравнивает
idоператором=, а штатный GiST-класс операторов знает только&&для диапазонов. Чтобы включить в один GiST-индекс проверку равенства по скалярной колонке, нужно расширениеbtree_gist. Без негоEXCLUDE USING gist (id WITH =, ...)упадёт с ошибкой «data type integer has no default operator class for access method gist». Поэтому сперва выполняемCREATE EXTENSION btree_gist;, и только затем создаём ограничение:ALTER TABLE employees ADD COLUMN shift tsrange; ALTER TABLE employees ADD CONSTRAINT no_shift_conflict EXCLUDE USING gist (id WITH =, shift WITH &&);Любая попытка вставить пересекающуюся смену для того же
idупадёт с ошибкой на уровне БД. ВClickHouseниOVERLAPS, ни range-типов нет — там используют ручное условиеa_start < b_end AND b_start < a_end.Итог: для разовой проверки в
WHEREберитеOVERLAPS, для переносимости — ручное сравнение, а для индексов и жёстких гарантий целостности — range-типы сEXCLUDE. Все три подхода описывают одно и то же пересечение полуоткрытых интервалов, поэтому переход между ними не меняет результат — меняется лишь читаемость, производительность и место, где живёт проверка.