PostgreSQL Рекурсивная Глубина Потомка

15

Мне нужно вычислить глубину потомка от его предка. Когда запись имеет object_id = parent_id = ancestor_id, она считается корневым узлом (предком). Я пытался запустить WITH RECURSIVEзапрос с PostgreSQL 9.4 .

Я не контролирую данные или столбцы. Схема данных и таблиц поступает из внешнего источника. Стол постоянно растет . Прямо сейчас около 30 тысяч записей в день. Любой узел в дереве может отсутствовать, и в какой-то момент он будет извлечен из внешнего источника. Обычно они обрабатываются по 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возможно еще не вытащили. Если 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 .

Diggity
источник
Таким образом, вы хотите, чтобы 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. Я хотел бы установить столбец генерации на любую глубину. Еще одно замечание: идентификатор object_id не является уникальным, поскольку customer_id 1 может иметь object_id 1, а customer_id 2 может иметь object_id 1. Основной идентификатор таблицы уникален.
Это единовременное обновление или вы постоянно добавляете в растущую таблицу? Похоже, последний случай. Делает большую разницу. И могут ли отсутствовать только корневые узлы (пока что) или какой-либо узел в дереве?
Эрвин Брандштеттер

Ответы:

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присоединяя результат 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_idи parent_idне нужно , чтобы быть в списке выбора (предок очевидно, родитель немного сложно понять, почему), так что вы можете держать их вSELECT запросе , если вы хотите , но вы можете удалить их из UPDATE.
  • (customer_id, object_id)кажется кандидатом наUNIQUE ограничение. Если ваши данные соответствуют этому, добавьте такое ограничение. Объединения, выполняемые в рекурсивном CTE, не имели бы смысла, если бы он не был уникальным (в противном случае узел мог бы иметь 2 родителей).
  • если вы добавите это ограничение, (customer_id, parent_id)то кандидатом на FOREIGN KEYограничение будет REFERENCES(уникальное) (customer_id, object_id). Вы, скорее всего, нет хотите добавлять это ограничение FK, поскольку по вашему описанию вы добавляете новые строки, и некоторые строки могут ссылаться на другие, которые еще не были добавлены.
  • Конечно, есть проблемы с эффективностью запроса, если он будет выполнен в большой таблице. Не в первом запуске, так как почти вся таблица будет обновлена ​​в любом случае. Но во второй раз вам нужно будет рассмотреть возможность обновления только новых строк (и тех, которые не были затронуты при первом запуске). CTE как таковой должен будет принести большой результат.
    В AND o.generation = -1окончательном обновлении убедитесь, что строки, которые были обновлены при первом запуске, не будут обновлены снова, но CTE по-прежнему является дорогостоящей частью.

Следующее является попыткой решения этих проблем: улучшите CTE, чтобы рассмотреть как можно меньше строк, и используйте (customer_id, obejct_id)вместо того, (id)чтобы идентифицировать строки (поэтому idон полностью удаляется из запроса. Он может использоваться как 1-е обновление или последующее:

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поэтому они должны быть вновь добавленными узлами. 2-я часть находит потомков (с generation=-1) родительских узлов, которые были ранее обновлены.
Третья, рекурсивная часть, как и прежде, находит всех потомков первых двух частей.

Протестировано на SQLfiddle-2

ypercubeᵀᴹ
источник
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 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 и значение по умолчанию будет NULL вместо -1. Тогда у меня не будет много фильтров «-1», и частичные индексы могут быть такими, ГДЕ поколение НЕДОСТАТОЧНО и т. Д.
Diggity
@Diggity: NULL должен работать просто отлично, если вы адаптируете остальные, да.
Эрвин Брандштеттер
@ Эрвин, милый. Я изначально думал, что похож на тебя. Индекс ON objects (customer_id, parent_id, object_id) WHERE generation = -1;и, возможно, другое ON objects (customer_id, object_id) WHERE generation > -1;. Обновление также должно будет «переключать» все обновленные строки с одного индекса на другой, поэтому не уверен, что это хорошая идея для первоначального запуска ОБНОВЛЕНИЯ.
ypercubeᵀᴹ
Индексирование для рекурсивных запросов может быть очень сложным.
ypercubeᵀᴹ