O que são conjuntos pais e filhos na prática
Conjuntos pais e filhos é uma forma de modelar relações hierárquicas onde um registro pai pode ter múltiplos registros filho, mas cada filho pertence a exatamente um pai. Isso aparece em todo lugar: categorias de produtos, estruturas organizacionais, árvores de arquivos, pedidos com itens, comentários encadeados. A coisa básica não tem mistério. Você tem uma tabela com auto-referência por ID ou duas tabelas ligadas por uma chave estrangeira.Implementando conjuntos pais e filhos com SQL
O modelo mais comum envolve duas tabelas. Digamos uma de departamentos e uma de funcionários.departments(id, name, manager_id)
employees(id, first_name, last_name, department_id) O department_id em employees aponta para o id em departments. Cada funcionário pertence a um único departamento. Um departamento tem vários funcionários. Isso é o básico de conjuntos pais e filhos que todo mundo aprende no primeiro módulo de banco de dados.
Para buscar todos os filhos de um pai específico, você faz um simples select com where department_id = X. Para buscar o pai de um filho, você junta as duas tabelas. O problema real começa quando você precisa navegar várias camadas de profundidade. Departamentos têm subdepartamentos. Subdepartamentos têm equipes. Equipes têm pessoas. Aqui entra a solução que resolve 90% dos casos: recursive common table expressions, a famosa CTE recursiva com WITH RECURSIVE no PostgreSQL e SQLite, ou recursive queries no SQL Server com OPTION (CONNECT BY) ou CTE recursiva. Vou mostrar a versão mais universal que funciona em quase qualquer banco moderno.
Suponha que departments tenha um campo parent_id apontando para outro department. Quer listar todos os subdepartamentos abaixo do ID 5, independentemente da profundidade. A query fica assim: WITH RECURSIVE dept_tree AS (
SELECT id, name, parent_id, 1 as level FROM departments WHERE id = 5
UNION ALL SELECT d.id, d.name, d.parent_id, dt.level + 1
👉 Clique no botão abaixo para saber mais sobre o assunto!
FROM departments d INNER JOIN dept_tree dt ON d.parent_id = dt.id
) SELECT * FROM dept_tree;
Isso retorna o departamento 5 mais todos os seus descendentes em todas as profundidades. O campo level mostra quantos passos do pai o registro está. Isso substitui loops aninhados no código aplicado que antigamente obrigavam a fazer query por query em memória. No MySQL, antes da versão 8.0, CTEs recursivas não existiam. Você usava soluções gambiarras como variáveis de sessão ou procedure com cursor. Atualizar pra versão 8 foi um alívio enorme. Se seu ambiente ainda roda MySQL 5.7 em produção, o caminho mais viável é implementar a lógica recursiva em Python ou PHP mesmo, com uma função que chama a si mesma até esgotar os filhos.
Pegadinhas que ninguém conta
O primeiro erro clássico é criar ciclos acidentais. Se por algum bug na inserção, o departamento A passa a ter B como parent e B passa a ter A como parent, sua CTE recursiva entra em loop infinito e o banco simplesmente matra a query. O PostgreSQL para depois de um limite configurable de iterações (default é 1000), mas em outros SGBDs você pode travar a thread por minutos. A solução prática é adicionar uma verificação de cycle no insert, com trigger ou constraint de aplicação, e sempre limitar a profundidade máxima nas queries de navegação.O segundo erro que vejo todo dia é tentar usar essa estrutura para dados que mudam de hierarquia com frequência. Se um filho pode ter múltiplos pais ao longo do tempo, ou se a relação é mais de grafo do que de árvore, conjuntos pais e filhos tradicionais não são a ferramenta certa. Você vai passar horas escrevendo queries quebra-galho e ainda assim não resolver casos extremos. Nesse cenário, graph databases como Neo4j ou até adjacência list com stored procedures especializadas entregam resultados muito mais previsíveis. Também é importante entender o custo de performance. Uma CTE recursiva bem estruturada em PostgreSQL com índices corretos em parent_id e id roda rapidamente para árvores de até algumas dezenas de milhares de nós. Acima disso, o plano de execução pode começar a subir e você precisa avaliar materialização intermediária ou pré-computação de path com o padrão de materialized path (armazenar o caminho como /5/12/34/ no campo path). O materialized path economiza queries recursivas mas introduz complexidade em operações de update quando um nó muda de posição na árvore.
Outro detalhe técnico: ao trabalhar com conjuntos pais e filhos em aplicações web, é tentador buscar tudo de uma vez e montar a árvore no servidor. Isso parece prático até a árvore crescer. Uma tabela de departamentos com 5.000 linhas gerando toda a hierarquia via recursive CTE pode levar de 2 a 5 segundos dependendo do índice e do plano de execução. Para esses casos, fetch sob demanda é muito mais eficiente. Você busca apenas os filhos diretos do nó atual e carrega os subfilhos sob demanda via AJAX. A experiência do usuário melhora porque a interface responde rápido e a carga no banco fica distribuída. A minha recomendação prática, depois de resolver problemas parecidos em sistemas reais: comece simples com foreign key bidirecional e recursive CTE. Valide se não há ciclos antes de permitir inserts. Coloque índice em parent_id. Limite a profundidade máxima na aplicação, mesmo que tecnicamente o banco suporte mais. Se perceber que a árvore vai crescer demais, migre para materialized path ou feza uma coluna de level calculada e indexada separadamente. E se a estrutura tiver múltiplas relações pai-filho no mesmo modelo, pare e repense o design antes de escrever a primeira linha de código.