まず、マニュアルページに記載されているアルゴリズムの説明を簡略化して明確にしてみましょう。単純化するために、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:
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
'完全なツリーの拡張 'ここでは、レベルと(おそらく)パスを計算しながら、人間が読める深さ優先順にツリーアイテムを並べ替えることを意味します。 (正しいソートとレベルまたはパスの計算の)両方のタスクは、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 null
を置き換えるだけで済みます。 perent_id=1
を使用 (例えば)。このクエリバリアントは、相対的なすべてのレベルとパスを返すことに注意してください。 Item 1
。
そして今、典型的な間違いについて 。再帰クエリに固有の最も顕著な典型的な間違いは、再帰選択で不正な停止条件を定義することです。これにより、無限のループが発生します。
たとえば、where n>1
を省略した場合 上記の階乗サンプルの条件では、再帰的選択を実行しても空のセットが得られることはなく(単一の行を除外する条件がないため)、ループは無限に続きます。
これが、クエリの一部がハングする最も可能性の高い理由です(他の非特定であるが、それでも考えられる理由は、非常に効果のない選択であり、有限であるが非常に長い時間で実行されます)。
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
この最も単純な外部選択は、外部レコードセット全体を出力します。これには、上記のサンプルと同様に、最初の選択からのすべての出力行と、ループ内のrecusriveselectのすべての実行が元の出力順序で含まれています。 limit 1000
パーツがぶら下がるのを防ぎ、見逃したストップポイントを確認できる特大の出力に置き換えます。
- 初期選択と再帰選択をデバッグした後、外部選択をビルドしてデバッグします。
そして最後に言及するのは、 union
の使用の違いです。 union all
の代わりに with recursive
句。行の一意性制約が導入され、実行擬似コードに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