PostgreSQL递归后代深度


15

我需要根据祖先计算后代的深度。当记录具有时object_id = parent_id = ancestor_id,它将被视为根节点(祖先)。我一直在尝试使WITH RECURSIVE查询与PostgreSQL 9.4一起运行。

我不控制数据或列。数据和表架构来自外部来源。桌子在不断增长。目前每天约有3万条记录。树中的任何节点都可能丢失,并且它们有时会从外部源中拉出。通常按created_at DESC顺序提取它们,但是使用异步后台作业提取数据。

最初,我们有一个解决此问题的代码,但现在有5M +行,需要近30分钟才能完成。

表定义和测试数据示例:

CREATE TABLE objects (
  id          serial NOT NULL PRIMARY KEY,
  customer_id integer NOT NULL,
  object_id   integer NOT NULL,
  parent_id   integer,
  ancestor_id integer,
  generation  integer NOT NULL DEFAULT 0
);

INSERT INTO objects(id, customer_id , object_id, parent_id, ancestor_id, generation)
VALUES (2, 1, 2, 1, 1, -1), --no parent yet
       (3, 2, 3, 3, 3, -1), --root node
       (4, 2, 4, 3, 3, -1), --depth 1
       (5, 2, 5, 4, 3, -1), --depth 2
       (6, 2, 6, 5, 3, -1), --depth 3
       (7, 1, 7, 7, 7, -1), --root node
       (8, 1, 8, 7, 7, -1), --depth 1
       (9, 1, 9, 8, 7, -1); --depth 2

请注意,这object_id不是唯一的,但组合(customer_id, object_id)是唯一的。
运行这样的查询:

WITH RECURSIVE descendants(id, customer_id, object_id, parent_id, ancestor_id, depth) AS (
  SELECT id, customer_id, object_id, parent_id, ancestor_id, 0
  FROM objects
  WHERE object_id = parent_id

  UNION

  SELECT o.id, o.customer_id, o.object_id, o.parent_id, o.ancestor_id, d.depth + 1
  FROM objects o
  INNER JOIN descendants d ON d.parent_id = o.object_id
  WHERE
    d.id <> o.id
  AND
    d.customer_id = o.customer_id
) SELECT * FROM descendants d;

我希望将generation列设置为计算出的深度。添加新记录时,将生成列设置为-1。在某些情况下,parent_id可能尚未将a 拉出。如果parent_id不存在,则应将生成列设置为-1。

最终数据应如下所示:

id | customer_id | object_id | parent_id | ancestor_id | generation
2    1             2           1           1            -1
3    2             3           3           3             0
4    2             4           3           3             1
5    2             5           4           3             2
6    2             6           5           3             3
7    1             7           7           7             0
8    1             8           7           7             1
9    1             9           8           7             2

查询的结果应该是将生成列更新为正确的深度。

我从有关SO的相关问题答案开始着手。


因此,您想将update递归CTE的结果与表格一起使用吗?
a_horse_with_no_name

是的,我希望将生成列更新到其深度。如果没有父对象(objects.parent_id与任何object.object_id不匹配),则生成将保持为-1。

所以ancestor_id已经设置好了,所以您只需要从CTE.depth分配代?

是的,已经从我们从API获取的数据中设置了object_id,parent_id和ancestor_id。我想将“生成”列设置为任何深度。另一个要注意的是,object_id不是唯一的,因为customer_id 1可以具有object_id 1,而customer_id 2可以具有object_id1。表上的主ID是唯一的。

这是一次性更新还是您正在不断添加到正在增长的表中?好像是后一种情况。有很大的不同。而且,是否只有根节点(树)或树中的任何节点都缺失?
Erwin Brandstetter,2016年

Answers:


14

您的查询基本上是正确的。唯一的错误是您拥有CTE的第二个(递归)部分:

INNER JOIN descendants d ON d.parent_id = o.object_id

应该是相反的方式:

INNER JOIN descendants d ON d.object_id = o.parent_id 

您要与对象的父母(已经找到)一起加入。

因此,可以编写用于计算深度的查询(没有其他更改,仅格式化):

-- calculate generation / depth, no updates
WITH RECURSIVE descendants
  (id, customer_id, object_id, parent_id, ancestor_id, depth) AS
 AS ( SELECT id, customer_id, object_id, parent_id, ancestor_id, 0
      FROM objects
      WHERE object_id = parent_id

      UNION ALL

      SELECT o.id, o.customer_id, o.object_id, o.parent_id, o.ancestor_id, d.depth + 1
      FROM objects o
      INNER JOIN descendants d ON  d.customer_id = o.customer_id
                               AND d.object_id = o.parent_id  
      WHERE d.id <> o.id
    ) 
SELECT * 
FROM descendants d
ORDER BY id ;

对于更新,您只需将last替换为SELECTUPDATE将cte的结果加入表中:

-- update nodes
WITH RECURSIVE descendants
    -- nothing changes here except
    -- ancestor_id and parent_id 
    -- which can be omitted form the select lists
    ) 
UPDATE objects o 
SET generation = d.depth 
FROM descendants d
WHERE o.id = d.id 
  AND o.generation = -1 ;          -- skip unnecessary updates

SQLfiddle上测试

