web-dev-qa-db-ja.com

WITH RECURSIVE句を使用して選択する方法

私はグーグルで このpostgreSQLマニュアルページ または このブログページ のようないくつかの記事を読んで、自分でクエリを作成してみましたが、ある程度成功しました(一部はハングしますが、他はうまく機能します)そして速い)、しかしこれまでのところ私は完全にこれを理解することができないmagic機能します。

階乗計算や(id,parent_id,name)テーブルからの完全なツリー展開などの一般的なサンプルに基づいて、このようなクエリのセマンティクスと実行プロセスを示す非常に明確な説明を誰かが提供できますか?

そして、良いwith recursiveクエリを作成するために知っておくべき基本的なガイドラインと典型的な間違いは何ですか?

21
user2754703

まず最初に、 manual page にあるアルゴリズムの説明を簡略化して明確にしてみましょう。簡単にするために、現時点ではunion all句のwith recursiveのみを考慮してください(後でunion)。

WITH RECURSIVE pseudo-entity-name(column-names) AS (
    Initial-SELECT
UNION ALL
    Recursive-SELECT using pseudo-entity-name
)
Outer-SELECT using pseudo-entity-name

それを明確にするために、疑似コードでクエリ実行プロセスを説明しましょう:

working-recordset = result of Initial-SELECT

append working-recordset to empty outer-recordset

while( working-recordset is not empty ) begin

    new working-recordset = result of Recursive-SELECT 
        taking previous working-recordset as pseudo-entity-name

    append working-recordset to outer-recordset

end

overall-result = result of Outer-SELECT 
    taking outer-recordset as pseudo-entity-name

またはさらに短い-データベースエンジンは初期選択を実行し、結果行をワーキングセットとして使用します。次に、ワーキングセットの内容を取得したクエリ結果に置き換えるたびに、ワーキングセットに対して再帰的な選択を繰り返し実行します。このプロセスは、再帰的な選択によって空のセットが返されると終了します。そして、最初に最初の選択によって、次に再帰的な選択によって与えられたすべての結果行が収集され、外側の選択に供給されます。この結果が全体的なクエリ結果になります。

このクエリは、3のfactorialを計算しています:

WITH RECURSIVE factorial(F,n) AS (
    SELECT 1 F, 3 n
UNION ALL
    SELECT F*n F, n-1 n from factorial where n>1
)
SELECT F from factorial where n=1

初期選択SELECT 1 F, 3 nは、初期値を提供します。引数は3、関数値は1です。
再帰的な選択SELECT F*n F, n-1 n from factorial where n>1は、最後の関数値に最後の引数値を乗算し、引数値をデクリメントする必要があるたびに、と述べています。
データベースエンジンは次のように実行します。

まず最初に、それは作業レコードセットの初期状態を与えるinitail selectを実行します:

F | n
--+--
1 | 3

次に、再帰クエリを使用して作業レコードセットを変換し、2番目の状態を取得します。

F | n
--+--
3 | 2

次に、3番目の状態:

F | n
--+--
6 | 1

3番目の状態では、再帰的選択でn>1条件に続く行がないため、ワーキングセットはループ終了です。

外部レコードセットは、初期および再帰的な選択によって返されるすべての行を保持するようになりました。

F | n
--+--
1 | 3
3 | 2
6 | 1

外部選択は、外部レコードセットからすべての中間結果をフィルターで除外し、全体的なクエリ結果となる最終的な階乗値のみを表示します。

F 
--
6

次に、テーブルforest(id,parent_id,name)について考えてみましょう。

id | parent_id | name
---+-----------+-----------------
1  |           | item 1
2  | 1         | subitem 1.1
3  | 1         | subitem 1.2
4  | 1         | subitem 1.3
5  | 3         | subsubitem 1.2.1
6  |           | item 2
7  | 6         | subitem 2.1
8  |           | item 3

ここで「Expanding full tr​​ee」とは、レベルと(たぶん)パスを計算するときに、人間が読める深さ優先の順序でツリー項目をソートすることを意味します。 WITH RECURSIVE句(またはPostgreSQLでサポートされていないOracle CONNECT BY句)を使用しないと、(正しいソートとレベルまたはパスの計算の)両方のタスクを1つ(または一定数の)SELECTで解決できません。しかし、この再帰的なクエリは仕事をします(まあ、ほぼそうです、下のメモを参照してください):

WITH RECURSIVE fulltree(id,parent_id,level,name,path) AS (
    SELECT id, parent_id, 1 as level, name, name||'' as path from forest where parent_id is null
UNION ALL
    SELECT t.id, t.parent_id, ft.level+1 as level, t.name, ft.path||' / '||t.name as path
    from forest t, fulltree ft where t.parent_id = ft.id
)
SELECT * from fulltree order by path

データベースエンジンは次のように実行します。

最初に、それはforestテーブルからすべての最高レベルのアイテム(ルート)を与えるinitail selectを実行します:

id | parent_id | level | name             | path
---+-----------+-------+------------------+----------------------------------------
1  |           | 1     | item 1           | item 1
8  |           | 1     | item 3           | item 3
6  |           | 1     | item 2           | item 2

