26

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

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

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

4

1 に答える 1

69

まず、マニュアルページに記載されているアルゴリズムの説明を単純化して明確にしてみましょう。簡単にするために、今のところ(および後で)節でのみ検討union allしてください。with recursiveunion

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 の階乗を計算しています。

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

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

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

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

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

最初に、initail select を実行します。これにより、forestテーブルからすべての最上位アイテム (ルート) が得られます。

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 2と、とその子孫 Item 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パーツはぶら下がりを防ぎ、見逃した停止ポイントを確認できる特大の出力に置き換えます。

  • 初期選択と再帰選択をデバッグした後、外部選択をビルドしてデバッグします。

最後に、 in句unionの代わりに使用することの違いについて説明します。行の一意性制約が導入され、実行擬似コードに 2 つの余分な行が発生します。union allwith recursive

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
于 2013-09-06T14:59:17.050 に答える