如何创建 MySQL 分层递归查询?

Posted

技术标签:

【中文标题】如何创建 MySQL 分层递归查询?【英文标题】:How to create a MySQL hierarchical recursive query? 【发布时间】:2018-07-09 14:33:14 【问题描述】:

我有一个 mysql 表如下:

id name parent_id
19 category1 0
20 category2 19
21 category3 20
22 category4 21
... ... ...

现在,我想要一个 MySQL 查询,我只需提供 id [例如说id=19] 然后我应该获取它的所有子 id [即结果应该有 id '20,21,22']....

孩子的等级是未知的;它可能会有所不同....

我知道如何使用for 循环来做到这一点...但是如何使用单个 MySQL 查询来实现呢?

【问题讨论】:

假设层次结构有 7 层深。您希望输出表是什么样的? MySQL(仍然)不支持分层查询(与其他现代 DBMS 一样)。您将需要编写存储过程或使用不同的数据模型。 What are the options for storing hierarchical data in a relational database?的可能重复 MYSQL 8.0 将支持使用 CTE(公用表表达式)的递归查询 如何获取从最后一个评论 ID 开始的完整帖子列表?还是最后一个孩子? 【参考方案1】:

来自博客Managing Hierarchical Data in 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),而只是一个平面列表。分层数据具有在关系数据库表中不自然表示的父子关系。 Read more

有关详细信息,请参阅博客。

编辑:

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

参考:How to do the Recursive SELECT query in Mysql?

【讨论】:

层次结构最多不超过4层就可以了。如果有 N 个级别,您必须知道这一点才能正确创建查询。 @Damodaran,感谢您的回复......我需要的是一个不知道孩子数量的条件......并且在使用内部连接概念的博客中,层次结构需要知道这不是我的情况......所以让我知道你对同样的看法......所以,简单来说,我需要一个查询来处理不知道'n'的'n'hirerachy级别。 .... @user3036105:在 MySQL 中使用 single SQL 查询是不可能做到这一点的。 MySQL 根本不够先进。如果您确实需要,请考虑升级到支持递归查询的 DBMS。 >大多数用户都曾在 SQL 数据库中处理过分层数据,并且毫无疑问地了解到分层数据的管理不是关系数据库的目的。也许您的意思是 MySQL 数据库。 Oracle 数据库可以很好地处理分层数据和查询。 "...分层数据的管理不是关系数据库的目的..." 虽然这可能不是关系数据库的初衷,但在现实世界的分层数据非常普遍,MySQL 应该反映人们在现实场景中实际需要如何使用他们的数据。【参考方案2】:

它有点棘手,检查它是否适合你

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

适当地替换为您的字段和表名称。

【讨论】:

在这种情况下不起作用sqlfiddle.com/#!2/19360/2,有了这个技巧,至少你应该先按层级排序。【参考方案3】:

我想出的最佳方法是

    使用沿袭来存储\排序\跟踪树。这绰绰有余,而且阅读速度比任何其他方法都要快数千倍。 即使数据库发生变化,它也允许保持该模式(因为任何数据库都允许使用该模式) 使用确定特定 ID 沿袭的函数。 根据需要使用它(在选择中,或在 CUD 操作中,甚至在作业中)。

沿袭方法描述。可以在任何地方找到,例如 Here 或 here。 至于功能 - that 是我的灵感来源。

最终 - 得到了或多或少简单、相对快速且简单的解决方案。

函数体

-- --------------------------------------------------------------------------------
-- 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)

希望它可以帮助某人:)

【讨论】:

【参考方案4】:

在这里为另一个问题做了同样的事情

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 &gt; 10;我不能为@pv 引用 F1.idFolder 我使用他们评论中显示的数据从 OP 的原始问题重新创建了表格,然后在此处运行您的查询,结果得到了一个 NULL。你知道为什么会这样吗?在数据库引擎方面是否存在先决条件,或者自从您做出使该查询过时的此答案后发生了某些变化?【参考方案5】:

我发现它更容易:

1) 创建一个函数来检查一个项目是否在另一个项目的父层次结构中的任何位置。像这样的东西(我不会写函数,用 WHILE DO 来做):

is_related(id, parent_id);

在你的例子中

is_related(21, 19) == 1;
is_related(20, 19) == 1;
is_related(21, 18) == 0;

2) 使用 sub-select ,如下所示:

select ...
from table t
join table pt on pt.id in (select i.id from table i where is_related(t.id,i.id));

【讨论】:

【参考方案6】:

对于 MySQL 8+: 使用递归 with 语法。 对于 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))

