В запросе нужно сопоставить товар со всеми подходящими ценовыми диапазонами. Как формируется результат этого INNER JOIN?
WITH products(id, price) AS (
VALUES (1, 10), (2, 75), (3, 150)
), bands(name, min_price, max_price) AS (
VALUES ('regular', 0, 100), ('special', 50, 200)
)
SELECT p.id, b.name
FROM products AS p
JOIN bands AS b
ON p.price BETWEEN b.min_price AND b.max_price;
Каждая пара строк из products и bands попадёт в результат, если условие ON истинно. Это соединение по произвольному предикату, а не сопоставление по равенству: один товар может соответствовать нескольким диапазонам, одному диапазону или ни одному.
В примере товар с ценой 75 появится дважды — для диапазонов regular и special. Цена 10 даст только regular, а цена 150 — только special.
В реляционной алгебре соединение можно рассматривать как декартово произведение отношений, отфильтрованное условием. Равенство ключей — лишь самый распространённый частный случай, но для практических задач нужны сравнения, диапазоны и другие предикаты.
SQL сохранил эту общую модель: условие ON может описывать не только равенство, но и любой подходящий логический предикат. Такой вариант часто называют theta join, а соединение по диапазону — его частным практическим случаем.
Если разработчик предполагает, что соединение всегда сопоставляет одну строку слева с одной строкой справа, он может неверно посчитать суммы, дублировать товары или потерять строки при последующей агрегации.
В приведённом запросе диапазоны перекрываются. Поэтому строка товара с ценой 75 соединяется с двумя строками bands. INNER JOIN не устраняет такие повторы: каждая подходящая пара является отдельной строкой результата.
Логически СУБД рассматривает возможные пары строк и проверяет условие p.price BETWEEN b.min_price AND b.max_price. BETWEEN эквивалентен проверке p.price >= b.min_price AND p.price <= b.max_price, то есть обе границы включаются.
Для внутреннего соединения в результат попадают только пары, для которых условие ON имеет значение TRUE. Значения FALSE и UNKNOWN не создают строку результата; поэтому NULL в цене или в одной из границ обычно не даёт совпадения.
Количество строк определяется числом подходящих пар, а не количеством строк в одной из таблиц. Одна строка товара может породить несколько строк результата, если диапазоны пересекаются. Если диапазоны не пересекаются и полностью покрывают нужные значения, для каждого товара может быть не более одного совпадения, но это свойство следует из данных и ограничений, а не из самого JOIN.
Для внутреннего соединения аналогичная логика обычно выражается как декартово произведение с фильтрацией:
Однако это логическая эквивалентность для данного типа соединения, а не требование к физическому плану. Оптимизатор может выбрать индексный поиск, соединение с циклом или другой план, не материализуя полный декартов результат.
INNER JOIN подходит, когда нужны только найденные соответствия. Если требуется сохранить товары без подходящего диапазона, нужен LEFT JOIN; при этом условия, относящиеся к правой таблице, важно размещать в ON, иначе последующая фильтрация в WHERE может исключить строки без соответствия.
В интернет-магазине нужно применить тариф доставки по весу заказа. Вариант с соединением заказа с таблицей диапазонов позволяет хранить тарифы как данные и менять их без переписывания запроса. Его преимущество — наглядность и возможность использовать дополнительные признаки, например регион или тип клиента; недостаток — риск нескольких совпадений при пересечении диапазонов.
Второй вариант — искать тариф в прикладном коде. Он может быть быстрым для одного заказа, но усложняет согласованность правил, не подходит для массового SQL-расчёта и создаёт риск различий между сервисами.
Третий вариант — заранее сопоставлять каждое возможное значение с тарифом в отдельной таблице. Такой подход упрощает равенственное соединение, но увеличивает объём данных и неудобен для непрерывных числовых диапазонов.
Практически выбирают соединение по диапазону, если правила должны храниться в базе. При этом отдельно обеспечивают отсутствие перекрытий диапазонов — например, процессом публикации тарифов или ограничениями, доступными конкретной СУБД. Стандартный SQL не предоставляет универсального ограничения, запрещающего пересечение произвольных интервалов.
INNER JOIN по диапазону вернуть больше строк, чем есть товаров?Да. Каждая подходящая пара строк становится отдельной строкой результата. Если один товар соответствует трём диапазонам, он появится три раза. Поэтому перед COUNT, SUM или записью результата в таблицу нужно проверить, допускаются ли множественные совпадения.
NULL?Сравнения с NULL дают UNKNOWN, а не TRUE или FALSE. Следовательно, условие p.price BETWEEN ... не выполнится, и такой товар не попадёт во внутреннее соединение. В LEFT JOIN товар сохранится, но столбцы диапазона будут заполнены NULL.
ON в WHERE может изменить результат при замене на LEFT JOIN?В LEFT JOIN условие ON определяет, какие строки правой таблицы считаются совпавшими, но сохраняет левую строку даже при отсутствии совпадения. Условие WHERE применяется уже после формирования результата и может удалить строки, где правые столбцы имеют NULL.
Например, проверка b.name = 'special' в WHERE фактически исключит товары без диапазона, тогда как та же проверка в ON сохранит их с пустыми правыми столбцами. Для INNER JOIN такая перестановка обычно не меняет логический результат, но для внешнего соединения это уже разные операции.