PostgreSQL 재귀 하위 깊이


15

조상으로부터 후손의 깊이를 계산해야합니다. 레코드에가 ​​있으면 object_id = parent_id = ancestor_id루트 노드 (조상)로 간주됩니다. WITH RECURSIVEPostgreSQL 9.4 에서 쿼리를 실행 하려고했습니다 .

데이터 또는 열을 제어하지 않습니다. 데이터 및 테이블 스키마는 외부 소스에서 가져옵니다. 테이블은 지속적으로 성장하고 있습니다 . 현재 하루에 약 30k 레코드가 있습니다. 트리의 모든 노드가 누락 될 수 있으며 어느 시점에서 외부 소스에서 가져옵니다. 일반적으로 created_at DESC순서대로 가져 오지만 데이터는 비동기 백그라운드 작업으로 가져옵니다.

처음에는이 문제에 대한 코드 솔루션이 있었지만 이제 5 백만 개 이상의 행이 있으므로 완료하는 데 거의 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로 설정됩니다. A는 경우가 있습니다 parent_id아직 뽑아되지 않았을 수도. 이 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가 objects.object_id와 일치하지 않음) 생성은 -1로 유지됩니다.

따라서 ancestor_id이미 설정되었으므로 CTE.depth?

예, object_id, parent_id 및 ancestor_id는 API에서 얻은 데이터에서 이미 설정되어 있습니다. 생성 열을 깊이에 관계없이 설정하고 싶습니다. customer_id 1에 object_id 1이 있고 customer_id 2에 object_id 1이있을 수 있으므로 object_id는 고유하지 않습니다. 테이블의 기본 ID는 고유합니다.

이것은 일회성 업데이트입니까 아니면 계속해서 증가하는 테이블에 추가하고 있습니까? 후자의 경우처럼 보입니다. 차이를 만듭니다 . 루트 노드 만 (아직) 누락되었거나 트리의 노드 만있을 수 있습니까?
Erwin Brandstetter

답변:


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 ;

업데이트를 들어, 단순히 마지막 교체 SELECT으로, UPDATE열팽창 계수의 결과에 합류, 테이블에 다시 :

-- 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_id와는 parent_id당신이 그들을 유지할 수 있도록, (조상, 부모 아웃을 이유를 파악하기 어려운 명백한 비트입니다) 선택 목록에있을 필요하지 않습니다 SELECT당신이 원하는 경우 쿼리하지만 당신은 안전하게에서 제거 할 수 있습니다 UPDATE.
  • 그만큼 (customer_id, object_id) A에 대한 후보자 것 같다 UNIQUE제약. 데이터가이를 준수하는 경우 이러한 제한을 추가하십시오. 재귀 CTE에서 수행 된 조인은 고유하지 않은 경우에는 의미가 없습니다 (노드에는 2 개의 부모가있을 수 있음).
  • 해당 제한 조건을 추가 하면 (고유 한) 제한 조건 (customer_id, parent_id)의 후보가됩니다.FOREIGN KEYREFERENCES(customer_id, object_id) 됩니다. 설명에 따르면 새 행을 추가하고 일부 행은 아직 추가되지 않은 다른 행을 참조 할 수 있기 때문에 FK 제약 조건을 추가하고 싶지 않을 것입니다 .
  • 큰 테이블에서 수행 될 경우 쿼리 효율성에 문제가있을 수 있습니다. 어쨌든 거의 전체 테이블이 업데이트되므로 첫 실행에는 없습니다. 그러나 두 번째로, 새 행 (및 첫 번째 실행에서 건드리지 않은 행) 만 업데이트를 고려하려고합니다. CTE는 큰 결과를 가져와야합니다.
    그만큼AND o.generation = -1최종 갱신의 제 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은 이전에 업데이트 된 부모 노드의 자식 (with )을 찾습니다 .
세 번째 재귀 부분은 이전과 같이 처음 두 부분의 모든 자손을 찾습니다.

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의 재귀 부분에 인덱스 조건을 추가했습니다 (논리적으로 중복 되더라도).

또한 이미 언급 한 @ ypercube UNIQUE에 대한 제약 조건 이 있어야합니다 (object_id, customer_id). 또는 어떤 이유로 독창성을 강요 할 수없는 경우 (왜?) 대신 일반 색인을 추가하십시오. 인덱스 열의 순서는 중요합니다. btw :


1
나는 당신과 @ypercube가 제안한 색인과 제약을 추가 할 것이다. 데이터를 살펴보면 데이터가 발생하지 않은 이유를 알 수 없습니다 (때로는 parent_id가 아직 설정되지 않은 외래 키 제외). 또한 생성 열을 널 입력 가능으로 설정하고 기본 설정은 -1 대신 NULL로 설정합니다. 그런 다음 "-1"필터가 많지 않고 부분 인덱스는 생성이 NULL 인 곳 등이 될 수 있습니다.
Diggity

@ Diggity : 나머지를 적용하면 NULL이 올바르게 작동합니다.
Erwin Brandstetter

@Erwin 좋은. 나는 원래 당신과 비슷하다고 생각했습니다. 색인 ON objects (customer_id, parent_id, object_id) WHERE generation = -1;및 다른 색인 일 수 ON objects (customer_id, object_id) WHERE generation > -1;있습니다. 또한 업데이트는 업데이트 된 모든 행을 한 인덱스에서 다른 인덱스로 "전환"해야하므로 이것이 UPDATE의 초기 실행에 적합한 지 확실하지 않습니다.
ypercubeᵀᴹ

재귀 쿼리에 대한 인덱싱은 실제로 어려울 수 있습니다.
ypercubeᵀᴹ
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.