Bagaimana cara rekursi SQL bekerja?

19

Datang ke SQL dari bahasa pemrograman lain, struktur kueri rekursif terlihat agak aneh. Berjalan melaluinya langkah demi langkah, dan tampaknya berantakan.

Perhatikan contoh sederhana berikut ini:

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;

Mari kita jalani saja.

Pertama, anggota anchor dieksekusi dan set hasil dimasukkan ke dalam R. Jadi R diinisialisasi ke {3, 5, 7}.

Kemudian, eksekusi turun di bawah UNION ALL dan anggota rekursif dieksekusi untuk pertama kalinya. Ini dijalankan pada R (yaitu, pada R yang saat ini kami miliki: {3, 5, 7}). Ini menghasilkan {9, 25, 49}.

Apa hubungannya dengan hasil baru ini? Apakah itu menambahkan {9, 25, 49} ke {3, 5, 7} yang ada, melabelkan gabungan R yang dihasilkan, dan kemudian melanjutkan dengan rekursi dari sana? Atau apakah itu mendefinisikan ulang R menjadi hanya hasil baru ini {9, 25, 49} dan melakukan semua penyatuan nanti?

Tidak ada pilihan yang masuk akal.

Jika R sekarang {3, 5, 7, 9, 25, 49} dan kami menjalankan iterasi rekursi berikutnya, maka kami akan berakhir dengan {9, 25, 49, 81, 625, 2401} dan kami telah hilang {3, 5, 7}.

Jika R sekarang hanya {9, 25, 49}, maka kami memiliki masalah pemberian label yang salah. R dipahami sebagai gabungan dari himpunan hasil anggota jangkar dan semua set hasil anggota rekursif berikutnya. Sedangkan {9, 25, 49} hanya komponen R. Ini bukan R penuh yang telah kami dapatkan sejauh ini. Oleh karena itu, menulis anggota rekursif sebagai memilih dari R tidak masuk akal.


Saya tentu menghargai apa yang telah dijelaskan oleh @Max Vernon dan @Michael S. Yaitu, bahwa (1) semua komponen dibuat hingga batas rekursi atau set nol, dan kemudian (2) semua komponen disatukan. Ini adalah bagaimana saya memahami rekursi SQL untuk benar-benar berfungsi.

Jika kami mendesain ulang SQL, mungkin kami akan menerapkan sintaks yang lebih jelas dan eksplisit, seperti ini:

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;

Semacam bukti induktif dalam matematika.

Masalah dengan rekursi SQL seperti saat ini adalah bahwa itu ditulis dengan cara yang membingungkan. Cara itu ditulis mengatakan bahwa setiap komponen dibentuk dengan memilih dari R, tetapi itu tidak berarti R penuh yang telah (atau, tampaknya telah) dibangun sejauh ini. Itu hanya berarti komponen sebelumnya.

UnLogicGuys
sumber
"Jika R sekarang {3, 5, 7, 9, 25, 49} dan kami melaksanakan iterasi rekursi berikutnya, maka kami akan berakhir dengan {9, 25, 49, 81, 625, 2401} dan kami akan telah kehilangan {3, 5, 7}. " Saya tidak melihat bagaimana Anda kehilangan {3,5,7} jika bekerja seperti itu.
ypercubeᵀᴹ
@ yper-crazyhat-cubeᵀᴹ - Saya mengikuti dari hipotesis pertama yang saya usulkan, yaitu, bagaimana jika intermediate R adalah akumulasi dari segala sesuatu yang telah dihitung hingga titik itu? Kemudian, pada iterasi berikutnya dari anggota rekursif, setiap elemen R adalah kuadrat. Dengan demikian, {3, 5, 7} menjadi {9, 25, 49} dan kita tidak pernah lagi memiliki {3, 5, 7} dalam R. Dengan kata lain, {3, 5, 7} hilang dari R.
UnLogicGuys

Jawaban:

26

