Як реально працює рекурсія SQL?


19

Що стосується SQL з інших мов програмування, структура рекурсивного запиту виглядає досить дивним. Пройдіться по ньому крок за кроком, і воно, здається, розвалиться.

Розглянемо наступний простий приклад:

CREATE TABLE #NUMS
(N BIGINT);

INSERT INTO #NUMS
VALUES (3), (5), (7);

WITH R AS
(
    SELECT N FROM #NUMS
    UNION ALL
    SELECT N*N AS N FROM R WHERE N*N < 10000000
)
SELECT N FROM R ORDER BY N;

Давайте пройдемося по ньому.

Спочатку член якоря виконується і набір результатів вводиться в R. Отже, R ініціалізується до {3, 5, 7}.

Тоді виконання опускається нижче UNION ALL і рекурсивний член виконується вперше. Він виконується на R (тобто на R, який ми зараз маємо в руці: {3, 5, 7}). Це призводить до {9, 25, 49}.

Що це стосується цього нового результату? Чи додається він {9, 25, 49} до існуючого {3, 5, 7}, позначає отриманий об'єднання R, а потім продовжує рекурсію звідти? Або переосмислювати R бути лише цим новим результатом {9, 25, 49} і робити все об'єднання пізніше?

Жоден вибір не має сенсу.

Якщо R зараз {3, 5, 7, 9, 25, 49} і ми виконаємо наступну ітерацію рекурсії, тоді ми закінчимось {9, 25, 49, 81, 625, 2401} і ми програв {3, 5, 7}.

Якщо R зараз лише {9, 25, 49}, ми маємо проблему з неправильним маркуванням. R розуміється як об'єднання набору результатів члена якоря і всіх наступних рекурсивних наборів результатів. Тоді як {9, 25, 49} є лише складовою Р. До цього часу ми не нарахували повний R. Тому писати рекурсивний член як вибір з R не має сенсу.


Я, безумовно, ціную те, що @Max Vernon та @Michael S. детально описали нижче. А саме, що (1) всі компоненти створюються до межі рекурсії або нульового набору, а потім (2) всі компоненти об'єднуються разом. Це те, як я розумію, що рекурсія SQL фактично працює.

Якби ми переробляли SQL, можливо, ми застосували б більш чіткий і явний синтаксис, приблизно такий:

WITH R AS
(
    SELECT   N
    INTO     R[0]
    FROM     #NUMS
    UNION ALL
    SELECT   N*N AS N
    INTO     R[K+1]
    FROM     R[K]
    WHERE    N*N < 10000000
)
SELECT N FROM R ORDER BY N;

Начебто спонукальний доказ в математиці.

Проблема з рекурсією SQL, яка існує зараз, полягає в тому, що вона написана в заплутаному вигляді. Як написано, йдеться про те, що кожен компонент формується шляхом вибору з R, але це не означає повний R, який був побудований (або, здається, був побудований) до цього часу. Це просто означає попередній компонент.


"Якщо R зараз {3, 5, 7, 9, 25, 49} і ми виконаємо наступну ітерацію рекурсії, тоді ми закінчимось {9, 25, 49, 81, 625, 2401} і ми ' ви програли {3, 5, 7}. " Я не бачу, як ти втрачаєш {3,5,7}, якщо це працює так.
ypercubeᵀᴹ

@ yper-crazyhat-cubeᵀᴹ - Я випливав із першої запропонованої мною гіпотези, а саме: що, якщо проміжний R - це скупчення всього, що було обчислено до цього моменту? Тоді при наступній ітерації рекурсивного елемента кожен елемент R квадратується. Таким чином, {3, 5, 7} стає {9, 25, 49}, і у нас ніколи більше немає {3, 5, 7}. Іншими словами, {3, 5, 7} втрачається від R.
UnLogicGuys

Відповіді:


26

BOL опис рекурсивних CTE описує семантику рекурсивного виконання таким чином:

  1. Розділіть вираз CTE на прив’язні та рекурсивні члени.
  2. Запустіть елементи якоря, створивши перший виклик або базовий набір результатів (T0).
  3. Запустіть рекурсивні елементи (ти) з Ti як вхід і Ti + 1 як вихід.
  4. Повторіть крок 3, поки не повернеться порожній набір.
  5. Повернути набір результатів. Це Спілка ВСІХ від T0 до Tn.

