Рекурсивное удаление узлов из дерева в SQL одним запросом

Я использую PostgreSQL 9.5, и у меня есть таблица, представляющая дерево:

CREATE TABLE tree (
    dependent CHAR(1) NOT NULL,
    prereq CHAR(1) NOT NULL,
    PRIMARY KEY (dependent, prereq),
    CHECK (dependent != prereq)
);

INSERT INTO tree VALUES
    ('B', 'A'),
    ('C', 'B'),
    ('F', 'D'),
    ('F', 'E'),
    ('G', 'E'),
    ('H', 'F'),
    ('H', 'G'),
    ('J', 'I'),
    ('K', 'I'),
    ('K', 'L'),
    ('N', 'J'),
    ('N', 'M'),
    ('P', 'O'),
    ('Q', 'P');

Каждая строка в tree определяет границу между узлом dependent, которая зависит от необходимого узла (prereq). Когда все предпосылки зависимого удалены, иждивенец перестает существовать. (Чтобы было ясно, циклы не разрешены.) Я буду называть любой узел без каких-либо предварительных условий, только зависимые, как корневой узел.

Я ищу один SQL-запрос, который, учитывая список корневых узлов, подлежащих удалению, даст полный набор узлов, которые будут удалены из дерева. Я буду удалять только корневые узлы. Например, если бы мне пришлось удалить корневые узлы A, D, E и I, полный набор удаляемых узлов будет A, B, C, D, E, F, G, H, I и J. иллюстрация этого:

Рекурсивное удаление узлов из дерева в SQL одним запросом

Корневые узлы, заштрихованные красным, входят в начальный список узлов, подлежащих удалению. Узлы с красными границами и буквами - это узлы, которые удаляются в результате удаления всех необходимых для них узлов.

Я довольно близко подошел к этому запросу:

WITH RECURSIVE deletion AS (
    SELECT
        tree.*
    FROM
        tree
    WHERE
        prereq IN ('A', 'D', 'E', 'I')
    UNION
    SELECT
        tree.*
    FROM
        deletion
        JOIN tree ON tree.prereq = deletion.dependent
)
SELECT prereq FROM deletion
UNION
SELECT dependent FROM deletion
ORDER BY 1

Однако здесь указано слишком много узлов для удаления:

 prereq 
--------
 A
 B
 C
 D
 E
 F
 G
 H
 I
 J
 K
 N
(12 rows)

K и N не должны быть в списке, поскольку у них обоих есть обязательные узлы, которые не будут удалены, L и M соответственно.

Какой единый запрос SQL я могу использовать в PostgreSQL 9.5, чтобы получить полный список узлов, которые будут удалены, учитывая начальный набор корневых узлов, которые нужно удалить?

Как бы то ни было, моя настоящая таблица tree содержит около 100 000 строк.

(У меня есть несколько идей, которые я еще не смог реализовать, например, использовать пару вложенных анти-объединений или [ab] каким-то образом использовать COUNT в качестве оконной функции, но я еще не взломал это, и я Я надеюсь, что сообщество сможет придумать что-то более простое / элегантное.)

Ваши начальные узлы (A, D, E, I) всегда листовые узлы?

The Impaler 26.10.2018 23:32

Да, обновлю описание, спасибо.

Dale 26.10.2018 23:33

Затем вычтите два рекурсивных CTE: один с полным деревом из листовых узлов, которых нет в вашем списке, за вычетом полного дерева с листовыми узлами в вашем запросе. Сделал бы это за вас, если бы мне не нужно было бежать за 2 минуты. Завтра проверим, нужно ли вам решение.

The Impaler 26.10.2018 23:34

@TheImpaler Я поищу это завтра, так как не сразу понимаю, что вы имеете в виду. TIA!

Dale 26.10.2018 23:40
ReactJs | Supabase | Добавление данных в базу данных
ReactJs | Supabase | Добавление данных в базу данных
Это и есть ваш редактор таблиц в supabase.👇
Понимание Python и переход к SQL
Понимание Python и переход к SQL
Перед нами лабораторная работа по BloodOath:
3
4
462
3

