我有一个文件夹表,它连接到id
,parent_id
关系自己:
CREATE TABLE folders (
id int(10) unsigned NOT NULL AUTO_INCREMENT,
title nvarchar(255) NOT NULL,
parent_id int(10) unsigned DEFAULT NULL,
PRIMARY KEY (id)
);
INSERT INTO folders(id, title, parent_id) VALUES(1, 'root', null);
INSERT INTO folders(id, title, parent_id) values(2, 'one', 1);
INSERT INTO folders(id, title, parent_id) values(3, 'target', 2);
INSERT INTO folders(id, title, parent_id) values(4, 'child one', 3);
INSERT INTO folders(id, title, parent_id) values(5, 'child two', 3);
INSERT INTO folders(id, title, parent_id) values(6, 'root 2', null);
INSERT INTO folders(id, title, parent_id) values(7, 'other child one', 6);
INSERT INTO folders(id, title, parent_id) values(8, 'other child two', 6);
我想要一个返回该记录的所有父项的查询,直接返回路径和任何子项。
因此,如果我要求id=3
的文件夹,我会得到记录:1, 2, 3, 4, 5
。我被困在如何得到父母。
MYSQL的版本是5.7,并没有立即升级的计划,所以可悲的是CTE不是一个选择。
我创造了这个sql fiddle
在MySQL 8.0中,您可以使用qazxsw poi来解决此用例。
以下查询为您提供给定记录的父项(包括记录本身):
Recursive Common Table Expressions
| id | title | parent_id | | --- | ------ | --------- | | 3 | target | 2 | | 2 | one | 1 | | 1 | root | |
这是一个稍微不同的查询,它返回给定记录的子树:
with recursive parent_cte (id, title, parent_id) as (
select id, title, parent_id
from folders
where id = 3
union all
select f.id, f.title, f.parent_id
from folders f
inner join parent_cte pc on f.id = pc.parent_id
)
select * from parent_cte;
| id | title | parent_id | | --- | --------- | --------- | | 4 | child one | 3 | | 5 | child two | 3 |
两个查询器可以组合如下:
with recursive children_cte (id, title, parent_id) as (
select id, title, parent_id
from folders
where parent_id = 3
union all
select f.id, f.title, f.parent_id
from folders f
inner join children_cte cc on f.parent_id = cc.id
)
select * from children_cte;
| id | title | parent_id | | --- | --------- | --------- | | 3 | target | 2 | | 2 | one | 1 | | 1 | root | | | 4 | child one | 3 | | 5 | child two | 3 |
with recursive parent_cte (id, title, parent_id) as (
select id, title, parent_id
from folders
where id = 3
union all
select f.id, f.title, f.parent_id
from folders f
inner join parent_cte pc on f.id = pc.parent_id
),
children_cte (id, title, parent_id) as (
select id, title, parent_id
from folders
where parent_id = 3
union all
select f.id, f.title, f.parent_id
from folders f
inner join children_cte cc on f.parent_id = cc.id
)
select * from parent_cte
union all select * from children_cte;
在您的表设计中,Demo on DB Fiddle和ID
对应于用于存储树的“邻接列表模型”。
还有另一种设计,称为“嵌套集模型”,它可以更轻松地执行您想要的操作。
请参阅Mike Hillyer撰写的这篇优秀文章:PARENT_ID
综上所述:
树存储在如下表中:
managing-hierarchical-data-in-mysql
查找从根到给定节点的路径(此处为“FLASH”):
CREATE TABLE nested_category (
category_id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(20) NOT NULL,
lft INT NOT NULL,
rgt INT NOT NULL
);
查找给定节点的所有子节点(此处为“PORTABLE ELECTRONICS”):
SELECT parent.name
FROM nested_category AS node,
nested_category AS parent
WHERE node.lft BETWEEN parent.lft AND parent.rgt
AND node.name = 'FLASH'
ORDER BY parent.lft;
重命名到文件夹表后
解决方案是:
SELECT node.name, (COUNT(parent.name) - (sub_tree.depth + 1)) AS depth
FROM nested_category AS node,
nested_category AS parent,
nested_category AS sub_parent,
(
SELECT node.name, (COUNT(parent.name) - 1) AS depth
FROM nested_category AS node,
nested_category AS parent
WHERE node.lft BETWEEN parent.lft AND parent.rgt
AND node.name = 'PORTABLE ELECTRONICS'
GROUP BY node.name
ORDER BY node.lft
)AS sub_tree
WHERE node.lft BETWEEN parent.lft AND parent.rgt
AND node.lft BETWEEN sub_parent.lft AND sub_parent.rgt
AND sub_parent.name = sub_tree.name
GROUP BY node.name
HAVING depth <= 1
ORDER BY node.lft;
目标的路径:
CREATE TABLE folders (
id INT AUTO_INCREMENT PRIMARY KEY,
title VARCHAR(20) NOT NULL,
lft INT NOT NULL,
rgt INT NOT NULL
);
INSERT INTO folders(id, title, lft, rgt) values(1, 'root', 1, 10);
INSERT INTO folders(id, title, lft, rgt) values(2, 'one', 2, 9);
INSERT INTO folders(id, title, lft, rgt) values(3, 'target', 3, 8);
INSERT INTO folders(id, title, lft, rgt) values(4, 'child one', 4, 5);
INSERT INTO folders(id, title, lft, rgt) values(5, 'child two', 6, 7);
INSERT INTO folders(id, title, lft, rgt) values(6, 'root 2', 11, 16);
INSERT INTO folders(id, title, lft, rgt) values(7, 'other child one', 12, 13);
INSERT INTO folders(id, title, lft, rgt) values(8, 'other child two', 14, 15);
目标儿童:
SELECT parent.title
FROM folders AS node,
folders AS parent
WHERE node.lft BETWEEN parent.lft AND parent.rgt
AND node.title = 'target'
ORDER BY parent.lft;
见SELECT node.title, (COUNT(parent.title) - (sub_tree.depth + 1)) AS depth
FROM folders AS node,
folders AS parent,
folders AS sub_parent,
(
SELECT node.title, (COUNT(parent.title) - 1) AS depth
FROM folders AS node,
folders AS parent
WHERE node.lft BETWEEN parent.lft AND parent.rgt
AND node.title = 'target'
GROUP BY node.title
ORDER BY node.lft
)AS sub_tree
WHERE node.lft BETWEEN parent.lft AND parent.rgt
AND node.lft BETWEEN sub_parent.lft AND sub_parent.rgt
AND sub_parent.title = sub_tree.title
GROUP BY node.title
HAVING depth <= 1
ORDER BY node.lft;
要在单个查询中获取所有数据,sqlfiddle应该这样做。
我在过去用第二个表解决了这个问题,该表包含了树中所有路径的传递闭包。
union
使用所有祖先 - 后代对的元组加载此表,包括树中的节点引用自身的对象(长度为0的路径)。
mysql> CREATE TABLE folders_closure (
ancestor INT UNSIGNED NOT NULL,
descendant INT UNSIGNED NOT NULL,
PRIMARY KEY (ancestor, descendant),
depth INT UNSIGNED NOT NULL
);
现在,您可以通过查询从顶部节点开始的所有路径来查询给定节点下面的树,并将该路径的后代连接到mysql> INSERT INTO folders_closure VALUES
(1,1,0), (2,2,0), (3,3,0), (4,4,0), (5,5,0), (6,6,0),
(1,2,1), (2,3,1), (3,4,1), (3,5,1), (1,4,2), (1,5,2),
(6,7,1), (6,8,1);
表。
folders
我看到很多人推荐mysql> SELECT d.id, d.title, cl.depth FROM folders_closure cl
JOIN folders d ON d.id=cl.descendant WHERE cl.ancestor=1;
+----+-----------+-------+
| id | title | depth |
+----+-----------+-------+
| 1 | root | 0 |
| 2 | one | 1 |
| 4 | child one | 2 |
| 5 | child two | 2 |
+----+-----------+-------+
这是Nested Sets solution,并且在Joe Celko于1995年将它包含在他的书introduced in 1992之后变得流行。但我不喜欢嵌套集技术,因为这些数字实际上并不是对主键的引用。树中的节点,并且在添加或删除节点时需要重新编号多行。
我在SQL for Smarties和一些What is the most efficient/elegant way to parse a flat table into a tree?中写过关于闭包表的方法。
我做了一个关于它的演讲:my other answers with the hierarchical-data tag。
我还在我的书Models for Hierarchical Data的一章中介绍了这一点。
如果确保子节点的id始终高于其父节点,则可以使用用户变量。
得到后代:
SQL Antipatterns: Avoiding the Pitfalls of Database Programming
结果:
select f.*, @l := concat_ws(',', @l, id) as dummy
from folders f
cross join (select @l := 3) init_list
where find_in_set(parent_id, @l)
order by id
获得祖先(包括其本身):
id | title | parent_id | dummy
---|-----------|-----------|------
4 | child one | 3 | 3,4
5 | child two | 3 | 3,4,5
结果:
select f.*, @l := concat_ws(',', @l, parent_id) as dummy
from folders f
cross join (select @l := 3) init_list
where find_in_set(id, @l)
order by id desc
id | title | parent_id | dummy
3 | target | 2 | 3,2
2 | one | 1 | 3,2,1
1 | root | null | 3,2,1
请注意,此技术依赖于未记录的评估顺序,并且在将来的版本中将无法实现。
此外,它不是非常高效,因为两个查询都需要全表扫描,但对于较小的表可能没问题。但是 - 对于小表,我只需获取完整的表并使用应用程序代码中的递归函数来解决任务。
对于更大的表,我会考虑更复杂的解决方案,如下面的存储过程:
Demo
使用它
create procedure get_related_nodes(in in_id int)
begin
set @list = in_id;
set @parents = @list;
repeat
set @sql = '
select group_concat(id) into @children
from folders
where parent_id in ({parents})
';
set @sql = replace(@sql, '{parents}', @parents);
prepare stmt from @sql;
execute stmt;
set @list = concat_ws(',', @list, @children);
set @parents = @children;
until (@children is null) end repeat;
set @child = in_id;
repeat
set @sql = '
select parent_id into @parent
from folders
where id = ({child})
';
set @sql = replace(@sql, '{child}', @child);
prepare stmt from @sql;
execute stmt;
set @list = concat_ws(',', @parent, @list);
set @child = @parent;
until (@parent is null) end repeat;
set @sql = '
select *
from folders
where id in ({list})
';
set @sql = replace(@sql, '{list}', @list);
prepare stmt from @sql;
execute stmt;
end
这将返回
call get_related_nodes(3)
id | title | parent_id
---|-----------|----------
1 | root |
2 | one | 1
3 | target | 2
4 | child one | 3
5 | child two | 3
我希望此过程的性能与递归CTE查询一样好。无论如何,你应该在Demo上有一个索引。
如果你的parent_id总是以升序排列,那么下面的查询就是一个很好的解决方案。
如果你得到结果你的id为null父值,那么请按照链接parent_id
(当传递id = 6时)http://www.sqlfiddle.com/#!9/b40b8/258(当传递id = 3时)
http://www.sqlfiddle.com/#!9/b40b8/259
要么
您不会将您的传递ID传递给父母,请点击以下链接。 SELECT * FROM folders f
WHERE id = 3
OR
(Parent_id <=3 AND Parent_id >=
(SELECT id FROM folders Where id <= 3 AND parent_id IS NULL Order by ID desc LIMIT 1)) OR (id <= 3 AND IFNULL(Parent_id,0) = 0)
AND id >= (SELECT id FROM folders Where id <= 3 AND parent_id IS NULL Order by ID desc LIMIT 1);
(当传递id = 3时)
http://www.sqlfiddle.com/#!9/b40b8/194(当传递id = 6时)
http://www.sqlfiddle.com/#!9/b40b8/208
注意我的解决方案与@Marc Alff大致相同。在编辑器中输入/准备响应之前没有意识到它已经存在。
在不使用CTE或其他分层查询支持的情况下(例如,事先在Oracle中连接),很难获得查询来实现您的目标(或层次数据集的其他典型要求)。这是数据库提出CTE等的主要驱动力。
许多年前,当数据库中没有这种对分层实体建模的支持时,您和许多其他相关部分所概述的要求通过略微不同地对这些实体进行建模来解决。
这个概念很简单。从本质上讲,在层次表中引入了另外两个属性(或者是一个外键键入分层表的单独表),称为left_boundary和right_boundary(在名称中的所有内容之后调用您想要的任何内容)。对于每一行,选择这些属性的值(数字),使它们涵盖所有子项的这些属性的值。换句话说,孩子的左右边界将在其父母的左右边界之间。
顺便举例说明
SELECT
*
FROM
folders f
WHERE
id = 3 OR Parent_id <=3
OR (id <= 3 AND IFNULL(Parent_id,0) = 0);
创建此层次结构曾经是清晨批处理作业的一部分,或者在设计时间内选择的边界非常宽,以至于它们很容易覆盖所有树的深度。
我将使用此解决方案来实现您的目标。首先我将介绍第二个表(可能在同一个表中引入了属性,决定不打扰您的数据模型)
此表的数据基于您的数据集
CREATE TABLE folder_boundaries (
id int(10) unsigned NOT NULL AUTO_INCREMENT,
folder_id int(10) unsigned NOT NULL,
left_boundary int(10) unsigned,
right_boundary int(10) unsigned,
PRIMARY KEY (id),
FOREIGN KEY (folder_id) REFERENCES folders(id)
);
以下是实现您所追求的目标的查询
NSERT INTO folder_boundaries(folder_id, left_boundary, right_boundary) VALUES(1, 1, 10);
INSERT INTO folder_boundaries(folder_id, left_boundary, right_boundary) VALUES(2, 2, 9);
INSERT INTO folder_boundaries(folder_id, left_boundary, right_boundary) VALUES(3, 3, 8);
INSERT INTO folder_boundaries(folder_id, left_boundary, right_boundary) VALUES(4, 4, 4);
INSERT INTO folder_boundaries(folder_id, left_boundary, right_boundary) VALUES(5, 4, 4);
INSERT INTO folder_boundaries(folder_id, left_boundary, right_boundary) VALUES(6, 21, 25);
INSERT INTO folder_boundaries(folder_id, left_boundary, right_boundary) VALUES(7, 22, 22);
INSERT INTO folder_boundaries(folder_id, left_boundary, right_boundary) VALUES(7, 22, 22);
结果
select f.id, f.title
from folders f
join folder_boundaries fb on f.id = fb.folder_id
where fb.left_boundary < (select left_boundary from folder_boundaries where folder_id = 3)
and fb.right_boundary > (select right_boundary from folder_boundaries where folder_id = 3)
union all
select f.id, f.title
from folders f
join folder_boundaries fb on f.id = fb.folder_id
where fb.left_boundary >= (select left_boundary from folder_boundaries where folder_id = 3)
and fb.right_boundary <= (select right_boundary from folder_boundaries where folder_id = 3)
使用存储过程的小代码,在5.6上测试:
dbfiddle
下面是对代码的调用:
drop procedure if exists test;
DELIMITER //
create procedure test(in testid int)
begin
DECLARE parent int;
set parent = testid;
drop temporary table if exists pars;
CREATE temporary TABLE pars (
id int(10) unsigned NOT NULL AUTO_INCREMENT,
title nvarchar(255) NOT NULL,
parent_id int(10) unsigned DEFAULT NULL,
PRIMARY KEY (id)
);
#For getting heirarchy
while parent is not null do
insert into pars
select * from folders where id = parent;
set parent = (select parent_id from folders where id = parent);
end while;
#For getting child
insert into pars
select * from folders where parent_id = testid;
select * from pars;
end //
DELIMITER ;
输出是:
call test(3);
最终结果可以根据需要使用字符串进行格式化,一旦我们得到表格,休息应该很容易我猜。此外,如果可以对id进行排序,那么格式化将非常有用。
更不用说字段id和parent_id应该是索引,以便有效地工作。
假设你知道树的最大深度,你可以“创建”一个循环来获得你想要的东西:
获取父节点:
获取子节点:
SELECT @id :=
(
SELECT parent_id
FROM folders
WHERE id = @id
) AS folderId, vars.id
FROM (
SELECT @id := 7 AS id
) vars
INNER JOIN (
SELECT 0 AS nbr UNION ALL SELECT 1 UNION ALL SELECT 2
UNION ALL SELECT 3 UNION ALL SELECT 4 UNION ALL SELECT 5
UNION ALL SELECT 6 UNION ALL SELECT 7 UNION ALL SELECT 8
UNION ALL SELECT 9) temp
WHERE @id IS NOT NULL
这是作品
SELECT @id :=
(
SELECT GROUP_CONCAT(id)
FROM folders
WHERE FIND_IN_SET(parent_id, @id)
) AS folderIds, vars.id
FROM (
SELECT @id := 1 AS id
) vars
INNER JOIN (
SELECT 0 AS nbr UNION ALL SELECT 1 UNION ALL SELECT 2
UNION ALL SELECT 3 UNION ALL SELECT 4 UNION ALL SELECT 5
UNION ALL SELECT 6 UNION ALL SELECT 7 UNION ALL SELECT 8
UNION ALL SELECT 9) temp
WHERE @id IS NOT NULL
和静态10行集之间创建连接(最大深度)连接的目的是创建10行的结果集,以便select中的子查询执行10次。
或者,如果您不知道最大深度,则可以使用替换已连接的子查询
(SELECT @id := 1 AS id)
或者为了避免上面所有的联合选择,使用限制:
INNER JOIN (
SELECT 1 FROM folder) temp
参考文献: - INNER JOIN (
SELECT 1 FROM folder LIMIT 100) temp