SQL再帰は実際にどのように機能しますか?


19

他のプログラミング言語からSQLに移行すると、再帰クエリの構造はかなり奇妙に見えます。一歩ずつ歩いていくと、バラバラになっているようです。

次の簡単な例を考えてみましょう。

CREATE TABLE #NUMS
(N BIGINT);

INSERT INTO #NUMS
VALUES (3), (5), (7);

WITH R AS
(
    SELECT N FROM #NUMS
    UNION ALL
    SELECT N*N AS N FROM R WHERE N*N < 10000000
)
SELECT N FROM R ORDER BY N;

それを見てみましょう。

最初に、アンカーメンバーが実行され、結果セットがRに格納されます。したがって、Rは{3、5、7}に初期化されます。

次に、実行はUNION ALLを下回り、再帰メンバーが初めて実行されます。Rで実行されます(つまり、現在手元にあるRで実行されます:{3、5、7})。この結果は{9、25、49}になります。

この新しい結果はどうなりますか?既存の{3、5、7}に{9、25、49}を追加し、結果のユニオンRにラベルを付け、そこから再帰を続行しますか?または、Rをこの新しい結果{9、25、49}のみに再定義し、後ですべての結合を行いますか?

どちらの選択も意味がありません。

Rが{3、5、7、9、25、49}であり、再帰の次の反復を実行すると、{9、25、49、81、625、2401}になり、 {3、5、7}を失った。

Rが{9、25、49}のみの場合、ラベル付けの問題があります。Rは、アンカーメンバーの結果セットと後続のすべての再帰メンバーの結果セットの和集合であると理解されます。一方、{9、25、49}はRの構成要素にすぎません。これまでに発生した完全なRではありません。したがって、Rから選択するように再帰メンバーを記述することは意味がありません。


@Max Vernonと@Michael S.が以下に詳述していることは確かに感謝しています。つまり、(1)すべてのコンポーネントが再帰制限またはヌルセットまで作成され、(2)すべてのコンポーネントが結合されます。これが、SQL再帰が実際に機能することを理解する方法です。

SQLを再設計する場合は、次のような、より明確で明示的な構文を適用する可能性があります。

WITH R AS
(
    SELECT   N
    INTO     R[0]
    FROM     #NUMS
    UNION ALL
    SELECT   N*N AS N
    INTO     R[K+1]
    FROM     R[K]
    WHERE    N*N < 10000000
)
SELECT N FROM R ORDER BY N;

数学の帰納的証明のようなもの。

現在のSQL再帰の問題は、混乱を招くように書かれていることです。書かれた方法では、各コンポーネントはRから選択することによって形成されると言いますが、これまでに構築された(または構築されたように見える)完全なRを意味するものではありません。前のコンポーネントを意味するだけです。


「Rが現在{3、5、7、9、25、49}であり、再帰の次の反復を実行すると、{9、25、49、81、625、2401}になり、 veは{3、5、7}を失いました。」{3,5,7}がそのように機能する場合、どのように失うかわかりません。
ypercubeᵀᴹ

@yper-crazyhat-c​​ubeᵀᴹ—私が提案した最初の仮説、つまり、中間Rがその時点までに計算されたすべての累積であるとしたらどうでしょうか。次に、再帰メンバーの次の反復で、Rのすべての要素が二乗されます。したがって、{3,5,7}は{9、25、49}になり、我々は決して再びすなわちRに{7,5、3}を有し、{3,5,7}はR.から失われる
UnLogicGuys

回答:


26

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

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

したがって、各レベルには、これまでに蓄積された結果セット全体ではなく、上のレベルのみが入力されます。

上記は論理的にどのように機能するかです。物理的に再帰的なCTEは現在、ネストされたループとSQL Serverのスタックスプールで常に実装されています。これはここここで説明されており、実際には、各再帰的要素はレベル全体ではなく、前のレベルの親行でのみ機能していることを意味します。ただし、再帰CTEで許可される構文のさまざまな制限により、このアプローチが機能することを意味します。

ORDER BYクエリからを削除すると、結果は次のように並べられます

+---------+
|    N    |
+---------+
|       3 |
|       5 |
|       7 |
|      49 |
|    2401 |
| 5764801 |
|      25 |
|     625 |
|  390625 |
|       9 |
|      81 |
|    6561 |
+---------+

これは、実行計画が次のように動作するためです。 C#

using System;
using System.Collections.Generic;
using System.Diagnostics;

public class Program
{
    private static readonly Stack<dynamic> StackSpool = new Stack<dynamic>();

    private static void Main(string[] args)
    {
        //temp table #NUMS
        var nums = new[] { 3, 5, 7 };

        //Anchor member
        foreach (var number in nums)
            AddToStackSpoolAndEmit(number, 0);

        //Recursive part
        ProcessStackSpool();

        Console.WriteLine("Finished");
        Console.ReadLine();
    }

    private static void AddToStackSpoolAndEmit(long number, int recursionLevel)
    {
        StackSpool.Push(new { N = number, RecursionLevel = recursionLevel });
        Console.WriteLine(number);
    }

    private static void ProcessStackSpool()
    {
        //recursion base case
        if (StackSpool.Count == 0)
            return;

        var row = StackSpool.Pop();

        int thisLevel = row.RecursionLevel + 1;
        long thisN = row.N * row.N;

        Debug.Assert(thisLevel <= 100, "max recursion level exceeded");

        if (thisN < 10000000)
            AddToStackSpoolAndEmit(thisN, thisLevel);

        ProcessStackSpool();
    }
}

