web-dev-qa-db-ja.com

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

他のプログラミング言語から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)すべてのコンポーネントが再帰制限またはnullセットまで作成され、次に(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を意味するものではありません。それは単に前のコンポーネントを意味します。

19
UnLogicGuys

再帰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:スタックスプール自体は、非一意のクラスター化インデックスとして実装されているようです。再帰レベルのキー列と必要に応じて追加された一意名( ソース

26
Martin Smith

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

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. {81,625,2401}に適用される再帰ステートメント、つまり{6561,390625,5764801}です。 {6561,390625,5764801}が結果セットに追加されます。
  5. 次の反復でWHERE句がfalseを返すため、カーソルは完全です。

この動作は、再帰CTEの実行プランで確認できます。

enter image description here

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

enter image description here

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

enter image description here

5
Max Vernon

SQL ServerのドキュメントTについて言及およびTi + 1は、あまり理解できず、実際の実装を正確に説明するものでもありません。

基本的な考え方は、クエリの再帰部分は以前のすべての結果を調べますが、一度だけです。

sameの結果を取得するために)他のデータベースがこれをどのように実装しているかを調べると役立つ場合があります。 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-select とともに recursive-select、次に同じ行が以前にキューに追加されていない場合にのみ、行をキューに追加します。繰り返し行は、再帰ステップによって繰り返し行がすでにキューから抽出されている場合でも、キューに追加される前に破棄されます。演算子がUNION ALLの場合、両方によって生成されたすべての行 initial-select そしてその recursive-select 繰り返しであっても常にキューに追加されます。
    […]
4
CL.

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

計画はここから来ます:

プランの貼り付けで確認してください

SQL Server Explain Plan

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

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

0
Michael S.