分层表 - 如何获取项目的路径[MySQL中的链接列表]

时间:2012-08-20 08:47:28

标签: mysql sql linked-list hierarchical-data

我在MySQL中有一个分层表:每个项目的parent字段指向其父项的id字段。对于每个项目,我可以使用query described here获取所有父项的列表[无论深度]。使用GROUP_CONCAT,我将完整路径作为单个字符串:

SELECT GROUP_CONCAT(_id SEPARATOR ' > ') FROM (
SELECT  @r AS _id,
         (
         SELECT  @r := parent
         FROM    t_hierarchy
         WHERE   id = _id
         ) AS parent,
         @l := @l + 1 AS lvl
 FROM    (
         SELECT  @r := 200,
                 @l := 0
         ) vars,
         t_hierarchy h
WHERE    @r <> 0
ORDER BY lvl DESC
) x

只有在项目的id被修复后才能使这项工作正常[在这种情况下它是200

我想对所有行执行相同操作:使用一个附加字段(path)检索整个表,该字段将显示完整路径。我想到的唯一解决方案是将此查询包装在另一个select中,设置一个临时变量@id并在子查询中使用它。但它不起作用。我在NULL字段中获得了path

SELECT @id := id, parent, (
    SELECT GROUP_CONCAT(_id SEPARATOR ' > ') FROM (
    SELECT  @r AS _id,
             (
             SELECT  @r := parent
             FROM    t_hierarchy
             WHERE   id = _id
             ) AS parent,
             @l := @l + 1 AS lvl
     FROM    (
             SELECT  @r := @id,
                     @l := 0
             ) vars,
             t_hierarchy h
    WHERE    @r <> 0
    ORDER BY lvl DESC
    ) x
) as path
 FROM t_hierarchy

PS 我知道我可以将路径存储在单独的字段中并在插入/更新时更新它们,但我需要一个基于链接列表技术的解决方案。 / p>

更新:我希望看到一个不使用递归或forwhile等结构的解决方案。上述查找路径的方法不使用任何循环或函数。我想在同一个逻辑中找到一个解决方案。或者,如果不可能,请尝试解释原因!

2 个答案:

答案 0 :(得分:2)

考虑以下两个查询之间的区别:

SELECT @id := id as id, parent, (
    SELECT concat(id, ': ', @id)
) as path
FROM t_hierarchy;

SELECT @id := id as id, parent, (
    SELECT concat(id, ': ', _id)
    FROM (SELECT @id as _id) as x
) as path
FROM t_hierarchy;

它们看起来几乎相同,但结果却截然不同。在我的MySQL版本中,第二个查询中的_id对于其结果集中的每一行都是相同的,并且等于最后一行的id。但是,最后一位才是真的,因为我按给定的顺序执行了两个查询;例如,在SET @id := 1之后,我可以看到_id始终等于SET语句中的值。

那么这里发生了什么? EXPLAIN产生了一个线索:

mysql>     explain SELECT @id := id as id, parent, (
    ->         SELECT concat(id, ': ', _id)
    ->         FROM (SELECT @id as _id) as x
    ->     ) as path
    ->     FROM t_hierarchy;
+----+--------------------+-------------+--------+---------------+------------------+---------+------+------+----------------+
| id | select_type        | table       | type   | possible_keys | key              | key_len | ref  | rows | Extra          |
+----+--------------------+-------------+--------+---------------+------------------+---------+------+------+----------------+
|  1 | PRIMARY            | t_hierarchy | index  | NULL          | hierarchy_parent | 9       | NULL | 1398 | Using index    |
|  2 | DEPENDENT SUBQUERY | <derived3>  | system | NULL          | NULL             | NULL    | NULL |    1 |                |
|  3 | DERIVED            | NULL        | NULL   | NULL          | NULL             | NULL    | NULL | NULL | No tables used |
+----+--------------------+-------------+--------+---------------+------------------+---------+------+------+----------------+
3 rows in set (0.00 sec)

第三行,即没有使用表的DERIVED表,向MySQL表明它可以在任何时间准确计算一次。服务器没有注意到派生表使用查询中其他位置定义的变量,并且不知道您希望每行运行一次。您被user-defined variables上的MySQL文档中提到的行为所困扰:

  

作为一般规则,您不应该为用户变量赋值   并在同一语句中读取值。你可能会得到   你期望的结果,但这不能保证。的顺序   涉及用户变量的表达式的评估是未定义的   可能会根据给定声明中包含的元素进行更改;   另外,这个顺序不保证是相同的   发布MySQL服务器。

在我的情况下,它选择在外部@id(重新)定义SELECT之前先计算该表。事实上,这正是原始分层数据查询工作的原因; @r定义是由MySQL在查询中的任何其他内容之前计算的,正是因为它是那种派生表。但是,我们需要一种方法来为每个表行重置@r一次,而不是仅对整个查询重置一次。为此,我们需要一个看起来像原始查询的查询,手动重置@r

SELECT  @r := if(
          @c = th1.id,
          if(
            @r is null,
            null,
            (
              SELECT  parent
              FROM    t_hierarchy
              WHERE   id = @r
            )
          ),
          th1.id
        ) AS parent,
        @l := if(@c = th1.id, @l + 1, 0) AS lvl,
        @c := th1.id as _id
FROM    (
        SELECT  @c := 0,
                @r := 0,
                @l := 0
        ) vars
        left join t_hierarchy as th1 on 1
        left join t_hierarchy as th2 on 1
HAVING  parent is not null

此查询以与原始查询相同的方式使用第二个t_hierarchy,以确保结果中有足够的行供父子查询循环。它还为每个包含自身作为父项的_id添加一行;没有它,任何根对象(在父字段中都有NULL)将根本不会出现在结果中。

奇怪的是,通过GROUP_CONCAT运行结果似乎会破坏排序。幸运的是,该函数有自己的ORDER BY子句:

SELECT  _id,
        GROUP_CONCAT(parent ORDER BY lvl desc SEPARATOR ' > ') as path,
        max(lvl) as depth
FROM    (
  SELECT  @r := if(
            @c = th1.id,
            if(
              @r is null,
              null,
              (
                SELECT  parent
                FROM    t_hierarchy
                WHERE   id = @r
              )
            ),
            th1.id
          ) AS parent,
          @l := if(@c = th1.id, @l + 1, 0) AS lvl,
          @c := th1.id as _id
  FROM    (
          SELECT  @c := 0,
                  @r := 0,
                  @l := 0
          ) vars
          left join t_hierarchy as th1 on 1
          left join t_hierarchy as th2 on 1
  HAVING  parent is not null
  ORDER BY th1.id
) as x
GROUP BY _id;

公平警告:这些查询隐含地依赖于@r更新之前发生的@l@c更新。 MySQL不保证该顺序,并且可能随任何版本的服务器而改变。

答案 1 :(得分:1)

定义getPath函数并运行以下查询:

select id, parent, dbo.getPath(id) as path from t_hierarchy 

定义getPath函数:

create function dbo.getPath( @id int)
returns varchar(400)
as
begin
declare @path varchar(400)
declare @term int
declare @parent varchar(100)
set @path = ''
set @term = 0
while ( @term <> 1 )
begin
   select @parent = parent from t_hierarchy where id = @id
   if ( @parent is null or @parent = '' or  @parent = @id )
        set @term = 1
   else
        set @path = @path + @parent   
   set @id = @parent     
end
return @path
end