[英]get parents and children of tree folder structure in my sql < 8 and no CTEs
我有一個文件夾表,它在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 小提琴
在您的表設計中, ID
和PARENT_ID
對應於用於存儲樹的“鄰接列表模型”。
還有另一種設計,稱為“嵌套集模型”,它可以更輕松地在此處執行您想要的操作。
請參閱 Mike Hillyer 的這篇出色的文章,描述了兩者:management -hierarchical-data-in-mysql
總之:
該樹存儲在一個表中,如:
CREATE TABLE nested_category (
category_id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(20) NOT NULL,
lft INT NOT NULL,
rgt INT NOT NULL
);
查找從根到給定節點的路徑(此處為“FLASH”):
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;
要在單個查詢中獲取所有數據,應該使用union
。
在 MySQL 8.0 中,您可以使用遞歸公用表表達式來解決此用例。
以下查詢為您提供給定記錄的父項(包括記錄本身):
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 | | --- | ------ | --------- | | 3 | target | 2 | | 2 | one | 1 | | 1 | root | |
這是一個稍微不同的查詢,它返回給定記錄的子樹:
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 | | --- | --------- | --------- | | 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;
| id | title | parent_id | | --- | --------- | --------- | | 3 | target | 2 | | 2 | one | 1 | | 1 | root | | | 4 | child one | 3 | | 5 | child two | 3 |
我過去用第二個表解決了這個問題,其中包含通過樹的所有路徑的傳遞閉包。
mysql> CREATE TABLE folders_closure (
ancestor INT UNSIGNED NOT NULL,
descendant INT UNSIGNED NOT NULL,
PRIMARY KEY (ancestor, descendant),
depth INT UNSIGNED NOT NULL
);
使用所有祖先-后代對的元組加載此表,包括樹中節點引用自身的元組(長度為 0 的路徑)。
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 |
+----+-----------+-------+
我看到很多人推薦1992年引入的Nested Sets 解決方案,並在 Joe Celko 於 1995 年將其包含在他的SQL for Smarties一書中后開始流行。但我不喜歡 Nested Sets 技術,因為這些數字實際上並不是對樹中節點主鍵的引用,並且在添加或刪除節點時需要對許多行重新編號。
我在將平面表解析為樹的最有效/最優雅的方法是什么? 以及我的一些其他帶有分層數據標簽的答案。
我做了一個關於它的演示: 分層數據模型。
我也在我的書SQL 反模式:避免數據庫編程的陷阱 的一章中介紹了這一點。
如果保證子節點的 id 總是高於它的父節點,那么你可以使用user variables 。
獲取后代:
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
請注意,此技術依賴於未記錄的評估順序,並且在未來版本中將無法實現。
而且它的性能不是很好,因為兩個查詢都需要全表掃描,但對於較小的表可能沒問題。 但是 - 對於小表,我只會獲取完整的表並在應用程序代碼中使用遞歸函數解決任務。
對於更大的表,我會考慮更復雜的解決方案,如以下存儲過程:
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查詢一樣好。 在任何情況下,您都應該在parent_id
上有一個索引。
如果您的 parent_id 總是按升序出現,那么下面的查詢是很好的解決方案。
如果你得到的結果你的 id 為空父值,那么請點擊鏈接http://www.sqlfiddle.com/#!9/b40b8/258 (當傳遞 id = 6 時) http://www.sqlfiddle.com/ #!9/b40b8/259 (當傳遞 id = 3 時)
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 置於父級的頂部,然后請按照以下鏈接操作。 http://www.sqlfiddle.com/#!9/b40b8/194 (當傳遞 id =3 時)
http://www.sqlfiddle.com/#!9/b40b8/208 (當傳遞 id =6 時)
SELECT
*
FROM
folders f
WHERE
id = 3 OR Parent_id <=3
OR (id <= 3 AND IFNULL(Parent_id,0) = 0);
注意我的解決方案或多或少與@Marc Alff 相同。 在編輯器中輸入/准備回復之前沒有意識到它已經存在。
如果不使用 CTE 或其他分層查詢支持(例如,在 Oracle 中預先連接),則很難通過查詢來實現您的目標(或分層數據集的其他典型要求)。 這是數據庫提出 CTE 等的主要驅動力。
許多年前,當數據庫中沒有對分層實體建模的這種支持時,您和許多其他相關人員概述的需求是通過對此類實體進行稍微不同的建模來解決的。
這個概念很簡單。 從本質上講,在分層表(或一個單獨的表外鍵進入分層表)中引入了另外兩個屬性,稱為 left_boundary 和 right_boundary(畢竟名稱中的內容,請隨意調用)。 對於每一行,選擇這些屬性的值(數字)以使其涵蓋所有子項的這些屬性的值。 換句話說,孩子的左右邊界將在其父母的左右邊界之間。
舉個例子
創建此層次結構曾經是清晨批處理作業的一部分,或者在設計時選擇的邊界相距很遠,以至於它們很容易覆蓋樹的所有深度。
我將使用此解決方案來實現您的目標。 首先我將介紹第二張表(可以在同一張表中引入屬性,決定不打擾您的數據模型)
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)
結果
您可以像這樣在父行和子行之間執行聯合:
select title, id, @parent:=parent_id as parent from
(select @parent:=3 ) a join (select * from folders order by id desc) b where @parent=id
union select title, id, parent_id as parent from folders where parent_id=3 ORDER BY id
這里是一個示例dbfiddle
使用存儲過程的小代碼,在 5.6 上測試:
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
這工作由
(SELECT @id := 1 AS id)
和 10 行的靜態集之間創建連接(最大深度)join的目的是創建一個10行的結果集,讓select中的子查詢執行10次。
或者,如果您不知道最大深度,則可以將連接的子查詢替換為
INNER JOIN (
SELECT 1 FROM folder) temp
或者為了避免上面的所有聯合選擇,請使用限制:
INNER JOIN (
SELECT 1 FROM folder LIMIT 100) temp
參考資料: - MySQL 中的分層查詢
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.