Використання 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

Я натрапив на це, намагаючись відповісти на запитання про Stack Overflow.

Відповіді:


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), а керування передає дерево вниз до індексу Spool, Concatenation, а потім до сканування таблиці верхнього рівня.

Перший рядок від сканування передає дерево вгору і зберігається (a), зберігається в котушці стека, і (b) повертається клієнту. Котрий рядок перший не визначено, але припустимо, що це рядок зі значенням {1}, для аргументу. Перший ряд, який з’явиться, є {1}.

Контроль знову переходить до таблиці сканування (оператор з’єднання споживає всі рядки з самого зовнішнього входу перед відкриттям наступного). Сканування випускає другий рядок (значення {2}), і це знову передає дерево, яке зберігається у стеку та виводиться клієнту. Тепер клієнт отримав послідовність {1}, {2}.

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

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

Отримавши рядок на зовнішньому вході, Внутрішнє з'єднання передає контроль над своїм внутрішнім входом до лівого анти-напівз'єднання (LASJ). Це вимагає рядка з його зовнішнього вводу, передаючи управління сортуванню. Сортування - це ітератор, що блокує, тому він зчитує всі рядки зі змінної таблиці та сортує їх за зростанням (як це відбувається).

Перший рядок, який випромінює сортування, є значенням {1}. Внутрішня сторона LASJ повертає поточне значення рекурсивного елемента (значення щойно вискочило з стека), яке дорівнює {2}. Значення на LASJ - {1} і {2}, тому {1} ​​випромінюється, оскільки значення не збігаються.

Цей рядок {1} передається вгору по дереву плану запитів до котушки Index (Stack), де він додається до стеку, який тепер містить {1, 1}, і випромінюється клієнту. Тепер клієнт отримав послідовність {1}, {2}, {1}.

Тепер контроль передається назад до Concatenation, назад по внутрішній стороні (він повернувся рядок минулого разу, можливо, знову), вниз через Внутрішнє з'єднання, до LASJ. Він знову читає свій внутрішній вхід, отримуючи значення {2} від сортування.

Рекурсивний член все ще {2}, тому цього разу LASJ знаходить {2} і {2}, в результаті чого жодного ряду не випускається. Не знаходячи більше рядків на внутрішньому вході (сортування зараз поза рядами), управління передається назад до внутрішнього з'єднання.

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

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


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,5як 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 Ківі зрозуміло і те, чому це відбувається, і що це не вся історія в тому, що на стеку залишається безліч речей, які ніколи не піддаються обробці.

Якір випускає 1,2,3вміст стека клієнта3,2,1

3 вискочив стек, вміст стека 2,1

LASJ повертається 1,2,4,5, Stack Contents5,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навіть у цьому випадку дозволений.

Доповнення: Корпорація Майкрософт відповіла на мій зворотній зв'язок Connect, як показано нижче

Здогадка Джека правильна: це мала бути синтаксична помилка; в EXCEPTпунктах дійсно не слід допускати рекурсивних посилань . Ми плануємо вирішити цю помилку у майбутньому випуску сервісу. Тим часом я пропоную уникати рекурсивних посилань у EXCEPT пунктах.

За обмеження рекурсії EXCEPTми дотримуємось стандарту ANSI SQL, який включив це обмеження з моменту введення рекурсії (я вважаю, в 1999 році). Не існує поширеної згоди щодо того, якою має бути семантика для рекурсії EXCEPT(також її називають "нестратифікованим запереченням") в декларативних мовах, таких як SQL. Крім того, вкрай важко (якщо не неможливо) реалізувати таку семантику ефективно (для баз даних розумного розміру) в системі RDBMS.

І схоже, що остаточна реалізація була здійснена у 2014 році для баз даних із рівнем сумісності 120 або вище .

Рекурсивні посилання в пункті EXCEPT генерують помилку відповідно до стандарту ANSI SQL.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.