在我的sql <8中获取树文件夹结构的父级和子级,并且没有CTE

时间:2019-03-21 20:34:29

标签: mysql sql hierarchical-data

我有一个以idparent_id关系与其自身连接的文件夹表:

CREATE TABLE folders (
  id int(10) unsigned NOT NULL AUTO_INCREMENT,
  title nvarchar(255) NOT NULL,
  parent_id int(10) unsigned DEFAULT NULL,
  PRIMARY KEY (id)
);

INSERT INTO folders(id, title, parent_id) VALUES(1, 'root', null);
INSERT INTO folders(id, title, parent_id) values(2, 'one', 1);
INSERT INTO folders(id, title, parent_id) values(3, 'target', 2);
INSERT INTO folders(id, title, parent_id) values(4, 'child one', 3);
INSERT INTO folders(id, title, parent_id) values(5, 'child two', 3);
INSERT INTO folders(id, title, parent_id) values(6, 'root 2', null);
INSERT INTO folders(id, title, parent_id) values(7, 'other child one', 6);
INSERT INTO folders(id, title, parent_id) values(8, 'other child two', 6);

我想要一个查询,以返回该记录的所有父级,直接返回到路线和任何子级。

因此,如果我请求带有id=3的文件夹,则会得到记录:1, 2, 3, 4, 5。我被困在如何养父母的路上。

MYSQL的版本是5.7,并且没有立即进行升级的计划,因此遗憾的是CTE不是一个选择。

我已经创建了这个sql fiddle

9 个答案:

答案 0 :(得分:6)

在MySQL 8.0中,您可以使用Recursive Common Table Expressions来解决该用例。

以下查询为您提供了给定记录的父母(包括记录本身):

with recursive parent_cte (id, title, parent_id) as (
  select id, title, parent_id
  from folders
  where id = 3
  union all
  select  f.id, f.title, f.parent_id
  from folders f
  inner join parent_cte pc on f.id = pc.parent_id
)
select * from parent_cte;
| id  | title  | parent_id |
| --- | ------ | --------- |
| 3   | target | 2         |
| 2   | one    | 1         |
| 1   | root   |           |

这是一个稍有不同的查询,它返回给定记录的子树:

with recursive children_cte (id, title, parent_id) as (
  select id, title, parent_id
  from folders
  where parent_id = 3
  union all
  select  f.id, f.title, f.parent_id
  from folders f
  inner join children_cte cc on f.parent_id = cc.id
)
select * from children_cte;
| id  | title     | parent_id |
| --- | --------- | --------- |
| 4   | child one | 3         |
| 5   | child two | 3         |

两个查询器可以组合如下:

with recursive parent_cte (id, title, parent_id) as (
  select id, title, parent_id
  from folders
  where id = 3
  union all
  select  f.id, f.title, f.parent_id
  from folders f
  inner join parent_cte pc on f.id = pc.parent_id
),
children_cte (id, title, parent_id) as (
  select id, title, parent_id
  from folders
  where parent_id = 3
  union all
  select  f.id, f.title, f.parent_id
  from folders f
  inner join children_cte cc on f.parent_id = cc.id
)
select * from parent_cte
union all select * from children_cte;
| id  | title     | parent_id |
| --- | --------- | --------- |
| 3   | target    | 2         |
| 2   | one       | 1         |
| 1   | root      |           |
| 4   | child one | 3         |
| 5   | child two | 3         |

Demo on DB Fiddle

答案 1 :(得分:6)

在表设计中,IDPARENT_ID对应于用于存储树的“ 邻接表模型”。

还有另一种设计,称为“ 嵌套集模型”,它使在此处执行所需的操作更加容易。

请参阅Mike Hillyer的这篇出色的文章,其中介绍了以下两个方面: managing-hierarchical-data-in-mysql

总结:

树存储在一个表中,例如:

CREATE TABLE nested_category (
        category_id INT AUTO_INCREMENT PRIMARY KEY,
        name VARCHAR(20) NOT NULL,
        lft INT NOT NULL,
        rgt INT NOT NULL
);