Таким чином, кожен рівень має як вхідний рівень вище, а не весь набір результатів, накопичений до цього часу.

Сказане - як це логічно працює . Фізично рекурсивні CTE в даний час завжди реалізуються з вкладеними циклами та котушкою стека в SQL Server. Це описано тут і тут і означає, що на практиці кожен рекурсивний елемент просто працює з батьківським рядком з попереднього рівня, а не з усім рівнем. Але різні обмеження допустимого синтаксису в рекурсивних CTE означають, що цей підхід працює.

Якщо ви видалите ORDER BYз запиту результати, впорядковуються наступним чином

+---------+
|    N    |
+---------+
|       3 |
|       5 |
|       7 |
|      49 |
|    2401 |
| 5764801 |
|      25 |
|     625 |
|  390625 |
|       9 |
|      81 |
|    6561 |
+---------+

Це тому, що план виконання працює дуже аналогічно наступному C#

using System;
using System.Collections.Generic;
using System.Diagnostics;

public class Program
{
    private static readonly Stack<dynamic> StackSpool = new Stack<dynamic>();

    private static void Main(string[] args)
    {
        //temp table #NUMS
        var nums = new[] { 3, 5, 7 };

        //Anchor member
        foreach (var number in nums)
            AddToStackSpoolAndEmit(number, 0);

        //Recursive part
        ProcessStackSpool();

        Console.WriteLine("Finished");
        Console.ReadLine();
    }

    private static void AddToStackSpoolAndEmit(long number, int recursionLevel)
    {
        StackSpool.Push(new { N = number, RecursionLevel = recursionLevel });
        Console.WriteLine(number);
    }

    private static void ProcessStackSpool()
    {
        //recursion base case
        if (StackSpool.Count == 0)
            return;

        var row = StackSpool.Pop();

        int thisLevel = row.RecursionLevel + 1;
        long thisN = row.N * row.N;

        Debug.Assert(thisLevel <= 100, "max recursion level exceeded");

        if (thisN < 10000000)
            AddToStackSpoolAndEmit(thisN, thisLevel);

        ProcessStackSpool();
    }
}

NB1: Як було сказано вище, до моменту, коли перша дитина-учасник якоря 3обробляє всю інформацію про своїх братів 5і сестер, а 7також про їхніх нащадків, вже викинута з котушки і більше не доступна.

NB2: вищевказаний C # має таку ж загальну семантику, як і план виконання, але потік у плані виконання не є ідентичним, оскільки там оператори працюють у конвеєрному виконанні. Це спрощений приклад для демонстрації суті підходу. Детальнішу інформацію щодо самого плану див. У попередніх посиланнях.

NB3: Сам котушок стека, очевидно, реалізується як не унікальний кластерний індекс з ключовим стовпцем рівня рекурсії та додаються унікальні пристрої при необхідності ( джерело )


6
Рекурсивні запити в SQL Server завжди перетворюються з рекурсії в ітерацію (із складанням) під час розбору. Правило реалізації для ітерації IterateToDepthFirst- Iterate(seed,rcsv)->PhysIterate(seed,rcsv). Просто FYI. Відмінна відповідь.
Пол Білий каже, що GoFundMonica

Між іншим, UNION також дозволений замість UNION ALL, але SQL Server цього не зробить.
Джошуа

5

Це просто (напів) освічена здогадка, і, мабуть, абсолютно неправильна. Цікаве питання, до речі.

T-SQL - декларативна мова; можливо, рекурсивний CTE переводиться в операцію в стилі курсору, де результати з лівого боку UNION ALL додаються до тимчасової таблиці, тоді права сторона UNION ALL застосовується до значень з лівого боку.

