Я использую 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. иллюстрация этого:
Корневые узлы, заштрихованные красным, входят в начальный список узлов, подлежащих удалению. Узлы с красными границами и буквами - это узлы, которые удаляются в результате удаления всех необходимых для них узлов.
Я довольно близко подошел к этому запросу:
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
в качестве оконной функции, но я еще не взломал это, и я Я надеюсь, что сообщество сможет придумать что-то более простое / элегантное.)
Да, обновлю описание, спасибо.
Затем вычтите два рекурсивных CTE: один с полным деревом из листовых узлов, которых нет в вашем списке, за вычетом полного дерева с листовыми узлами в вашем запросе. Сделал бы это за вас, если бы мне не нужно было бежать за 2 минуты. Завтра проверим, нужно ли вам решение.
@TheImpaler Я поищу это завтра, так как не сразу понимаю, что вы имеете в виду. TIA!
демонстрация: 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
Хотя я показал решение с одним рекурсивным запросом. Но я не уверен, хорошо ли он работает с огромными и сложными структурами данных.
Я бы попробовал создать простую функцию (скетч) вторым способом:
Это работает! Думаю, меня беспокоит то же, что и у вас: я ожидаю, что PostgreSQL предварительно вычислит CTE dependents
для таблицы весьtree
, что может быть неэффективным, если tree
вырастет до большого количества строк. В этом случае было бы хорошо, если бы PostgreSQL [9.5] мог оптимизировать между CTE, чтобы он мог знать, что ему требуется только (потенциально) небольшое подмножество CTE dependents
.
Проще говоря, вы можете использовать два 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
?
Вот возможность:
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
Ваши начальные узлы (A, D, E, I) всегда листовые узлы?