再帰共通テーブル式でEXCEPTを使用する


33

次のクエリが無限行を返すのはなぜですか?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 に関する質問に答えようとしているときに、私はこれに出会いました。

回答:


26

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

何を見ていたか、そしてその理由を説明するには:

ここでは、アンカー値と再帰項目の区別を明確にするためにテーブル変数を使用しています(セマンティックは変更されません)。

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)

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

再帰的CTEプラン

実行はプランのルート(SELECT)で開始され、制御はツリーを下ってインデックススプール、連結、そして最上位のテーブルスキャンに渡されます。

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

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

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

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

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

したがって、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プランで使用されるStackスプールについての私のお気に入りの説明は、Craig Freedmanのものです。


31

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

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

上記は論理的な説明です。操作の物理的な順序は、ここに示すように多少異なる場合があります

これを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,3T0

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

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この場合でも許可されているのは見落としです。

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

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

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

そして、120以上の互換性レベルを持つデータベースに対して、2014年に最終的な実装が行われたように見えます。

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

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.