Deskripsi BOL dari CTE rekursif menggambarkan semantik eksekusi rekursif adalah sebagai berikut:

  1. Membagi ekspresi CTE menjadi anggota jangkar dan rekursif.
  2. Jalankan anggota anchor (s) membuat set doa pertama atau hasil basis (T0).
  3. Jalankan anggota rekursif dengan Ti sebagai input dan Ti + 1 sebagai output.
  4. Ulangi langkah 3 hingga set kosong dikembalikan.
  5. Kembalikan set hasil. Ini adalah UNION ALL dari T0 hingga Tn.

Jadi masing-masing level hanya memiliki input level di atas, bukan seluruh set hasil yang terakumulasi sejauh ini.

Di atas adalah cara kerjanya secara logis . CTE rekursif fisik saat ini selalu diimplementasikan dengan loop bersarang dan tumpukan gulungan di SQL Server. Ini dijelaskan di sini dan di sini dan berarti bahwa dalam praktiknya setiap elemen rekursif hanya bekerja dengan baris induk dari level sebelumnya, bukan seluruh level. Tetapi berbagai batasan pada sintaks yang diijinkan dalam CTE rekursif berarti pendekatan ini berfungsi.

Jika Anda menghapus ORDER BYdari kueri, hasilnya akan dipesan sebagai berikut

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

Ini karena rencana eksekusi beroperasi sangat mirip dengan yang berikut ini 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: Seperti di atas pada saat anak pertama dari anggota jangkar 3sedang diproses semua informasi tentang saudara kandungnya, 5dan 7, dan keturunan mereka, telah dibuang dari kumparan dan tidak lagi dapat diakses.

NB2: C # di atas memiliki semantik keseluruhan yang sama dengan rencana eksekusi tetapi aliran dalam rencana eksekusi tidak identik, karena di sana para operator bekerja dengan cara exection pipeline. Ini adalah contoh yang disederhanakan untuk menunjukkan inti dari pendekatan. Lihat tautan sebelumnya untuk detail lebih lanjut tentang rencana itu sendiri.

NB3: Spool stack itu sendiri tampaknya diimplementasikan sebagai indeks cluster tidak unik dengan kolom kunci tingkat rekursi dan unikifiers ditambahkan sesuai kebutuhan ( sumber )

Martin Smith
sumber
6
Kueri rekursif dalam SQL Server selalu dikonversi dari rekursi menjadi iterasi (dengan susun) selama parsing. Aturan implementasi untuk iterasi adalah IterateToDepthFirst- Iterate(seed,rcsv)->PhysIterate(seed,rcsv). Hanya FYI. Jawaban yang sangat bagus.
Paul White mengatakan GoFundMonica
Kebetulan, UNION juga diizinkan bukan UNION ALL tetapi SQL Server tidak akan melakukannya.
Joshua
5

Ini hanya tebakan (semi) berpendidikan, dan mungkin sepenuhnya salah. Pertanyaan yang menarik.

T-SQL adalah bahasa deklaratif; mungkin CTE rekursif diterjemahkan ke dalam operasi gaya kursor di mana hasil dari sisi kiri UNION ALL ditambahkan ke tabel sementara, maka sisi kanan UNION ALL diterapkan ke nilai-nilai di sisi kiri.

Jadi, pertama kita masukkan output dari sisi kiri UNION ALL ke dalam set hasil, kemudian kita masukkan hasil dari sisi kanan UNION ALL yang diterapkan ke sisi kiri, dan masukkan itu ke dalam set hasil. Sisi kiri kemudian diganti dengan output dari sisi kanan, dan sisi kanan diterapkan lagi ke sisi kiri "baru". Sesuatu seperti ini:

  1. {3,5,7} -> kumpulan hasil
  2. pernyataan rekursif diterapkan pada {3,5,7}, yaitu {9,25,49}. {9,25,49} ditambahkan ke set hasil, dan menggantikan sisi kiri UNION ALL.
  3. pernyataan rekursif diterapkan pada {9,25,49}, yaitu {81,625,2401}. {81.625.2401} ditambahkan ke set hasil, dan menggantikan sisi kiri UNION ALL.
  4. pernyataan rekursif diterapkan pada {81.625.2401}, yaitu {6561.390625,5764801}. {6561.390625,5764801} ditambahkan ke set hasil.
  5. Kursor selesai, karena iterasi berikutnya menghasilkan klausa WHERE yang salah.

