如何创建MySQL分层递归查询

Tar*_*ani 237 mysql sql recursive-query hierarchical-data

我有一个MySQL表,如下所示:

id | name        | parent_id
19 | category1   | 0
20 | category2   | 19
21 | category3   | 20
22 | category4   | 21
......
Run Code Online (Sandbox Code Playgroud)

现在,我想要一个MySQL查询,我只提供id [例如说'id = 19']然后我应该得到它的所有子id [即结果应该有id',21,22']. ...而且,孩子们的等级不知道它可以变化....

另外,我已经有了使用for循环的解决方案.....如果可能的话,让我知道如何使用单个MySQL查询来实现相同的功能.

tri*_*cot 328

如果您使用的是MySQL 8,请使用recursive with子句:

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;
Run Code Online (Sandbox Code Playgroud)

指定的值parent_id = 19应设置为id要选择所有后代的父级.

在MySQL 8之前

对于不支持公用表表达式(最高版本为5.7)的MySQL版本,您可以使用以下查询来实现此目的:

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))
Run Code Online (Sandbox Code Playgroud)

这是一个小提琴.

此处,指定的值@pv := '19'应设置为id要选择所有后代的父级.

如果父母有多个孩子,这也可以.但是,要求每条记录都满足条件parent_id < id,否则结果将不完整.

查询中的变量赋值

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

  • from首先评估该子句.这就是@pv初始化的地方.
  • 根据wherefrom别名中检索的顺序对每个记录评估该子句.因此,这是一个条件,只包括父项已被识别为在后代树中的记录(主要父项的所有后代逐渐添加到其中@pv).
  • 本节中的条件按where顺序进行评估,一旦总结果确定,评估就会中断.因此,第二个条件必须位于第二位,因为它将其添加id到父列表中,并且只有在id传递第一个条件时才会发生这种情况.length仅调用该函数以确保此条件始终为true,即使该pv字符串由于某种原因会产生伪值.

总而言之,人们可能会发现这些假设风险太大而无法依赖.该文件警告说:

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

因此,即使它与上述查询一致,评估顺序仍可能会更改,例如,当您添加条件或将此查询用作较大查询中的视图或子查询时.这是一个将在未来的MySQL版本中删除的"功能" :

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

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

效率

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

选择1: with recursive,connect by

