关于sql:如何创建MySQL分层递归查询

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,那么使用递归with子句:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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 8之前

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

1
2
3
4
5
6
7
8
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初始化的地方。
  • 按照从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开始,您应该使用递归with语法。

效率

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

备选方案1:with recursiveconnect 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值,事情会变得容易得多:路径。例如,在您的情况下,这可能如下所示:

1
2
3
4
5
ID       | NAME
19       | category1  
19/1     | category2  
19/1/1   | category3  
19/1/1/1 | category4

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

1
2
3
4
select  id,
        name
from    products
where   id like '19/%'

备选方案3:重复自连接

如果您知道层次结构树可以变深的上限,则可以使用标准的sql查询,如下所示:

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;

看到这个小提琴

where条件指定要检索哪个父级的父级。您可以根据需要使用更多级别扩展此查询。


来自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
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';

产量

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
select @pv:=category_id as category_id, name, parent from category
join
(select @pv:=19)tmp
where parent=@pv

输出:

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

递归存储过程:

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

选择示例:

1
SELECT id, name, getpath(id) AS path FROM category;

输出:

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
SELECT id, name, getpath(id) AS path FROM category HAVING path LIKE 'category1/category2%';

输出:

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


我想出的最佳方法是

  • 使用lineage存储 sort trace树。这绰绰有余,阅读速度比其他任何方法快数千倍。
    它也允许保持该模式,即使DB将改变(因为任何数据库将允许使用该模式)
  • 使用确定特定ID的谱系的函数。
  • 根据需要使用它(在选择中,或在CUD操作中,甚至通过作业)。
  • 谱系方法描述。例如,可以在任何地方找到
    在这里或这里。
    至于功能 - 这就是让我受宠的东西。

    最终 - 获得了或多或少的简单,相对快速和简单的解决方案。

    功能的身体

    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
    select get_lineage(the_id)

    希望它有助于某人:)


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

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

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

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


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

    1
    2
    3
    4
    select @pv:=id as id, name, parent_id
    from products
    join (select @pv:=19)tmp
    where parent_id=@pv

    结果:

    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
    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的解决方案列出所有孩子的:

    1
    2
    3
    4
    5
    6
    7
    8
    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


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

    另一种方法是存储两个额外的数据位,左右两个值。左侧和右侧值来自您正在表示的树结构的预先遍历遍历。

    这称为Modified Preorder Tree Traversal,允许您运行简单查询以立即获取所有父值。它也被称为"嵌套集"。


    enter image description here

    这是一个类别表。

    1
    2
    3
    4
    5
    6
    7
    8
    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)

    输出::
    enter image description here


    只需使用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
    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

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


    这对我有用,希望这也适合你。它将为您提供任何特定菜单的记录设置Root to Child。根据您的要求更改字段名称。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    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;

    这里没有提到的东西,虽然有点类似于接受的答案的第二个替代方案但是大层次查询和简单(插入更新删除)项目的不同且低成本,将为每个项目添加持久路径列。

    一些像:

    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
    -- 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编码优化路径长度和ORDER BY path而不是实际数字路径id

    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
    select ...
    from table t
    join table pt on pt.id in (select i.id from table i where is_related(t.id,i.id));

    我已经为你查了一下。这将为您提供单个查询的递归类别:

    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

    这是一个小提琴。