Использование EXCEPT в рекурсивном общем табличном выражении

33

Почему следующий запрос возвращает бесконечные строки? Я бы ожидал, что EXCEPTпункт прекратить рекурсию ..

with cte as (
    select *
    from (
        values(1),(2),(3),(4),(5)
    ) v (a)
)
,r as (
    select a
    from cte
    where a in (1,2,3)
    union all
    select a
    from (
        select a
        from cte
        except
        select a
        from r
    ) x
)
select a
from r

Я сталкивался с этим, пытаясь ответить на вопрос о переполнении стека.

Том Хантер
источник

Ответы:

26

См . Ответ Мартина Смита для получения информации о текущем статусе EXCEPTрекурсивного CTE.

Чтобы объяснить, что вы видели и почему:

Я использую переменную таблицы здесь, чтобы сделать различие между значениями привязки и рекурсивным элементом более четким (это не меняет семантику).

DECLARE @V TABLE (a INTEGER NOT NULL)
INSERT  @V (a) VALUES (1),(2)
;
WITH rCTE AS 
(
    -- Anchor
    SELECT
        v.a
    FROM @V AS v

    UNION ALL

    -- Recursive
    SELECT
        x.a
    FROM
    (
        SELECT
            v2.a
        FROM @V AS v2

        EXCEPT

        SELECT
            r.a
        FROM rCTE AS r
    ) AS x
)
SELECT
    r2.a
FROM rCTE AS r2
OPTION (MAXRECURSION 0)

План запроса:

План рекурсивного CTE

Выполнение начинается с корня плана (SELECT), и элемент управления переходит по дереву к катушке индекса, объединению, а затем к сканированию таблицы верхнего уровня.

Первая строка сканирования проходит вверх по дереву и (а) сохраняется в буфере стека и (б) возвращается клиенту. Какая строка является первой, не определено, но давайте предположим, что это строка со значением {1}, в качестве аргумента. Поэтому первая строка, которая должна появиться, это {1}.

Управление снова переходит к сканированию таблицы (оператор конкатенации потребляет все строки с самого внешнего входа до открытия следующего). При сканировании генерируется вторая строка (значение {2}), и это снова передает дерево, которое будет сохранено в стеке, и выводится клиенту. Клиент получил последовательность {1}, {2}.

Приняв соглашение, где вершина стека LIFO находится слева, стек теперь содержит {2, 1}. Когда управление снова переходит к сканированию таблицы, оно сообщает об отсутствии строк, и управление переходит обратно к оператору конкатенации, который открывает свой второй вход (ему нужна строка для передачи в буферизацию стека), а управление переходит к внутреннему соединению в первый раз.

Внутреннее соединение вызывает таблицу Spool на своем внешнем входе, которая считывает верхнюю строку из стека {2} и удаляет ее из рабочего стола. Стек теперь содержит {1}.

Получив строку на своем внешнем входе, Inner Join передает управление по своему внутреннему входу в Left Anti-Semi Join (LASJ). Это запрашивает строку из ее внешнего ввода, передавая управление Сортировке. Sort является блокирующим итератором, поэтому он считывает все строки из табличной переменной и сортирует их по возрастанию (как это происходит).

Следовательно, первая строка, выдаваемая сортировкой, является значением {1}. Внутренняя сторона LASJ возвращает текущее значение рекурсивного члена (значение, только что извлеченное из стека), которое равно {2}. Значения в LASJ: {1} и {2}, поэтому {1} ​​выводится, поскольку значения не совпадают.

Эта строка {1} направляет дерево плана запросов в буферный индекс (стек), где он добавляется в стек, который теперь содержит {1, 1}, и отправляется клиенту. Клиент получил последовательность {1}, {2}, {1}.

Теперь управление переходит обратно в Конкатенацию, обратно вниз по внутренней стороне (в прошлый раз он возвратил строку, может повторить), через Внутреннее соединение к LASJ. Он снова читает свой внутренний ввод, получая значение {2} из сортировки.

Рекурсивный член по-прежнему {2}, поэтому на этот раз LASJ находит {2} и {2}, в результате чего строка не выводится. Не найдя больше строк на своем внутреннем входе (теперь сортировка вне строк), управление переходит обратно к внутреннему соединению.

Внутреннее соединение считывает свой внешний ввод, в результате чего значение {1} выталкивается из стека {1, 1}, в результате чего в стеке остается только {1}. Теперь процесс повторяется, причем значение {2} из нового вызова сканирования и сортировки таблиц проходит тест LASJ и добавляется в стек, а затем передается клиенту, который теперь получил {1}, {2}, {1}, {2} ... и поехали.

Мое любимое объяснение катушки Stack, используемой в рекурсивных планах CTE, - Крейг Фридман.

Пол Уайт говорит, что GoFundMonica
источник
31

