提问人:Tarun Parswani 提问时间:11/26/2013 最后编辑:trincotTarun Parswani 更新时间:2/19/2021 访问量:443277
如何创建MySQL分层递归查询?
How to create a MySQL hierarchical recursive query?
问:
我有一个MySQL表,如下所示:
编号 | 名字 | parent_id |
---|---|---|
19 | 类别1 | 0 |
20 | 类别2 | 19 |
21 | 类别3 | 20 |
22 | 类别4 | 21 |
... | ... | ... |
现在,我想要一个MySQL查询,我只需提供id[例如,说],然后我应该获取其所有子id[即结果应具有ids'20,21,22']......id=19
子级的等级制度尚不清楚;它可能会有所不同......
我知道如何使用循环来做到这一点......但是如何使用单个MySQL查询实现相同的目的呢?for
答:
摘自博客 在MySQL中管理分层数据
表结构
+-------------+----------------------+--------+
| category_id | name | parent |
+-------------+----------------------+--------+
| 1 | ELECTRONICS | NULL |
| 2 | TELEVISIONS | 1 |
| 3 | TUBE | 2 |
| 4 | LCD | 2 |
| 5 | PLASMA | 2 |
| 6 | PORTABLE ELECTRONICS | 1 |
| 7 | MP3 PLAYERS | 6 |
| 8 | FLASH | 7 |
| 9 | CD PLAYERS | 6 |
| 10 | 2 WAY RADIOS | 6 |
+-------------+----------------------+--------+
查询:
SELECT t1.name AS lev1, t2.name as lev2, t3.name as lev3, t4.name as lev4
FROM category AS t1
LEFT JOIN category AS t2 ON t2.parent = t1.category_id
LEFT JOIN category AS t3 ON t3.parent = t2.category_id
LEFT JOIN category AS t4 ON t4.parent = t3.category_id
WHERE t1.name = 'ELECTRONICS';
输出
+-------------+----------------------+--------------+-------+
| lev1 | lev2 | lev3 | lev4 |
+-------------+----------------------+--------------+-------+
| ELECTRONICS | TELEVISIONS | TUBE | NULL |
| ELECTRONICS | TELEVISIONS | LCD | NULL |
| ELECTRONICS | TELEVISIONS | PLASMA | NULL |
| ELECTRONICS | PORTABLE ELECTRONICS | MP3 PLAYERS | FLASH |
| ELECTRONICS | PORTABLE ELECTRONICS | CD PLAYERS | NULL |
| ELECTRONICS | PORTABLE ELECTRONICS | 2 WAY RADIOS | NULL |
+-------------+----------------------+--------------+-------+
大多数用户都曾处理过 SQL 数据库中的分层数据,毫无疑问,他们了解到分层数据的管理不是关系数据库的用途。关系数据库的表不是分层的(如 XML),而只是一个平面列表。分层数据具有父子关系,该关系不会在关系数据库表中自然表示。阅读更多
有关更多详细信息,请参阅博客。
编辑:
select @pv:=category_id as category_id, name, parent from category
join
(select @pv:=19)tmp
where parent=@pv
输出:
category_id name parent
19 category1 0
20 category2 19
21 category3 20
22 category4 21
评论
这有点棘手,请检查它是否适合您
select a.id,if(a.parent = 0,@varw:=concat(a.id,','),@varw:=concat(a.id,',',@varw)) as list from (select * from recursivejoin order by if(parent=0,id,parent) asc) a left join recursivejoin b on (a.id = b.parent),(select @varw:='') as c having list like '%19,%';
SQL 小提琴链接 http://www.sqlfiddle.com/#!2/e3cdf/2
适当地替换为您的字段和表名称。
评论
我想出的最好的方法是
- 使用世系来存储\排序\跟踪树。这绰绰有余,而且阅读速度比任何其他方法快数千倍。 即使数据库会更改,它也允许保持该模式(因为任何数据库都允许使用该模式)
- 使用确定特定 ID 的世系的函数。
- 随心所欲地使用它(在选择中,或在 CUD 操作中,甚至在作业中)。
世系方法描述。可以在任何地方找到,例如 这里 或 这里. 至于功能——这就是激励我的原因。
最后 - 得到了或多或少简单、相对快速和简单的解决方案。
函数的主体
-- --------------------------------------------------------------------------------
-- Routine DDL
-- Note: comments before and after the routine body will not be stored by the server
-- --------------------------------------------------------------------------------
DELIMITER $$
CREATE DEFINER=`root`@`localhost` FUNCTION `get_lineage`(the_id INT) RETURNS text CHARSET utf8
READS SQL DATA
BEGIN
DECLARE v_rec INT DEFAULT 0;
DECLARE done INT DEFAULT FALSE;
DECLARE v_res text DEFAULT '';
DECLARE v_papa int;
DECLARE v_papa_papa int DEFAULT -1;
DECLARE csr CURSOR FOR
select _id,parent_id -- @n:=@n+1 as rownum,T1.*
from
(SELECT @r AS _id,
(SELECT @r := table_parent_id FROM table WHERE table_id = _id) AS parent_id,
@l := @l + 1 AS lvl
FROM
(SELECT @r := the_id, @l := 0,@n:=0) vars,
table m
WHERE @r <> 0
) T1
where T1.parent_id is not null
ORDER BY T1.lvl DESC;
DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = TRUE;
open csr;
read_loop: LOOP
fetch csr into v_papa,v_papa_papa;
SET v_rec = v_rec+1;
IF done THEN
LEAVE read_loop;
END IF;
-- add first
IF v_rec = 1 THEN
SET v_res = v_papa_papa;
END IF;
SET v_res = CONCAT(v_res,'-',v_papa);
END LOOP;
close csr;
return v_res;
END
然后你就
select get_lineage(the_id)
希望它能帮助到:)
在这里对另一个问题做了同样的事情
Mysql select recursive get all child with multiple level
查询将是:
SELECT GROUP_CONCAT(lv SEPARATOR ',') FROM (
SELECT @pv:=(
SELECT GROUP_CONCAT(id SEPARATOR ',')
FROM table WHERE parent_id IN (@pv)
) AS lv FROM table
JOIN
(SELECT @pv:=1)tmp
WHERE parent_id IN (@pv)
) a;
评论
SELECT idFolder, (SELECT GROUP_CONCAT(lv SEPARATOR ',') FROM ( SELECT @pv:=(SELECT GROUP_CONCAT(idFolder SEPARATOR ',') FROM Folder WHERE idFolderParent IN (@pv)) AS lv FROM Folder JOIN (SELECT @pv:= F1.idFolder )tmp WHERE idFolderParent IN (@pv)) a) from folder F1 where id > 10
NULL
我发现它更容易:
1) 创建一个函数,用于检查一个项目是否在另一个项目的父层次结构中的任何位置。像这样的东西(我不会写函数,用 WHILE DO 制作):
is_related(id, parent_id);
在您的示例中
is_related(21, 19) == 1;
is_related(20, 19) == 1;
is_related(21, 18) == 0;
2)使用子选择,如下所示:
select ...
from table t
join table pt on pt.id in (select i.id from table i where is_related(t.id,i.id));
对于 MySQL 8+:使用带有语法的递归。
对于 MySQL 5.x:使用内联变量、路径 ID 或自连接。
MySQL的8+
with recursive cte (id, name, parent_id) as (
select id,
name,
parent_id
from products
where parent_id = 19
union all
select p.id,
p.name,
p.parent_id
from products p
inner join cte
on p.parent_id = cte.id
)
select * from cte;
中指定的值应设置为要选择其所有后代的父级。parent_id = 19
id
MySQL 5.x
对于不支持通用表表达式的 MySQL 版本(最高版本 5.7),您可以通过以下查询实现此目的:
select id,
name,
parent_id
from (select * from products
order by parent_id, id) products_sorted,
(select @pv := '19') initialisation
where find_in_set(parent_id, @pv)
and length(@pv := concat(@pv, ',', id))
这是一把小提琴。
在这里,指定的值应设置为要选择其所有后代的父项。@pv := '19'
id
如果父母有多个孩子,这也将起作用。但是,要求每条记录都满足条件,否则结果将不完整。parent_id < id
查询中的变量赋值
此查询使用特定的 MySQL 语法:在执行过程中分配和修改变量。对执行顺序做了一些假设:
- 首先计算子句。所以这就是初始化的地方。
from
@pv
- 按照从别名中检索的顺序对每条记录计算子句。因此,这是条件仅包括父项已被标识为在后代树中的记录(主父项的所有子项都逐步添加到 )。
where
from
@pv
- 该条款中的条件按顺序进行评估,一旦确定总结果,评估就会中断。因此,第二个条件必须排在第二位,因为它将 添加到父列表中,并且只有在通过第一个条件时才会发生这种情况。调用该函数只是为了确保此条件始终为真,即使字符串由于某种原因会产生虚假值。
where
id
id
length
pv
总而言之,人们可能会发现这些假设风险太大,无法依赖。文档警告:
你可能会得到你所期望的结果,但这并不能保证 [...] 涉及用户变量的表达式的计算顺序是未定义的。
因此,即使它与上述查询一致,评估顺序仍可能更改,例如,当您添加条件或将此查询用作较大查询中的视图或子查询时。这是一个“功能”,将在未来的MySQL版本中删除:
MySQL的早期版本可以在除 之外的语句中为用户变量赋值。MySQL 8.0 支持此功能以实现向后兼容性,但在 MySQL 的未来版本中可能会删除此功能。
SET
如上所述,从 MySQL 8.0 开始,您应该使用递归语法。with
效率
对于非常大的数据集,此解决方案可能会变慢,因为find_in_set
操作不是在列表中查找数字的最理想方法,当然不是在大小与返回的记录数相同数量级的列表中。
备选方案1: ,with recursive
connect by
越来越多的数据库实现了 SQL:1999 ISO 标准 WITH [RECURSIVE]
语法用于递归查询(例如 Postgres 8.4+、SQL Server 2005+、DB2、Oracle 11gR2+、SQLite 3.8.4+、Firebird 2.1+、H2、HyperSQL 2.1.0+、Teradata、MariaDB 10.2.2+)。 从8.0版开始,MySQL也支持它。请参阅此答案的顶部,了解要使用的语法。
某些数据库具有用于分层查找的替代非标准语法,例如 Oracle、DB2、Informix、CUBRID 和其他数据库上可用的子句。CONNECT BY
MySQL 5.7 版不提供此类功能。当您的数据库引擎提供此语法时,或者您可以迁移到提供此语法的语法时,这当然是最佳选择。如果没有,那么还要考虑以下替代方案。
备选方案 2:路径样式标识符
如果您分配包含分层信息的值(路径),事情就会变得容易得多。例如,在您的案例中,这可能如下所示:id
编号 | 名字 |
---|---|
19 | 类别1 |
19/1 | 类别2 |
19/1/1 | 类别3 |
19/1/1/1 | 类别4 |
那么你的会看起来像这样:select
select id,
name
from products
where id like '19/%'
备选方案 3:重复自联接
如果您知道层次结构树可以变得多深的上限,则可以使用如下所示的标准查询:sql
select p6.parent_id as parent6_id,
p5.parent_id as parent5_id,
p4.parent_id as parent4_id,
p3.parent_id as parent3_id,
p2.parent_id as parent2_id,
p1.parent_id as parent_id,
p1.id as product_id,
p1.name
from products p1
left join products p2 on p2.id = p1.parent_id
left join products p3 on p3.id = p2.parent_id
left join products p4 on p4.id = p3.parent_id
left join products p5 on p5.id = p4.parent_id
left join products p6 on p6.id = p5.parent_id
where 19 in (p1.parent_id,
p2.parent_id,
p3.parent_id,
p4.parent_id,
p5.parent_id,
p6.parent_id)
order by 1, 2, 3, 4, 5, 6, 7;
看到这把小提琴
该条件指定要检索其后代的父项。您可以根据需要使用更多级别扩展此查询。where
评论
parent_id > id
on p.parent_id = cte.id
on p.id = cte.parent_id
在其他数据库中,您可以使用递归查询(性能上的 YMMV)轻松完成此操作。
另一种方法是存储两个额外的数据位,一个左值和右值。左边和右边的值派生自你所表示的树结构的预序遍历。
这称为修改后的预排序树遍历,允许您运行简单的查询来一次获取所有父值。它也被称为“嵌套集”。
评论
如果您需要快速的读取速度,最好的选择是使用闭合表。闭包表包含每个祖先/后代对的一行。因此,在您的示例中,闭包表如下所示
ancestor | descendant | depth
0 | 0 | 0
0 | 19 | 1
0 | 20 | 2
0 | 21 | 3
0 | 22 | 4
19 | 19 | 0
19 | 20 | 1
19 | 21 | 3
19 | 22 | 4
20 | 20 | 0
20 | 21 | 1
20 | 22 | 2
21 | 21 | 0
21 | 22 | 1
22 | 22 | 0
有了这个表,分层查询就变得非常简单快捷。要获取类别 20 的所有后代,请执行以下操作:
SELECT cat.* FROM categories_closure AS cl
INNER JOIN categories AS cat ON cat.id = cl.descendant
WHERE cl.ancestor = 20 AND cl.depth > 0
当然,每当你使用这样的非规范化数据时,都会有一个很大的缺点。您需要将关闭表与类别表放在一起。最好的方法可能是使用触发器,但正确跟踪闭包表的插入/更新/删除有些复杂。与任何事情一样,您需要查看您的要求并决定哪种方法最适合您。
编辑:请参阅问题在关系数据库中存储分层数据的选项有哪些?针对不同的情况有不同的最佳解决方案。
请尝试以下操作:
表定义:
DROP TABLE IF EXISTS category;
CREATE TABLE category (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(20),
parent_id INT,
CONSTRAINT fk_category_parent FOREIGN KEY (parent_id)
REFERENCES category (id)
) engine=innodb;
实验行:
INSERT INTO category VALUES
(19, 'category1', NULL),
(20, 'category2', 19),
(21, 'category3', 20),
(22, 'category4', 21),
(23, 'categoryA', 19),
(24, 'categoryB', 23),
(25, 'categoryC', 23),
(26, 'categoryD', 24);
递归存储过程:
DROP PROCEDURE IF EXISTS getpath;
DELIMITER $$
CREATE PROCEDURE getpath(IN cat_id INT, OUT path TEXT)
BEGIN
DECLARE catname VARCHAR(20);
DECLARE temppath TEXT;
DECLARE tempparent INT;
SET max_sp_recursion_depth = 255;
SELECT name, parent_id FROM category WHERE id=cat_id INTO catname, tempparent;
IF tempparent IS NULL
THEN
SET path = catname;
ELSE
CALL getpath(tempparent, temppath);
SET path = CONCAT(temppath, '/', catname);
END IF;
END$$
DELIMITER ;
存储过程的包装函数:
DROP FUNCTION IF EXISTS getpath;
DELIMITER $$
CREATE FUNCTION getpath(cat_id INT) RETURNS TEXT DETERMINISTIC
BEGIN
DECLARE res TEXT;
CALL getpath(cat_id, res);
RETURN res;
END$$
DELIMITER ;
选择示例:
SELECT id, name, getpath(id) AS path FROM category;
输出:
+----+-----------+-----------------------------------------+
| id | name | path |
+----+-----------+-----------------------------------------+
| 19 | category1 | category1 |
| 20 | category2 | category1/category2 |
| 21 | category3 | category1/category2/category3 |
| 22 | category4 | category1/category2/category3/category4 |
| 23 | categoryA | category1/categoryA |
| 24 | categoryB | category1/categoryA/categoryB |
| 25 | categoryC | category1/categoryA/categoryC |
| 26 | categoryD | category1/categoryA/categoryB/categoryD |
+----+-----------+-----------------------------------------+
筛选具有特定路径的行:
SELECT id, name, getpath(id) AS path FROM category HAVING path LIKE 'category1/category2%';
输出:
+----+-----------+-----------------------------------------+
| id | name | path |
+----+-----------+-----------------------------------------+
| 20 | category2 | category1/category2 |
| 21 | category3 | category1/category2/category3 |
| 22 | category4 | category1/category2/category3/category4 |
+----+-----------+-----------------------------------------+
评论
(20, 'category2', 19), (21, 'category3', 20), (22, 'category4', 20),
IF (tempparent IS NULL OR tempparent = 0)
IF tempparent IS NULL
IF tempparent = 0
只需使用 BlueM/tree php 类在 mysql 中制作自关系表的树即可。
Tree 和 Tree\Node 是 PHP 类,用于处理使用父 ID 引用分层构建的数据。一个典型的示例是关系数据库中的表,其中每条记录的“父”字段引用另一条记录的主键。当然,Tree 不能只使用来自数据库的数据,而是使用任何东西:您提供数据,Tree 使用它,而不管数据来自哪里以及如何处理。阅读更多
以下是使用 BlueM/tree 的示例:
<?php
require '/path/to/vendor/autoload.php'; $db = new PDO(...); // Set up your database connection
$stm = $db->query('SELECT id, parent, title FROM tablename ORDER BY title');
$records = $stm->fetchAll(PDO::FETCH_ASSOC);
$tree = new BlueM\Tree($records);
...
列出第一个递归子项的简单查询:
select @pv:=id as id, name, parent_id
from products
join (select @pv:=19)tmp
where parent_id=@pv
结果:
id name parent_id
20 category2 19
21 category3 20
22 category4 21
26 category24 22
...使用左连接:
select
@pv:=p1.id as id
, p2.name as parent_name
, p1.name name
, p1.parent_id
from products p1
join (select @pv:=19)tmp
left join products p2 on p2.id=p1.parent_id -- optional join to get parent name
where p1.parent_id=@pv
@tincot列出所有子项的解决方案:
select id,
name,
parent_id
from (select * from products
order by parent_id, id) products_sorted,
(select @pv := '19') initialisation
where find_in_set(parent_id, @pv) > 0
and @pv := concat(@pv, ',', id)
使用 Sql Fiddle 在线测试并查看所有结果。
http://sqlfiddle.com/#!9/a318e3/4/0
这里没有提到的东西,虽然有点类似于接受答案的第二种选择,但对于大型层次结构查询和简单的(插入更新删除)项目来说,不同且成本低,将为每个项目添加一个持久路径列。
比如:
id | name | path
19 | category1 | /19
20 | category2 | /19/20
21 | category3 | /19/20/21
22 | category4 | /19/20/21/22
例:
-- get children of category3:
SELECT * FROM my_table WHERE path LIKE '/19/20/21%'
-- Reparent an item:
UPDATE my_table SET path = REPLACE(path, '/19/20', '/15/16') WHERE path LIKE '/19/20/%'
使用 base36 编码而不是实际数字路径 ID 优化路径长度和 ORDER BY 路径
// base10 => base36
'1' => '1',
'10' => 'A',
'100' => '2S',
'1000' => 'RS',
'10000' => '7PS',
'100000' => '255S',
'1000000' => 'LFLS',
'1000000000' => 'GJDGXS',
'1000000000000' => 'CRE66I9S'
https://en.wikipedia.org/wiki/Base36
通过使用固定长度和填充到编码的 id 来抑制斜杠“/”分隔符
详细优化说明如下: https://bojanz.wordpress.com/2014/04/25/storing-hierarchical-data-materialized-path/
待办事项
构建一个函数或过程来拆分一个项目的检索祖先的路径
评论
Select comment FROM comments WHERE ... (subject or user or theme or whatever condition) ... ORDER BY posting_date DESC
GROUP BY user ODER BY posting date
DESC
path
path
我为你提出了一个问题。这将为您提供具有单个查询的递归类别:
SELECT id,NAME,'' AS subName,'' AS subsubName,'' AS subsubsubName FROM Table1 WHERE prent is NULL
UNION
SELECT b.id,a.name,b.name AS subName,'' AS subsubName,'' AS subsubsubName FROM Table1 AS a LEFT JOIN Table1 AS b ON b.prent=a.id WHERE a.prent is NULL AND b.name IS NOT NULL
UNION
SELECT c.id,a.name,b.name AS subName,c.name AS subsubName,'' AS subsubsubName FROM Table1 AS a LEFT JOIN Table1 AS b ON b.prent=a.id LEFT JOIN Table1 AS c ON c.prent=b.id WHERE a.prent is NULL AND c.name IS NOT NULL
UNION
SELECT d.id,a.name,b.name AS subName,c.name AS subsubName,d.name AS subsubsubName FROM Table1 AS a LEFT JOIN Table1 AS b ON b.prent=a.id LEFT JOIN Table1 AS c ON c.prent=b.id LEFT JOIN Table1 AS d ON d.prent=c.id WHERE a.prent is NULL AND d.name IS NOT NULL
ORDER BY NAME,subName,subsubName,subsubsubName
这是一把小提琴。
这对我有用,希望这也对你有用。它将为您提供任何特定菜单的记录集 Root to Child。根据您的要求更改字段名称。
SET @id:= '22';
SELECT Menu_Name, (@id:=Sub_Menu_ID ) as Sub_Menu_ID, Menu_ID
FROM
( SELECT Menu_ID, Menu_Name, Sub_Menu_ID
FROM menu
ORDER BY Sub_Menu_ID DESC
) AS aux_table
WHERE Menu_ID = @id
ORDER BY Sub_Menu_ID;
评论
这是一个类别表。
SELECT id,
NAME,
parent_category
FROM (SELECT * FROM category
ORDER BY parent_category, id) products_sorted,
(SELECT @pv := '2') initialisation
WHERE FIND_IN_SET(parent_category, @pv) > 0
AND @pv := CONCAT(@pv, ',', id)
评论
根据@trincot答案,解释得很好,我使用语句使用当前页面创建面包屑,并在层次结构中向后查找表中的每一步。WITH RECURSIVE ()
id
parent
route
因此,@trincot解决方案在这里以相反的方向进行调整,以找到父母而不是后代。
我还添加了可用于反转结果顺序的值(否则面包屑会颠倒)。depth
WITH RECURSIVE cte (
`id`,
`title`,
`url`,
`icon`,
`class`,
`parent_id`,
`depth`
) AS (
SELECT
`id`,
`title`,
`url`,
`icon`,
`class`,
`parent_id`,
1 AS `depth`
FROM `route`
WHERE `id` = :id
UNION ALL
SELECT
P.`id`,
P.`title`,
P.`url`,
P.`icon`,
P.`class`,
P.`parent_id`,
`depth` + 1
FROM `route` P
INNER JOIN cte
ON P.`id` = cte.`parent_id`
)
SELECT * FROM cte ORDER BY `depth` DESC;
在升级到 mySQL 8+ 之前,我一直在使用 vars,但它已被弃用,不再在我的 8.0.22 版本上工作!
编辑 2021-02-19:分层菜单示例
@david评论之后,我决定尝试制作一个完整的分层菜单,其中包含所有节点并根据需要排序(带有对每个深度的项目进行排序的列)。对我的用户/授权矩阵页面非常有用。sorting
这确实简化了我的旧版本,每个深度(PHP 循环)都有一个查询。
此示例将 INNER JOIN 与表集成,以按网站(多网站 CMS 系统)筛选路由。url
您可以看到包含以正确方式对菜单进行排序的功能的基本列。path
CONCAT()
SELECT R.* FROM (
WITH RECURSIVE cte (
`id`,
`title`,
`url`,
`icon`,
`class`,
`parent`,
`depth`,
`sorting`,
`path`
) AS (
SELECT
`id`,
`title`,
`url`,
`icon`,
`class`,
`parent`,
1 AS `depth`,
`sorting`,
CONCAT(`sorting`, ' ' , `title`) AS `path`
FROM `route`
WHERE `parent` = 0
UNION ALL SELECT
D.`id`,
D.`title`,
D.`url`,
D.`icon`,
D.`class`,
D.`parent`,
`depth` + 1,
D.`sorting`,
CONCAT(cte.`path`, ' > ', D.`sorting`, ' ' , D.`title`)
FROM `route` D
INNER JOIN cte
ON cte.`id` = D.`parent`
)
SELECT * FROM cte
) R
INNER JOIN `url` U
ON R.`id` = U.`route_id`
AND U.`site_id` = 1
ORDER BY `path` ASC
评论