Postgresql递归自联接

时间:2013-06-23 14:34:04

标签: sql database postgresql

我在postgres中的表如下所示,表存储了ID之间的链式关系,我想要一个可以产生结果的查询,如“vc1” - > “rc7”或“vc3” - >“rc7”,我只会查询第一列ID1中的ID

ID1     ID2
"vc1"   "vc2"
"vc2"   "vc3"
"vc3"   "vc4"
"vc4"   "rc7"

所以我想在这里提供一些“head”id,我必须获取尾部(链中的最后一个)id。

2 个答案:

答案 0 :(得分:24)

这是对PostgreSQL 8.4及更高版本中提供的简单递归公用表表达式(WITH RECURSIVE)的经典用法。

在此演示:http://sqlfiddle.com/#!12/78e15/9

将示例数据指定为SQL:

CREATE TABLE Table1
    ("ID1" text, "ID2" text)
;

INSERT INTO Table1
    ("ID1", "ID2")
VALUES
    ('vc1', 'vc2'),
    ('vc2', 'vc3'),
    ('vc3', 'vc4'),
    ('vc4', 'rc7')
;

你可以写:

WITH RECURSIVE chain(from_id, to_id) AS (
  SELECT NULL, 'vc2'
  UNION
  SELECT c.to_id, t."ID2"
  FROM chain c
  LEFT OUTER JOIN Table1 t ON (t."ID1" = to_id)
  WHERE c.to_id IS NOT NULL
)
SELECT from_id FROM chain WHERE to_id IS NULL;

这样做是迭代地遍历链,将每一行添加到chain表中作为从指针到指针。当它遇到一行时,其中的' to'参考不存在它将添加一个空值'该行的参考。下一次迭代会注意到' to' reference为null并产生零行,这导致迭代结束。

外部查询然后通过不存在的to_id来选择已被确定为链的末尾的行。

需要花费一些精力来了解递归CTE。他们要理解的关键事项是:

  • 它们从初始查询的输出开始,它们反复与"递归部分"的输出结合。 (UNIONUNION ALL之后的查询),直到递归部分不添加任何行。这会停止迭代。

  • 它们并不是真正的递归,更具迭代性,尽管它们对于你可能使用递归的各种东西都有好处。

所以你基本上是在一个循环中构建一个表。您无法删除行或更改行,只能添加新行,因此您通常需要一个外部查询来过滤结果以获取所需的结果行。您经常会添加包含中间数据的额外列,用于跟踪迭代状态,控制停止条件等。

有助于查看未经过滤的结果。如果我用简单的SELECT * FROM chain替换最终的摘要查询,我可以看到生成的表格:

 from_id | to_id 
---------+-------
         | vc2
 vc2     | vc3
 vc3     | vc4
 vc4     | rc7
 rc7     | 
(5 rows)

第一行是手动添加的起点行,您可以在其中指定要查找的内容 - 在本例中为vc2。每个后续行都是由UNION ed递归术语添加的,该术语在前一个结果上执行LEFT OUTER JOIN并返回一组与上一个to_id配对的新行(现在在{ {1}}列)到下一个from_id。如果to_id没有匹配,则LEFT OUTER JOIN将为空,导致下一次调用现在返回行并结束迭代。

因为此查询每次都不会尝试仅添加 last 行,所以它实际上每次迭代都会重复一些工作。为了避免这种情况,您需要使用更像Gordon的方法,但是当您扫描输入表时,还要在前一个深度字段上进行过滤,因此您只加入了最新的行。在实践中,这通常是不必要的,但它可能是非常大的数据集的关注点,或者您无法创建适当的索引。

可以在the PostgreSQL documentation on CTEs中学习更多内容。

答案 1 :(得分:17)

以下是使用递归CTE的SQL:

with recursive tr(id1, id2, level) as (
      select t.id1, t.id2, 1 as level
      from t union all
      select t.id1, tr.id2, tr.level + 1
      from t join
           tr
           on t.id2 = tr.id1
     )
select *
from (select tr.*,
             max(level) over (partition by id1) as maxlevel
      from tr
     ) tr
where level = maxlevel;

Here是SQLFiddle