PostgreSQL
 sql >> Datenbank >  >> RDS >> PostgreSQL

Wie erstelle ich eine Baumtabelle ohne zyklische Beziehung?

Die wahrscheinlich einfachste und gebräuchlichste SQL-Implementierung eines Baums ist eine selbstreferenzierende Tabelle, z. B.:

create table tree(
    id int primary key, 
    parent int references tree(id));

insert into tree values
    (1, null),
    (2, 1),
    (3, 1),
    (4, 2),
    (5, 4);

Sie können den Baum mit einer rekursiven Abfrage wie dieser von oben nach unten durchlaufen:

with recursive top_down as (
    select id, parent, array[id] as path
    from tree
    where parent is null
union all
    select t.id, t.parent, path || t.id
    from tree t
    join top_down r on t.parent = r.id
)
select *
from top_down;

 id | parent |   path    
----+--------+-----------
  1 |        | {1}
  2 |      1 | {1,2}
  3 |      1 | {1,3}
  4 |      2 | {1,2,4}
  5 |      4 | {1,2,4,5}
(5 rows)

Siehe auch diese Antwort für ein Bottom-up-Beispiel.

Integrität

Sie können keinen Knoten entfernen, der einem anderen übergeordnet ist. Der Fremdschlüssel verhindert, dass der Baum in einzelne Teile geteilt wird:

delete from tree
where id = 2;

ERROR:  update or delete on table "tree" violates foreign key constraint "tree_parent_fkey" on table "tree"
DETAIL:  Key (id)=(2) is still referenced from table "tree".    

Optional können Sie sicherstellen, dass der Baum nur eine Wurzel hat, indem Sie einen partiellen eindeutigen Index verwenden:

create unique index tree_one_root_idx on tree ((parent is null)) where parent is null;

insert into tree
values(6, null);

ERROR:  duplicate key value violates unique constraint "tree_one_root_idx"
DETAIL:  Key ((parent IS NULL))=(t) already exists. 

Zyklen

Sie können die Möglichkeit der Eingabe von Zyklen mit einem Trigger ausschließen. Die Funktion prüft, ob einer der Vorfahren eines eingefügten oder aktualisierten Knotens der Knoten selbst sein könnte:

create or replace function before_insert_or_update_on_tree()
returns trigger language plpgsql as $$
declare rec record;
begin
    if exists(
        with recursive bottom_up as (
            select new.id, new.parent, array[]::int[] as path, false as cycle
        union all
            select r.id, t.parent, path || t.id, new.id = any(path)
            from tree t
            join bottom_up r on r.parent = t.id and not cycle
        )
        select *
        from bottom_up
        where cycle or (id = parent))
    then raise exception 'Cycle detected on node %.', new.id;
    end if;
    return new;
end $$;

create trigger before_insert_or_update_on_tree
before insert or update on tree
for each row execute procedure before_insert_or_update_on_tree();

Überprüfen Sie:

insert into tree values (6, 7), (7, 6);

ERROR:  Cycle detected on node 7.

update tree
set parent = 4
where id = 2;

ERROR:  Cycle detected on node 2.