这是fiddle。

这里,@pv := '19' 中指定的值应设置为要选择其所有后代的父级的id

如果父母有 多个 孩子,这也适用。但要求每条记录都满足条件parent_id &lt; id,否则结果不完整。

查询中的变量赋值

此查询使用特定的 MySQL 语法:在执行期间分配和修改变量。对执行顺序做了一些假设:

首先评估from 子句。这就是 @pv 被初始化的地方。 where 子句按照从 from 别名中检索的顺序对每条记录进行评估。因此,这里设置了一个条件,只包括其父级已被识别为在后代树中的记录(主要父级的所有后代逐渐添加到@pv)。 此where 子句中的条件按顺序评估,一旦确定总结果,评估就会中断。因此,第二个条件必须排在第二位,因为它将id 添加到父列表中,并且只有当id 通过第一个条件时才会发生这种情况。 length 函数仅被调用以确保此条件始终为真,即使 pv 字符串由于某种原因会产生虚假值。

总而言之,人们可能会发现依赖这些假设太冒险了。 documentation 警告:

您可能会得到您期望的结果,但这不能保证 [...] 涉及用户变量的表达式的求值顺序是未定义的。

因此,即使它与上述查询一致,评估顺序仍可能发生变化,例如当您添加条件或将此查询用作较大查询中的视图或子查询时。 will be removed in a future MySQL release:

以前的 MySQL 版本可以在 SET 以外的语句中为用户变量赋值。 MySQL 8.0 支持此功能以实现向后兼容性,但可能会在 MySQL 的未来版本中删除。

如上所述,从 MySQL 8.0 开始,您应该使用递归 with 语法。

效率

对于非常大的数据集,此解决方案可能会变慢,因为find_in_set 操作不是在列表中查找数字的最理想方法,当然不是在达到与返回的记录数。

备选方案 1:with recursiveconnect by

越来越多的数据库为递归查询实现SQL:1999 ISO standard WITH [RECURSIVE] syntax(例如Postgres 8.4+、SQL Server 2005+、DB2、Oracle 11gR2+、SQLite 3.8.4+、Firebird 2.1+、H2、HyperSQL 2.1.0+、@ 987654336@,MariaDB 10.2.2+)。截至version 8.0, also MySQL supports it。请参阅此答案的顶部以了解要使用的语法。

某些数据库具有用于分层查找的替代非标准语法,例如 Oracle、DB2、Informix、CUBRID 和其他数据库上可用的 CONNECT BY 子句。

MySQL 5.7 版不提供这样的功能。如果您的数据库引擎提供了这种语法,或者您可以迁移到提供这种语法的引擎,那么这无疑是最好的选择。如果不是,那么还要考虑以下替代方案。

备选方案 2:路径样式标识符

如果您分配包含分层信息的id 值,事情会变得容易得多:路径。例如,在您的情况下,这可能如下所示:

ID NAME
19 category1
19/1 category2
19/1/1 category3
19/1/1/1 category4

那么您的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;

看到这个fiddle

where 条件指定您要检索其后代的父代。您可以根据需要将此查询扩展为更多级别。

【讨论】:

我喜欢你的解释。它不只是给出答案,它解释了为什么它解决了问题,以便我们可以从中实际学习。 编辑: 它不依赖于事先知道关卡的数量也很棒。 @Avión,这不是你必须放在某个地方的东西,这是一个要求,对于所有记录,这个条件都是正确的。如果您有一条或多条记录,其中parent_id &gt; id 则无法使用此解决方案。 @trincot 是否可以将其更改为“反向”工作?所以抓住所有的父母,祖父母等?我已经使用您的第一个查询来获取后代,但我想获取祖先?也是。 对于任何希望使用WITH RECURSIVE 方法的人,我发现the following article 对于递归深度、不同点以及检测和关闭循环等不同场景非常有用 我在自己的表上尝试了 MySQL5.7 上的主要解决方案,但由于与子句 @pv := concat(@pv, ', ', id) 计算结果为假。我通过将其更改为 length(@pv := concat(@pv, ',', id)) > 0 来修复它,所以它总是正确的。【参考方案7】:

您可以使用递归查询(YMMV 性能)很容易地在其他数据库中执行此操作。

另一种方法是存储两个额外的数据位,一个左值和一个右值。左值和右值来自您所代表的树结构的前序遍历。

这被称为修改的预排序树遍历,让您可以运行一个简单的查询来一次获取所有父值。它也被称为“嵌套集”。

【讨论】:

