Мне нужно вычислить глубину потомка от его предка. Когда запись имеет 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 .
источник
update
таблица с результатом вашего рекурсивного CTE?ancestor_id
он уже установлен, поэтому вам нужно только назначить генерацию из CTE.depth?Ответы:
Ваш запрос в основном правильный. Единственная ошибка во второй (рекурсивной) части CTE, где у вас есть:
Должно быть наоборот:
Вы хотите объединить объекты со своими родителями (которые уже были найдены).
Таким образом, запрос, который вычисляет глубину, может быть записан (ничего не изменилось, только форматирование):
Для обновления вы просто заменяете последний
SELECT
,UPDATE
присоединяя результат cte к таблице:Проверено на 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, поскольку по вашему описанию вы добавляете новые строки, и некоторые строки могут ссылаться на другие, которые еще не были добавлены.В
AND o.generation = -1
окончательном обновлении убедитесь, что строки, которые были обновлены при первом запуске, не будут обновлены снова, но CTE по-прежнему является дорогостоящей частью.Следующее является попыткой решения этих проблем: улучшите CTE, чтобы рассмотреть как можно меньше строк, и используйте
(customer_id, obejct_id)
вместо того,(id)
чтобы идентифицировать строки (поэтомуid
он полностью удаляется из запроса. Он может использоваться как 1-е обновление или последующее:Обратите внимание, что CTE состоит из 3 частей. Первые два являются стабильными частями. Первая часть находит корневые узлы, которые не были обновлены ранее и до сих пор,
generation=-1
поэтому они должны быть вновь добавленными узлами. 2-я часть находит потомков (сgeneration=-1
) родительских узлов, которые были ранее обновлены.Третья, рекурсивная часть, как и прежде, находит всех потомков первых двух частей.
Протестировано на SQLfiddle-2
источник
@ypercube уже дает достаточно объяснений, так что я перейду к тому, что мне нужно добавить.
Я предполагаю, что это должно применяться рекурсивно, то есть остальная часть дерева всегда имеет
generation = -1
после любого отсутствующего узла.Если какой-либо узел в дереве может отсутствовать (пока), нам нужно найти строки с
generation = -1
этим ...... являются корневыми узлами
... или иметь родительский элемент с
generation > -1
.И пройти через дерево оттуда. Дочерние узлы этого выбора также должны иметь
generation = -1
.Возьмите
generation
родительское значение, увеличенное на единицу, или отступите к 0 для корневых узлов:Нерекурсивная часть в
SELECT
этом смысле является синглом , но логически эквивалентна двум объединенным @ ypercubeSELECT
. Не уверен, что быстрее, вам придется проверить.Гораздо более важный момент для производительности:
Показатель!
Если вы неоднократно добавляете строки в большую таблицу таким образом, добавьте частичный индекс :
Это позволит достичь большего по производительности, чем все другие улучшения, о которых говорилось ранее, - при многократных небольших добавлениях в большую таблицу.
Я добавил условие индекса в рекурсивную часть CTE (хотя и логически избыточно), чтобы помочь планировщику запросов понять, что частичный индекс применим.
Кроме того, у вас, вероятно, также должно быть
UNIQUE
ограничение на(object_id, customer_id)
тот @ypercube, который уже упоминался. Или, если вы не можете навязать уникальность по какой-то причине (почему?), Вместо этого добавьте простой индекс. Порядок столбцов индекса имеет значение, кстати:источник
ON objects (customer_id, parent_id, object_id) WHERE generation = -1;
и, возможно, другоеON objects (customer_id, object_id) WHERE generation > -1;
. Обновление также должно будет «переключать» все обновленные строки с одного индекса на другой, поэтому не уверен, что это хорошая идея для первоначального запуска ОБНОВЛЕНИЯ.