找到从根到给定节点(此处为“ FLASH”)的路径:

SELECT parent.name
FROM nested_category AS node,
        nested_category AS parent
WHERE node.lft BETWEEN parent.lft AND parent.rgt
        AND node.name = 'FLASH'
ORDER BY parent.lft;

查找给定节点的所有子节点(此处为“便携式电子”):

SELECT node.name, (COUNT(parent.name) - (sub_tree.depth + 1)) AS depth
FROM nested_category AS node,
        nested_category AS parent,
        nested_category AS sub_parent,
        (
                SELECT node.name, (COUNT(parent.name) - 1) AS depth
                FROM nested_category AS node,
                        nested_category AS parent
                WHERE node.lft BETWEEN parent.lft AND parent.rgt
                        AND node.name = 'PORTABLE ELECTRONICS'
                GROUP BY node.name
                ORDER BY node.lft
        )AS sub_tree
WHERE node.lft BETWEEN parent.lft AND parent.rgt
        AND node.lft BETWEEN sub_parent.lft AND sub_parent.rgt
        AND sub_parent.name = sub_tree.name
GROUP BY node.name
HAVING depth <= 1
ORDER BY node.lft;

重命名到文件夹表后

  • TABLE nested_category-> TABLE文件夹
  • 列category_id->列ID
  • 列名->列标题

解决方案是:

CREATE TABLE folders (
        id INT AUTO_INCREMENT PRIMARY KEY,
        title VARCHAR(20) NOT NULL,
        lft INT NOT NULL,
        rgt INT NOT NULL
);

INSERT INTO folders(id, title, lft, rgt) values(1, 'root', 1, 10);
INSERT INTO folders(id, title, lft, rgt) values(2, 'one', 2, 9);
INSERT INTO folders(id, title, lft, rgt) values(3, 'target', 3, 8);
INSERT INTO folders(id, title, lft, rgt) values(4, 'child one', 4, 5);
INSERT INTO folders(id, title, lft, rgt) values(5, 'child two', 6, 7);
INSERT INTO folders(id, title, lft, rgt) values(6, 'root 2', 11, 16);
INSERT INTO folders(id, title, lft, rgt) values(7, 'other child one', 12, 13);
INSERT INTO folders(id, title, lft, rgt) values(8, 'other child two', 14, 15);

目标路径:

SELECT parent.title
FROM folders AS node,
        folders AS parent
WHERE node.lft BETWEEN parent.lft AND parent.rgt
        AND node.title = 'target'
ORDER BY parent.lft;

目标儿童:

SELECT node.title, (COUNT(parent.title) - (sub_tree.depth + 1)) AS depth
    FROM folders AS node,
            folders AS parent,
            folders AS sub_parent,
            (
              SELECT node.title, (COUNT(parent.title) - 1) AS depth
                    FROM folders AS node,
                            folders AS parent
                    WHERE node.lft BETWEEN parent.lft AND parent.rgt
                            AND node.title = 'target'
                    GROUP BY node.title
                    ORDER BY node.lft
            )AS sub_tree
    WHERE node.lft BETWEEN parent.lft AND parent.rgt
            AND node.lft BETWEEN sub_parent.lft AND sub_parent.rgt
            AND sub_parent.title = sub_tree.title
    GROUP BY node.title
    HAVING depth <= 1
    ORDER BY node.lft;

请参见sqlfiddle

要在单个查询中获取所有数据,应使用union

答案 2 :(得分:2)

我过去用第二个表解决了这个问题,该表包含通过树的所有路径的传递闭包。

mysql> CREATE TABLE folders_closure (
 ancestor INT UNSIGNED NOT NULL,
 descendant INT UNSIGNED NOT NULL,
 PRIMARY KEY (ancestor, descendant),
 depth INT UNSIGNED NOT NULL
);

在此表中加载所有祖先后代对的元组,包括树中某个节点引用其自身(长度为0的路径)的元组。