次に、再帰的な選択を実行し、forestテーブルからすべての第2レベルの項目を取得します。

id | parent_id | level | name             | path
---+-----------+-------+------------------+----------------------------------------
2  | 1         | 2     | subitem 1.1      | item 1 / subitem 1.1
3  | 1         | 2     | subitem 1.2      | item 1 / subitem 1.2
4  | 1         | 2     | subitem 1.3      | item 1 / subitem 1.3
7  | 6         | 2     | subitem 2.1      | item 2 / subitem 2.1

次に、再帰的な選択を再度実行して、3Dレベルのアイテムを取得します。

id | parent_id | level | name             | path
---+-----------+-------+------------------+----------------------------------------
5  | 3         | 3     | subsubitem 1.2.1 | item 1 / subitem 1.2 / subsubitem 1.2.1

そして今度は再帰的な選択を再度実行し、4番目のレベルの項目を取得しようとしますが、どれもないため、ループが終了します。

外側のSELECTは、人間が読める正しい行順序を設定し、パス列でソートします。

id | parent_id | level | name             | path
---+-----------+-------+------------------+----------------------------------------
1  |           | 1     | item 1           | item 1
2  | 1         | 2     | subitem 1.1      | item 1 / subitem 1.1
3  | 1         | 2     | subitem 1.2      | item 1 / subitem 1.2
5  | 3         | 3     | subsubitem 1.2.1 | item 1 / subitem 1.2 / subsubitem 1.2.1
4  | 1         | 2     | subitem 1.3      | item 1 / subitem 1.3
6  |           | 1     | item 2           | item 2
7  | 6         | 2     | subitem 2.1      | item 2 / subitem 2.1
8  |           | 1     | item 3           | item 3

[〜#〜]注[〜#〜]:項目に句読文字の前に/がある句読点文字がない限り、結果の行の順序は正しいままです。名前。 Item 2Item 1 *の名前を変更すると、Item 1とその子孫の間にある行の順序が崩れます。
より安定した解決策は、クエリのパス区切り文字としてタブ文字(E'\t')を使用することです(これは、後でより読みやすいパス区切り文字に置き換えることができます:外側の選択で、人間などに説明する前に)。タブで区切られたパスは、アイテム名にタブまたは制御文字があるまで正しい順序を保持します。これは、使いやすさを損なうことなく簡単にチェックおよび除外できます。

最後のクエリを変更して任意のサブツリーを展開するのは非常に簡単です。たとえば、parent_id is nullperent_id=1で置き換えるだけです(たとえば)。このクエリバリアントはすべてのレベルとパス相対Item 1を返すことに注意してください。

そして今、典型的な間違いについてです。再帰クエリに固有の最も顕著な典型的な間違いは、再帰的選択で不適切な停止条件を定義することであり、これにより無限ループが発生します。

たとえば、上記の階乗サンプルでwhere n>1条件を省略した場合、再帰的選択を実行しても空のセットが得られることはなく(単一の行を除外する条件がないため)、ループは無限に続行されます。

これが、いくつかのクエリがハングする最も可能性の高い理由です(他の特定されない可能性のある理由は、非常に効果的でない選択であり、有限で非常に長い時間実行されます)。

私が知る限り、言及するRECURSIVE固有のクエリガイドラインはあまりありません。しかし、私は(かなり明白な)ステップバイステップの再帰的なクエリ構築手順を提案したいと思います。

  • 最初の選択を個別にビルドしてデバッグします。

  • それを足場WITH RECURSIVE構造でラップします
    そして、再帰選択の構築とデバッグを開始します。

推奨される足場構成は次のとおりです。

WITH RECURSIVE rec( <Your column names> ) AS (
    <Your ready and working initial SELECT>
UNION ALL
    <Recursive SELECT that you are debugging now>
)
SELECT * from rec limit 1000

この最も簡単な外部選択は、外部レコードセット全体を出力します。これには、ご存知のように、最初の選択からのすべての出力行と、上記のサンプルと同様に、ループ内の再帰選択のすべての実行が元の出力順序で含まれています。 limit 1000の部分はハングするのを防ぎ、見過ごされたストップポイントを確認できる特大の出力に置き換えます。

  • 初期および再帰的な選択ビルドをデバッグした後、外部選択をデバッグします。

そして最後に言及すべきことは、union all句でwith recursiveの代わりにunionを使用することの違いです。これにより、行の一意性制約が導入され、実行疑似コードに2行が追加されます。

working-recordset = result of Initial-SELECT

discard duplicate rows from working-recordset /*union-specific*/

append working-recordset to empty outer-recordset

while( working-recordset is not empty ) begin

    new working-recordset = result of Recursive-SELECT 
        taking previous working-recordset as pseudo-entity-name

    discard duplicate rows and rows that have duplicates in outer-recordset 
        from working-recordset /*union-specific*/

    append working-recordset to outer-recordset

end

overall-result = result of Outer-SELECT 
    taking outer-recordset as pseudo-entity-name
54
mas.morozov