Почему соединение с условием OR может вернуть несколько строк для одной строки слева, даже если каждый участвующий ключ уникален?
Уникальность каждого отдельного ключа не гарантирует единственность результата условия с OR. Одна строка слева может соответствовать разным строкам справа: одна — по первому предикату, другая — по второму. При этом одна и та же строка справа, удовлетворяющая обеим частям OR, сама по себе не дублируется.
JOIN в реляционном SQL задаёт множество пар строк, для которых истинно общее условие соединения. Возможность использовать произвольные логические выражения, включая OR, нужна для моделирования альтернативных правил соответствия, когда связь может определяться несколькими признаками.
Проблема возникает, если разработчик переносит в такое условие интуицию обычного соединения по одному уникальному ключу. Уникальность действует на конкретный ключ или ограничение, но не автоматически на объединение нескольких условий.
Предположим, строка клиента может быть сопоставлена с контактом либо по идентификатору клиента, либо по адресу электронной почты. Даже если customer_id и email уникальны по отдельности, разные контакты могут удовлетворить разные части условия.
В результате строка клиента появится несколько раз. Это особенно опасно перед агрегацией: сумма, количество или другие показатели могут быть завышены.
Логически соединение проверяет условие для каждой пары «строка слева — строка справа». Для условия с OR пара подходит, если истинна хотя бы одна его часть. SQL не выполняет две независимые выборки с последующим обязательным удалением пересечений; он формирует результат соединения по подходящим парам.
Например:
Для клиента с идентификатором 1 подходят две разные строки контактов. Поэтому результат содержит две строки, хотя каждый из столбцов customer_id и email уникален в приведённых данных.
Важно различать два случая. Если одна строка справа удовлетворяет обеим частям OR, она возвращается один раз, поскольку результатом является одна подходящая пара. Несколько строк появляются только тогда, когда существует несколько подходящих пар.
Безопасная замена такого соединения зависит от бизнес-правила. Если нужен максимум один контакт, необходимо явно определить приоритет и способ выбора: например, сначала сопоставлять по идентификатору, а по email использовать только при отсутствии результата. Простое добавление DISTINCT обычно лишь скрывает проблему: оно удаляет совпавшие результирующие строки, но не определяет, какая строка должна победить, и может удалить действительно разные данные.
Для контроля результата можно сначала разделить альтернативные правила, пометить источник совпадения и затем выбрать одну строку по определённому приоритету. При этом нужно отдельно решить, что делать с конфликтом, когда у клиента есть совпадения по обоим правилам.
В системе миграции клиентов запись из нового источника сопоставляли с историческим клиентом по условию «совпадает старый идентификатор или email». Для части клиентов оба признака указывали на разные исторические записи, поэтому последующая агрегация заказов удваивала оборот.
Рассматривались два варианта. DISTINCT был простым, но не устранял неоднозначность и мог скрыть конфликт данных. Разделение на две ветви с последующим приоритетным выбором было прозрачнее, но требовало явно определить правила разрешения конфликтов.
Выбрали второй вариант: совпадение по идентификатору получило более высокий приоритет, email использовался только при отсутствии такого совпадения, а случаи двух независимых совпадений отправлялись на проверку качества данных. Это сохранило корректность агрегатов и сделало спорные сопоставления наблюдаемыми.
Нет. Для конкретной пары строк условие либо истинно, либо ложно; истинность обеих частей не создаёт две копии пары. Дублирование возникает из-за нескольких различных строк справа, каждая из которых образует подходящую пару с одной строкой слева.
Не обязательно. DISTINCT удаляет только полностью одинаковые результирующие строки. Если совпавшие строки справа различаются хотя бы одним выбранным столбцом, обе останутся. Даже если строки выглядят одинаково после проекции, DISTINCT скроет факт неоднозначного сопоставления, не определив правильную запись.
Уникальный составной ключ гарантирует уникальность комбинации его компонентов, но условие с OR может использовать разные наборы столбцов. Строка, найденная по одному набору признаков, и строка, найденная по другому, могут быть разными и обе удовлетворять общему условию. Гарантия единственности появляется только при доказательстве, что объединённое условие функционально определяет не более одной строки справа для каждой строки слева.