附加评论:

  • ancestor_idparent_id不需要的是在选择列表(祖先是显而易见的,父母有点棘手弄清楚为什么),这样你就可以让他们在SELECT查询,如果你想要,但你可以安全地从删除它们UPDATE
  • (customer_id, object_id)看起来像一个候选人UNIQUE的约束。如果您的数据符合此要求,请添加此类约束。如果它不是唯一的,则在递归CTE中执行的联接是没有意义的(否则一个节点可以有2个父节点)。
  • 如果添加该约束,(customer_id, parent_id)则将成为(unique)FOREIGN KEY约束的候选者。你最有可能做希望添加FK约束虽然,因为通过你的描述,你添加新的行和一些行可以引用尚未加入别人。REFERENCES(customer_id, object_id)
  • 如果要在一个大表中执行查询,则肯定存在效率问题。不在第一次运行中,因为无论如何几乎都会更新整个表。但是第二次,您只希望考虑新行(以及第一次运行未触及的行)进行更新。现有的CTE必须取得重大成就。
    AND o.generation = -1在最终更新将确保这是在第一次运行更新的行不会再更新,但CTE仍然是一个昂贵的部分。

以下是尝试解决这些问题的尝试:改进CTE以考虑尽可能少的行,并使用它(customer_id, obejct_id)代替(id)标识行(因此id已从查询中完全删除。它可以用作第一次更新或后续更新:

WITH RECURSIVE descendants 
  (customer_id, object_id, depth) 
 AS ( SELECT customer_id, object_id, 0
      FROM objects
      WHERE object_id = parent_id
        AND generation = -1

      UNION ALL

      SELECT o.customer_id, o.object_id, p.generation + 1
      FROM objects o
        JOIN objects p ON  p.customer_id = o.customer_id
                       AND p.object_id = o.parent_id
                       AND p.generation > -1
      WHERE o.generation = -1

      UNION ALL

      SELECT o.customer_id, o.object_id, d.depth + 1
      FROM objects o
      INNER JOIN descendants d ON  o.customer_id = d.customer_id
                               AND o.parent_id = d.object_id
      WHERE o.parent_id <> o.object_id
        AND o.generation = -1
    )
UPDATE objects o 
SET generation = d.depth 
FROM descendants d
WHERE o.customer_id = d.customer_id
  AND o.object_id = d.object_id
  AND o.generation = -1        -- this is not really needed

请注意CTE如何由3部分组成。前两个是稳定部分。第一部分找到之前尚未更新的根节点,并且仍然存在,generation=-1因此它们必须是新添加的节点。第二部分查找generation=-1先前已更新的父节点的子节点(带有)。
与以前一样,第三个递归部分查找前两个部分的所有后代。

SQLfiddle-2上测试


3

@ypercube已经提供了足够的解释,因此我将逐一介绍我必须添加的内容。

如果parent_id不存在,则应将生成列设置为-1。

我认为这是应该递归应用,即在树的其余部分始终generation = -1任何丢失节点之后。

如果树中的任何节点都可能丢失(但),我们需要查找具有generation = -1
节点的行...是根节点
或具有的父节点generation > -1
然后从那里穿过那棵树。此选择的子节点也必须具有generation = -1

generation递增一父或回退到0为根节点:

WITH RECURSIVE tree AS (
   SELECT c.customer_id, c.object_id, COALESCE(p.generation + 1, 0) AS depth
   FROM   objects      c
   LEFT   JOIN objects p ON c.customer_id = p.customer_id
                        AND c.parent_id   = p.object_id
                        AND p.generation > -1
   WHERE  c.generation = -1
   AND   (c.parent_id = c.object_id OR p.generation > -1)
       -- root node ... or parent with generation > -1

   UNION ALL
   SELECT customer_id, c.object_id, p.depth + 1
   FROM   objects c
   JOIN   tree    p USING (customer_id)
   WHERE  c.parent_id  = p.object_id
   AND    c.parent_id <> c.object_id  -- exclude root nodes
   AND    c.generation = -1           -- logically redundant, but see below!
   )
UPDATE objects o 
SET    generation = t.depth
FROM   tree t
WHERE  o.customer_id = t.customer_id
AND    o.object_id   = t.object_id;

这种方式的非递归部分是一个SELECT,但在逻辑上等效于@ypercube的两个union'ed SELECT。不知道哪个更快,您必须进行测试。
性能上更重要的一点是:

指数!

如果您以这种方式反复向表中添加行,请添加部分索引

CREATE INDEX objects_your_name_idx ON objects (customer_id, parent_id, object_id)
WHERE  generation = -1;

与迄今为止讨论的所有其他改进相比,这将在性能上取得更大的成就-重复向大表中添加少量内容。

我将索引条件添加到CTE的递归部分(即使在逻辑上是冗余的),以帮助查询计划者理解部分索引是适用的。

另外,您可能还应该UNIQUE(object_id, customer_id)已经提到的@ypercube施加约束。或者,如果由于某种原因而不能施加唯一性(为什么?),请添加一个普通索引。索引列的顺序很重要,顺便说一句:


1
我将添加您和@ypercube建议的索引和约束。查看数据,我看不到它们无法发生的任何原因(除了外键,因为有时尚未设置parent_id)。我还将生成列设置为可为空,默认设置为NULL而不是-1。然后,我将不会有很多“ -1”过滤器,并且部分索引可以是WHERE生成为NULL等
。– Diggity

@Diggity:如果您调整其余部分,则NULL应该可以正常工作,是的。
Erwin Brandstetter,2016年

@Erwin很好。我本来以为和你相似。一个索引ON objects (customer_id, parent_id, object_id) WHERE generation = -1;,也许另一个ON objects (customer_id, object_id) WHERE generation > -1;。更新还必须将所有更新的行从一个索引“切换”到另一个索引,因此不确定对于UPDATE的初次运行是否是个好主意。
ypercubeᵀᴹ

为递归查询建立索引可能非常困难。
ypercubeᵀᴹ
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.