mysql> INSERT INTO folders_closure VALUES
     (1,1,0), (2,2,0), (3,3,0), (4,4,0), (5,5,0), (6,6,0),
     (1,2,1), (2,3,1), (3,4,1), (3,5,1), (1,4,2), (1,5,2),
     (6,7,1), (6,8,1);

现在,您可以通过查询从顶部节点开始的所有路径来查询给定节点下的树,并将该路径的后代连接到folders表中。

mysql> SELECT d.id, d.title, cl.depth FROM folders_closure cl
     JOIN folders d ON d.id=cl.descendant WHERE cl.ancestor=1;
+----+-----------+-------+
| id | title     | depth |
+----+-----------+-------+
|  1 | root      |     0 |
|  2 | one       |     1 |
|  4 | child one |     2 |
|  5 | child two |     2 |
+----+-----------+-------+

我看到很多人都推荐Nested Sets solution这个introduced in 1992,并在1995年Joe Celko在他的书SQL for Smarties中加入它后开始流行。但是我不喜欢嵌套集技巧,因为数字实际上并不是树中节点的主键的引用,并且在添加或删除节点时需要重新编号许多行。

我在What is the most efficient/elegant way to parse a flat table into a tree?和某些my other answers with the hierarchical-data tag中写了关于闭包表的方法。

我做了一个介绍:Models for Hierarchical Data

我也在我的书SQL Antipatterns: Avoiding the Pitfalls of Database Programming的一章中对此进行了介绍。

答案 3 :(得分:2)

如果可以确保子节点的ID始终高于其父节点,则可以使用用户变量

获取后代:

select f.*, @l := concat_ws(',', @l, id) as dummy
from folders f
cross join (select @l := 3) init_list
where find_in_set(parent_id, @l)
order by id

结果:

id | title     | parent_id | dummy
---|-----------|-----------|------
 4 | child one |         3 | 3,4
 5 | child two |         3 | 3,4,5

获取祖先(包括其祖先):

select f.*, @l := concat_ws(',', @l, parent_id) as dummy
from folders f
cross join (select @l := 3) init_list
where find_in_set(id, @l)
order by id desc

结果:

id | title  | parent_id | dummy
 3 | target |         2 | 3,2
 2 | one    |         1 | 3,2,1
 1 | root   |      null | 3,2,1

Demo

请注意,此技术依赖于未记录的评估顺序,在将来的版本中将无法使用。

它也不是很有效,因为两个查询都需要全表扫描,但是对于较小的表可能很好。但是,对于小表,我只需要获取整个表并使用应用程序代码中的递归函数来解决任务即可。

对于更大的表,我会考虑更复杂的解决方案,例如以下存储过程

create procedure get_related_nodes(in in_id int)
begin
  set @list = in_id;
  set @parents = @list;

  repeat
    set @sql = '
      select group_concat(id) into @children
      from folders
      where parent_id in ({parents})
    ';
    set @sql = replace(@sql, '{parents}', @parents);
    prepare stmt from @sql;
    execute stmt;
    set @list = concat_ws(',', @list, @children);
    set @parents = @children;
  until (@children is null) end repeat;

  set @child = in_id;
  repeat
    set @sql = '
      select parent_id into @parent
      from folders
      where id = ({child})
    ';
    set @sql = replace(@sql, '{child}', @child);
    prepare stmt from @sql;
    execute stmt;
    set @list = concat_ws(',', @parent, @list);
    set @child = @parent;
  until (@parent is null) end repeat;

  set @sql = '
    select *
    from folders
    where id in ({list})
  ';
  set @sql = replace(@sql, '{list}', @list);
  prepare stmt from @sql;
  execute stmt;
end

一起使用
call get_related_nodes(3)

这将返回

id | title     | parent_id
---|-----------|----------
 1 | root      | 
 2 | one       | 1
 3 | target    | 2
 4 | child one | 3
 5 | child two | 3

Demo

