我们有这个漂亮的 Postgres 树生成器。然而,它会同时产生一棵树的切割,而不是一棵整棵树:
item_id jsonb_pretty
1 {
"title": "PARENT",
"item_id": 1,
"children": {
"title": "LEVEL 2",
"item_id": 2,
"children": {
"title": "LEVEL 3.2",
"item_id": 6
}
}
}
1 {
"title": "PARENT",
"item_id": 1,
"children": {
"title": "LEVEL 2",
"item_id": 2,
"children": {
"title": "LEVEL 3.1",
"item_id": 3,
"children": {
"title": "LEVEL 4.1",
"item_id": 4
}
}
}
}
1 {
"title": "PARENT",
"item_id": 1,
"children": {
"title": "LEVEL 2",
"item_id": 2,
"children": {
"title": "LEVEL 3.1",
"item_id": 3,
"children": {
"title": "LEVEL 4.2",
"item_id": 5
}
}
}
}
我想用这样的数组从中取出一个树对象:
1 {
"title": "PARENT",
"item_id": 1,
"children": [{
"title": "LEVEL 2",
"item_id": 2,
"children": [{
"title": "LEVEL 3.2",
"item_id": 6
},
{
"title": "LEVEL 3.1",
"item_id": 3,
"children": [{
"title": "LEVEL 4.1",
"item_id": 4
},
{
"title": "LEVEL 4.2",
"item_id": 5
}]
}]
}]
}
这是生成器:
CREATE TABLE items (
item_id serial PRIMARY KEY,
title text
);
CREATE TABLE joins (
id serial PRIMARY KEY,
item_id int,
child_id int
);
INSERT INTO items (item_id,title) VALUES
(1,'PARENT'),
(2,'LEVEL 2'),
(3,'LEVEL 3.1'),
(4,'LEVEL 4.1'),
(5,'LEVEL 4.2'),
(6,'LEVEL 3.2');
INSERT INTO joins (item_id, child_id) VALUES
(1,2),
(2,3),
(3,4),
(3,5),
(2,6);
WITH RECURSIVE t(item_id, json, level) AS (
SELECT item_id, to_jsonb(items), 1
FROM items
WHERE NOT EXISTS (
SELECT 2
FROM joins
WHERE items.item_id = joins.item_id
)
UNION ALL
SELECT parent.item_id, to_jsonb(parent) || jsonb_build_object( 'children', t.json ),
level + 1
FROM t
JOIN joins AS j
ON t.item_id = j.child_id
JOIN items AS parent
ON j.item_id = parent.item_id
WHERE level < 7
)
SELECT item_id, jsonb_pretty(json)
FROM t
WHERE item_id = 1;
我将如何更改这样的生成器以在 Postgres 13+ 中生成一棵树?
递归 CTE ( rCTE ) 是遵循路径并识别树的所有节点的正确工具。但它不允许在递归项中进行聚合。有一个优雅的选择:
递归函数
db<>在这里摆弄
使用默认最大级别 (7) 调用:
产生您想要的结果:
只需 3 个级别即可调用:
请注意递归过早停止时的特殊输出:
当然,外部
jsonb_pretty()
是可选的。您可以将外部调用包装到另一个函数中或将其塞进同一个函数中,可能使用 PL/pgSQL 并使用
IF
. 这是品味和风格的问题。我在这里保持简单。这是实际的递归,而rCTE 实际上只是iterate。你需要理解这个概念才能理解函数:它调用自己。每次调用减少最大值
_level
是最终终止递归的原因。我为最后一个递归级别构建了特殊行为:如果最大级别切断了更多子级,该函数会插入一个计数和一个提示。
(可选!)调用
jsonb_strip_nulls()
...删除空的子字段(带
NULL
值)。如果您想保留其他具有 NULL 值的字段,请考虑jsonb_set_lax()
在函数中代替,就像下面使用纯 SQL 解决方案一样。理想情况下,您有一个索引
joins(item_id, child_id)
- 或该索引隐式附带的UNIQUE
约束。(item_id, child_id)
有关的:
纯 SQL
虽然只有一堆级别,但您也可以拼出级联子查询或具有聚合的非递归 CTE。
查询变得相当冗长,但与原始查询相比,性能应该仍然不错。很大程度上是因为我恢复了最初的查询方法。由于您对给定的根 ID 感兴趣,因此从根而不是叶子开始应该更有效- 坚持树作为隐喻。这样,我们只选择属于树的行。
按照您的方式,正在处理整个表的所有行,只是为了过滤最后源自给定根的一棵树。这是很多浪费的努力。
此外,我在选择所有相关节点后加入
items
一次,而不是像你那样每次都在递归术语中。选择树的所有节点后,需要再次自顶向下聚合,从叶子到根,因为每个较低级别都集成了下一个较高级别的聚合数组。
最多查询4 个级别以覆盖您的测试数据:
产生您想要的结果。也适用于更少的级别。或者(与您的原始版本不同)甚至是根本没有任何子项的裸根项。
如何扩展查询以允许更多级别应该很明显。我添加了7 个级别的版本以适应您
WHERE level < 7
的小提琴,以防万一:db<>在这里摆弄