AdTech AI PostgreSQL О компании
PostgreSQL · Деревья 14 февраля, 2025 Сергей Томулевич

Управление деревьями Adjacency List в PostgreSQL

Управление деревьями Adjacency List в PostgreSQL: внешний ключ, триггеры на циклические зависимости, денормализация уровней и счётчиков, варианты удаления.

Введение

Управление древовидными структурами данных (Adjacency List) на уровне PostgreSQL. Реализация на MySQL значительно сложнее или невозможна в некоторых случаях.

Правила

При работе с Adjacency List необходимо соблюдать два основных правила:

Решения

Внешний ключ

ALTER TABLE "public"."my_tree"
    ADD CONSTRAINT "my_tree_fk" FOREIGN KEY ("pid")
        REFERENCES "public"."my_tree"("id")
        MATCH FULL
        ON DELETE CASCADE
        NOT DEFERRABLE;

Триггер на проверку циклических зависимостей

CREATE OR REPLACE FUNCTION "public"."my_tree_update_trigger" ()
    RETURNS TRIGGER AS
$body$
DECLARE
    tmp_id INTEGER;
BEGIN
    IF NEW.pid IS NOT NULL AND (NEW.pid <> OLD.pid OR OLD.pid IS NULL) THEN
        WITH RECURSIVE parents AS (
            SELECT t.id, t.pid, ARRAY[t.id] AS exist, FALSE AS cycle
            FROM my_tree AS t
            WHERE id = NEW.pid
            UNION ALL
                SELECT t.id, t.pid, exist || t.id, t.id = ANY(exist)
                FROM parents AS p, my_tree AS t
                WHERE t.id = p.pid AND NOT p.cycle
        )
        SELECT id INTO tmp_id FROM parents
        WHERE id = NEW.id AND NOT cycle LIMIT 1;

        IF tmp_id IS NOT NULL THEN
            RAISE NOTICE 'Нельзя ставить потомком родителя!';
            NEW.pid := OLD.pid;
        END IF;
    END IF;
    RETURN NEW;
END;
$body$
LANGUAGE 'plpgsql';

Денормализация

Для оптимизации часто запрашиваемых параметров добавляются денормализованные поля:

Триггер на вставку

CREATE OR REPLACE FUNCTION "public"."my_tree_insert_trigger" ()
    RETURNS TRIGGER AS
$body$
DECLARE
    parent my_tree;
BEGIN
    NEW.counter := 0;
    NEW.level := 0;

    IF NEW.pid IS NOT NULL OR NEW.pid > 0 THEN
        UPDATE my_tree
            SET counter = CASE WHEN counter IS NULL
                               THEN 1
                               ELSE counter + 1
                          END
            WHERE id = NEW.pid
            RETURNING * INTO parent;

        IF parent.id IS NULL THEN
            RAISE NOTICE 'ОШИБКА! Родителя с таким ID нет (%)';
            NEW.pid := 0;
        ELSE
            NEW.level := CASE WHEN parent.level IS NULL OR parent.level = 0
                              THEN 1
                              ELSE parent.level + 1
                         END;
        END IF;
    END IF;
    RETURN NEW;
END;
$body$
LANGUAGE 'plpgsql';

Триггер на удаление

Для управления удалением используется пользовательская переменная user_vars.tree_delete с тремя возможными значениями:

Конфигурация PostgreSQL

custom_variable_classes = 'user_vars'
user_vars.tree_delete = cascade

Использование

С явной транзакцией:

BEGIN;
SELECT set_config('user_vars.tree_delete', 'levelup', TRUE);
DELETE FROM my_tree WHERE id = [item.id];
COMMIT;

Без явной транзакции:

DELETE FROM my_tree
    USING (
        SELECT set_config('user_vars.tree_delete', 'root', TRUE)
        ) AS conf
    WHERE id = [item.id];

Готовы посчитать собственный стек? Расскажем, какие модули и какая нагрузка нужны под вашу задачу.

to@prototypes.ventures