web-dev-qa-db-ja.com

再帰的共通テーブル式でのEXCEPTの使用

次のクエリで無限行が返されるのはなぜですか? EXCEPT句が再帰を終了することを期待していました。

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

Stack Overflowで question に回答しようとしたときにこれに遭遇しました。

33
Tom Hunter

再帰CTEにおけるEXCEPTの現在のステータスについては、 Martin Smithの回答 を参照してください。

あなたが見ていたものとその理由を説明するには:

ここでは、テーブル値を使用して、アンカー値と再帰的なアイテムの違いを明確にしています(セマンティクスは変更していません)。

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)

クエリプランは次のとおりです。

Recursive CTE Plan

実行はプランのルート(SELECT)から始まり、制御はツリーを下ってインデックススプール、連結、次にトップレベルのテーブルスキャンに渡されます。

スキャンの最初の行はツリーの上を通過し、(a)スタックスプールに格納され、(b)クライアントに返されます。最初の行は定義されていませんが、引数として、値が{1}の行であると仮定します。したがって、最初に表示される行は{1}です。

制御は再びテーブルスキャンに渡されます(連結演算子は、次の行を開く前に、最も外側の入力からすべての行を消費します)。スキャンは2番目の行(値{2})を発行し、これは再びツリーに渡され、スタックに格納されてクライアントに出力されます。クライアントはシーケンス{1}、{2}を受信しました。

LIFOスタックの上部が左側にあるという規則を採用すると、スタックには{2、1}が含まれるようになります。制御が再びテーブルスキャンに渡されると、それ以上の行は報告されません。制御は2番目の入力を開く連結演算子に戻り(スタックスプールに渡すには行が必要です)、制御は初めて内部結合に渡されます。

内部結合は、外部入力でテーブルスプールを呼び出します。これは、スタック{2}から先頭行を読み取り、ワークテーブルから削除します。スタックには{1}が含まれています。

外部入力で行を受け取った内部結合は、制御をその内部入力から左半結合(LASJ)に渡します。これは、外部入力からの行を要求し、ソートに制御を渡します。 Sortはブロッキングイテレータであるため、テーブル変数からすべての行を読み取り、昇順でソートします(発生時)。

したがって、Sortによって出力される最初の行は値{1}です。 LASJの内側は、再帰メンバーの現在の値(スタックからポップされたばかりの値)、つまり{2}を返します。 LASJの値は{1}および{2}であるため、値が一致しないため、{1}が発行されます。

この行{1}はクエリプランツリーを上に向かってインデックス(スタック)スプールに流れ、そこでスタックに追加されます。このスタックには{1、1}が含まれ、クライアントに発行されます。クライアントはシーケンス{1}、{2}、{1}を受信しました。

これで、制御は連結に戻り、内側を下に戻り(前回は行を返しましたが、再度行う可能性があります)、内部結合を介してLASJに戻ります。内部入力を再度読み取り、Sortから値{2}を取得します。

再帰メンバーはまだ{2}であるため、今回はLASJが{2}と{2}を検出するため、行は発行されません。内部入力で行が見つからなくなった場合(ソートは行の外になりました)、制御は内部結合に戻ります。

内部結合はその外部入力を読み取ります。その結果、値{1}がスタック{1、1}からポップされ、スタックは{1}だけになります。これでプロセスが繰り返され、テーブルスキャンとソートの新しい呼び出しからの値{2}がLASJテストに合格してスタックに追加され、クライアントに渡されます。クライアントは{1}、{2}を受け取っています。 {1}、{2} ...そしてラウンドを始めます。

再帰的なCTE計画で使用されるスタックスプールの私のお気に入り 説明 は、Craig Freedmanのものです。

26
Paul White 9

再帰CTEのBOL記述 は、再帰実行のセマンティクスを次のように説明します。

  1. CTE式をアンカーメンバーと再帰メンバーに分割します。
  2. アンカーメンバーを実行して、最初の呼び出しまたは基本結果セット(T0)を作成します。
  3. 入力としてTi、出力としてTi + 1を使用して再帰メンバーを実行します。
  4. 空のセットが返されるまで、手順3を繰り返します。
  5. 結果セットを返します。これはT0からTnのUNION ALLです。

上記はlogicalの説明であることに注意してください。 ここに示すように、操作の物理的な順序は多少異なる場合があります

これをCTEに適用すると、次のパターンの無限ループが予想されます。

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

なぜなら

_select a
from cte
where a in (1,2,3)
_

アンカー式です。これは_1,2,3_を_T0_として明確に返します

その後、再帰式が実行されます

_select a
from cte
except
select a
from r
_

_1,2,3_を入力として_4,5_の出力を_T1_として出力すると、次の再帰ラウンドのために再度プラグインすると_1,2,3_が返されます。

しかし、これは実際に起こることではありません。これらは最初の5回の呼び出しの結果です

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

OPTION (MAXRECURSION 1)を使用し、_1_の増分で上方に調整することにより、連続する各レベルが_1,2,3,4_と_1,2,3,5_の出力を継続的に切り替えるサイクルに入ることがわかります。

@ Quassnoiこのブログ投稿 で議論されています。観測された結果のパターンは、各呼び出しが_(1),(2),(3),(4),(5) EXCEPT (X)_を実行している場合と同じです。ここで、Xは前の呼び出しの最後の行です。

編集:読んだ後 SQL Kiwiの優れた答え これが発生する理由とこれがその全体の話ではないことの両方が明らかですまだ処理できないスタックがまだ残っています。

アンカーが_1,2,3_をクライアントスタックコンテンツに放出_3,2,1_

3スタックから飛び出し、スタックの内容_2,1_

LASJは_1,2,4,5_、スタック内容_5,4,2,1,2,1_を返します

5スタックから飛び出し、スタックの内容_4,2,1,2,1_

LASJは_1,2,3,4_スタック内容_4,3,2,1,5,4,2,1,2,1_を返します

4スタックから飛び出し、スタックの内容_3,2,1,5,4,2,1,2,1_

LASJは_1,2,3,5_スタック内容_5,3,2,1,3,2,1,5,4,2,1,2,1_を返します

5スタックから飛び出し、スタックの内容_3,2,1,3,2,1,5,4,2,1,2,1_

LASJは_1,2,3,4_スタック内容_4,3,2,1,3,2,1,3,2,1,5,4,2,1,2,1_を返します

再帰メンバーを論理的に等価な(重複/ NULLがない場合)式に置き換えようとした場合

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

これは許可されておらず、「サブクエリでは再帰的な参照は許可されていません」というエラーが発生します。そのため、この場合でもEXCEPTが許可されているのは見落としでしょう。

追加:マイクロソフトは次のように Connectフィードバック に応答しました

ジャック の推測は正しいです。これは構文エラーであったはずです。 EXCEPT句では、再帰参照を実際に許可しないでください。今後のサービスリリースでこのバグに対処する予定です。その間、EXCEPT句での再帰的な参照を避けることをお勧めします。

EXCEPTの再帰を制限する際には、再帰が導入されて以来(1999年に私は信じています)、ANSI SQL標準に従っています。 SQLなどの宣言型言語でのEXCEPTの再帰(「非層化否定」とも呼ばれる)のセマンティクスが何であるかについては、広範な合意はありません。さらに、RDBMSシステムで(適度なサイズのデータ​​ベースに対して)このようなセマンティクスを効率的に実装することは(不可能ではないにしても)非常に難しいことです。

そして最終的に2014年にデータベースの実装が行われたようです 互換性レベルは120以上

EXCEPT句での再帰参照は、ANSI SQL標準に準拠したエラーを生成します。

31
Martin Smith