越来越多的数据库执行SQL:1999 ISO标准WITH [RECURSIVE]语法的递归查询(如Postgres的8.4+,SQL Server的2005+,DB2,甲骨文11gR2的+,SQLite的3.8.4+,火鸟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包含分层信息的值,那么事情会变得容易得多:路径.例如,在您的情况下,这可能如下所示:

ID       | NAME
19       | category1   
19/1     | category2  
19/1/1   | category3  
19/1/1/1 | category4  
Run Code Online (Sandbox Code Playgroud)

然后你select会看起来像这样:

select  id,
        name 
from    products
where   id like '19/%'
Run Code Online (Sandbox Code Playgroud)

备选方案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;
Run Code Online (Sandbox Code Playgroud)

看到这个小提琴

where条件指定要检索的后代父母哪一方.您可以根据需要使用更多级别扩展此查询.

  • 我喜欢你的解释.它不只是给出答案,它解释了**为什么**它解决了问题,所以我们可以从中学习.**编辑:**它也很好,它不依赖于预先知道级别的数量. (22认同)
  • @Avión,它不是你必须放在某个地方的东西,它是*要求*对于所有记录这个条件都是真的.如果您有一条或多条记录,其中`parent_id> id`,则您无法使用此解决方案. (2认同)
  • @trincot 是否可以将其更改为“反向”工作?所以抓住所有的父母,祖父母等?我已经使用您的第一个查询来获取后代,但我想获取祖先?以及。 (2认同)
  • 对于任何希望使用`WITH RECURSIVE` 方法的人,我发现了[以下文章](http://mysqlserverteam.com/mysql-8-0-1-recursive-common-table-expressions-in-mysql-ctes-第四部分深度优先或广度优先遍历传递闭包循环避免/) 对不同的场景非常有帮助,例如递归深度、distincts 以及检测和关闭循环 (2认同)
  • 如果其他人正在寻找 @shreddish 提出的问题的答案,解决方案是将 `on p.parent_id = cte.id` 更改为 `on p.id = cte.parent_id` (2认同)
  • 我在我的计算机上,在我自己的表上尝试了MySQL5.7上的主要解决方案,但由于相应的条款@pv:= concat(@ pv,',',id)的计算结果为false,因此无效.我通过将其更改为长度(@pv:= concat(@ pv,',',id))> 0来修复它,因此它始终为真. (2认同)

Dam*_*ran 78

来自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 |
+-------------+----------------------+--------+
Run Code Online (Sandbox Code Playgroud)

查询:

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';
Run Code Online (Sandbox Code Playgroud)

产量

+-------------+----------------------+--------------+-------+
| 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  |
+-------------+----------------------+--------------+-------+
Run Code Online (Sandbox Code Playgroud)

大多数用户曾经在SQL数据库中处理过分层数据,毫无疑问,他们了解到分层数据的管理不是关系数据库的用途.关系数据库的表不是分层的(如XML),而只是一个平面列表.分层数据具有父子关系,该关系不是在关系数据库表中自然表示的. 阅读更多

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

编辑:

select @pv:=category_id as category_id, name, parent from category
join
(select @pv:=19)tmp
where parent=@pv
Run Code Online (Sandbox Code Playgroud)

输出:

category_id name    parent
19  category1   0
20  category2   19
21  category3   20
22  category4   21
Run Code Online (Sandbox Code Playgroud)

参考:如何在Mysql中进行递归SELECT查询?

  • 只要层次结构中最多不超过4个级别,那就没问题.如果有N个级别,则必须知道要正确创建查询. (19认同)
  • >大多数用户曾经在SQL数据库中处理过分层数据,毫无疑问,他们了解到分层数据的管理不是关系数据库的用途.也许你的意思是MySQL数据库.Oracle数据库可以很好地处理分层数据和查询. (5认同)
  • @Damodaran,感谢您的回复......我需要的是一个不知道孩子数量的条件......并且在使用内部联接概念的博客中,需要知道层次结构,而不是在我的情况下......所以让我知道你对它的看法...所以,简单来说,我需要一个查询来处理'n'hirerachy级别'n'不知道..... (2认同)

Fan*_*nto 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;
Run Code Online (Sandbox Code Playgroud)

实验行:

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);
Run Code Online (Sandbox Code Playgroud)

递归存储过程:

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 ;
Run Code Online (Sandbox Code Playgroud)

存储过程的包装函数:

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 ;
Run Code Online (Sandbox Code Playgroud)

选择示例:

SELECT id, name, getpath(id) AS path FROM category;
Run Code Online (Sandbox Code Playgroud)

输出:

+----+-----------+-----------------------------------------+
| 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 |
+----+-----------+-----------------------------------------+
Run Code Online (Sandbox Code Playgroud)

过滤具有特定路径的行:

SELECT id, name, getpath(id) AS path FROM category HAVING path LIKE 'category1/category2%';
Run Code Online (Sandbox Code Playgroud)

输出:

+----+-----------+-----------------------------------------+
| id | name      | path                                    |
+----+-----------+-----------------------------------------+
| 20 | category2 | category1/category2                     |
| 21 | category3 | category1/category2/category3           |
| 22 | category4 | category1/category2/category3/category4 |
+----+-----------+-----------------------------------------+
Run Code Online (Sandbox Code Playgroud)

  • 我很确定它适用于不止一个孩子.我甚至再次测试过它. (3认同)

Der*_*ger 7

我想出的最佳方法是

  1. 使用lineage存储\ sort\trace树.这绰绰有余,阅读速度比其他任何方法快数千倍.它也允许保持该模式,即使DB将改变(因为任何数据库将允许使用该模式)
  2. 使用确定特定ID的谱系的函数.
  3. 根据需要使用它(在选择中,或在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
Run Code Online (Sandbox Code Playgroud)

然后你就是

select get_lineage(the_id)
Run Code Online (Sandbox Code Playgroud)

希望它有助于某人:)


Dhe*_*rni 7

这里有另一个问题做同样的事情

Mysql选择递归获取具有多个级别的所有子级

查询将是:

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;
Run Code Online (Sandbox Code Playgroud)


MTK*_*MTK 7

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

有些喜欢:

id | name        | path
19 | category1   | /19
20 | category2   | /19/20
21 | category3   | /19/20/21
22 | category4   | /19/20/21/22
Run Code Online (Sandbox Code Playgroud)

例子:

-- 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/%'
Run Code Online (Sandbox Code Playgroud)

优化路径长度并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'
Run Code Online (Sandbox Code Playgroud)

https://en.wikipedia.org/wiki/Base36

通过使用固定长度和填充编码 ID 来抑制斜杠“/”分隔符

详细的优化说明在这里: https://bojanz.wordpress.com/2014/04/25/storing-hierarchical-data-materialized-path/

去做

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


小智 6

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

select @pv:=id as id, name, parent_id
from products
join (select @pv:=19)tmp
where parent_id=@pv
Run Code Online (Sandbox Code Playgroud)

结果:

id  name        parent_id
20  category2   19
21  category3   20
22  category4   21
26  category24  22
Run Code Online (Sandbox Code Playgroud)

...与左连接:

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
Run Code Online (Sandbox Code Playgroud)

@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)
Run Code Online (Sandbox Code Playgroud)

使用Sql Fiddle在线测试并查看所有结果。

http://sqlfiddle.com/#!9/a318e3/4/0


Jus*_*ard 5

如果需要快速读取,最好的选择是使用闭合表。闭合表为每个祖先/后代对包含一行。因此,在您的示例中,闭包表看起来像

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
Run Code Online (Sandbox Code Playgroud)

拥有该表后,分层查询变得非常容易和快速。要获取类别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
Run Code Online (Sandbox Code Playgroud)

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

编辑:请参阅问题在关系数据库中存储分层数据的选项有哪些?有关更多选项。针对不同情况有不同的最佳解决方案。


Mel*_*man 5

基于该@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;
Run Code Online (Sandbox Code Playgroud)

在升级到 mySQL 8+ 之前,我使用的是 vars,但它已被弃用,并且不再适用于我的 8.0.22 版本

编辑 2021-02-19分层菜单示例

在@david 评论之后,我决定尝试使用所有节点制作一个完整的分层菜单,并根据需要进行排序(sorting列对每个深度的项目进行排序)。对我的用户/授权矩阵页面非常有用。

这确实简化了我的旧版本,每个深度(PHP 循环)一个查询

ERP授权矩阵

此示例将 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  
Run Code Online (Sandbox Code Playgroud)