Anda dapat melihat perilaku ini dalam rencana eksekusi untuk CTE rekursif:

masukkan deskripsi gambar di sini

Ini adalah langkah 1 di atas, di mana sisi kiri UNION ALL ditambahkan ke output:

masukkan deskripsi gambar di sini

Ini adalah sisi kanan UNION ALL di mana output digabungkan ke set hasil:

masukkan deskripsi gambar di sini

Max Vernon
sumber
4

The SQL Server dokumentasi , yang menyebutkan T i dan T i + 1 , yang tidak sangat dimengerti, atau deskripsi akurat tentang pelaksanaan yang sebenarnya.

Ide dasarnya adalah bahwa bagian rekursif dari kueri melihat semua hasil sebelumnya, tetapi hanya sekali .

Mungkin bermanfaat untuk melihat bagaimana database lain menerapkan ini (untuk mendapatkan hasil yang sama ). The dokumentasi Postgres mengatakan:

Evaluasi Permintaan Rekursif

  1. Mengevaluasi istilah non-rekursif. Untuk UNION(tetapi tidak UNION ALL), buang baris duplikat. Sertakan semua baris yang tersisa dalam hasil kueri rekursif, dan juga letakkan di tabel kerja sementara .
  2. Selama meja kerja tidak kosong, ulangi langkah-langkah ini:
    1. Mengevaluasi istilah rekursif, menggantikan konten saat ini dari tabel kerja untuk referensi diri rekursif. Untuk UNION(tetapi tidak UNION ALL), buang baris duplikat dan baris yang menduplikasi baris hasil sebelumnya. Sertakan semua baris yang tersisa dalam hasil kueri rekursif, dan juga tempatkan dalam tabel perantara sementara .
    2. Ganti konten tabel kerja dengan isi tabel perantara, lalu kosongkan tabel perantara.

Catatan
Sebenarnya, proses ini adalah iterasi bukan rekursi, tetapi RECURSIVEterminologi yang dipilih oleh komite standar SQL.

The dokumentasi SQLite petunjuk pada implementasi yang sedikit berbeda, dan ini algoritma satu-baris-di-a-waktu mungkin yang paling mudah untuk memahami:

Algoritma dasar untuk menghitung konten dari tabel rekursif adalah sebagai berikut:

  1. Jalankan initial-selectdan tambahkan hasilnya ke antrian.
  2. Sementara antrian tidak kosong:
    1. Ekstrak satu baris dari antrian.
    2. Masukkan baris itu ke dalam tabel rekursif
    3. Berpura-pura bahwa baris tunggal yang baru saja diekstraksi adalah satu-satunya baris dalam tabel rekursif dan jalankan recursive-select, menambahkan semua hasil ke antrian.

Prosedur dasar di atas dapat dimodifikasi oleh aturan tambahan berikut:

  • Jika operator UNION menghubungkan initial-selectdengan recursive-select, maka hanya tambahkan baris ke antrian jika sebelumnya tidak ada baris yang identik ditambahkan ke antrian. Baris yang berulang dibuang sebelum ditambahkan ke antrian bahkan jika baris yang diulang telah diekstraksi dari antrian oleh langkah rekursi. Jika operator UNION ALL, maka semua baris yang dihasilkan oleh keduanya initial-selectdan recursive-selectselalu ditambahkan ke antrian bahkan jika mereka berulang.
    [...]
CL.
sumber
0

Pengetahuan saya ada di DB2 khusus tetapi melihat diagram menjelaskan tampaknya sama dengan SQL Server.

Rencananya berasal dari sini:

Lihat di Tempel Paket

SQL Server Menjelaskan Paket

Pengoptimal tidak benar-benar menjalankan gabungan semua untuk setiap kueri rekursif. Dibutuhkan struktur kueri dan menetapkan bagian pertama dari serikat semua untuk "anggota jangkar" maka akan berjalan melalui bagian kedua dari serikat semua (disebut "anggota rekursif" secara rekursif sampai mencapai batas yang ditentukan. Setelah rekursi selesai, optimizer menggabungkan semua catatan bersama.

Pengoptimal hanya menganggapnya sebagai saran untuk melakukan operasi yang ditentukan sebelumnya.

Michael S.
sumber