Esta dica tentará responder às seguintes perguntas:
- Como podemos representar uma árvore de dados no postgres
- Como podemos consultar com eficiência qualquer nó único inteiro e todos os seus filhos (e filhos dos filhos).
Os dados de teste
Como queremos manter isso simples, assumiremos que nossos dados são apenas um monte de sections
. Uma seção tem apenas um name
e cada section
uma tem um único pai section
.
Section A
|--- Section A.1
Section B
|--- Section B.1
|--- Section B.1
|--- Section B.1.1
Usaremos esses dados simples para os exemplos abaixo.
Auto-referência simples
Ao projetar uma tabela autorreferencial (algo que se junta a si mesmo), a escolha mais óbvia é ter algum tipo de parent_id
coluna em nossa tabela que faça referência a si mesmo.
CREATE TABLE section (
id INTEGER PRIMARY KEY,
name TEXT,
parent_id INTEGER REFERENCES section,
);
ALTER TABLE page ADD COLUMN parent_id INTEGER REFERENCES page;
CREATE INDEX section_parent_id_idx ON section (parent_id);
Agora insira nossos dados de exemplo, usando o parent_id
para relacionar os nós:
INSERT INTO section (id, name, parent_id) VALUES (1, 'Section A', NULL);
INSERT INTO section (id, name, parent_id) VALUES (2, 'Section A.1', 1);
INSERT INTO section (id, name, parent_id) VALUES (3, 'Section B', NULL);
INSERT INTO section (id, name, parent_id) VALUES (4, 'Section B.1', 3);
INSERT INTO section (id, name, parent_id) VALUES (5, 'Section B.2', 3);
INSERT INTO section (id, name, parent_id) VALUES (6, 'Section B.2.1', 5);
Isso funciona muito bem para consultas simples, como buscar os filhos diretos da Seção B :
SELECT * FROM section WHERE parent = 3
mas exigirá consultas complexas ou recursivas para questões como buscar todos os filhos e filhos dos filhos da Seção B :
WITH RECURSIVE nodes(id,name,parent_id) AS (
SELECT s1.id, s1.name, s1.parent_id
FROM section s1 WHERE parent_id = 3
UNION
SELECT s2.id, s2.name, s2.parent_id
FROM section s2, nodes s1 WHERE s2.parent_id = s1.id
)
SELECT * FROM nodes;
Portanto, respondemos a parte “como construir uma árvore” da pergunta, mas não estamos satisfeitos com a parte “como consultar um nó e todos os seus filhos”.
Digite ltree. (Abreviação de “árvore de rótulo” – eu acho?).
A extensão ltree
A extensão ltree é uma ótima opção para consultar dados hierárquicos. Isso é especialmente verdadeiro para relacionamentos autorreferenciais.
Vamos reconstruir o exemplo acima usando ltree. Usaremos as chaves primárias da página como os “rótulos” em nossos caminhos de ltree e um rótulo especial de “raiz” para denotar o topo da árvore.
CREATE EXTENSION ltree;
CREATE TABLE section (
id INTEGER PRIMARY KEY,
name TEXT,
parent_path LTREE
);
CREATE INDEX section_parent_path_idx ON section USING GIST (parent_path);
Adicionaremos nossos dados novamente, desta vez em vez de usar o id
para o pai, construiremos um caminho de árvore que representa o nó pai.
INSERT INTO section (id, name, parent_path) VALUES (1, 'Section 1', 'root');
INSERT INTO section (id, name, parent_path) VALUES (2, 'Section 1.1', 'root.1');
INSERT INTO section (id, name, parent_path) VALUES (3, 'Section 2', 'root');
INSERT INTO section (id, name, parent_path) VALUES (4, 'Section 2.1', 'root.3');
INSERT INTO section (id, name, parent_path) VALUES (4, 'Section 2.2', 'root.3');
INSERT INTO section (id, name, parent_path) VALUES (5, 'Section 2.2.1', 'root.3.4');
Legal. Portanto, agora podemos fazer uso dos operadores ltree @>
e <@
responder à nossa pergunta original como:
SELECT * FROM section WHERE parent_path <@ 'root.3';
No entanto, introduzimos alguns problemas.
- Nossa
parent_id
versão simples garantiu consistência referencial fazendo uso daREFERENCES
restrição. Perdemos isso ao mudar para os caminhos da ltree. - Garantir que os caminhos da ltree sejam válidos pode ser um pouco doloroso e, se os caminhos se tornarem obsoletos por algum motivo, suas consultas podem retornar resultados inesperados ou você pode “órfãos” de nós.
A solução final
Para corrigir esses problemas, queremos um híbrido de nosso original parent_id
(para a consistência referencial e simplicidade do relacionamento filho / pai) e nossos caminhos ltree (para poder de consulta / indexação aprimorado). Para conseguir isso, vamos ocultar o gerenciamento do caminho ltree atrás de um gatilho e apenas atualizar a parent_id
coluna.
CREATE EXTENSION ltree;
CREATE TABLE section (
id INTEGER PRIMARY KEY,
name TEXT,
parent_id INTEGER REFERENCES section,
parent_path LTREE
);
CREATE INDEX section_parent_path_idx ON section USING GIST (parent_path);
CREATE INDEX section_parent_id_idx ON section (parent_id);
CREATE OR REPLACE FUNCTION update_section_parent_path() RETURNS TRIGGER AS $$
DECLARE
path ltree;
BEGIN
IF NEW.parent_id IS NULL THEN
NEW.parent_path = 'root'::ltree;
ELSEIF TG_OP = 'INSERT' OR OLD.parent_id IS NULL OR OLD.parent_id != NEW.parent_id THEN
SELECT parent_path || id::text FROM section WHERE id = NEW.parent_id INTO path;
IF path IS NULL THEN
RAISE EXCEPTION 'Invalid parent_id %', NEW.parent_id;
END IF;
NEW.parent_path = path;
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER parent_path_tgr
BEFORE INSERT OR UPDATE ON section
FOR EACH ROW EXECUTE PROCEDURE update_section_parent_path();
Muito melhor.
Mais
Use json e plv8 para trabalhar com dados de árvore