私はグーグルで このpostgreSQLマニュアルページ または このブログページ のようないくつかの記事を読んで、自分でクエリを作成してみましたが、ある程度成功しました(一部はハングしますが、他はうまく機能します)そして速い)、しかしこれまでのところ私は完全にこれを理解することができないmagic機能します。
階乗計算や(id,parent_id,name)
テーブルからの完全なツリー展開などの一般的なサンプルに基づいて、このようなクエリのセマンティクスと実行プロセスを示す非常に明確な説明を誰かが提供できますか?
そして、良いwith recursive
クエリを作成するために知っておくべき基本的なガイドラインと典型的な間違いは何ですか?
まず最初に、 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 tree」とは、レベルと(たぶん)パスを計算するときに、人間が読める深さ優先の順序でツリー項目をソートすることを意味します。 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 2
でItem 1 *
の名前を変更すると、Item 1
とその子孫の間にある行の順序が崩れます。
より安定した解決策は、クエリのパス区切り文字としてタブ文字(E'\t'
)を使用することです(これは、後でより読みやすいパス区切り文字に置き換えることができます:外側の選択で、人間などに説明する前に)。タブで区切られたパスは、アイテム名にタブまたは制御文字があるまで正しい順序を保持します。これは、使いやすさを損なうことなく簡単にチェックおよび除外できます。
最後のクエリを変更して任意のサブツリーを展開するのは非常に簡単です。たとえば、parent_id is null
をperent_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