NB1:時間によって、上記のようアンカー部材の最初の子は、3その兄弟に関するすべての情報を処理し、されている57、とその子孫、すでにスプールから破棄されなかった、もはやアクセスできました。

NB2:上記のC#の全体的なセマンティクスは実行プランと同じですが、演算子がパイプライン化された実行方法で動作するため、実行プランのフローは同一ではありません。これは、アプローチの要点を示すための簡単な例です。計画自体の詳細については、以前のリンクを参照してください。

NB3:スタックスプール自体は、再帰レベルのキー列と必要に応じて一意の識別子が追加された、一意でないクラスター化インデックスとして実装されているようです(ソース


6
SQL Serverの再帰クエリは、解析中に常に再帰から反復(スタックあり)に変換されます。反復の実装規則はIterateToDepthFirst- Iterate(seed,rcsv)->PhysIterate(seed,rcsv)です。ちょっとだけ。素晴らしい答え。
ポール・ホワイトによるGoFundMonicaの発言

ちなみに、UNION ALLの代わりにUNIONも使用できますが、SQL Serverではできません。
ジョシュア

5

これは単なる(半)知識に基づいた推測であり、おそらく完全に間違っています。ところで、興味深い質問です。

T-SQLは宣言型言語です。おそらく、再帰CTEはカーソル形式の操作に変換され、UNION ALLの左側からの結果が一時テーブルに追加され、次にUNION ALLの右側が左側の値に適用されます。

そのため、まずUNION ALLの左側の出力を結果セットに挿入し、次にUNION ALLの右側の結果を左側に適用してから、結果セットに挿入します。次に、左側が右側からの出力に置き換えられ、右側が「新しい」左側に再び適用されます。このようなもの:

  1. {3,5,7}->結果セット
  2. {3,5,7}、つまり{9,25,49}に適用される再帰ステートメント。{9,25,49}は結果セットに追加され、UNION ALLの左側を置き換えます。
  3. {9,25,49}、つまり{81,625,2401}に適用される再帰ステートメント。{81,625,2401}は結果セットに追加され、UNION ALLの左側を置き換えます。
  4. {656,390625,5764801}である{81,625,2401}に適用される再帰ステートメント。{6561,390625,5764801}が結果セットに追加されます。
  5. 次の反復によりWHERE句がfalseを返すため、カーソルは完全です。

この動作は、再帰CTEの実行計画で確認できます。

ここに画像の説明を入力してください

これは上記のステップ1で、UNION ALLの左側が出力に追加されます。

ここに画像の説明を入力してください

これは、出力が結果セットに連結されるUNION ALLの右側です。

ここに画像の説明を入力してください


4

SQL Serverのドキュメントに言及、T IおよびTのI + 1は、どちらも非常に理解し、また実際の実装の正確な説明です。

基本的な考え方は、クエリの再帰部分は以前のすべての結果を一度だけ見るというものです。

同じ結果を得るために)他のデータベースがこれをどのように実装しているかを見ると役立つかもしれません。Postgresのドキュメントは言います:

再帰クエリ評価

  1. 非再帰的用語を評価します。以下のためにUNION(ではないUNION ALL)、重複行を破棄。残りのすべての行を再帰クエリの結果に含め、それらを一時作業テーブルに配置します
  2. 作業テーブルが空でない限り、次の手順を繰り返します。
    1. 再帰的な用語を評価し、作業テーブルの現在の内容を再帰的な自己参照に置き換えます。ためUNION(ただし、UNION ALL)以前の結果行を複製し、廃棄重複行と行。残りのすべての行を再帰クエリの結果に含め、それらを一時的な中間テーブルに配置します
    2. 作業テーブルの内容を中間テーブルの内容に置き換えてから、中間テーブルを空にします。


厳密に言えば、このプロセスは反復ではなく反復でありRECURSIVE、SQL標準委員会が選択した用語です。

SQLiteのドキュメントわずかに異なる実装のヒント、そしてこの1行ずつ行われたアルゴリズムは、理解するのが最も簡単かもしれません。

再帰テーブルのコンテンツを計算するための基本的なアルゴリズムは次のとおりです。

  1. を実行しinitial-select、結果をキューに追加します。
  2. キューが空ではない間:
    1. キューから単一の行を抽出します。
    2. その単一の行を再帰的なテーブルに挿入します
    3. 抽出した単一の行が再帰テーブル内の唯一の行であると仮定しrecursive-select、を実行して、すべての結果をキューに追加します。

上記の基本手順は、次の追加ルールによって変更される場合があります。

  • UNIONオペレータが接続した場合initial-selectrecursive-select全く同じ行が以前にキューに追加されていない場合、のみキューに行を追加します。繰り返し行は、再帰ステップによってキューから既に抽出されている場合でも、キューに追加される前に破棄されます。演算子がUNION ALLの場合、initial-selectとの両方で生成されたすべての行は、recursive-selectたとえそれらが繰り返されていても常にキューに追加されます。
    […]

0

私の知識は特にDB2にありますが、説明図を見るのはSQL Serverでも同じようです。

計画はここから来ます:

計画の貼り付けでご覧ください

SQL Server Explain Plan

オプティマイザーは、再帰的なクエリごとに文字通りユニオンを実行しません。クエリの構造を取り、ユニオンの最初の部分をすべて「アンカーメンバー」に割り当ててから、ユニオンの後半部分をすべて実行します(定義された制限に達するまで再帰的に「再帰メンバー」と呼ばれます)。再帰が完了すると、オプティマイザーはすべてのレコードを結合します。

オプティマイザーは、事前定義された操作を行うための提案としてそれを受け取ります。

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