祖先から子孫の深さを計算する必要があります。レコードにobject_id = parent_id = ancestor_id
、それはルートノード(祖先)と見なされます。 WITH RECURSIVE
PostgreSQLで実行されるクエリ9.4。
データや列を制御しません。データとテーブルスキーマは外部ソースから取得されます。テーブルは継続的に成長しています。現在、1日あたり約3万件のレコードがあります。 ツリー内のすべてのノードが欠落している可能性があり、それらはある時点で外部ソースからプルされます。それらは通常created_at DESC
順序ですが、データは非同期のバックグラウンドジョブで取得されます。
最初はこの問題のコードソリューションがありましたが、現在は500万行以上あり、完了するまでに約30分かかります。
テーブル定義とテストデータの例:
CREATE TABLE objects (
id serial NOT NULL PRIMARY KEY,
customer_id integer NOT NULL,
object_id integer NOT NULL,
parent_id integer,
ancestor_id integer,
generation integer NOT NULL DEFAULT 0
);
INSERT INTO objects(id, customer_id , object_id, parent_id, ancestor_id, generation)
VALUES (2, 1, 2, 1, 1, -1), --no parent yet
(3, 2, 3, 3, 3, -1), --root node
(4, 2, 4, 3, 3, -1), --depth 1
(5, 2, 5, 4, 3, -1), --depth 2
(6, 2, 6, 5, 3, -1), --depth 3
(7, 1, 7, 7, 7, -1), --root node
(8, 1, 8, 7, 7, -1), --depth 1
(9, 1, 9, 8, 7, -1); --depth 2
ご了承ください object_id
は一意ではありませんが、組み合わせ(customer_id, object_id)
ユニークです。
次のようなクエリを実行します:
WITH RECURSIVE descendants(id, customer_id, object_id, parent_id, ancestor_id, depth) AS (
SELECT id, customer_id, object_id, parent_id, ancestor_id, 0
FROM objects
WHERE object_id = parent_id
UNION
SELECT o.id, o.customer_id, o.object_id, o.parent_id, o.ancestor_id, d.depth + 1
FROM objects o
INNER JOIN descendants d ON d.parent_id = o.object_id
WHERE
d.id <> o.id
AND
d.customer_id = o.customer_id
) SELECT * FROM descendants d;
generation
列を計算された深度として設定したいのですが。新しいレコードが追加されると、生成列は-1に設定されます。 parent_id
はまだプルされていない可能性があります。 parent_id
は存在しません。生成列を-1に設定したままにする必要があります。
最終的なデータは次のようになります。
id | customer_id | object_id | parent_id | ancestor_id | generation
2 1 2 1 1 -1
3 2 3 3 3 0
4 2 4 3 3 1
5 2 5 4 3 2
6 2 6 5 3 3
7 1 7 7 7 0
8 1 8 7 7 1
9 1 9 8 7 2
クエリの結果は、生成列を正しい深さに更新する必要があります。
私は SOに関するこの関連質問への回答 から作業を開始しました。
あなたが持っているクエリは基本的に正しいです。唯一の間違いは、CTEの2番目の(再帰的)部分にあります。
_INNER JOIN descendants d ON d.parent_id = o.object_id
_
それは逆になるはずです:
_INNER JOIN descendants d ON d.object_id = o.parent_id
_
オブジェクトをそれらの親(すでに検出されている)と結合します。
したがって、深さを計算するクエリを書くことができます(他に何も変更されず、フォーマットのみ):
_-- calculate generation / depth, no updates
WITH RECURSIVE descendants
(id, customer_id, object_id, parent_id, ancestor_id, depth) AS
AS ( SELECT id, customer_id, object_id, parent_id, ancestor_id, 0
FROM objects
WHERE object_id = parent_id
UNION ALL
SELECT o.id, o.customer_id, o.object_id, o.parent_id, o.ancestor_id, d.depth + 1
FROM objects o
INNER JOIN descendants d ON d.customer_id = o.customer_id
AND d.object_id = o.parent_id
WHERE d.id <> o.id
)
SELECT *
FROM descendants d
ORDER BY id ;
_
更新の場合は、最後のSELECT
をUPDATE
に置き換え、cteの結果を結合してテーブルに戻します。
_-- update nodes
WITH RECURSIVE descendants
-- nothing changes here except
-- ancestor_id and parent_id
-- which can be omitted form the select lists
)
UPDATE objects o
SET generation = d.depth
FROM descendants d
WHERE o.id = d.id
AND o.generation = -1 ; -- skip unnecessary updates
_
テスト済みSQLfiddle
追加コメント:
ancestor_id
_と_parent_id
_を選択リストに含める必要はありません(祖先は明白ですが、親を理解するには少し注意が必要です)。したがって、必要に応じてSELECT
クエリにそれらを保持できます。 UPDATE
から安全に削除できます。(customer_id, object_id)
_は、UNIQUE
制約の候補のようです。データがこれに準拠している場合は、そのような制約を追加します。再帰CTEで実行された結合は、一意でないと意味がありません(ノードに2つの親が含まれる場合があります)。(customer_id, parent_id)
_は_FOREIGN KEY
_制約の候補となり、REFERENCES
(一意の)_(customer_id, object_id)
_になります。あなたはおそらく、そのFK制約を追加したいではないので、説明により、新しい行を追加しており、一部の行は他の行を参照できますまだ追加されていません。AND o.generation = -1
_により、1回目の実行で更新された行が再度更新されなくなりますが、CTEは依然として高価な部分です。以下は、これらの問題に対処するための試みです。できるだけ少ない行を考慮するようにCTEを改善し、_(customer_id, obejct_id)
_ではなく_(id)
_を使用して行を識別します(したがって、id
はクエリから完全に削除されます。最初の更新または後続として使用する:
_WITH RECURSIVE descendants
(customer_id, object_id, depth)
AS ( SELECT customer_id, object_id, 0
FROM objects
WHERE object_id = parent_id
AND generation = -1
UNION ALL
SELECT o.customer_id, o.object_id, p.generation + 1
FROM objects o
JOIN objects p ON p.customer_id = o.customer_id
AND p.object_id = o.parent_id
AND p.generation > -1
WHERE o.generation = -1
UNION ALL
SELECT o.customer_id, o.object_id, d.depth + 1
FROM objects o
INNER JOIN descendants d ON o.customer_id = d.customer_id
AND o.parent_id = d.object_id
WHERE o.parent_id <> o.object_id
AND o.generation = -1
)
UPDATE objects o
SET generation = d.depth
FROM descendants d
WHERE o.customer_id = d.customer_id
AND o.object_id = d.object_id
AND o.generation = -1 -- this is not really needed
_
CTEが3つの部分に分かれていることに注意してください。最初の2つは安定したパーツです。最初の部分では、以前に更新されておらず、_generation=-1
_がまだ残っているルートノードを見つけます。そのため、これらは新しく追加されたノードである必要があります。 2番目の部分は、以前に更新された親ノードの子(_generation=-1
_付き)を見つけます。
以前のように、3番目の再帰パートでは、最初の2つのパートのすべての子孫が検索されます。
テスト済みSQLfiddle-2
@ ypercube既に は十分な説明を提供するので、追加する必要があるものを追跡します。
parent_id
が存在しない場合は、生成列を-1に設定したままにする必要があります。
これは再帰的に適用されることになっていると思います。つまり、ツリーの残りの部分は常に不足しているノードの後にgeneration = -1
があります。
ツリーのノードが(まだ)見つからない場合、generation = -1
という行を見つける必要があります...
...はルートノードです
...またはgeneration > -1
の親がいます。
そこから木を横断します。この選択の子ノードには、generation = -1
も必要です。
ルートノードの場合、親のgeneration
を1つインクリメントするか、0にフォールバックします。
WITH RECURSIVE tree AS (
SELECT c.customer_id, c.object_id, COALESCE(p.generation + 1, 0) AS depth
FROM objects c
LEFT JOIN objects p ON c.customer_id = p.customer_id
AND c.parent_id = p.object_id
AND p.generation > -1
WHERE c.generation = -1
AND (c.parent_id = c.object_id OR p.generation > -1)
-- root node ... or parent with generation > -1
UNION ALL
SELECT customer_id, c.object_id, p.depth + 1
FROM objects c
JOIN tree p USING (customer_id)
WHERE c.parent_id = p.object_id
AND c.parent_id <> c.object_id -- exclude root nodes
AND c.generation = -1 -- logically redundant, but see below!
)
UPDATE objects o
SET generation = t.depth
FROM tree t
WHERE o.customer_id = t.customer_id
AND o.object_id = t.object_id;
この方法では、非再帰部分は単一のSELECT
ですが、@ ypercubeの2つの結合されたSELECT
と論理的に同等です。どちらが速いかわかりません。テストする必要があります。
パフォーマンスにとってより重要なポイントは次のとおりです。
この方法でbigテーブルに行を繰り返し追加する場合は、 部分インデックスを追加します :
CREATE INDEX objects_your_name_idx ON objects (customer_id, parent_id, object_id)
WHERE generation = -1;
これにより、これまでに説明した他のすべての改善よりもパフォーマンスが向上します-大きなテーブルへの小さな追加の繰り返し。
CTEの再帰部分に(論理的には冗長ですが)インデックス条件を追加して、クエリプランナーが部分インデックスが適用可能であることを理解できるようにしました。
さらに、おそらく、@ ypercubeがすでに言及した(object_id, customer_id)
に対するUNIQUE
制約も必要です。または、何らかの理由で一意性を課すことができない場合(なぜですか)、代わりにプレーンインデックスを追加します。インデックス列の順序は重要です、ところで: