Программирование SQLJOIN, подзапросы и CTEРазработчик SQL и аналитических запросов

При разбиении фильтра с OR на две ветви UNION ALL в каком случае изменится число строк результата?

При разбиении фильтра с OR на две ветви UNION ALL в каком случае изменится число строк результата?

Проходите собеседования с ИИ помощником Hintsage

Краткий ответ

Число строк увеличится, если одна исходная строка удовлетворяет обоим условиям: исходный фильтр с OR возвращает её один раз, а UNION ALL добавляет её из каждой подходящей ветви. Такое преобразование эквивалентно только при взаимоисключающих условиях либо при дополнительном контроле дубликатов.

Исторический контекст

Разбиение условия OR на несколько запросов применяют, чтобы дать оптимизатору возможность независимо использовать индексы или разные стратегии доступа для каждой ветви. Особенно полезен этот подход, когда сложное условие плохо оптимизируется как единое выражение.

Однако SQL обычно сохраняет дубликаты, если явно не указано обратное. Поэтому преобразование должно учитывать не только логическую эквивалентность условий, но и правила формирования множества строк.

Постановка проблемы

Пусть строка подходит одновременно под оба предиката. В исходном запросе логическое выражение условие_1 OR условие_2 имеет истинное значение, и строка попадает в результат один раз.

После разбиения с UNION ALL та же строка попадёт в первую и вторую ветви. В результате появится две строки. Ошибка особенно опасна в отчётах, подсчётах и дальнейшей агрегации: суммы и количества могут быть завышены.

Подробное решение

UNION ALL объединяет результаты без удаления дубликатов. Поэтому преобразование безопасно, если ветви не пересекаются или если бизнес-логика допускает повторное появление строк.

Например, условия по статусу и региону пересекаются для европейских заказов со статусом paid:

WITH orders(id, status, region) AS ( VALUES (1, 'paid', 'EU'), (2, 'paid', 'US'), (3, 'new', 'EU') ) SELECT id FROM orders WHERE status = 'paid' OR region = 'EU'; SELECT id FROM orders WHERE status = 'paid' UNION ALL SELECT id FROM orders WHERE region = 'EU';

Первый запрос возвращает три строки, а второй — четыре: заказ с идентификатором 1 присутствует в обеих ветвях. Замена UNION ALL на UNION уберёт повторяющиеся строки результата, но это не всегда эквивалентно исходному запросу: если исходная таблица или соединение уже содержат две одинаковые строки, UNION дополнительно их дедуплицирует.

Надёжный вариант — сделать ветви взаимоисключающими. Вторая ветвь должна выбирать строки, подходящие под второе условие, но не подходящие под первое. При этом необходимо учитывать трёхзначную логику SQL: значение UNKNOWN, возникающее из-за NULL, не равно TRUE, поэтому простое отрицание условия может требовать отдельной проверки.

Если корректно построить непересекающиеся ветви невозможно, следует сохранить исходную форму запроса либо применять дедупликацию с чётко определёнными правилами. Цена безопасного решения — возможная дополнительная сортировка, хеширование или потеря преимуществ независимого доступа по индексам.

Ситуация из практики

В отчёте нужно выбрать товары, которые либо имеют статус discounted, либо относятся к категории featured. Разработчик разделил запрос на две индексируемые ветви через UNION ALL. Товары, удовлетворяющие обоим признакам, начали учитываться дважды, поэтому количество товаров и сумма продаж выросли.

Вариант с UNION устранил повторения, но оказался неверным для отчёта, где одинаковые строки из разных источников должны сохраняться. Вариант с одной ветвью и OR был семантически корректен, однако использовал менее эффективный план.

Выбрали две взаимоисключающие ветви: первая выбирала товары со статусом discounted, а вторая — featured, исключая уже отобранные первой ветвью. Это сохранило возможность раздельной оптимизации и исходную кратность строк; после проверки случаев с NULL результат совпал с исходной логикой.

Что кандидаты часто упускают

  1. Достаточно ли заменить UNION ALL на UNION, чтобы получить эквивалентный запрос?

Нет. UNION удаляет дубликаты полного результата, тогда как исходный фильтр с OR не удаляет дубликаты, уже существующие в исходном наборе или появившиеся после соединения. Такая замена может изменить как количество строк, так и результаты последующей агрегации.

  1. Можно ли сделать ветви взаимоисключающими простым добавлением отрицания первого условия ко второй ветви?

Не всегда. При NULL отрицание может дать UNKNOWN, а не TRUE. Например, если первое условие сравнивает столбец со значением, то строка с NULL может не попасть ни в ветвь первого условия, ни в ветвь с его обычным отрицанием. Взаимоисключение нужно проверять с учётом того, какие строки исходное выражение OR считает истинными.

  1. Почему проблема дубликатов становится заметнее после JOIN?

Соединение само может порождать несколько строк для одной сущности, если правая сторона не уникальна. Разбиение фильтра через UNION ALL затем может добавить одну и ту же уже размноженную строку из нескольких ветвей. Поэтому проверять эквивалентность нужно по фактическим строкам результата и их кратности, а не только по уникальным идентификаторам сущностей.