BOL-описание рекурсивных CTE описывает семантику рекурсивного выполнения следующим образом:

  1. Разделите выражение CTE на якорные и рекурсивные члены.
  2. Запустите элемент привязки, создавая первый вызов или базовый набор результатов (T0).
  3. Запустите рекурсивный элемент (ы) с Ti в качестве входа и Ti + 1 в качестве выхода.
  4. Повторяйте шаг 3, пока не будет возвращен пустой набор.
  5. Вернуть набор результатов. Это СОЮЗ ВСЕХ от T0 до Tn.

Обратите внимание, что выше приведено логическое описание. Физический порядок операций может несколько отличаться, как показано здесь

Применяя это к вашему CTE, я бы ожидал бесконечный цикл со следующей схемой

+-----------+---------+---+---+---+
| Invocation| Results             |
+-----------+---------+---+---+---+
|         1 |       1 | 2 | 3 |   |
|         2 |       4 | 5 |   |   |
|         3 |       1 | 2 | 3 |   |
|         4 |       4 | 5 |   |   |
|         5 |       1 | 2 | 3 |   |
+-----------+---------+---+---+---+ 

Потому что

select a
from cte
where a in (1,2,3)

Якорное выражение. Это явно возвращается 1,2,3какT0

После этого выполняется рекурсивное выражение

select a
from cte
except
select a
from r

Если в 1,2,3качестве входных данных будет получен вывод 4,5as, T1то подключение к нему для следующего раунда рекурсии будет возвращаться 1,2,3и так до бесконечности.

Это не то, что на самом деле происходит, однако. Это результаты первых 5 вызовов

+-----------+---------+---+---+---+
| Invocation| Results             |
+-----------+---------+---+---+---+
|         1 |       1 | 2 | 3 |   |
|         2 |       1 | 2 | 4 | 5 |
|         3 |       1 | 2 | 3 | 4 |
|         4 |       1 | 2 | 3 | 5 |
|         5 |       1 | 2 | 3 | 4 |
+-----------+---------+---+---+---+

Из использования OPTION (MAXRECURSION 1)и корректировки с приращением вверх 1видно, что он входит в цикл, где каждый последующий уровень будет непрерывно переключаться между выводом 1,2,3,4и 1,2,3,5.

Как обсуждается @Quassnoi в этом блоге . Картина наблюдаемых результатов , как если бы каждый вызов делает , (1),(2),(3),(4),(5) EXCEPT (X)где Xэто последняя строка из предыдущего вызова.

Редактирование: После прочтения превосходного ответа SQL Kiwi становится понятно, почему это происходит, и что это не вся история, поскольку в стеке все еще остается множество вещей, которые никогда не могут быть обработаны.

Якорь отправляет 1,2,3содержимое стека клиента3,2,1

3 вытолкнутых стека, содержимое стека 2,1

LASJ возвращается 1,2,4,5, содержимое стека5,4,2,1,2,1

5 оторванных стеков, содержимое стека 4,2,1,2,1

LASJ возвращает 1,2,3,4 содержимое стека4,3,2,1,5,4,2,1,2,1

4 вытолкнутых стека, содержимое стека 3,2,1,5,4,2,1,2,1

LASJ возвращает 1,2,3,5 содержимое стека5,3,2,1,3,2,1,5,4,2,1,2,1

5 оторванных стеков, содержимое стека 3,2,1,3,2,1,5,4,2,1,2,1

LASJ возвращает 1,2,3,4 содержимое стека 4,3,2,1,3,2,1,3,2,1,5,4,2,1,2,1

Если вы попытаетесь заменить рекурсивный член логически эквивалентным (при отсутствии дубликатов / NULL) выражением

select a
from (
    select a
    from cte
    where a not in 
    (select a
    from r)
) x

Это недопустимо и вызывает ошибку «Рекурсивные ссылки не разрешены в подзапросах». так что, возможно, это недосмотр EXCEPTв данном случае.

Дополнение: Microsoft ответила на мой отзыв о подключении, как показано ниже

Догадка Джека верна: это должна была быть синтаксическая ошибка; рекурсивные ссылки действительно не должны быть разрешены в EXCEPTпунктах. Мы планируем устранить эту ошибку в следующем выпуске службы. А пока я бы предложил избегать рекурсивных ссылок в EXCEPT статьях.

Ограничивая рекурсию, EXCEPTмы следуем стандарту ANSI SQL, который включал это ограничение с тех пор, как была введена рекурсия (я полагаю, в 1999 году). Не существует широко распространенного соглашения о том, какой должна быть семантика для рекурсии EXCEPT(также называемой «безусловным отрицанием») в декларативных языках, таких как SQL. Кроме того, общеизвестно, что трудно (если не невозможно) эффективно реализовать такую ​​семантику (для баз данных разумного размера) в системе RDBMS.

И похоже, что окончательная реализация была сделана в 2014 году для баз данных с уровнем совместимости 120 или выше .

Рекурсивные ссылки в предложении EXCEPT генерируют ошибку в соответствии со стандартом ANSI SQL.

Мартин Смит
источник