Convert recursive function to viewに基づいて、ノードの親のスナップショットを作成することにより、グラフ内の任意のノードからルートへのパスの取得を高速化したいと考えています。再帰的なツリー ウォークは、それ以上の再帰を回避して実行時間を短縮する中間スナップショットを持つことによって制限されるという考え方です。私は負荷テストを行っていないので、この単純な例を超えてこれがどのように機能するかはわかりませんが、初期の試行ではすでにいくつかのボトルネックが示されています. クエリを高速化/簡素化する方法についてコメントをいただければ幸いです。Postgres 9.2.2.0 (20) を使用しています。
DROP TABLE IF EXISTS revision CASCADE;
CREATE TABLE revision (
id serial primary key,
parent_revision_id int references revision(id),
username varchar(128),
ts timestamp without time zone
);
DROP TABLE IF EXISTS revision_snapshot CASCADE;
CREATE TABLE revision_snapshot (
id serial primary key,
revision_id int,
parent_revision_id int,
depth int
);
CREATE OR REPLACE FUNCTION create_revision_snapshot(_rev int)
RETURNS void AS
$func$
DELETE FROM revision_snapshot WHERE revision_id=$1;
INSERT INTO revision_snapshot (revision_id, parent_revision_id, depth)
(SELECT $1, id, depth FROM revision_tree($1));
$func$ LANGUAGE sql;
-- Recursively return path from '_rev' to root
CREATE OR REPLACE FUNCTION revision_tree(_rev int)
RETURNS TABLE(id int, parent_revision_id int, depth int) AS
$func$
WITH RECURSIVE rev_list(id, parent_revision_id, depth) AS (
SELECT t.id, t.parent_revision_id, 1
FROM revision t
WHERE t.id = $1
UNION ALL
SELECT t.id, t.parent_revision_id, r.depth + 1
FROM rev_list r
JOIN revision t ON t.id = r.parent_revision_id
)
SELECT t.id, t.parent_revision_id, t.depth
FROM rev_list t
ORDER BY t.id;
$func$ LANGUAGE sql;
-- Fast version of 'revision_tree' (to be). This version will return the
-- revision tree making use of snapshots (recursively returning the path from
-- specified revision id to last snapshot of the path to the root + the snapshot)
CREATE OR REPLACE FUNCTION revision_tree_perf(_rev int)
RETURNS TABLE(parent_revision_id int) AS
$func$
BEGIN
CREATE TEMP TABLE graph_result ON COMMIT DROP AS
WITH RECURSIVE rev_list(id, parent_revision_id, depth) AS (
SELECT t.id, t.parent_revision_id, 1
FROM revision t
WHERE t.id = $1
UNION ALL
SELECT t.id, t.parent_revision_id, r.depth + 1
FROM rev_list r
JOIN revision t ON t.id = r.parent_revision_id
WHERE not(t.id in (select revision_id from revision_snapshot))
)
SELECT t.id, t.parent_revision_id, t.depth
FROM rev_list t
ORDER BY t.id;
RETURN QUERY
SELECT g.parent_revision_id FROM graph_result AS g WHERE g.parent_revision_id IS NOT NULL
UNION
SELECT s.parent_revision_id FROM revision_snapshot AS s WHERE
s.revision_id = (SELECT min(q.parent_revision_id) FROM graph_result as q) ORDER BY parent_revision_id;
END;
$func$
LANGUAGE 'plpgsql';
-- Example tree
--
-- +-- <10>
-- /
-- +-- <4> -- <8> --- <9> -+- <11> --- <15> --- <16> --- <17>
-- /
-- <1> --- <2> --- <3> -+
-- \
-- +-- <5> --- <6> --- <7> --- <12> -+- <14> --- <18>
-- \
-- \
-- \
-- \
-- +-- <13> --- <19> --- <20> --- <21>
--
INSERT INTO revision (username, ts, parent_revision_id) VALUES
('someone', now(), null) -- 1
,('someone', now(), 1) -- 2
,('someone', now(), 2) -- 3
,('someone', now(), 3) -- 4
,('someone', now(), 3) -- 5
,('someone', now(), 5) -- 6
,('someone', now(), 6) -- 7
,('someone', now(), 4) -- 8
,('someone', now(), 8) -- 9
,('someone', now(), 9) -- 10
,('someone', now(), 9) -- 11
,('someone', now(), 7) -- 12
,('someone', now(), 12) -- 13
,('someone', now(), 12) -- 14
,('someone', now(), 11) -- 15
,('someone', now(), 15) -- 16
,('someone', now(), 16) -- 17
,('someone', now(), 14) -- 18
,('someone', now(), 13) -- 19
,('someone', now(), 19) -- 20
,('someone', now(), 20); -- 21
-- Create a revision snapsnot
select create_revision_snapshot(13);
-- This query is meant to be faster ...
select * from revision_tree_perf(21);
-- ... than this one
select * from revision_tree(21);
上記の例
select create_revision_snapshot(13);
select * from revision_tree_perf(21);
は、21 からルートへのパスを示すレコード セットを生成することを目的としてい(21, 20, 19, 13, 12, 7, 6, 5, 3, 2, 1)
ます。ソリューションの一部は、ツリー (13 のスナップショットがあるため、21 から 13 まで、つまり 13 のスナップショットがあるため、これ以上ツリーをたどる必要はありません) をたどり、13 からルートへの既に「キャッシュされた」パスを使用することによって取得されます (リビジョン_スナップショット)。少しでも分かりやすくなれば幸いです...
更新:
改善の可能性を思いつきました。exists
これは闇の中での一刺しにすぎませんが、句はかなり高価であることが想像できます。リビジョン テーブルにスナップショットの存在をマークしました。
CREATE TABLE revision (
id serial primary key,
parent_revision_id int references revision(id),
username varchar(128),
has_snapshot boolean default false,
ts timestamp without time zone
);
CREATE OR REPLACE FUNCTION create_revision_snapshot(_rev int) RETURNS void AS $func$
DELETE FROM revision_snapshot WHERE revision_id=$1;
INSERT INTO revision_snapshot (revision_id, parent_revision_id, depth)
(SELECT $1, id, depth FROM revision_tree($1));
-- Mark revision table to avoid costly exists/in clause
UPDATE revision SET has_snapshot = true WHERE id=$1;
$func$ LANGUAGE sql;
revision_tree_perf
これにより、 SPの CTE 部分が次のように変更されます。
WITH RECURSIVE rev_list(id, parent_revision_id, depth) AS (
SELECT t.id, t.parent_revision_id, 1 -- AS depth
FROM revision t
WHERE t.id = $1
UNION ALL
SELECT t.id, t.parent_revision_id, r.depth + 1
FROM rev_list r
JOIN revision t ON t.id = r.parent_revision_id
WHERE t.has_snapshot = false
)
SELECT t.id, t.parent_revision_id, t.depth FROM rev_list t ORDER BY t.id;
これは非常に迅速に実行されるはずです。パズルのもう 1 つの部分は、revision id where から Revision_snapshot のコンテンツを返し、has_snapshot=true
これら 2 つの結果を結合することです。問題は、CTE からこのリビジョン ID を取得する方法です。CTE のクエリ結果を一時テーブルに保存し、リビジョン ID をクエリするか、CTE を記述せずにループとして記述することをお勧めします。このようにして、ループが終了するリビジョン ID を追跡できます (いつhas_snapshot = true
)。しかし、これが CTE とどのように比較されるかはわかりません。
人々はこのアプローチについてどう思いますか?