Proč následující dotaz vrací nekonečné řádky? Očekával bych, že doložka EXCEPT
ukončí rekurzi.
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
Narazil jsem na to, když jsem se snažil odpovědět na otázku otázka na Stack Overflow.
Viz odpověď Martina Smitha pro informace o aktuálním stavu EXCEPT
v rekurzivním CTE.
Vysvětlit, co jste viděli a proč:
Zde používám proměnnou tabulky, abych lépe rozlišil mezi hodnotami ukotvení a rekurzivními položkami (nemění sémantiku).
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)
Plán dotazů je:
Provedení začíná v kořenovém adresáři plánu (VYBRAT) a ovládací prvek předává strom do zařazování indexů, zřetězení a poté do nejvyšší úrovně tabulky.
První řádek ze skenování prochází stromem a je (a) uložen v zásobníku cívky a (b) vrácen klientovi. Který řádek je první není definován, ale předpokládejme, že je to řádek s hodnotou {1}, kvůli argumentaci. První řádek, který se má zobrazit, je proto {1}.
Řízení znovu přejde dolů ke skenování tabulky (operátor zřetězení spotřebuje všechny řádky ze svého nejvzdálenějšího vstupu před otevřením dalšího). Skenování vyšle druhý řádek (hodnota {2}), a to opět předá strom, který se má uložit do zásobníku a vydat klientovi. Klient nyní obdržel sekvenci {1}, {2}.
Přijetí konvence, kde horní část zásobníku LIFO je vlevo, obsahuje nyní nyní {2, 1}). Jakmile ovládací prvek znovu přejde do skenování tabulky, nehlásí žádné další řádky a ovládací prvek přejde zpět na operátor zřetězení, který otevře svůj druhý vstup (potřebuje řádek, aby prošel do zásobníku cívky), a ovládací prvek poprvé přejde do vnitřního spojení.
Vnitřní spojení zavolá tabulku zařazování na svůj vnější vstup, který přečte horní řádek ze zásobníku {2} a odstraní jej z pracovní tabulky. Zásobník nyní obsahuje {1}.
Poté, co obdržel řádek na svém vnějším vstupu, vstupy vnitřního spojení řídí jeho vnitřní vstup do levého anti-polospojení (LASJ). To vyžaduje řádek od jeho vnějšího vstupu, předávající ovládání třídění. Třídění je blokující iterátor, takže čte všechny řádky z proměnné tabulky a třídí je vzestupně (jak se to stane).
První řádek vyzařovaný tříděním je proto hodnota {1}. Vnitřní strana LASJ vrací aktuální hodnotu rekurzivního člena (hodnota právě vyskočená ze zásobníku), která je {2}. Hodnoty na LASJ jsou {1} a {2}, takže se vysílá {1}, protože hodnoty se neshodují.
Tento řádek {1} protéká stromem plánu dotazů nahoru do zařazovací oblasti indexu (zásobníku), kde je přidán do zásobníku, který nyní obsahuje {1, 1}, a vyslán klientovi. Klient nyní obdržel sekvenci {1}, {2}, {1}.
Ovládání nyní přechází zpět do zřetězení, zpět dolů z vnitřní strany (naposledy se vrátil řádek, může to udělat znovu), dolů přes vnitřní spojení do LASJ. Znovu načte svůj vnitřní vstup a získá hodnotu {2} z řazení.
Rekurzivní člen je stále {2}, takže tentokrát LASJ najde {2} a {2}, což má za následek, že nebude vydán žádný řádek. Pokud na svém vnitřním vstupu nebudou žádné další řádky (řazení je nyní mimo řádky), ovládací prvek přejde zpět k vnitřnímu spojení.
Vnitřní spojení čte svůj vnější vstup, což má za následek, že hodnota {1} je vyskočena ze zásobníku {1, 1}, přičemž zásobník zůstane pouze {1}. Proces se nyní opakuje s hodnotou {2} z nového vyvolání tabulky Scan and Sort, která prošla testem LASJ a byla přidána do zásobníku, a předána klientovi, který nyní obdržel {1}, {2}, {1}, {2} ... a jdeme.
Moje nejoblíbenější vysvětlení z cívky zásobníku používané v rekurzivních plánech CTE je Craig Freedman.
BOL popis rekurzivních CTE popisuje sémantiku rekurzivního provádění následovně:
Všimněte si, že výše je logický popis. Fyzické pořadí operací se může poněkud lišit, jak je znázorněno zde
Při použití tohoto na váš CTE bych očekával nekonečnou smyčku s následujícím vzorcem
+-----------+---------+---+---+---+
| Invocation| Results |
+-----------+---------+---+---+---+
| 1 | 1 | 2 | 3 | |
| 2 | 4 | 5 | | |
| 3 | 1 | 2 | 3 | |
| 4 | 4 | 5 | | |
| 5 | 1 | 2 | 3 | |
+-----------+---------+---+---+---+
Protože
select a
from cte
where a in (1,2,3)
je Anchor výraz. To jasně vrátí 1,2,3
Jako T0
Poté běží rekurzivní výraz
select a
from cte
except
select a
from r
S 1,2,3
Jako vstupem, který vyprodukuje výstup 4,5
Jako T1
, Připojením, které se vrátí pro další kolo rekurze, se vrátí 1,2,3
Atd. na dobu neurčitou.
To se však ve skutečnosti nestane. Toto jsou výsledky prvních 5 vyvolání
+-----------+---------+---+---+---+
| 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 |
+-----------+---------+---+---+---+
Z použití funkce OPTION (MAXRECURSION 1)
a úpravy směrem nahoru v krocích po 1
Je vidět, že vstupuje do cyklu, kde se každá následující úroveň bude neustále přepínat mezi výstupem 1,2,3,4
A 1,2,3,5
.
Jak diskutoval @ Quassnoi v tento blogový příspěvek . Vzor pozorovaných výsledků je, jako by každé vyvolání dělalo (1),(2),(3),(4),(5) EXCEPT (X)
, Kde X
je poslední řádek z předchozího vyvolání.
Edit: Po přečtení vynikající odpověď SQL Kiwi je jasné, proč k tomu dochází a že to není celý příběh v tom Na zásobníku stále zbývá spousta věcí, které se nikdy nemohou zpracovat.
Anchor Emits
1,2,3
Do klientského zásobníku Obsah3,2,1
3 vyskočený zásobník, obsah zásobníku
2,1
LASJ vrátí
1,2,4,5
, Obsah zásobníku5,4,2,1,2,1
5 vyskočený zásobník, obsah zásobníku
4,2,1,2,1
LASJ vrátí
1,2,3,4
Obsah zásobníku4,3,2,1,5,4,2,1,2,1
4 vyskočený zásobník, obsah zásobníku
3,2,1,5,4,2,1,2,1
LASJ vrátí
1,2,3,5
Obsah zásobníku5,3,2,1,3,2,1,5,4,2,1,2,1
5 vyskočený zásobník, obsah zásobníku
3,2,1,3,2,1,5,4,2,1,2,1
LASJ vrátí
1,2,3,4
Obsah zásobníku4,3,2,1,3,2,1,3,2,1,5,4,2,1,2,1
Pokud se pokusíte nahradit rekurzivního člena logicky ekvivalentním (v případě absence duplikátů/NULL) výrazu
select a
from (
select a
from cte
where a not in
(select a
from r)
) x
To není povoleno a zvyšuje se chyba „Rekurzivní odkazy nejsou povoleny v poddotazech.“ takže snad je to dohled, že v tomto případě je EXCEPT
dokonce povolen.
Navíc: Microsoft nyní odpověděl na můj Connect Feedback , jak je uvedeno níže
Jack odhad je správný: měla to být chyba syntaxe; rekurzivní odkazy by neměly být povoleny v klauzulích
EXCEPT
. Tuto chybu plánujeme řešit v nadcházejícím vydání služby. Mezitím bych doporučil vyhnout se rekurzivním odkazům v klauzulíchEXCEPT
.Při omezování rekurze nad
EXCEPT
se řídíme standardem ANSI SQL, který toto omezení zahrnoval od doby, kdy byla rekurze zavedena (v roce 1999 věřím). V deklarativních jazycích, jako je SQL, neexistuje žádná všeobecná shoda o tom, jaká by měla být sémantika pro rekurzi nadEXCEPT
(také nazývaná „unstratified negation“). Navíc je notoricky obtížné (ne-li nemožné) účinně implementovat takovou sémantiku (pro databáze přiměřeně velké velikosti) v systému RDBMS.
A vypadá to, že případná implementace byla provedena v roce 2014 pro databáze s úrovní kompatibility 120 nebo vyšší .
Rekurzivní odkazy v klauzuli EXCEPT generují chybu v souladu se standardem ANSI SQL.