我希望此过程的性能与递归CTE 查询一样好。无论如何,您都应该在parent_id上有一个索引。

答案 4 :(得分:2)

如果您的parent_id总是按升序排列,那么下面的查询是一个很好的解决方案。

如果您得到的结果是您的ID为空的父值,则请点击链接 http://www.sqlfiddle.com/#!9/b40b8/258(通过id = 6时) http://www.sqlfiddle.com/#!9/b40b8/259(通过id = 3时)

SELECT * FROM folders f
WHERE id = 3 
OR 
  (Parent_id <=3 AND Parent_id >= 
  (SELECT id FROM folders Where id <= 3 AND parent_id IS NULL Order by ID desc LIMIT 1)) OR (id <= 3 AND IFNULL(Parent_id,0) = 0)
  AND id >= (SELECT id FROM folders Where id <= 3 AND parent_id IS NULL Order by ID desc LIMIT 1);

OR

您不会在父级的顶部获得通过的ID,请点击以下链接。  http://www.sqlfiddle.com/#!9/b40b8/194(通过id = 3时)
http://www.sqlfiddle.com/#!9/b40b8/208(通过id = 6时)

SELECT 
      * 
FROM 
     folders f
WHERE 
    id = 3 OR Parent_id <=3 
    OR (id <= 3  AND IFNULL(Parent_id,0) = 0);

答案 5 :(得分:1)

注意,我的解决方案与@Marc Alff差不多。在编辑器中键入/准备响应之前,没有意识到它已经存在。

如果不使用CTE或其他分层查询支持(例如,事先在Oracle中进行连接),就很难获得查询来实现目标(或分层数据集的其他典型要求)。这是数据库提供CTE等的主要驱动力。

许多年前,当数据库中没有这种对分层实体建模的支持时,通过对这种实体建模略有不同,可以解决您和其他相关人员概述的需求。

这个概念很简单。本质上,在层次结构表(或一个单独的外键到层次结构表的表)中又引入了两个属性,分别称为left_boundary和right_boundary(称呼所有您想要的名称)。对于每一行,选择这些属性的值(数字),使其覆盖所有子项的这些属性的值。换句话说,孩子的左右边界将在其父母的左右边界之间。

通过示例

enter image description here

创建这种层次结构曾经是清晨批处理工作的一部分,或者在设计期间选择了如此宽的边界,以至于它们可以轻松覆盖树木的所有深度。

我将使用此解决方案来实现您的目标。 首先,我将介绍第二个表(可以在同一表中引入属性,决定不打扰您的数据模型)

CREATE TABLE folder_boundaries (
  id int(10) unsigned NOT NULL AUTO_INCREMENT,
  folder_id int(10) unsigned NOT NULL,
  left_boundary int(10) unsigned,
  right_boundary int(10) unsigned,
  PRIMARY KEY (id),
  FOREIGN KEY (folder_id) REFERENCES folders(id)
);

基于您的数据集的此表的数据

NSERT INTO folder_boundaries(folder_id, left_boundary, right_boundary) VALUES(1, 1, 10);
INSERT INTO folder_boundaries(folder_id, left_boundary, right_boundary) VALUES(2, 2, 9);
INSERT INTO folder_boundaries(folder_id, left_boundary, right_boundary) VALUES(3, 3, 8);
INSERT INTO folder_boundaries(folder_id, left_boundary, right_boundary) VALUES(4, 4, 4);
INSERT INTO folder_boundaries(folder_id, left_boundary, right_boundary) VALUES(5, 4, 4);
INSERT INTO folder_boundaries(folder_id, left_boundary, right_boundary) VALUES(6, 21, 25);
INSERT INTO folder_boundaries(folder_id, left_boundary, right_boundary) VALUES(7, 22, 22);
INSERT INTO folder_boundaries(folder_id, left_boundary, right_boundary) VALUES(7, 22, 22);

这是实现您所追求的目标的查询

