Menggunakan KECUALI dalam ekspresi tabel umum rekursif

33

Mengapa kueri berikut mengembalikan baris tak terbatas? Saya akan mengharapkan EXCEPTklausul untuk mengakhiri rekursi ..

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

Saya menemukan ini ketika mencoba untuk menjawab pertanyaan tentang Stack Overflow.

Tom Hunter
sumber

Jawaban:

26

Lihat jawaban Martin Smith untuk informasi tentang status terkini EXCEPTdalam CTE rekursif.

Untuk menjelaskan apa yang Anda lihat, dan mengapa:

Saya menggunakan variabel tabel di sini, untuk membuat perbedaan antara nilai jangkar dan item rekursif lebih jelas (itu tidak mengubah semantik).

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)

Rencana kueri adalah:

Rencana CTE rekursif

Eksekusi dimulai pada akar rencana (PILIH) dan kontrol melewati pohon ke Indeks Spool, Concatenation, dan kemudian ke Pemindaian Tabel tingkat atas.

Baris pertama dari pemindaian melewati pohon dan (a) disimpan dalam Stack Spool, dan (b) dikembalikan ke klien. Baris mana yang pertama kali tidak didefinisikan, tetapi mari kita asumsikan itu adalah baris dengan nilai {1}, demi argumen. Karena itu, baris pertama yang muncul adalah {1}.

Kontrol kembali turun ke Pemindaian Tabel (operator Rangkaian menghabiskan semua baris dari input terluarnya sebelum membuka yang berikutnya). Pemindaian memancarkan baris kedua (nilai {2}), dan ini lagi melewatkan pohon untuk disimpan di stack dan output ke klien. Klien sekarang telah menerima urutan {1}, {2}.

Mengadopsi konvensi di mana bagian atas tumpukan LIFO di sebelah kiri, tumpukan sekarang berisi {2, 1}. Ketika kontrol kembali ke Pemindaian Tabel, kontrol tidak melaporkan lagi baris, dan kontrol melewati kembali ke operator Concatenation, yang membuka input kedua (membutuhkan baris untuk meneruskan ke tumpukan tumpukan), dan kontrol melewati ke Inner Join untuk pertama kalinya.

Gabungan dalam memanggil Spool Tabel pada input luarnya, yang membaca baris atas dari tumpukan {2} dan menghapusnya dari meja kerja. Tumpukan sekarang berisi {1}.

Setelah menerima satu baris pada input luarnya, Join Join melewati kontrol input inputnya ke Left Anti-Semi Join (LASJ). Ini meminta baris dari input luarnya, melewati kontrol ke Sort. Sortir adalah iterator pemblokiran, jadi ia membaca semua baris dari variabel tabel dan mengurutkannya naik (seperti yang terjadi).

Karenanya, baris pertama yang dipancarkan oleh Sort adalah nilai {1}. Sisi dalam LASJ mengembalikan nilai saat ini dari anggota rekursif (nilai baru saja muncul dari tumpukan), yaitu {2}. Nilai-nilai di LASJ adalah {1} dan {2} jadi {1} dipancarkan, karena nilai-nilai tidak cocok.

Baris ini {1} mengalir ke atas pohon rencana kueri ke Spool Indeks (Stack) tempat ditambahkan ke tumpukan, yang sekarang berisi {1, 1}, dan dipancarkan ke klien. Klien sekarang telah menerima urutan {1}, {2}, {1}.

Kontrol sekarang melewati kembali ke Rangkaian, kembali ke sisi dalam (itu mengembalikan baris terakhir kali, mungkin lakukan lagi), turun melalui Bergabung Gabung, ke LASJ. Itu membaca input dalamnya lagi, mendapatkan nilai {2} dari Sort.

Anggota rekursif masih {2}, jadi kali ini LASJ menemukan {2} dan {2}, sehingga tidak ada baris yang dipancarkan. Menemukan tidak ada lagi baris pada input dalamnya (Sortir sekarang keluar dari baris), kontrol melewati kembali ke Inner Join.

Inner Join membaca input luarnya, yang menghasilkan nilai {1} dikeluarkan dari stack {1, 1}, meninggalkan stack hanya dengan {1}. Proses sekarang berulang, dengan nilai {2} dari doa baru dari Table Scan and Sort melewati uji LASJ dan ditambahkan ke stack, dan diteruskan ke klien, yang sekarang telah menerima {1}, {2}, {1}, {2} ... dan kita mulai lagi.