Ответы 3

демонстрация: db <> рабочий пример

WITH RECURSIVE dependents AS (                          -- 1.
    SELECT
        dependent,
        array_agg(prereq) as prereqs
    FROM 
        tree
    GROUP BY dependent
), deletions AS (
    SELECT array_cat(ARRAY['A', 'D', 'E', 'I'], array_agg(dependent))             -- 3.
    FROM dependents
    WHERE prereqs <@ ARRAY['A', 'D', 'E', 'I']          -- 2.

    UNION

    SELECT DISTINCT array_cat(del.array_cat, array_agg(dep.dependent) OVER ())
    FROM dependents dep
    JOIN deletions del
    ON NOT(dep.dependent = ANY(del.array_cat)) AND dep.prereqs <@ del.array_cat   -- 4.
)

SELECT * FROM deletions
  1. Получите все прямые требования каждого иждивенца
  2. Проверьте, есть ли массив предварительных требований, который полностью вписывается в ваш массив удаления.
  3. объединить все зависимые от этих результатов и исходный массив в один как новый массив для удаленных узлов.
  4. рекурсивная часть: снова: проверьте, есть ли какие-либо иждивенцы (новые, еще не в списке), чей массив prereqs вписывается в расширенные узлы удаления, и добавьте их в список.

Хотя я показал решение с одним рекурсивным запросом. Но я не уверен, хорошо ли он работает с огромными и сложными структурами данных.

Я бы попробовал создать простую функцию (скетч) вторым способом:

  1. Найдите все элементы, которые являются только листами без предварительных требований. Удалите их.
  2. Найдите всех иждивенцев без предварительных детей. Удалите их.
  3. Повторяйте (2), пока не останется элемент с пустыми предварительными требованиями.

Это работает! Думаю, меня беспокоит то же, что и у вас: я ожидаю, что PostgreSQL предварительно вычислит CTE dependents для таблицы весьtree, что может быть неэффективным, если tree вырастет до большого количества строк. В этом случае было бы хорошо, если бы PostgreSQL [9.5] мог оптимизировать между CTE, чтобы он мог знать, что ему требуется только (потенциально) небольшое подмножество CTE dependents.

Dale 28.10.2018 00:55

Проще говоря, вы можете использовать два CTE (общие табличные выражения) для идентификации:

  • «Узлы-кандидаты»: это все узлы, относящиеся к вашим корневым узлам, которые потенциально могут быть удалены.
  • «Защищенные узлы»: это все узлы, которые все еще находятся в игре, которые связаны с корневыми узлами Другие, и их не следует удалять.

Как только вы получите оба набора, желаемый результат - это узлы-кандидаты, защищенные узлами не. Запрос мог бы выглядеть так:

with recursive cand as ( -- get the candidates nodes
  select distinct prereq as root, prereq as node, null as prereq
    from tree where prereq in ('A', 'D', 'E', 'I')
  union all
  select cand.root, t.dependent, t.prereq
    from cand
    join tree t on t.prereq = cand.node
),
prot as ( -- get the protected nodes
select distinct prereq as root, prereq as node, null as prereq
  from tree
  where prereq not in (select dependent from tree) 
    and prereq not in ('A', 'D', 'E', 'I')
  union all
  select prot.root, t.dependent, t.prereq
    from prot
    join tree t on t.prereq = prot.node
)
select distinct node -- choose candidates that are not protected
  from cand 
  where node not in (select node from prot)
  order by node

Результат:

node  
----
A
B
C
D
E
F
G
H
I
J

Теперь, когда я снова это вижу, я понимаю, что для узлов-кандидатов вы можете использовать полную таблицу вместо дерева. Если хотите, можете упростить первую часть этого запроса.

Это работает! Очень хорошо. Поскольку CTE являются границами оптимизации в PostgreSQL, я обеспокоен тем, что prot может оказаться очень большим, если tree будет иметь много строк, и особенно в случае, когда дерево относительно неглубоко (т.е. много листьев) и набор узлов для delete маленький, как в примере. Есть ли способ ограничить prot рассмотрением только тех узлов, которые могут существовать в cand?

