Modelar organogramas no PostgreSQL: listas de adjacência vs tabela de fechamento
Modele organogramas no PostgreSQL comparando listas de adjacência e tabelas de fechamento, com exemplos claros de filtragem, relatórios e checagens de permissão.

O que um organograma precisa suportar
Um organograma é um mapa de quem reporta a quem e como times se agregam em departamentos. Ao modelar organogramas no PostgreSQL, você não está apenas salvando um manager_id em cada pessoa. Você está suportando trabalho real: navegação pelo organograma, relatórios e regras de acesso.
A maioria dos usuários espera três coisas instantâneas: explorar a organização, encontrar pessoas e filtrar resultados para “minha área”. Também esperam que as atualizações sejam seguras. Quando um gerente muda, o organograma deve atualizar em todos os lugares sem quebrar relatórios ou permissões.
Na prática, um bom modelo precisa responder algumas perguntas recorrentes:
- Qual é a cadeia de comando desta pessoa (até o topo)?
- Quem está sob este gerente (relatórios diretos e toda a subárvore)?
- Como as pessoas se agrupam em times e departamentos para dashboards?
- Como reorganizações acontecem sem falhas?
- Quem pode ver o quê, com base na estrutura organizacional?
Fica mais difícil do que uma árvore simples porque organizações mudam com frequência. Times mudam de departamento, gerentes trocam grupos e algumas vistas não são puramente “pessoas reportam a pessoas”. Por exemplo: uma pessoa pertence a um time, e times pertencem a departamentos. Permissões adicionam outra camada: a forma da organização vira parte do seu modelo de segurança, não apenas um diagrama.
Alguns termos ajudam a manter o design claro:
- Um nó é um item na hierarquia (uma pessoa, um time ou um departamento).
- Um pai é o nó diretamente acima (um gerente, ou um departamento que possui um time).
- Um ancestral é qualquer nó acima a qualquer distância (o gerente do seu gerente).
- Um descendente é qualquer nó abaixo a qualquer distância (todas as pessoas sob você).
Exemplo: se Sales for movido sob um novo VP, duas coisas devem permanecer verdadeiras imediatamente. Dashboards ainda filtram “todo o Sales”, e as permissões do novo VP cobrem Sales automaticamente.
Decisões a tomar antes de escolher um design de tabela
Antes de definir um esquema, esclareça o que seu app precisa responder todo dia. “Quem reporta a quem?” é só o começo. Muitos organogramas também precisam mostrar quem lidera um departamento, quem aprova folgas para um time e quem pode ver um relatório.
Anote as perguntas exatas que suas telas e checagens de permissão farão. Se você não souber nomear as perguntas, acabará com um esquema que parece certo, mas é difícil de consultar.
As decisões que moldam tudo:
- Quais consultas devem ser rápidas: gerente direto, cadeia até o CEO, subárvore completa sob um líder, ou “todos neste departamento”?
- É uma árvore estrita (um gerente) ou uma org matricial (mais de um gerente ou líder)?
- Departamentos são nós na mesma hierarquia que pessoas, ou um atributo separado (como
department_idem cada pessoa)? - Alguém pode pertencer a múltiplos times (serviços compartilhados, squads)?
- Como as permissões fluem: para baixo, para cima ou ambos?
Essas escolhas definem o que significa ter dados “corretos”. Se Alex lidera tanto Support quanto Onboarding, um único manager_id ou a regra “um líder por time” pode não funcionar. Você pode precisar de uma tabela de junção (líder para time) ou uma política clara como “um time primário, mais times em linha tracejada”.
Departamentos são outro ponto de bifurcação. Se departamentos são nós, você pode expressar “Departamento A contém Time B contém Pessoa C”. Se departamentos são separados, você filtrará com department_id = X, o que é mais simples, mas pode se quebrar quando times atravessam departamentos.
Por fim, defina permissões em linguagem simples. “Um gerente pode ver salário de todos sob ele, mas não pares” é uma regra que desce pela árvore. “Qualquer pessoa pode ver sua cadeia de gestão” é uma regra que sobe pela árvore. Decida isso cedo porque altera qual modelo de hierarquia parecerá natural e qual forçará consultas caras depois.
Lista de adjacência: um esquema simples para gerentes e times
Se você quer menos peças móveis, a lista de adjacência é o ponto de partida clássico. Cada pessoa guarda um ponteiro para seu gerente direto, e a árvore é criada seguindo esses ponteiros.
Um setup mínimo fica assim:
create table departments (
id bigserial primary key,
name text not null unique
);
create table teams (
id bigserial primary key,
department_id bigint not null references departments(id),
name text not null,
unique (department_id, name)
);
create table employees (
id bigserial primary key,
full_name text not null,
team_id bigint references teams(id),
manager_id bigint references employees(id)
);
Você também pode pular as tabelas separadas e manter department_name e team_name como colunas em employees. Isso é mais rápido para começar, mas mais difícil de manter limpo (erros de digitação, times renomeados e reporting inconsistente). Tabelas separadas tornam filtros e regras de permissão mais fáceis de expressar consistentemente.
Adicione guardrails cedo. Dados ruins na hierarquia são dolorosos de consertar depois. No mínimo, previna auto-gestão (manager_id <> id). Decida também se um gerente pode estar fora do mesmo time ou departamento, e se precisa de soft deletes ou mudanças históricas (para auditoria das linhas de reporte).
Com listas de adjacência, a maioria das mudanças é um write simples: mudar um gerente atualiza employees.manager_id, mover times atualiza employees.team_id (frequentemente junto com o gerente). O problema é que uma pequena escrita pode ter grandes efeitos colaterais. Rollups de relatórios mudam, e qualquer regra “gerente pode ver todos os relatórios” agora deve seguir a nova cadeia.
Essa simplicidade é a maior força da lista de adjacência. A fraqueza aparece quando você frequentemente filtra por “todos sob este gerente”, porque normalmente você conta com consultas recursivas para caminhar a árvore a cada vez.
Lista de adjacência: consultas comuns para filtros e relatórios
Com lista de adjacência, muitas perguntas úteis do organograma viram consultas recursivas. Se você modela organogramas no PostgreSQL dessa forma, esses são os padrões que você usará constantemente.
Relatórios diretos (um nível)
O caso mais simples é a equipe imediata de um gerente:
SELECT id, full_name, title
FROM employees
WHERE manager_id = $1
ORDER BY full_name;
Isto é rápido e legível, mas só vai um nível abaixo.
Cadeia de comando (para cima)
Para mostrar a quem alguém reporta (gerente, gerente do gerente e assim por diante), use um CTE recursivo:
WITH RECURSIVE chain AS (
SELECT id, full_name, manager_id, 0 AS depth
FROM employees
WHERE id = $1
UNION ALL
SELECT e.id, e.full_name, e.manager_id, c.depth + 1
FROM employees e
JOIN chain c ON e.id = c.manager_id
)
SELECT *
FROM chain
ORDER BY depth;
Isso suporta aprovações, caminhos de escalonamento e breadcrumbs de gerente.
Subárvore completa (para baixo)
Para obter todos sob um líder (todos os níveis), inverta a recursão:
WITH RECURSIVE subtree AS (
SELECT id, full_name, manager_id, department_id, 0 AS depth
FROM employees
WHERE id = $1
UNION ALL
SELECT e.id, e.full_name, e.manager_id, e.department_id, s.depth + 1
FROM employees e
JOIN subtree s ON e.manager_id = s.id
)
SELECT *
FROM subtree
ORDER BY depth, full_name;
Um relatório comum é “todos em department X sob o líder Y”:
WITH RECURSIVE subtree AS (
SELECT id, department_id
FROM employees
WHERE id = $1
UNION ALL
SELECT e.id, e.department_id
FROM employees e
JOIN subtree s ON e.manager_id = s.id
)
SELECT e.*
FROM employees e
JOIN subtree s ON s.id = e.id
WHERE e.department_id = $2;
Consultas com lista de adjacência podem ser arriscadas para permissões porque checagens de acesso muitas vezes dependem do caminho completo (o visualizador é um ancestral desta pessoa?). Se um endpoint esquecer a recursão ou aplicar filtros no lugar errado, você pode vazar linhas. Também fique atento a problemas de dados como ciclos e managers faltando. Um registro ruim pode quebrar a recursão ou retornar resultados surpreendentes, então queries de permissão precisam de proteções e bons constraints.
Tabela de fechamento: como ela armazena toda a hierarquia
Uma tabela de fechamento armazena toda relação ancestral-descendente, não apenas o link de gerente direto. Em vez de caminhar a árvore passo a passo, você pode perguntar: “Quem está sob este líder?” e obter a resposta completa com um join simples.
Normalmente você mantém duas tabelas: uma para nós (pessoas ou times) e outra para caminhos da hierarquia.
-- nodes
employees (
id bigserial primary key,
name text not null,
manager_id bigint null references employees(id)
)
-- closure
employee_closure (
ancestor_id bigint not null references employees(id),
descendant_id bigint not null references employees(id),
depth int not null,
primary key (ancestor_id, descendant_id)
)
A tabela de fechamento armazena pares como (Alice, Bob) significando “Alice é ancestral de Bob”. Ela também armazena uma linha onde ancestor_id = descendant_id com depth = 0. Essa auto-linha parece estranha inicialmente, mas torna muitas consultas mais limpas.
depth diz o quão distantes dois nós estão: depth = 1 é gerente direto, depth = 2 é gerente do gerente, e assim por diante. Isso importa quando relatórios diretos devem ser tratados de forma diferente dos indiretos.
O principal benefício são leituras previsíveis e rápidas:
- Buscas por subárvore inteira são rápidas (todos sob um diretor).
- Cadeias de comando são simples (todos os gerentes acima de alguém).
- Você pode separar relações diretas e indiretas usando
depth.
O custo é a manutenção nas atualizações. Se Bob mudar de gerente de Alice para Dana, você deve reconstruir as linhas de closure para Bob e todos abaixo de Bob. A abordagem típica é: deletar caminhos ancestrais antigos para essa subárvore e então inserir novos caminhos combinando os ancestrais de Dana com cada nó na subárvore de Bob e recalcular depth.
Tabela de fechamento: consultas comuns para filtros rápidos
Uma tabela de fechamento armazena todo par ancestral-descendente antecipadamente (frequentemente como org_closure(ancestor_id, descendant_id, depth)). Isso torna filtros organizacionais rápidos porque a maioria das perguntas vira um único join.
Para listar todos sob um gerente, faça um join e filtre por depth:
-- Descendants (everyone in the subtree)
SELECT e.*
FROM employees e
JOIN org_closure c
ON c.descendant_id = e.id
WHERE c.ancestor_id = :manager_id
AND c.depth > 0;
-- Direct reports only
SELECT e.*
FROM employees e
JOIN org_closure c
ON c.descendant_id = e.id
WHERE c.ancestor_id = :manager_id
AND c.depth = 1;
Para a cadeia de comando (todos os ancestrais de um funcionário), inverta o join:
SELECT m.*
FROM employees m
JOIN org_closure c
ON c.ancestor_id = m.id
WHERE c.descendant_id = :employee_id
AND c.depth > 0
ORDER BY c.depth;
Filtragem fica previsível. Exemplo: “todas as pessoas sob o líder X, mas apenas no departamento Y”:
SELECT e.*
FROM employees e
JOIN org_closure c ON c.descendant_id = e.id
WHERE c.ancestor_id = :leader_id
AND e.department_id = :department_id;
Como a hierarquia é pré-computada, contagens são diretas também (sem recursão). Isso ajuda dashboards e totais com escopo de permissão, e funciona bem com paginação e busca já que você pode aplicar ORDER BY, LIMIT/OFFSET e filtros diretamente no conjunto de descendentes.
Como cada modelo afeta permissões e checagens de acesso
Uma regra comum de organograma é simples: um gerente pode ver (e às vezes editar) tudo sob ele. O esquema que você escolher muda com que frequência você paga o custo de descobrir “quem está sob quem”.
Com lista de adjacência, a checagem de permissão geralmente precisa de recursão. Se um usuário abre uma página que lista 200 funcionários, normalmente você constrói o conjunto de descendentes com um CTE recursivo e filtra as linhas alvo contra ele.
Com uma tabela de fechamento, a mesma regra muitas vezes pode ser checada com um teste simples de existência: “O usuário atual é ancestral deste funcionário?” Se sim, permite-se.
-- Closure table permission check (conceptual)
SELECT 1
FROM org_closure c
WHERE c.ancestor_id = :viewer_id
AND c.descendant_id = :employee_id
LIMIT 1;
Essa simplicidade importa quando você introduz row-level security (RLS), onde cada consulta inclui automaticamente uma regra como “retorne apenas linhas que o visualizador pode ver”. Com listas de adjacência, a política muitas vezes incorpora recursão e pode ser mais difícil de ajustar. Com tabela de fechamento, a política costuma ser um EXISTS (...) direto.
Casos de borda são onde a lógica de permissões mais costuma falhar:
- Reporte em linha tracejada: uma pessoa tem efetivamente dois gerentes.
- Assistentes e delegados: acesso não é baseado na hierarquia, então armazene concessões explícitas (geralmente com expiração).
- Acesso temporário: permissões limitadas no tempo não devem ser embutidas na estrutura do organograma.
- Projetos cross-team: conceda acesso por participação no projeto, não pela cadeia de gerenciamento.
Se estiver construindo isso em AppMaster, uma tabela de fechamento frequentemente mapeia bem para um modelo de dados visual e mantém a checagem de acesso simples entre web e mobile apps.
Compromissos: velocidade, complexidade e manutenção
A maior escolha é o que você otimiza: escritas simples e um esquema pequeno, ou leituras rápidas para “quem está sob este gerente” e checagens de permissão.
Listas de adjacência mantêm a tabela pequena e atualizações fáceis. O custo aparece nas leituras: uma subárvore inteira geralmente significa recursão. Isso pode ser aceitável se sua organização for pequena, sua UI carregar apenas alguns níveis, ou filtros baseados na hierarquia forem usados em poucos lugares.
Tabelas de fechamento invertem o trade-off. Leituras ficam rápidas porque você pode responder “todos os descendentes” com joins regulares. Escritas ficam mais complexas porque um movimento ou reorg pode exigir inserir e deletar muitas linhas de relacionamento.
No trabalho real, o trade-off costuma se parecer com isto:
- Performance de leitura: adjacência precisa de recursão; closure é majoritariamente joins e permanece rápida conforme a org cresce.
- Complexidade de escrita: adjacência atualiza um
parent_id; closure atualiza muitas linhas para um único movimento. - Tamanho dos dados: adjacência cresce com pessoas/times; closure cresce com relacionamentos (no pior caso, aproximadamente N^2 para uma árvore profunda).
Indexação importa em ambos os modelos, mas o alvo difere:
- Lista de adjacência: indexe o ponteiro do pai (
manager_id), além de filtros comuns como uma flag “ativo”. - Tabela de fechamento: indexe
(ancestor_id, descendant_id)e tambémdescendant_idsozinho para buscas comuns.
Uma regra simples: se você raramente filtra por hierarquia e checagens de permissão são apenas “gerente vê relatórios diretos”, uma lista de adjacência costuma ser suficiente. Se você roda regularmente relatórios “todos sob o VP X”, filtra por árvores de departamento ou aplica permissões hierárquicas em muitas telas, tabelas de fechamento costumam compensar a manutenção extra.
Passo a passo: migrando de lista de adjacência para tabela de fechamento
Você não precisa escolher entre modelos no primeiro dia. Um caminho seguro é manter sua lista de adjacência (manager_id ou parent_id) e adicionar uma tabela de fechamento ao lado, migrando as leituras aos poucos. Isso reduz risco enquanto você valida como a nova hierarquia se comporta em consultas reais e checagens de permissão.
Comece criando uma tabela de fechamento (frequentemente chamada org_closure) com colunas como ancestor_id, descendant_id e depth. Mantenha-a separada da sua tabela employees ou teams para que você possa backfill e validar sem tocar nas funcionalidades atuais.
Um rollout prático:
- Crie a tabela de fechamento e índices mantendo a lista de adjacência como fonte da verdade.
- Preencha as linhas de fechamento a partir das relações atuais de manager, incluindo a linha de self (cada nó é seu próprio ancestral com depth 0).
- Valide com checagens pontuais: escolha alguns gerentes e confirme que o mesmo conjunto de subordinados aparece em ambos os modelos.
- Troque os caminhos de leitura primeiro: relatórios, filtros e permissões hierárquicas devem ler da tabela de fechamento antes de você mudar as escritas.
- Mantenha a tabela de fechamento atualizada em cada escrita (re-parent, hire, mover time). Quando estiver estável, aposente queries baseadas em recursão.
Ao validar, foque nos casos que costumam quebrar regras de acesso: mudanças de gerente, líderes no topo e usuários sem gerente.
Se estiver construindo em AppMaster, você pode manter os endpoints antigos rodando enquanto adiciona novos que leem da tabela de fechamento e só trocar quando os resultados baterem.
Erros comuns que quebram filtros de organograma ou permissões
A maneira mais rápida de quebrar features de organograma é deixar a hierarquia inconsistente. Os dados podem parecer corretos linha a linha, mas pequenos erros podem causar filtros errados, páginas lentas ou vazamento de permissões.
Um problema clássico é criar um ciclo acidentalmente: A gerencia B e mais tarde alguém define B para gerenciar A (ou um loop mais longo entre 3–4 pessoas). Consultas recursivas podem rodar indefinidamente, retornar linhas duplicadas ou estourar o tempo limite. Mesmo com uma tabela de fechamento, ciclos podem contaminar linhas de ancestralidade/descendência.
Outro problema comum é deriva na closure: você muda o gerente de alguém, mas atualiza apenas a relação direta e esquece de reconstruir as linhas de closure da subárvore. Então filtros como “todos sob este VP” retornam uma mistura da estrutura antiga e da nova. É difícil detectar porque páginas de perfil individuais ainda parecem corretas.
Organogramas também ficam bagunçados quando departamentos e linhas de reporte são misturados sem regras claras. Um departamento costuma ser um agrupamento administrativo, enquanto linhas de reporte são sobre gerentes. Se você tratá-los como a mesma árvore, pode acabar com comportamentos estranhos como um “mover de departamento” alterando acesso inesperadamente.
Permissões falham com mais frequência quando as checagens consideram apenas o gerente direto. Se você permite acesso quando viewer is manager of employee, perde a cadeia completa. O resultado é ou sobrebloqueio (gerentes acima de um nível não conseguem ver sua organização) ou sobrecompartilhamento (alguém ganha acesso por ter sido temporariamente definido como gerente direto).
Páginas de listas lentas normalmente vêm de rodar filtros recursivos a cada requisição (cada inbox, cada lista de tickets, cada busca de funcionários). Se o mesmo filtro é usado em muitos lugares, você quer ou um caminho pré-computado (tabela de fechamento) ou um conjunto em cache de IDs de funcionários permitidos.
Algumas proteções práticas:
- Bloqueie ciclos com validação antes de salvar mudanças de gerente.
- Decida o que “departamento” significa e mantenha isso separado de reporting.
- Se usar uma tabela de fechamento, reconstrua linhas de descendentes em mudanças de gerente.
- Escreva regras de permissão para a cadeia completa, não só o gerente direto.
- Pré-compute escopos organizacionais usados por páginas de lista em vez de recalcular recursões toda vez.
Se construir painéis administrativos em AppMaster, trate “mudar gerente” como um fluxo sensível: valide, atualize dados relacionados da hierarquia e só então deixe isso afetar filtros e acesso.
Checagens rápidas antes de lançar
Antes de chamar seu organograma de “pronto”, tenha certeza de que consegue explicar acesso em termos simples. Se alguém perguntar “Quem pode ver o funcionário X, e por quê?”, você deve apontar para uma regra e uma query (ou view) que prove isso.
Performance é a próxima checagem da realidade. Com uma lista de adjacência, “mostre todos sob este gerente” vira uma query recursiva cuja velocidade depende de profundidade e indexação. Com uma tabela de fechamento, leituras geralmente são rápidas, mas você deve confiar no caminho de escrita para manter a tabela correta após cada mudança.
Uma lista de verificação curta antes do lançamento:
- Escolha um funcionário e rastreie a visibilidade de ponta a ponta: qual cadeia concede acesso e qual papel nega.
- Faça benchmark de uma query de subárvore do gerente usando o tamanho esperado (por exemplo, 5 níveis e 50.000 funcionários).
- Bloqueie escritas ruins: previna ciclos, auto-gestão e nós órfãos com constraints e checagens em transação.
- Teste segurança em reorganizações: movimentos, fusões, mudanças de gerente e rollback quando algo falhar no meio.
- Adicione testes de permissão que afirmem tanto acessos permitidos quanto negados para papéis realistas (RH, gerente, líder de time, suporte).
Um cenário prático para validar: um agente de suporte pode ver apenas funcionários no departamento atribuído, enquanto um gerente pode ver sua subárvore completa. Se você conseguir modelar organogramas no PostgreSQL e provar ambas as regras com testes, você está perto de lançar.
Se estiver construindo isso como ferramenta interna em AppMaster, mantenha essas checagens como testes automatizados ao redor dos endpoints que retornam listas organizacionais e perfis de funcionários, não apenas consultas ao banco.
Cenário de exemplo e próximos passos
Imagine uma empresa com três departamentos: Sales, Support e Engineering. Cada departamento tem dois times, e cada time tem um líder. Sales Lead A pode aprovar descontos para seu time, Support Lead B pode ver todos os tickets de seu departamento, e o VP de Engineering pode ver tudo sob Engineering.
Então ocorre uma reorganização: um time de Support é movido para Sales, e um novo gerente é adicionado entre o Diretor de Sales e dois líderes de time. No dia seguinte, alguém solicita acesso: “Deixem Jamie (um analista de Sales) ver todas as contas de clientes do departamento Sales, mas não Engineering.”
Se você modelar organogramas no PostgreSQL com uma lista de adjacência, o esquema é simples, mas o trabalho no app se desloca para suas consultas e checagens de permissão. Filtros como “todo mundo em Sales” geralmente precisam de recursão. Ao adicionar aprovações (como “apenas gerentes na cadeia podem aprovar”), casos de borda após uma reorganização começam a importar.
Com uma tabela de fechamento, reorganizações significam mais trabalho de escrita (atualizar linhas de ancestor/descendant), mas o lado de leitura fica direto. Filtros e permissões frequentemente se tornam junções simples: “este usuário é ancestral daquele funcionário?” ou “este time está dentro da subárvore deste departamento?”.
Isso aparece diretamente nas telas que as pessoas constroem: seletores de pessoas escopados por departamento, roteamento de aprovação para o gerente mais próximo acima do solicitante, vistas administrativas para dashboards de departamento e auditorias que explicam por que alguém tinha acesso em uma data específica.
Próximos passos:
- Escreva as regras de permissão em linguagem simples (quem pode ver o quê, e por quê).
- Escolha um modelo que corresponda às checagens mais comuns (leituras rápidas vs escritas mais simples).
- Construa uma ferramenta administrativa interna que permita testar reorganizações, pedidos de acesso e aprovações de ponta a ponta.
Se quiser construir esses painéis administrativos sensíveis à organização rapidamente, AppMaster (appmaster.io) pode ser uma opção prática: permite modelar dados com backend PostgreSQL, implementar lógica de aprovação em um Business Process visual e entregar apps web e nativos a partir do mesmo backend.
FAQ
Use uma lista de adjacência quando sua organização for pequena, atualizações ocorrerem com frequência e a maioria das telas precisar apenas de relatórios diretos ou alguns níveis. Use uma tabela de fechamento quando você constantemente precisar de “todas as pessoas sob este líder”, filtros por árvore de departamento ou permissões baseadas na hierarquia em muitas páginas — as leituras se tornam junções simples e previsíveis conforme você cresce.
Comece com employees(manager_id) e busque relatórios diretos com uma consulta simples WHERE manager_id = ?. Adicione consultas recursivas só para recursos que realmente precisam de toda a ancestralidade ou de uma subárvore completa, como aprovações, filtros “minha org” ou dashboards de skip-level.
Bloqueie auto-gestão com uma verificação como manager_id <> id, e valide atualizações para nunca atribuir um gerente que já esteja na subárvore do funcionário. Na prática, a abordagem mais segura é checar ancestralidade antes de salvar uma mudança de gerente, porque um único ciclo pode quebrar recursões e corromper a lógica de permissões.
Um bom padrão é tratar departamentos como um agrupamento organizacional e linhas de reporte como uma árvore de gerência separada. Isso evita que um “mover de departamento” altere acidentalmente a quem alguém responde e torna filtros como “todos em Sales” mais claros quando as linhas de reporte não coincidem com os limites do departamento.
Geralmente você armazena um gerente principal no registro do funcionário e representa relações em linha tracejada separadamente, como uma relação de gerente secundário ou um mapeamento de “team lead”. Isso evita quebrar consultas hierárquicas básicas enquanto permite regras especiais como acesso por projeto ou delegação de aprovação.
Você deve deletar os antigos caminhos de ancestralidade para a subárvore do funcionário movido e então inserir novos caminhos combinando os ancestrais do novo gerente com cada nó da subárvore, recalculando depth. Faça isso dentro de uma transação para não deixar a tabela de fechamento parcialmente atualizada se algo falhar no meio do processo.
Para listas de adjacência, indexe employees(manager_id) porque quase toda consulta de organograma começa daí, e adicione índices para filtros comuns como team_id ou department_id. Para tabelas de fechamento, os índices-chave são a chave primária em (ancestor_id, descendant_id) e um índice separado em descendant_id para deixar checagens do tipo “quem pode ver esta linha?” rápidas.
Um padrão comum é usar EXISTS na tabela de fechamento: permita acesso quando o visualizador for um ancestral do funcionário alvo. Isso funciona bem com row-level security porque o banco pode aplicar a regra consistentemente, em vez de depender de cada endpoint da API lembrar a mesma lógica recursiva.
Armazene histórico explicitamente, normalmente em uma tabela separada que registra mudanças de gerente com datas de vigência, em vez de sobrescrever o gerente atual e perder o passado. Assim você pode responder “quem reportava a quem na data X” sem adivinhações e manter relatórios e auditorias consistentes após reorganizações.
Mantenha seu manager_id existente como fonte da verdade, crie a tabela de fechamento ao lado e preencha as linhas de fechamento a partir da árvore atual. Mude os caminhos de leitura primeiro (filtros, dashboards, checagens de permissão), depois faça as escritas atualizarem ambos os modelos, e só aposente queries recursivas quando você validar que os resultados batem em cenários reais.


