How to create a MySQL hierarchical recursive query
我有一个MySQL表,如下所示:
1 2 3 4 5 6 | id | name | parent_id 19 | category1 | 0 20 | category2 | 19 21 | category3 | 20 22 | category4 | 21 ...... |
现在,我想要一个MySQL查询,我只提供id [例如说'id = 19']然后我应该得到它的所有子ID [即 结果应该有ids '20,21,22'] ....
而且,孩子们的等级不知道它可以变化....
另外,我已经有了使用for循环的解决方案.....如果可能的话,让我知道如何使用单个MySQL查询来实现相同的功能。
如果你使用的是MySQL 8,那么使用递归
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
在MySQL 8之前
对于不支持公用表表达式(最高版本为5.7)的MySQL版本,您可以使用以下查询来实现此目的:
1 2 3 4 5 6 7 8 |
这是一个小提琴。
这里,
如果父母有多个孩子,这也可以。但是,要求每条记录满足条件
查询中的变量赋值
此查询使用特定的MySQL语法:在执行期间分配和修改变量。对执行顺序做了一些假设:
-
首先评估
from 子句。这就是@pv 初始化的地方。 -
按照从
from 别名中检索的顺序为每条记录计算where 子句。因此,这是一个条件,只包括父项已被识别为在后代树中的记录(主要父项的所有后代逐渐添加到@pv )。 -
按顺序评估此
where 子句中的条件,并在总结果确定后中断评估。因此,第二个条件必须位于第二位,因为它将id 添加到父列表,并且只有在id 通过第一个条件时才会发生这种情况。仅调用length 函数以确保此条件始终为true,即使pv 字符串由于某种原因会产生虚假值。
总而言之,人们可能会发现这些假设风险太大而无法依赖。文档警告:
you might get the results you expect, but this is not guaranteed [...] the order of evaluation for expressions involving user variables is undefined.
因此,即使它与上述查询一致,评估顺序仍可能会更改,例如,当您添加条件或将此查询用作较大查询中的视图或子查询时。这是一个将在未来的MySQL版本中删除的"功能":
Previous releases of MySQL made it possible to assign a value to a user variable in statements other than
SET . This functionality is supported in MySQL 8.0 for backward compatibility but is subject to removal in a future release of MySQL.
如上所述,从MySQL 8.0开始,您应该使用递归
效率
对于非常大的数据集,此解决方案可能会变慢,因为
备选方案1:
越来越多的数据库为递归查询实现SQL:1999 ISO标准
某些数据库具有用于分层查找的替代非标准语法,例如Oracle,DB2,Informix,CUBRID和其他数据库上可用的
MySQL 5.7版不提供这样的功能。当您的数据库引擎提供此语法或您可以迁移到那个语法时,那肯定是最好的选择。如果没有,那么还要考虑以下备选方案。
备选方案2:路径式标识符
如果要分配包含分层信息的
1 2 3 4 5 | ID | NAME 19 | category1 19/1 | category2 19/1/1 | category3 19/1/1/1 | category4 |
然后你的
备选方案3:重复自连接
如果您知道层次结构树可以变深的上限,则可以使用标准的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | 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; |
看到这个小提琴
来自MySQL中管理分层数据的博客
表结构
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | +-------------+----------------------+--------+ | 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 | +-------------+----------------------+--------+ |
查询:
1 2 3 4 5 6 |
产量
1 2 3 4 5 6 7 8 9 10 | +-------------+----------------------+--------------+-------+ | 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),而只是一个平面列表。分层数据具有父子关系,该关系不是在关系数据库表中自然表示的。
阅读更多
有关详细信息,请参阅博客。
编辑:
1 2 3 4 |
输出:
1 2 3 4 5 | category_id name parent 19 category1 0 20 category2 19 21 category3 20 22 category4 21 |
参考:如何在Mysql中进行递归SELECT查询?
试试这些:
表定义:
1 2 3 4 5 6 7 8 | 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; |
实验行:
1 2 3 4 5 6 7 8 9 |
递归存储过程:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | 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 ; |
存储过程的包装函数:
1 2 3 4 5 6 7 8 9 |
选择示例:
输出:
1 2 3 4 5 6 7 8 9 10 11 12 | +----+-----------+-----------------------------------------+ | 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 | +----+-----------+-----------------------------------------+ |
过滤具有特定路径的行:
输出:
1 2 3 4 5 6 7 | +----+-----------+-----------------------------------------+ | id | name | path | +----+-----------+-----------------------------------------+ | 20 | category2 | category1/category2 | | 21 | category3 | category1/category2/category3 | | 22 | category4 | category1/category2/category3/category4 | +----+-----------+-----------------------------------------+ |
这里有另一个问题做同样的事情
Mysql选择递归获取具有多个级别的所有子级
查询将是:
1 2 3 4 5 6 7 8 9 |
我想出的最佳方法是
它也允许保持该模式,即使DB将改变(因为任何数据库将允许使用该模式)
谱系方法描述。例如,可以在任何地方找到
在这里或这里。
至于功能 - 这就是让我受宠的东西。
最终 - 获得了或多或少的简单,相对快速和简单的解决方案。
功能的身体
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 | -- -------------------------------------------------------------------------------- -- 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 |
然后你就是
1 |
希望它有助于某人:)
如果您需要快速读取速度,最好的选择是使用闭包表。闭包表包含每个祖先/后代对的行。所以在你的例子中,闭包表看起来像
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | 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的后代:
1 2 3 |
当然,每当你使用像这样的非规范化数据时,都会有一个很大的缺点。您需要在类别表旁边维护闭包表。最好的方法可能是使用触发器,但正确跟踪闭包表的插入/更新/删除有点复杂。与任何事情一样,您需要查看您的要求并确定最适合您的方法。
编辑:查看问题在关系数据库中存储分层数据的选项有哪些?了解更多选择。针对不同情况有不同的最佳解决方案。
简单查询列出第一次递归的子项:
1 2 3 4 |
结果:
1 2 3 4 5 | id name parent_id 20 category2 19 21 category3 20 22 category4 21 26 category24 22 |
...左连接:
1 2 3 4 5 6 7 8 9 |
@tincot的解决方案列出所有孩子的:
1 2 3 4 5 6 7 8 |
使用Sql Fiddle在线测试并查看所有结果。
http://sqlfiddle.com/#!9/a318e3/4/0
您可以使用递归查询(性能上的YMMV)轻松地在其他数据库中执行此操作。
另一种方法是存储两个额外的数据位,左右两个值。左侧和右侧值来自您正在表示的树结构的预先遍历遍历。
这称为Modified Preorder Tree Traversal,允许您运行简单查询以立即获取所有父值。它也被称为"嵌套集"。
这是一个类别表。
1 2 3 4 5 6 7 8 |
输出::
只需使用BlueM / tree php类在mysql中创建自关系表的树。
Tree and Tree
ode are PHP classes for handling data that is structured hierarchically using parent ID references. A typical example is a table in a relational database where each record’s"parent" field references the primary key of another record. Of course, Tree cannot only use data originating from a database, but anything: you supply the data, and Tree uses it, regardless of where the data came from and how it was processed. read more
以下是使用BlueM / tree的示例:
1 2 3 4 5 6 | <?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); ... |
它有点棘手,检查它是否适合你
1 |
SQL小提琴链接http://www.sqlfiddle.com/#!2/e3cdf/2
适当地替换您的字段和表名称。
这对我有用,希望这也适合你。它将为您提供任何特定菜单的记录设置Root to Child。根据您的要求更改字段名称。
1 2 3 4 5 6 7 8 9 10 |
这里没有提到的东西,虽然有点类似于接受的答案的第二个替代方案但是大层次查询和简单(插入更新删除)项目的不同且低成本,将为每个项目添加持久路径列。
一些像:
1 2 3 4 5 | id | name | path 19 | category1 | /19 20 | category2 | /19/20 21 | category3 | /19/20/21 22 | category4 | /19/20/21/22 |
例:
1 2 3 4 |
使用base36编码优化路径长度和
1 2 3 4 5 6 7 8 9 10 | // 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来抑制斜杠'/'分隔符
详细的优化说明如下:
Storing hierarchical data: Materialized Path
去做
构建一个函数或过程来分割一个项目的retreive祖先的路径
我发现它更容易:
1)创建一个函数,检查项是否在另一个项的父层次结构中的任何位置。像这样的东西(我不会写这个函数,用WHILE做它):
1 | is_related(id, parent_id); |
在你的例子中
1 2 3 | is_related(21, 19) == 1; is_related(20, 19) == 1; is_related(21, 18) == 0; |
2)使用子选择,如下所示:
1 2 3 |
我已经为你查了一下。这将为您提供单个查询的递归类别:
1 2 3 4 5 6 7 8 | 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 |
这是一个小提琴。