Dale 28.10.2018 00:50

Вот возможность:

WITH RECURSIVE
    candidate AS (
        -- All edges for initial nodes to delete.
        SELECT
            tree.dependent,
            tree.prereq
        FROM
            tree
        WHERE
            tree.prereq IN ('A', 'D', 'E', 'I')
        UNION ALL
        -- Iteratively add any edges where the prereq is already in
        -- the candidate deletion set.
        SELECT
            tree.dependent,
            tree.prereq
        FROM
            tree
            JOIN candidate ON
                candidate.dependent = tree.prereq
    ),
    survivor AS (
        -- Find all leaf nodes from the candidate set which can
        -- survive because they have at least one prerequisite node
        -- that is *not* in the candidate set.
        SELECT
            candidate1.dependent AS node
        FROM
            candidate AS candidate1
            JOIN tree
                ON candidate1.dependent = tree.dependent
                AND candidate1.prereq != tree.prereq
        WHERE
            NOT EXISTS (
                SELECT 1 FROM
                    candidate AS candidate2
                WHERE
                    candidate2.prereq = tree.prereq
            )
        UNION ALL
        -- Iteratively add any nodes from the candidate set which are
        -- dependent upon a node we've already identified as a
        -- survivor.
        SELECT
            candidate.dependent
        FROM
            candidate
            JOIN survivor ON survivor.node = candidate.prereq
    )
(
    -- The dependent column contains all nodes to delete except the
    -- initial list of nodes to delete (see below).
    SELECT dependent FROM candidate
    EXCEPT
    SELECT node FROM survivor
)
UNION ALL
-- Add in the initial set of nodes to delete.
SELECT * FROM (VALUES ('A'), ('D'), ('E'), ('I')) AS t
ORDER BY 1;

candidate CTE создает подмножество строк из tree, которые могут быть удалены. candidate.dependent становится списком узлов-кандидатов на удаление. survivor затем создается путем поиска узлов, названных в candidate.dependent, у которых есть хотя бы одно ребро к узлу, который будет удален нет, а затем итеративно («рекурсивно») присваивая имена все большему количеству узлов из candidate.dependent, которые не будут удалены на основе выжившие узлы, ранее идентифицированные в CTE.

Странно выглядящий UNION ALL SELECT ... VALUES ... для включения начального списка узлов в выходные данные этого запроса используется вместо (SELECT dependent FROM candidate UNION ALL SELECT prereq FROM candidate), последний из которых, казалось, был заметно (но, возможно, не значительно) медленнее.


Обновлено: вот упрощенная версия вышеизложенного. К сожалению, я думаю, что он работает немного медленнее, но я также думаю, что его немного легче читать.

WITH RECURSIVE
    candidate AS (
        -- All initial nodes to delete.
        SELECT
            *
        FROM
            (VALUES ('A'), ('D'), ('E'), ('I')) AS t (node)
        UNION
        -- Iteratively add any nodes where the prereq is already in
        -- the candidate deletion set.
        SELECT
            tree.dependent
        FROM
            tree
            JOIN candidate ON
                candidate.node = tree.prereq
    ),
    survivor AS (
        -- Find all nodes from the candidate set which can
        -- survive because they have at least one prerequisite node
        -- that is *not* in the candidate set.
        SELECT
            c1.node
        FROM
            candidate AS c1
            JOIN tree
                ON c1.node = tree.dependent
            LEFT JOIN candidate AS c2 ON c2.node = tree.prereq
        WHERE
            c2.node IS NULL
        UNION
        -- Iteratively add any nodes from the candidate set which are
        -- dependent upon a node we've already identified as a
        -- survivor.
        SELECT
            candidate.node
        FROM
            candidate
            JOIN tree ON candidate.node = tree.dependent
            JOIN survivor ON survivor.node = tree.prereq
    )
SELECT node FROM candidate
EXCEPT ALL
SELECT node FROM survivor
ORDER BY 1

Другие вопросы по теме