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

Adjacency List. Введение

Метод Adjacency List для хранения деревьев. Простая схема (id, pid), рекурсивные запросы CTE и хранимые процедуры для выборки структуры.

Что это такое?

Adjacency List — один из самых простых методов хранения древовидных структур данных. Узел содержит прямую связь с родителем через два поля: id и pid. Однако наследование ограничено одним уровнем — за один шаг можно вычислить только родительский узел или детей, но не более дальних родственников.

Создание таблицы

CREATE TABLE my_tree (
    id              INTEGER         NOT NULL,
    pid             INTEGER,
    field1          TEXT,
    PRIMARY KEY (id)
);

Как использовать

Выборка данных из Adjacency List считается сложной задачей. Необходимо решить четыре основные задачи:

Родительский узел и подчинённые узлы

-- Родительский узел
SELECT * FROM my_tree WHERE id = [pid];

-- Подчинённые узлы
SELECT * FROM my_tree WHERE pid = [id];

Родительская ветка

Рекурсивный запрос PostgreSQL (8.4+)

WITH RECURSIVE parents AS (
    SELECT *, ARRAY[t.id] AS exist, FALSE AS cycle
    FROM my_tree AS t
    WHERE id = [item.pid]
    UNION ALL
        SELECT t.*, exist || t.id, t.id = ANY(exist)
        FROM parents AS p, my_tree AS t
        WHERE t.id = p.pid AND NOT cycle
)
SELECT * FROM parents WHERE NOT cycle LIMIT [max_deep];

Поле exist содержит массив уже полученных ID для предотвращения бесконечной рекурсии.

Хранимая процедура PostgreSQL

CREATE OR REPLACE FUNCTION "public"."get_parents" (
    pid_in      INTEGER,
    deep        INTEGER      DEFAULT NULL
) RETURNS SETOF "public"."my_tree" AS
$body$
DECLARE
    exist_ids   INTEGER[]   := ARRAY[0];
    pid_now     INTEGER     := pid_in;
    deep_now    INTEGER     := deep;
    item        my_tree;
BEGIN
    WHILE
        pid_now IS NOT NULL AND
        pid_now > 0 AND
        pid_now <> ALL (exist_ids) AND
        (deep_now IS NULL OR deep_now > 0)
    LOOP
        SELECT * INTO item
            FROM my_tree
            WHERE id = pid_now;
        IF item.id IS NULL THEN
            EXIT;
        END IF;
        RETURN NEXT item;
        pid_now := item.pid;
        exist_ids := exist_ids || item.id;
        IF deep_now IS NOT NULL THEN
            deep_now := deep_now - 1;
        END IF;
    END LOOP;
    RETURN;
END;
$body$
LANGUAGE 'plpgsql';

Подчинённые ветви

Основное отличие — подчинённых узлов может быть более одного на каждом уровне. Простые JOIN-запросы неэффективны.

Рекурсивный запрос PostgreSQL

WITH RECURSIVE children AS (
    SELECT *,
           pid || '|' || [order_field]          AS ord,
           ARRAY[id]                            AS exist,
           FALSE                                AS cycle
    FROM my_tree
    WHERE pid = [item.id]
    UNION ALL
        SELECT t.*,
               ord || '.' || t.pid || '|' || t.[order_field],
               exist || t.id,
               t.id = ANY(exist)
        FROM children AS c,
             my_tree  AS t
        WHERE t.pid = c.id AND
              NOT cycle AND
              array_length(exist, 1) < [max_deep]
)
SELECT * FROM children WHERE NOT cycle ORDER BY ord LIMIT [limit];

Оптимизированный циклевой алгоритм

Для избежания множественных запросов используется выборка списком для каждого уровня:

SELECT children.*
FROM my_tree AS children
    JOIN my_tree AS parents
        ON parents.id = children.pid
WHERE children.pid = ANY([parents_array])
ORDER BY parents.[order_field], children.[order_field];

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

to@prototypes.ventures