我想在你的评论中添加类似的评论,但既然你这样做了,我将只添加一个指向“嵌套集”的好例子的链接:mikehillyer.com/articles/managing-hierarchical-data-in-mysql【参考方案8】:

如果您需要快速阅读,最好的选择是使用闭包表。闭包表包含每个祖先/后代对的一行。所以在你的例子中,闭包表看起来像

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

当然,每当您使用这样的非规范化数据时,都会有一个很大的缺点。您需要在类别表旁边维护关闭表。最好的方法可能是使用触发器,但正确跟踪闭包表的插入/更新/删除有点复杂。与任何事情一样,您需要查看自己的要求并决定哪种方法最适合您。

编辑:查看问题What are the options for storing hierarchical data in a relational database? 了解更多选项。不同的情况有不同的最优解。

【讨论】:

【参考方案9】:

试试这些:

表定义:

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), 我很确定它适用于不止一个孩子。我什至再次测试了它。 @Fandi Susanto,非常感谢它对我的帮助。 该解决方案适用于我,但重要的是检查***父级(类别)是否由 NULL 或 0 的 parent_id 标识。因此,临时父级检查必须如下所示:IF (tempparent IS NULL OR tempparent = 0) 谢谢你!对我来说效果很好,我只需将 IF tempparent IS NULL 更改为 IF tempparent = 0 在我的情况下【参考方案10】:

只需使用BlueM/tree php 类在 mysql 中生成自关系表的树。

Tree 和 Tree\Node 是 PHP 类,用于处理使用父 ID 引用分层结构化的数据。一个典型的例子是关系数据库中的表,其中每条记录的“父”字段引用另一条记录的主键。当然,Tree 不仅可以使用源自数据库的数据,还可以使用任何东西:您提供数据,Tree 使用它,而不管数据来自何处以及如何处理。 read more

这是一个使用 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); 
...

【讨论】:

【参考方案11】:

列出第一次递归的孩子的简单查询:

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

【讨论】:

【参考方案12】:

这里没有提到的东西,虽然有点类似于已接受答案的第二种选择,但对于大层次结构查询和简单(插入更新删除)项目而言不同且成本较低,将为每个项目添加一个持久路径列。

有些喜欢:

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/%'

优化路径长度和ORDER BY path使用base36编码代替实数路径id

 // 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/

待办事项

构建一个函数或过程来分割路径以检索一个项目的祖先

【讨论】:

谢谢!有趣的是base36【参考方案13】:

我已为您查询。这将为您提供带有单个查询的递归类别:

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

这是fiddle。

【讨论】:

请删除/编辑您的答案以恢复您的良好声誉。【参考方案14】:

这对我有用,希望这对你也有用。它将为您提供任何特定菜单的记录集 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;

【讨论】:

如果孩子的 ID 大于父母的 ID,则似乎不适用于所有级别【参考方案15】:

这是一个类别表。

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)

输出::

【讨论】:

你能解释一下吗?但我保证这是有效的。谢谢。 请解释查询和@pv 的含义是什么?此查询中的循环如何工作? 如果有 ID 低于父母的孩子,似乎并不适用于所有级别。 :( @Jonas 花了我 20 分钟来确定实际问题,尝试使用不同的组合。你是对的。它不适用于低于其父 ID 的 ID。你有什么解决办法吗? @muaaz 我终于使用包含相应行的路径的“路径”字段解决了它,例如。 G。 ID 为 577 的行具有路径“/1/2/45/577/”。如果您要查找 ID 2 的所有子代,您可以简单地选择路径 LIKE "/1/2/%" 的所有行。唯一的缺点是您必须更新更新方法中的路径。但是对于 MySQL 5.6(兼容),它是唯一适合我的解决方案。【参考方案16】:

基于@trincot 的回答,很好的解释,我使用WITH RECURSIVE () 语句使用当前页面的id 创建面包屑在层次结构中向后移动 strong> 在我的route 表中找到每个parent

因此,@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 与 url 表集成以按网站过滤路由(多网站 CMS 系统)。

您可以看到基本的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  

【讨论】:

我将它用于带有面包屑路径的有用注释,但它也可以用于菜单。谢谢!顺便提一句。我在@trincot 的解决方案中使用了这个附加功能 是的@David 对于没有预定义深度的多级菜单,我们可以使用它,没想到,谢谢。

以上是关于如何创建 MySQL 分层递归查询?的主要内容,如果未能解决你的问题,请参考以下文章

NHibernate分层递归查询

mysql如何递归汇总?

mysql查询一个表,实现递归查询

mysql递归查询

MySQL怎样做递归查询

sql 怎么递归查询的方法: