PGSQL树关系
你好,我最近在项目中接触到这个,想分享一下我的总结。希望对你有所帮助。首先,我们需要了解一些前提知识。
本质上,这是使用递归调用的闭包表
解决方案。感谢那些幻灯片,它们非常有用。我希望在写这篇文章之前能看到它们 :)
前提条件
递归函数
这些是调用自身的函数,例如:
function factorial(n) {
if (n = 0) return 1;
return n * factorial(n - 1);
}
这很酷,幸运的是pgsql也有递归函数,但可能会有点复杂。我更喜欢函数式编程。
使用pgsql的cte
WITH RECURSIVE t(n) AS (
VALUES (1)
UNION ALL
SELECT n+1 FROM t WHERE n < 100
)
SELECT sum(n) FROM t;
递归WITH查询的一般形式始终是一个非递归项,然后是UNION(或UNION ALL),然后是递归项,只有递归项可以包含对查询自身输出的引用。执行这样的查询如下所示:
递归查询评估
1. 评估非递归项。对于UNION(但不是UNION ALL),丢弃重复的行。将所有剩余行包括在递归查询结果中,并将它们放置在一个临时工作表中。
2. 只要工作表不为空,就重复以下步骤:
a. 评估递归项,将工作表的当前内容替换为递归自引用。对于UNION(但不是UNION ALL),丢弃重复的行和任何重复的结果行。将所有剩余行包括在递归查询结果中,并将它们放置在一个临时中间表中。
b. 用中间表的内容替换工作表的内容,然后清空中间表。
要在SQL中执行类似阶乘的操作,您需要执行更多类似于
this so post的操作。
ALTER FUNCTION dbo.fnGetFactorial (@num int)
RETURNS INT
AS
BEGIN
DECLARE @n int
IF @num <= 1 SET @n = 1
ELSE SET @n = @num * dbo.fnGetFactorial(@num - 1)
RETURN @n
END
GO
树形数据结构(更像是一片森林:)
wikipedia
需要注意的是,树是图形的子集,这可以通过每个节点仅有一个父节点来简单地实现其关系。
在PGSQL中表示树形结构
我认为,在我们转向sql之前,更理论化地解决问题会更容易。
表示具有无需数据重复的图形关系的简单方法是将nodes(id, data)
与边界分开。
然后,我们可以限制edges(parent_id, child_id)
表以强制执行我们的约束条件。通过要求parent_id、child_id以及仅仅是child id唯一即可实现。
create table nodes (
id uuid default uuid_generate_v4() not null unique ,
name varchar(255) not null,
json json default '{}'::json not null,
remarks varchar(255),
);
create table edges (
id uuid default uuid_generate_v4() not null,
parent_id uuid not null,
child_id uuid not null,
meta json default '{}'::json,
constraint group_group_id_key
primary key (id),
constraint group_group_unique_combo
unique (parent_id, child_id),
constraint group_group_unique_child
unique (child_id),
foreign key (parent_id) references nodes
on update cascade on delete cascade,
foreign key (child_id) references nodes
on update cascade on delete cascade
);
请注意,理论上只需在节点表中放置parent_id即可使用一张表完成所有操作。
CREATE VIEW v_edges as (SELECT id as child_id, parent_id FROM nodes)
但是为了提出灵活性的建议,以便我们可以将其他图形结构纳入此框架,我将使用常见的多对多关系结构。这将理想地使这项研究扩展到其他图形算法。
让我们从一个样本数据结构开始。
INSERT (id, my_data) VALUES ('alpha', 'my big data') INTO nodes
INSERT (id, my_data) VALUES ('bravo', 'my big data') INTO nodes
INSERT (id, my_data) VALUES ('charly', 'my big data') INTO nodes
INSERT (id, my_data) VALUES ('berry', 'my big data') INTO nodes
INSERT (id, my_data) VALUES ('zeta', 'my big data') INTO nodes
INSERT (id, my_data) VALUES ('yank', 'my big data') INTO nodes
INSERT (parent_id, child_id) VALUES ('alpha', 'bravo') INTO edges
INSERT (parent_id, child_id) VALUES ('alpha', 'berry') INTO edges
INSERT (parent_id, child_id) VALUES ('bravo', 'charly') INTO edges
INSERT (parent_id, child_id) VALUES ('yank', 'zeta') INTO edges
请注意树的有趣属性:
(边数e)=(节点数n)-1
,每个子节点都只有一个父节点。
然后我们可以简化方程。
let n = node
let p = parent
let c = child
let ns = nodes = groups
let es = edges = group_group // because this is a relationship of a group entity to another group entity
现在我们需要问什么样的问题。
"假设有一个任意的组集合's',假设节点继承它们的子节点,那么图形的覆盖率是多少?"
这是一个棘手的问题,需要我们遍历图形并找到s中每个节点的所有子节点。
这是基于此堆栈溢出帖子的继续。
create view v_group_descendant as
with recursive descendants
(parent_id, descendant_id, lvl)
as
( select parent_id, child_id, 1
from group_group
union all
select d.parent_id, s.child_id, d.lvl + 1
from descendants d
join group_group s
on d.descendant_id = s.parent_id
)
select * from descendants;
comment on view v_group_descendant is 'This aggregates the children of each group RECURSIVELY WOO ALL THE WAY DOWN THE TREE :)';
有了这个视图之后,我们可以与我们的节点/组合并以获取数据,对于大部分步骤,我不会为每个单独的步骤提供这些示例,我们将只使用ID。
select d.*, g1.group_name as parent, g2.group_name as decendent
from v_group_descendant d, groups g1, groups g2
WHERE g1.id = d.parent_id and g2.id = d.descendant_id
order by parent_id, lvl, descendant_id;
样本输出
+------------------------------------+------------------------------------+---+----------+---------+
|parent_id |descendant_id |lvl|parent |decendent|
+------------------------------------+------------------------------------+---+----------+---------+
|3ef7050f-2f90-444a-a20d-c5cbac91c978|6c758087-a158-43ff-92d6-9f922699f319|1 |bravo |charly |
|c1529e8a-75b0-4242-a51a-ac60a0e48868|3ef7050f-2f90-444a-a20d-c5cbac91c978|1 |alpha |bravo |
|c1529e8a-75b0-4242-a51a-ac60a0e48868|7135b0c6-d59c-4c27-9617-ddcf3bc79419|1 |alpha |berry |
|c1529e8a-75b0-4242-a51a-ac60a0e48868|6c758087-a158-43ff-92d6-9f922699f319|2 |alpha |charly |
|42529e8a-75b0-4242-a51a-ac60a0e48868|44758087-a158-43ff-92d6-9f922699f319|1 |yank |zeta |
+------------------------------------+------------------------------------+---+----------+---------+
请注意,这仅是最小节点后代关系,并且实际上已经失去了所有子节点为0的节点,例如charly。
为了解决这个问题,我们需要添加回所有未出现在后代列表中的节点。
create view v_group_descendant_all as (
select * from v_group_descendant gd
UNION ALL
select null::uuid as parent_id,id as descendant_id, 0 as lvl from groups g
where not exists (select * from v_group_descendant gd where gd.descendant_id = g.id )
);
comment on view v_group_descendant is 'complete list of descendants including rank 0 root nodes descendant - parent relationship is duplicated for all levels / ranks';
preview
+------------------------------------+------------------------------------+---+----------+---------+
|parent_id |descendant_id |lvl|parent |decendent|
+------------------------------------+------------------------------------+---+----------+---------+
|3ef7050f-2f90-444a-a20d-c5cbac91c978|6c758087-a158-43ff-92d6-9f922699f319|1 |bravo |charly |
|c1529e8a-75b0-4242-a51a-ac60a0e48868|3ef7050f-2f90-444a-a20d-c5cbac91c978|1 |alpha |bravo |
|c1529e8a-75b0-4242-a51a-ac60a0e48868|7135b0c6-d59c-4c27-9617-ddcf3bc79419|1 |alpha |berry |
|c1529e8a-75b0-4242-a51a-ac60a0e48868|6c758087-a158-43ff-92d6-9f922699f319|2 |alpha |charly |
|42529e8a-75b0-4242-a51a-ac60a0e48868|44758087-a158-43ff-92d6-9f922699f319|1 |yank |zeta |
|null |c1529e8a-75b0-4242-a51a-ac60a0e48868|0 |null |alpha |
|null |42529e8a-75b0-4242-a51a-ac60a0e48868|0 |null |yank |
+------------------------------------+------------------------------------+---+----------+---------+
假设我们根据一个名为
users(id, data)
的表和一个
user_group(user_id, group_id)
关系来获取我们的一组群组基础,我们可以将其与另一个表连接起来,去除重复项,因为我们的用户组关系集合
s
可能会导致重复,例如如果一个用户被分配给alpha和charly两个群组。
+
| user | group |
+
| jane | alpha |
| jane | charly |
| kier | yank |
| kier | bravo |
+
CREATE VIEW v_user_group_recursive AS (
SELECT DISTINCT dd.descendant_id AS group_id, ug.user_id
FROM v_group_descendant_all dd , user_group ug
WHERE (ug.group_id = dd.descendant_id
OR ug.group_id = dd.parent_id)
);
SELECT * FROM v_user_group_recursive;
+------+--------+
| user | group |
+------+--------+
| jane | alpha |
| jane | bravo |
| jane | berry |
| jane | charly |
-- | jane | charly | Removed by DISTINCT
| kier | yank |
| kier | zeta |
| kier | bravo |
| kier | charly |
+------+--------+
如果我们想要按节点分组并连接,我们可以像以下这样做:
CREATE VIEW v_user_groups_recursive AS (
SELECT user_id, json_agg(json_build_object('id', id,'parent_id',parent_id, 'group_name', group_name, 'org_id', org_id, 'json', json, 'remarks', remarks)) as groups
FROM v_user_group_recursive ug, v_groups_parent g
WHERE ug.group_id = g.id GROUP BY user_id
);
comment on view v_user_group_recursive is 'This aggregates the groups for each user recursively ';
+
| user | groups |
+
| jane | [alpha, bravo, berry, charly] |
| kier | [yank, zeta, bravo, charly] |
+
这太棒了,我们已经回答了这个问题。现在我们只需要询问这个用户继承了哪些组。
SELECT * from v_user_groups_recursive where user_id = 'kier
在前端展示我们的努力成果
此外,我们可以使用类似jstree.com这样的工具来展示我们的结构。
async function getProjectTree(user_id) {
let res = await table.query(format('SELECT * from v_user_groups_recursive ug WHERE ug.user_id = %L', user_id));
if (res.success) {
let rows = res.data[0].groups.map(r => {
return {
id: r.id,
parent: r.parent_id==null?'#':r.parent_id,
text: r.group_name,
icon: 'P',
state: {
opened: true,
disabled: false,
selected: false,
},
li_attr: {},
a_attr: {}
}
})
return {success: true, data: rows, msg: 'Got all projects'}
} else return res;
}
<div id="v_project_tree" class="row col-10 mx-auto" style="height: 25vh"></div>
<script>
function buildTree() {
bs.sendJson('get', "/api/projects/getProjectTree").then(res => {
bs.resNotify(res);
if (!res.success) {
console.error(':(');
return
}
console.log(res.data);
$('#v_project_tree').jstree({
'core': {
'data': res.data
}
});
})
}
window.addEventListener('load', buildTree);
</script>
jstree预览
博客