Penjelasan favorit saya tentang gulungan Stack yang digunakan dalam rencana CTE rekursif adalah Craig Freedman.

Paul White mengatakan GoFundMonica
sumber
31

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.

Perhatikan di atas adalah deskripsi yang logis . Urutan fisik operasi dapat agak berbeda seperti yang diilustrasikan di sini

Menerapkan ini ke CTE Anda, saya akan mengharapkan loop tak terbatas dengan pola berikut

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

Karena

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

adalah ekspresi Anchor. Ini jelas kembali 1,2,3sebagaiT0

Setelah itu ekspresi rekursif berjalan

select a
from cte
except
select a
from r

Dengan 1,2,3sebagai input yang akan menghasilkan output 4,5saat T1kemudian memasukkan kembali untuk putaran rekursi berikutnya akan kembali 1,2,3dan seterusnya tanpa batas.

Namun, ini bukan yang sebenarnya terjadi. Ini adalah hasil dari 5 doa pertama

+-----------+---------+---+---+---+
| 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 |
+-----------+---------+---+---+---+

Dari menggunakan OPTION (MAXRECURSION 1)dan menyesuaikan ke atas secara bertahap 1dapat dilihat bahwa ia memasuki siklus di mana setiap tingkat berturut-turut akan terus berganti antara keluaran 1,2,3,4dan 1,2,3,5.

Seperti yang dibahas oleh @Quassnoi di posting blog ini . Pola hasil yang diamati adalah seolah-olah setiap doa melakukan di (1),(2),(3),(4),(5) EXCEPT (X)mana Xadalah baris terakhir dari doa sebelumnya.

Sunting: Setelah membaca jawaban SQL Kiwi yang sangat baik jelas mengapa ini terjadi dan bahwa ini bukan keseluruhan cerita karena masih ada banyak hal yang tersisa di tumpukan yang tidak pernah dapat diproses.

Anchor Emits 1,2,3ke Konten Stack klien3,2,1

3 muncul tumpukan, Stack Contents 2,1

LASJ kembali 1,2,4,5, Stack Contents5,4,2,1,2,1

5 muncul tumpukan, Stack Contents 4,2,1,2,1

LASJ mengembalikan 1,2,3,4 Stack Contents4,3,2,1,5,4,2,1,2,1

4 muncul tumpukan, Isi Stack 3,2,1,5,4,2,1,2,1

LASJ mengembalikan 1,2,3,5 Stack Contents5,3,2,1,3,2,1,5,4,2,1,2,1

5 muncul tumpukan, Stack Contents 3,2,1,3,2,1,5,4,2,1,2,1

LASJ mengembalikan 1,2,3,4 Stack Contents 4,3,2,1,3,2,1,3,2,1,5,4,2,1,2,1

Jika Anda mencoba dan mengganti anggota rekursif dengan ekspresi yang setara secara logis (tanpa adanya duplikat / NULL)

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

Ini tidak diizinkan dan memunculkan kesalahan "Referensi rekursif tidak diizinkan di subqueries." jadi mungkin itu adalah pengawasan yang EXCEPTbahkan diizinkan dalam kasus ini.

Tambahan: Microsoft sekarang telah menanggapi Umpan Balik Koneksi saya seperti di bawah ini

Tebakan Jack benar: ini seharusnya kesalahan sintaksis; referensi rekursif memang seharusnya tidak diizinkan dalam EXCEPTklausa. Kami berencana untuk mengatasi bug ini dalam rilis layanan mendatang. Sementara itu, saya menyarankan untuk menghindari referensi berulang dalam EXCEPT klausa.

Dalam membatasi rekursi EXCEPTkita mengikuti standar SQL ANSI, yang telah memasukkan pembatasan ini sejak rekursi diperkenalkan (pada tahun 1999 saya percaya). Tidak ada kesepakatan luas tentang apa yang semantik harus untuk rekursi EXCEPT(juga disebut "negasi unstratified") dalam bahasa deklaratif seperti SQL. Selain itu, sangat sulit (jika bukan tidak mungkin) untuk mengimplementasikan semantik seperti itu secara efisien (untuk basis data berukuran cukup) dalam sistem RDBMS.

Dan sepertinya implementasi akhirnya dibuat pada 2014 untuk database dengan tingkat kompatibilitas 120 atau lebih tinggi .

Referensi rekursif dalam klausa KECUALI menghasilkan kesalahan dalam kepatuhan dengan standar SQL ANSI.

Martin Smith
sumber