it-swarm.asia

Menggunakan KECUALI dalam ekspresi tabel umum rekursif

Mengapa kueri berikut mengembalikan baris tak terbatas? Saya akan mengharapkan klausa EXCEPT 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 pada Stack Overflow.

33
Tom Hunter

Lihat jawaban Martin Smith untuk informasi tentang status saat ini EXCEPT dalam 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:

Recursive CTE Plan

Eksekusi dimulai pada akar rencana (PILIH) dan kontrol melewati pohon ke Index Spool, Concatenation, dan kemudian ke Scan 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. Karenanya, 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 ada di sebelah kiri, tumpukan sekarang berisi {2, 1}. Saat kontrol kembali ke Pemindaian Tabel, tidak ada lagi baris yang dilaporkan, dan kontrol melewati kembali ke operator Concatenation, yang membuka input kedua (membutuhkan baris untuk melewati tumpukan stack), 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 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 hanya 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, memperoleh 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.

Favorit saya penjelasan dari tumpukan Stack yang digunakan dalam rencana CTE rekursif adalah Craig Freedman's.

26
Paul White 9

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

  1. Membagi ekspresi CTE menjadi anggota jangkar dan rekursif.
  2. Jalankan anggota anchor (s) membuat doa pertama atau set hasil dasar (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 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 dengan jelas mengembalikan 1,2,3 Sebagai T0

Setelah itu ekspresi rekursif berjalan

select a
from cte
except
select a
from r

Dengan 1,2,3 Sebagai input yang akan menghasilkan output 4,5 Sebagai T1 Kemudian memasukkan itu kembali untuk putaran rekursi berikutnya akan mengembalikan 1,2,3 Dan 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 dalam peningkatan 1 Dapat dilihat bahwa ia memasuki siklus di mana setiap tingkat berturut-turut akan terus berganti antara menghasilkan 1,2,3,4 Dan 1,2,3,5.

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

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

Anchor Emits 1,2,3 Ke klien Stack Contents 3,2,1

3 muncul tumpukan, Isi Stack 2,1

LASJ mengembalikan 1,2,4,5, Isi Stack 5,4,2,1,2,1

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

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

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

LASJ mengembalikan 1,2,3,5 Isi Stack 5,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 Isi Stack 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 ini adalah kekhilafan bahwa EXCEPT bahkan diizinkan dalam kasus ini.

Tambahan: Microsoft sekarang telah menanggapi tanggapan Hubungkan saya seperti di bawah ini

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

Dalam membatasi rekursi lebih dari EXCEPT kita 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 lebih dari 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 wajar) 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.

31
Martin Smith