【问题标题】:Recursive query challenge - simple parent/child example递归查询挑战 - 简单的父/子示例
【发布时间】:2015-09-28 10:48:24
【问题描述】:

注意:在 RhodiumToad 在#postgresql 上的帮助下,我找到了一个解决方案,并将其发布为答案。如果有人可以改进这一点,请加入!

我无法使previous recursive query solution 适应以下包含多个“根”(无祖先)节点的有向无环图。我正在尝试编写一个查询,其输出通常称为闭包表:一个多对多表,存储从每个节点到其每个后代和自身的每条路径:

1  2  11  8  4  5  7
 \/    |  |   \ | /
  3    |   \    6
   \   |    \  /
    9  |     10
     \/     /
     12    13
       \  /
        14

CREATE TABLE node (
  id        SERIAL PRIMARY KEY,
  node_name VARCHAR(50) NOT NULL
);

CREATE TABLE node_relations (
  id                 SERIAL PRIMARY KEY,
  ancestor_node_id   INT REFERENCES node(id),
  descendant_node_id INT REFERENCES node(id)
);

INSERT into node (node_name)
SELECT 'node ' || g FROM generate_series(1,14) g;

INSERT INTO node_relations(ancestor_node_id, descendant_node_id) VALUES
(1,3),(2,3),(4,6),(5,6),(7,6),(3,9),(6,10),(8,10),(9,12),(11,12),(10,13),(12,14),(13,14);

很难查明问题 - 我是否缺少 node_relation 行?查询错了吗?

WITH RECURSIVE node_graph AS (
   SELECT ancestor_node_id, ARRAY[descendant_node_id] AS path, 0 AS level
   FROM   node_relations

   UNION  ALL
   SELECT nr.ancestor_node_id,  ng.path || nr.descendant_node_id,ng.level + 1 AS level
   FROM   node_graph ng
   JOIN   node_relations nr ON nr.descendant_node_id = ng.ancestor_node_id 
)
SELECT path[array_upper(path,1)] AS ancestor,
       path[1] AS descendant,
       path, 
       level as depth
FROM   node_graph
ORDER  BY level, ancestor;

预期输出:

ancestor | descendant | path
---------+------------+------------------
1        | 3          | "{1,3}"
1        | 9          | "{1,3,9}"
1        | 12         | "{1,3,9,12}"
1        | 14         | "{1,3,9,12,14}"
2        | 3          | "{2,3}"
2        | 9          | "{2,3,9}"
2        | 12         | "{2,3,9,12}"
2        | 14         | "{2,3,9,12,14}"
3        | 9          | "{3,9}"
3        | 12         | "{3,9,12}"
3        | 14         | "{3,9,12,14}"
4        | 6          | "{4,6}"
4        | 10         | "{4,6,10}"
4        | 13         | "{4,6,10,13}"
4        | 14         | "{4,6,10,13,14}"
5        | 6          | "{5,6}"
5        | 10         | "{5,6,10}"
5        | 13         | "{5,6,10,13}"
5        | 14         | "{5,6,10,13,14}"
6        | 10         | "{6,10}"
6        | 13         | "{6,10,13}"
6        | 14         | "{6,10,13,14}"
7        | 6          | "{7,6}"
7        | 10         | "{7,6,10}"
7        | 13         | "{7,6,10,13}"
7        | 14         | "{7,6,10,13,14}"
8        | 10         | "{8,10}"
8        | 13         | "{8,10,13}"
8        | 14         | "{8,10,13,14}"
9        | 12         | "{9,12}"
9        | 14         | "{9,12,14}"
10       | 13         | "{10,13}"
10       | 14         | "{10,13,14}"
11       | 12         | "{11,12}"
11       | 14         | "{11,12,14}"
12       | 14         | "{12,14}"
13       | 14         | "{13,14}"

【问题讨论】:

  • 还有:什么问题? (精彩的曝光,不过......)
  • 我上面提供的查询不正确——正确的查询是什么?我是否也缺少 node_relation 记录,例如循环记录?不确定缺少什么
  • 不清楚您拥有的代码的实际行为是什么。描述实际输出和预期输出之间的差异是有意义的。如果它只是抛出一些错误 - 错误消息也会很糟糕。
  • 您希望以何种方式改进您的工作解决方案?

标签: sql postgresql recursive-query transitive-closure-table


【解决方案1】:

在 #postgresql 上 RhodiumToad 的帮助下,我找到了这个解决方案:

WITH RECURSIVE node_graph AS (
    SELECT ancestor_node_id as path_start, descendant_node_id as path_end,
           array[ancestor_node_id, descendant_node_id] as path 
    FROM node_relations

    UNION ALL 

    SELECT ng.path_start, nr.descendant_node_id as path_end,
           ng.path || nr.descendant_node_id as path
    FROM node_graph ng
    JOIN node_relations nr ON ng.path_end = nr.ancestor_node_id
) 
SELECT * from node_graph order by path_start, array_length(path,1);

结果完全符合预期。

【讨论】:

    【解决方案2】:

    另一种方法是以相反的顺序遍历图形:

    WITH RECURSIVE cte AS (
       SELECT array[r.ancestor_node_id, r.descendant_node_id] AS path
       FROM   node_relations r
       LEFT   JOIN node_relations r0 ON r0.ancestor_node_id = r.descendant_node_id
       WHERE  r0.ancestor_node_id IS NULL  -- start at the end
    
       UNION ALL 
       SELECT r.ancestor_node_id || c.path
       FROM   cte c
       JOIN   node_relations r ON r.descendant_node_id = c.path[1]
       ) 
    SELECT path
    FROM   cte
    ORDER  BY path;
    

    这会产生一个子集,其中包含从每个根节点到其最终后代的每条路径。对于也散布很多的深层树,这将需要更少的连接操作。要额外添加每个子路径,您可以将 LATERAL 连接附加到外部 SELECT

    WITH RECURSIVE cte AS (
       SELECT array[r.ancestor_node_id, r.descendant_node_id] AS path
       FROM   node_relations r
       LEFT   JOIN node_relations r0 ON r0.ancestor_node_id = r.descendant_node_id
       WHERE  r0.ancestor_node_id IS NULL  -- start at the end
    
       UNION ALL 
       SELECT r.ancestor_node_id || c.path
       FROM   cte c
       JOIN   node_relations r ON r.descendant_node_id = c.path[1]
       ) 
    SELECT l.path
    FROM   cte, LATERAL (
       SELECT path[1:g] AS path
       FROM   generate_series(2, array_length(path,1)) g
       ) l
    ORDER  BY l.path;

    我进行了快速测试,但它的运行速度并不比 RhodiumToad 的解决方案快。对于大表或宽表,它可能仍然更快。尝试使用您的数据。

    【讨论】:

      【解决方案3】:

      我发现查询有两个问题:

      1. 非递归部分没有指定根节点。您需要使用WHERE descendant_node_id = 14 或“动态地”使用:

        SELECT ..
        FROM   node_relations nr1
        WHERE  NOT EXISTS (SELECT 1
                           FROM node_relations nr2
                           WHERE nr2.ancestor_node_id = nr1.descendant_node_id)
        
      2. 有了正确的起点,路径不完整,因为它会在递归部分的聚合过程中错过最终节点。因此,在外部查询中,您需要将ancestor_node_id 附加到生成的路径中。

      所以查询看起来像这样:

      WITH RECURSIVE node_graph AS (
         SELECT nr1.id, nr1.ancestor_node_id, nr1.descendant_node_id, ARRAY[nr1.descendant_node_id] AS path, 0 as level
         FROM   node_relations nr1
         WHERE  NOT EXISTS (SELECT 1
                            FROM node_relations nr2
                            WHERE nr2.ancestor_node_id = nr1.descendant_node_id)
      
         UNION  ALL
      
         SELECT nr.id, nr.ancestor_node_id, nr.descendant_node_id, nr.descendant_node_id || ng.path, ng.level + 1 as level
         FROM node_relations nr
           JOIN node_graph ng ON ng.ancestor_node_id = nr.descendant_node_id
      
      )
      SELECT ancestor_node_id||path as path, -- add the last element of the sub-tree to the path
             level as depth
      FROM   node_graph
      ORDER  BY level
      

      这里是 SQLFiddle:http://sqlfiddle.com/#!15/e646b/3

      【讨论】:

      • 这很接近,但结果与我在帖子底部指定的不太一样。我最近添加了结果,因此您可能在看到它们之前就已经开始回复了。抱歉,如果是这样..
      • @Dowwie:啊,所以你也想要中间级别。通过反转串联可以轻松更改元素的顺序。
      • 正确,最底部的节点未包含在您的初始结果中