Отже, спочатку вставляємо висновок лівої сторони UNION ALL в набір результатів, потім вставляємо результати правої частини UNION ALL, застосованої до лівої, і вставляємо його в набір результатів. Потім ліву сторону замінюють на вихід з правого боку, а праву сторону знову застосовують до "нової" лівої сторони. Щось на зразок цього:

  1. {3,5,7} -> набір результатів
  2. рекурсивні заяви, застосовані до {3,5,7}, що становить {9,25,49}. {9,25,49} додається до набору результатів і замінює ліву частину UNION ALL.
  3. рекурсивні заяви, застосовані до {9,25,49}, що становить {81,625,2401}. {81,625,2401} додається до набору результатів і замінює ліву частину UNION ALL.
  4. рекурсивні заяви застосовуються до {81,625,2401}, що становить {6561,390625,5764801}. {6561,390625,5764801} додано до набору результатів.
  5. Курсор завершений, оскільки наступна ітерація призводить до того, що пункт WHERE повертає значення false.

Ви можете бачити таку поведінку в плані виконання рекурсивного CTE:

введіть тут опис зображення

Це крок 1 вище, де ліва частина UNION ALL додається до виводу:

введіть тут опис зображення

Це права сторона UNION ALL, де вихід з'єднується з набором результатів:

введіть тут опис зображення


4

Документація на SQL Server , в якій згадуються T i і T i + 1 , не є ні дуже зрозумілою, ні точним описом фактичної реалізації.

Основна ідея полягає в тому, що рекурсивна частина запиту переглядає всі попередні результати, але лише один раз .

Можливо, буде корисно подивитися, як інші бази даних реалізують це (щоб отримати той же результат). У документації Postgres написано:

Рекурсивна оцінка запитів

  1. Оцініть нерекурсивний термін. Для UNION(але ні UNION ALL) відмовтеся від повторних рядків. До результатів рекурсивного запиту включіть усі решта рядків, а також помістіть їх у тимчасовий робочий стіл .
  2. Поки робочий стіл не порожній, повторіть ці кроки:
    1. Оцініть рекурсивний термін, замінивши поточний зміст робочого столу для рекурсивної самонавірки. Для UNION(але ні UNION ALL) відмовтесь від повторних рядків і рядків, які дублюють будь-який попередній рядок результатів. До результатів рекурсивного запиту включіть усі решта рядків, а також помістіть їх у тимчасову проміжну таблицю .
    2. Замініть вміст робочого столу вмістом проміжної таблиці, а потім випорожніть проміжну таблицю.

Примітка
Строго кажучи, цей процес - це ітерація не рекурсія, а RECURSIVEтермінологія, обрана комітетом стандартів SQL.

Документація на SQLite натякає на дещо іншу реалізацію, і цей алгоритм "один за одним" може бути найпростішим для розуміння:

Основний алгоритм обчислення вмісту рекурсивної таблиці полягає в наступному:

  1. Запустіть initial-selectі додайте результати до черги.
  2. Поки черга не порожня:
    1. Витягніть один ряд з черги.
    2. Вставте цей єдиний рядок у рекурсивну таблицю
    3. Зробіть вигляд, що щойно витягнутий рядок - це єдиний рядок у рекурсивній таблиці та запустіть recursive-selectдодавання всіх результатів у чергу.

Основна процедура, описана вище, може змінюватися такими додатковими правилами:

  • Якщо оператор UNION з'єднує initial-selectз recursive-select, тоді додайте рядки до черги лише у тому випадку, якщо до черги раніше не було додано однакових рядків. Повторні рядки відкидаються перед додаванням до черги, навіть якщо повторні рядки вже були вилучені з черги на етапі рекурсії. Якщо оператор UNION ALL, то всі рядки, згенеровані як initial-selectі the recursive-select, завжди додаються до черги, навіть якщо вони повторюються.
    […]

0

Мої знання спеціально в DB2, але дивлячись на діаграми пояснення, схоже, те саме, що і у SQL Server.

План звідси:

Дивіться це на Вставте план

Поясніть план SQL Server

Оптимізатор буквально не виконує об'єднання для кожного рекурсивного запиту. Він приймає структуру запиту і присвоює першу частину об'єднання всім "якірному члену", тоді він буде проходити через другу половину об'єднання всі (називається "рекурсивним членом" рекурсивно, поки не досягне визначених обмежень. Після рекурсія завершена, оптимізатор приєднує всі записи разом.

Оптимізатор просто сприймає це як пропозицію зробити заздалегідь задану операцію.

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