PostgreSQL 9.4で隣接リストとして保存されている典型的なツリー構造があります。
gear_category (
id INTEGER PRIMARY KEY,
name TEXT,
parent_id INTEGER
);
カテゴリに添付されたアイテムのリストと同様に:
gear_item (
id INTEGER PRIMARY KEY,
name TEXT,
category_id INTEGER REFERENCES gear_category
);
葉だけでなく、どのカテゴリーにもギアアイテムを付けることができます。
速度上の理由から、マテリアライズドビューを生成するために使用する各カテゴリに関するいくつかのデータを事前に計算したいと思います。
望ましい出力:
speedy_materialized_view (
gear_category_id INTEGER,
count_direct_child_items INTEGER,
count_recursive_child_items INTEGER
);
count_recursive_child_items
は、現在のカテゴリまたは子カテゴリに関連付けられているGearItemsの累積数です。カテゴリごとに1つの行があり、0のカウントはすべて0です。
これを計算するには、再帰CTEを使用してツリーをトラバースする必要があります。
WITH RECURSIVE children(id, parent_id) AS (
--base case
SELECT gear_category.id AS id, gear_category.parent_id AS parent_id
FROM gear_category
WHERE gear_category.id = 37 -- setting this to id includes current object
-- setting to parent_id includes only children
--combine with recursive part
UNION ALL
SELECT gear_category.id AS gear_category_id
, gear_category.parent_id AS gear_category_parent_id
FROM gear_category, children
WHERE children.id = gear_category.parent_id
)
TABLE children;
この子カテゴリーのリストに添付されている子ギアのアイテムを数えるのは簡単です:
--Subselect variant
SELECT count(gear_item.id) AS count_recursive_child_items_for_single_cat
FROM gear_item
WHERE gear_item.category_id IN (
SELECT children.id AS children_id
FROM children);
-- JOIN variant
SELECT count(gear_item.id) AS count_recursive_child_items_for_single_cat
FROM gear_item, children
WHERE gear_item.category_id = children.id;
しかし、CTEを見ると、「37」の開始カテゴリーIDがハードコーディングされています。これらのクエリを組み合わせて、単一のカテゴリだけでなく、すべてのカテゴリのcount_recursive_child_itemsを生成する方法がわかりません。
これらを組み合わせるにはどうすればよいですか?
また、現在、各カテゴリについて、すべての子カテゴリを計算しているため、多くの重複作業が発生します。これを削除する方法がわかりません。たとえば、祖父母>親>葉があるとします。現在、祖父母と親の子カテゴリを別々に計算しています。つまり、親>葉の関係を2回計算しています。
また、各カテゴリのcount_direct_child_items
はすでに返されているため、現在のように最初からカウントするよりも、count_recursive_child_items
を計算するときにこれらを使用する方が速い場合があります。
それとは別に、これらの各概念は私にとって意味があります。それらを1つのエレガントで最適化されたクエリに組み合わせる方法を理解できません。
これは仕事をします:
CREATE MATERIALIZED VIEW speedy_materialized_view AS
WITH RECURSIVE tree AS (
SELECT id, parent_id, ARRAY[id] AS path
FROM gear_category
WHERE parent_id IS NULL
UNION ALL
SELECT c.id, c.parent_id, path || c.id
FROM tree t
JOIN gear_category c ON c.parent_id = t.id
)
, tree_ct AS (
SELECT t.id, t.path, COALESCE(i.item_ct, 0) AS item_ct
FROM tree t
LEFT JOIN (
SELECT category_id AS id, count(*) AS item_ct
FROM gear_item
GROUP BY 1
) i USING (id)
)
SELECT t.id
, t.item_ct AS count_direct_child_items
, sum(t1.item_ct) AS count_recursive_child_items
FROM tree_ct t
LEFT JOIN tree_ct t1 ON t1.path[1:array_upper(t.path, 1)] = t.path
GROUP BY t.id, t.item_ct;
count_recursive_child_items
はカテゴリごとに個別にカウントされるので、これがディープツリーの最速の方法であるとは思いません。
ただし、集計関数は再帰CTEでは許可されていません。
厳密に言えば、それは実際には反復的ですが、「再帰的」CTEもそうです。
一時テーブルを操作する関数を作成できます。 plpgsqlの使い方を理解する必要があるか、説明が多すぎます。
CREATE OR REPLACE FUNCTION f_tree_ct()
RETURNS TABLE (id int, count_direct_child_items int, count_recursive_child_items int) AS
$func$
DECLARE
_lvl int;
BEGIN
-- basic table with added path and count
CREATE TEMP TABLE t1 AS
WITH RECURSIVE tree AS (
SELECT c.id, c.parent_id, '{}'::int[] AS path, 0 AS lvl
FROM gear_category c
WHERE c.parent_id IS NULL
UNION ALL
SELECT c.id, c.parent_id, path || c.parent_id, lvl + 1
FROM tree t
JOIN gear_category c ON c.parent_id = t.id
)
, tree_ct AS (
SELECT t.id, t.parent_id, t.path, t.lvl, COALESCE(i.item_ct, 0) AS item_ct
FROM tree t
LEFT JOIN (
SELECT i.category_id AS id, count(*)::int AS item_ct
FROM gear_item i
GROUP BY 1
) i USING (id)
)
TABLE tree_ct;
-- CREATE INDEX ON t1 (lvl); -- only for very deep trees
SELECT INTO _lvl max(lvl) FROM t1; -- identify max lvl to start bottom up
-- recursively aggregate each level in 2nd temp table
CREATE TEMP TABLE t2 AS
SELECT t1.id, t1.parent_id, t1.lvl
, t1.item_ct
, t1.item_ct AS sum_ct
FROM t1
WHERE t1.lvl = _lvl;
IF _lvl > 0 THEN
FOR i IN REVERSE _lvl .. 1 LOOP
INSERT INTO t2
SELECT t1.id, t1.parent_id, t1.lvl, t1.item_ct
, CASE WHEN t2.sum_ct IS NULL THEN t1.item_ct ELSE t1.item_ct + t2.sum_ct END
FROM t1
LEFT JOIN (
SELECT t2.parent_id AS id, sum(t2.sum_ct) AS sum_ct
FROM t2
WHERE t2.lvl = i
GROUP BY 1
) t2 USING (id)
WHERE t1.lvl = i - 1;
END LOOP;
END IF;
RETURN QUERY -- only requested columns, unsorted
SELECT t2.id, t2.item_ct, t2.sum_ct FROM t2;
DROP TABLE t1, t2; -- to allow repeated execution in one transaction
RETURN;
END
$func$ LANGUAGE plpgsql;
これは、一時テーブルを使用するため、CREATE MATERIALIZED VIEW
ステートメントに含めることはできません。手動で維持される「マテリアライズドビュー」として機能する、別の(一時)テーブルを作成することもできます。
CREATE TABLE speedy_materialized_view AS
SELECT * FROM f_tree_ct();
または、関数でTRUNCATE speedy_materialized_view
を使用して、関数に直接書き込むこともできます。関数は代わりにRETURNS void
になるか、行数などのメタ情報を返すことができます...
余談:
出力列名は非再帰的な用語によってのみ決定されるため、CTEの再帰的な用語の列のエイリアスは単なるドキュメントです。