如何创build一个MySQL分层recursion查询

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

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循环的解决scheme…..让我知道如果可能的话,使用单个MySQL查询来实现同样的目的。

对于不支持公用表expression式(高达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) > 0 and @pv := concat(@pv, ',', id) 

这是一个小提琴 。

@pv := '19'指定的值应该被设置为你想要select所有后代的父代的id

如果父母有多个孩子,这也将起作用。 但是,要求每条logging都满足parent_id < id的条件,否则结果将不完整。

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

  • from子句首先被评估。 这就是@pv被初始化的地方。
  • where子句按照from别名中检索的顺序对每个logging进行评估。 所以这是一个条件,只包括其父已经被确定为在后代树(主要父母的所有后代逐步添加到@pv )的logging。
  • 这个条款中的条件按顺序进行评估,一旦总体结果是确定的,评估就会中断。 因此,第二个条件必须排在第二位,因为它将id添加到父列表中,并且只有当id通过第一个条件时才会发生这种情况。

总而言之,人们可能会发现这些假设过于依赖 – 它们没有文件保证,即使它始终如一地工作,当您将此查询用作视图或子视图时,评估顺序在理论上仍然可能会发生变化,在更大的查询中查询。

还要注意,对于非常大的数据集,这个解决scheme可能会变慢,因为find_in_set操作并不是在列表中find一个数字的最理想的方式,当然不是在与数量相同的数量级上达到大小的列表中的logging返回。

scheme1: WITH RECURSIVECONNECT BY

越来越多的数据库为recursion查询(例如Postgres 8.4 + , SQL Server 2005 + , DB2 , Oracle 11gR2 + , SQLite 3.8.4 + , Firebird 2.1 + , H2 , HyperSQL 2.1)实现SQL:1999 ISO标准WITH [RECURSIVE]语法 。 0+ , Teradata , MariaDB 10.2.2+ )。 而从8.0版本开始,MySql也支持它 。 使用该语法,查询如下所示:

 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; 

一些数据库具有用于分层查找的替代非标准语法,例如Oracle数据库上可用的CONNECT BY子句。 DB2也支持这种替代语法。

MySql版本5.7不提供这样的function。 当你的数据库引擎提供这个语法时,那当然是最好的select。 如果不是的话,那么也考虑下面的select。

备选scheme2:path样式标识符

如果您分配包含分层信息的id值,则事情变得更加容易:path。 例如,在你的情况下,这可能看起来像这样:

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

select3:重复的自我连接

如果您知道您的层次结构树可以变得多深的上限,则可以使用如下的标准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; 

看到这个小提琴

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

pipe理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数据库中处理过分层数据,并且毫无疑问地知道分层数据的pipe理不是关系数据库的目的。 关系数据库的表格不是分层的(比如XML),而只是一个简单的列表。 分层数据具有在关系数据库表中不自然表示的父子关系。 阅读更多

请参阅博客了解更多详情。

编辑:

 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 

参考: 如何在Mysql中执行recursionSELECT查询?

我最喜欢的是

  1. 使用谱系来存储\sorting\跟踪树。 这已经足够了,阅读速度比其他方法快上千倍。 它也允许保持这种模式,即使数据库会改变(因为任何数据库将允许使用该模式)
  2. 使用确定沿袭特定ID的function。
  3. 如你所愿(在select,或在CUD操作,甚至工作)使用它。

沿袭方法descr。 可以在任何地方find,例如在这里或这里 。 作为功​​能 – 这是我所激发的。

最后 – 得到了或多或less简单,相对快速和简单的解决scheme。

function的身体

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

希望它有助于某人:)

同样的事情在这里另一个伫立

Mysql selectrecursion获得具有多个级别的所有子级

查询将是:

 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是使用封闭表。 封闭表包含每个祖先/后代对的一行。 所以在你的例子中,闭包表看起来像

 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 

当然,使用这样的非规格化数据会有很大的不利之处。 你需要维护封闭表旁边的类别表。 最好的方法可能是使用触发器,但正确跟踪闭包表的插入/更新/删除有点复杂。 至于什么,你需要看看你的要求,并决定哪种方法最适合你。

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

试试这些:

表格定义:

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

recursion存储过程:

 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示例:

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

以特定path过滤行:

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

您可以使用recursion查询(YMMV on performance)轻松地在其他数据库中这样做。

另一种方式是存储两个额外的数据位,一个左值和右值。 左边和右边的值是从你正在表示的树结构的预遍历导出的。

这被称为修改预定义树遍历,并允许您运行一个简单的查询来一次获取所有父值。 它也被称为“嵌套集合”。

简单的查询来列出第一次recursion的子节点:

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

 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在线testing它并查看所有结果。

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

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

 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

用适当的字段和表名replace。

在mysql中使用BlueM / tree php类来创build一个自关系表的树。

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

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

我发现它更容易:

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

 is_related(id, parent_id); 

在你的例子

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

2)使用子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));