select f.id, f.title
from folders f
join folder_boundaries fb on f.id = fb.folder_id
where fb.left_boundary < (select left_boundary from folder_boundaries where folder_id = 3)
and fb.right_boundary > (select right_boundary from folder_boundaries where folder_id = 3)
union all
select f.id, f.title
from folders f
join folder_boundaries fb on f.id = fb.folder_id
where fb.left_boundary >= (select left_boundary from folder_boundaries where folder_id = 3)
and fb.right_boundary <= (select right_boundary from folder_boundaries where folder_id = 3)

结果

enter image description here

答案 6 :(得分:0)

您可以像这样在父行和子行之间进行联合:

select title, id, @parent:=parent_id as parent from
               (select @parent:=3 ) a join (select * from folders order by id desc) b where @parent=id
union select title, id, parent_id as parent from folders where  parent_id=3 ORDER BY id

这里是一个示例dbfiddle

答案 7 :(得分:0)

使用存储过程的小代码,在5.6上进行了测试:

drop procedure if exists test;
DELIMITER //
create procedure test(in testid int)
begin
    DECLARE parent int;
    set parent = testid;

    drop temporary table if exists pars;
    CREATE temporary TABLE pars (
      id int(10) unsigned NOT NULL AUTO_INCREMENT,
      title nvarchar(255) NOT NULL,
      parent_id int(10) unsigned DEFAULT NULL,
      PRIMARY KEY (id)
    );

    #For getting heirarchy
    while parent is not null do
        insert into pars
        select  * from folders where id = parent;
        set parent = (select parent_id from folders where id = parent);
    end while;

    #For getting child
    insert into pars
    select  * from folders where parent_id = testid;

    select * from pars;
end //
DELIMITER ;

下面是对代码的调用:

call test(3);

输出为:

enter image description here

最终结果可以根据需要使用组合的字符串进行格式化,一旦我们获得了表,剩下的就应该很容易了。另外,如果可以对id进行排序,则非常适合格式化。

更不用说字段ID和parent_id都应为索引,以使其有效地工作。

答案 8 :(得分:0)

假设您知道树的最大深度,则可以“创建”循环以获取所需的内容:

获取父节点:

SELECT  @id :=
        (
        SELECT  parent_id
        FROM    folders
        WHERE   id = @id
        ) AS folderId, vars.id
FROM    (
        SELECT  @id := 7 AS id
        ) vars
INNER JOIN (
    SELECT 0 AS nbr UNION ALL SELECT 1 UNION ALL SELECT 2 
 UNION ALL SELECT 3 UNION ALL SELECT 4 UNION ALL SELECT 5 
 UNION ALL SELECT 6 UNION ALL SELECT 7 UNION ALL SELECT 8 
 UNION ALL SELECT 9) temp
WHERE @id IS NOT NULL

获取子节点:

SELECT  @id :=
        (
        SELECT  GROUP_CONCAT(id)
        FROM    folders
        WHERE   FIND_IN_SET(parent_id, @id)
        ) AS folderIds, vars.id
FROM    (
        SELECT  @id := 1 AS id
        ) vars
INNER JOIN (
    SELECT 0 AS nbr UNION ALL SELECT 1 UNION ALL SELECT 2 
 UNION ALL SELECT 3 UNION ALL SELECT 4 UNION ALL SELECT 5 
 UNION ALL SELECT 6 UNION ALL SELECT 7 UNION ALL SELECT 8 
 UNION ALL SELECT 9) temp
WHERE @id IS NOT NULL

这是

  • 在这种情况下(最大深度)在静态变量子查询(SELECT @id := 1 AS id)和10行静态集合之间创建联接
  • 在选择项中使用子查询来遍历树并找到所有父节点或子节点

联接的目的是创建一个10行的结果集,以便选择中的子查询被执行10次。

或者,如果您不知道最大深度,则可以

替换合并的子查询
INNER JOIN (
SELECT 1 FROM folder) temp

或者为了避免所有以上的联合选择,请使用限制:

INNER JOIN (
SELECT 1 FROM folder LIMIT 100) temp

参考:  -entry