As CTEs recursivas são uma das funcionalidades mais poderosas do SQL, permitindo trabalhar com dados hierárquicos e estruturas em árvore. Nesta lição, exploraremos como usar Expressões de Tabela Comum recursivas para consultar dados que possuem relações pai-filho, como organogramas, árvores de categorias e listas de materiais.
Uma CTE Recursiva é uma Expressão de Tabela Comum que se referencia a si mesma, permitindo percorrer estruturas de dados hierárquicas. Ao contrário das CTEs regulares que executam uma vez, as CTEs recursivas executam iterativamente até que uma condição de terminação seja atendida.
Casos de uso comuns para CTEs recursivas:
A sintaxe geral para uma CTE recursiva é:
WITH RECURSIVE nome_cte AS (
-- Membro âncora (caso base)
SELECT ...
FROM tabela
WHERE condicao
UNION ALL
-- Membro recursivo (caso recursivo)
SELECT ...
FROM tabela
JOIN nome_cte ON condicao
)
SELECT * FROM nome_cte;
Componentes:
O processo de execução:
Vamos começar com um exemplo simples que gera uma sequência de números:
WITH RECURSIVE sequencia_numeros AS (
-- Membro âncora: começar com 1
SELECT 1 AS n
UNION ALL
-- Membro recursivo: adicionar 1 ao valor anterior
SELECT n + 1
FROM sequencia_numeros
WHERE n < 10
)
SELECT n
FROM sequencia_numeros;
Resultado:
n
--
1
2
3
4
5
6
7
8
9
10
Como funciona:
11 + 1 = 22 + 1 = 3n < 10 ser falso10, mas 10 < 10 é falso, então a recursão paraVamos criar uma tabela representando uma estrutura organizacional:
-- Tabela de funcionários exemplo
CREATE TABLE employee (
employee_id INT PRIMARY KEY,
employee_name VARCHAR(100),
manager_id INT,
title VARCHAR(100)
);
-- Dados exemplo
INSERT INTO employee VALUES
(1, 'Alice Johnson', NULL, 'CEO'),
(2, 'Bob Smith', 1, 'VP de Engenharia'),
(3, 'Carol White', 1, 'VP de Vendas'),
(4, 'David Brown', 2, 'Gerente de Engenharia'),
(5, 'Eve Davis', 2, 'Gerente de Engenharia'),
(6, 'Frank Miller', 3, 'Gerente de Vendas'),
(7, 'Grace Wilson', 4, 'Desenvolvedor Sênior'),
(8, 'Henry Moore', 4, 'Desenvolvedor'),
(9, 'Ivy Taylor', 5, 'Desenvolvedor'),
(10, 'Jack Anderson', 6, 'Representante de Vendas');
Para encontrar todos os funcionários que se reportam a um gerente específico (direta ou indiretamente):
WITH RECURSIVE subordinados AS (
-- Âncora: Começar com o gerente
SELECT
employee_id,
employee_name,
manager_id,
title,
0 AS nivel
FROM
employee
WHERE
employee_name = 'Bob Smith'
UNION ALL
-- Recursivo: Encontrar subordinados diretos
SELECT
e.employee_id,
e.employee_name,
e.manager_id,
e.title,
s.nivel + 1
FROM
employee e
INNER JOIN
subordinados s ON e.manager_id = s.employee_id
)
SELECT
employee_id,
employee_name,
title,
nivel
FROM
subordinados
ORDER BY
nivel, employee_name;
Resultado:
employee_id | employee_name | title | nivel
------------|-----------------|------------------------|-------
2 | Bob Smith | VP de Engenharia | 0
4 | David Brown | Gerente de Engenharia | 1
5 | Eve Davis | Gerente de Engenharia | 1
7 | Grace Wilson | Desenvolvedor Sênior | 2
8 | Henry Moore | Desenvolvedor | 2
9 | Ivy Taylor | Desenvolvedor | 2
Para exibir a hierarquia completa desde o CEO:
WITH RECURSIVE organograma AS (
-- Âncora: Começar com o CEO (sem gerente)
SELECT
employee_id,
employee_name,
manager_id,
title,
0 AS nivel,
CAST(employee_name AS VARCHAR(1000)) AS caminho
FROM
employee
WHERE
manager_id IS NULL
UNION ALL
-- Recursivo: Adicionar cada nível
SELECT
e.employee_id,
e.employee_name,
e.manager_id,
e.title,
oc.nivel + 1,
CONCAT(oc.caminho, ' > ', e.employee_name)
FROM
employee e
INNER JOIN
organograma oc ON e.manager_id = oc.employee_id
)
SELECT
REPEAT(' ', nivel) || employee_name AS hierarquia,
title,
nivel,
caminho
FROM
organograma
ORDER BY
caminho;
Resultado:
hierarquia | title | nivel | caminho
-------------------------------|-------------------------|-------|----------------------------------
Alice Johnson | CEO | 0 | Alice Johnson
Bob Smith | VP de Engenharia | 1 | Alice Johnson > Bob Smith
David Brown | Gerente de Engenharia | 2 | Alice Johnson > Bob Smith > David Brown
Grace Wilson | Desenvolvedor Sênior | 3 | Alice Johnson > Bob Smith > David Brown > Grace Wilson
Henry Moore | Desenvolvedor | 3 | Alice Johnson > Bob Smith > David Brown > Henry Moore
Eve Davis | Gerente de Engenharia | 2 | Alice Johnson > Bob Smith > Eve Davis
Ivy Taylor | Desenvolvedor | 3 | Alice Johnson > Bob Smith > Eve Davis > Ivy Taylor
Carol White | VP de Vendas | 1 | Alice Johnson > Carol White
Frank Miller | Gerente de Vendas | 2 | Alice Johnson > Carol White > Frank Miller
Jack Anderson | Representante de Vendas | 3 | Alice Johnson > Carol White > Frank Miller > Jack Anderson
Vamos trabalhar com uma hierarquia de categorias de produtos:
-- Tabela de categorias exemplo
CREATE TABLE category (
category_id INT PRIMARY KEY,
category_name VARCHAR(100),
parent_category_id INT
);
-- Dados exemplo
INSERT INTO category VALUES
(1, 'Eletrônicos', NULL),
(2, 'Computadores', 1),
(3, 'Telefones', 1),
(4, 'Notebooks', 2),
(5, 'Desktops', 2),
(6, 'Notebooks Gamer', 4),
(7, 'Notebooks Profissionais', 4),
(8, 'Smartphones', 3),
(9, 'Telefones Básicos', 3);
Para encontrar todas as subcategorias sob "Computadores":
WITH RECURSIVE arvore_categorias AS (
-- Âncora: Começar com Computadores
SELECT
category_id,
category_name,
parent_category_id,
0 AS profundidade,
CAST(category_name AS VARCHAR(1000)) AS caminho
FROM
category
WHERE
category_name = 'Computadores'
UNION ALL
-- Recursivo: Obter todas as subcategorias
SELECT
c.category_id,
c.category_name,
c.parent_category_id,
ct.profundidade + 1,
CONCAT(ct.caminho, ' > ', c.category_name)
FROM
category c
INNER JOIN
arvore_categorias ct ON c.parent_category_id = ct.category_id
)
SELECT
category_id,
REPEAT(' ', profundidade) || category_name AS hierarquia_categoria,
profundidade,
caminho
FROM
arvore_categorias
ORDER BY
caminho;
Resultado:
category_id | hierarquia_categoria | profundidade | caminho
------------|------------------------------|--------------|--------------------------------------
2 | Computadores | 0 | Computadores
4 | Notebooks | 1 | Computadores > Notebooks
6 | Notebooks Gamer | 2 | Computadores > Notebooks > Notebooks Gamer
7 | Notebooks Profissionais | 2 | Computadores > Notebooks > Notebooks Profissionais
5 | Desktops | 1 | Computadores > Desktops
Para encontrar todas as categorias pai de uma categoria específica:
WITH RECURSIVE ancestrais_categoria AS (
-- Âncora: Começar com Notebooks Gamer
SELECT
category_id,
category_name,
parent_category_id,
0 AS nivel_acima
FROM
category
WHERE
category_name = 'Notebooks Gamer'
UNION ALL
-- Recursivo: Obter categorias pai
SELECT
c.category_id,
c.category_name,
c.parent_category_id,
ca.nivel_acima + 1
FROM
category c
INNER JOIN
ancestrais_categoria ca ON c.category_id = ca.parent_category_id
)
SELECT
category_id,
category_name,
nivel_acima
FROM
ancestrais_categoria
ORDER BY
nivel_acima;
Resultado:
category_id | category_name | nivel_acima
------------|------------------|-------------
6 | Notebooks Gamer | 0
4 | Notebooks | 1
2 | Computadores | 2
1 | Eletrônicos | 3
Um caso de uso clássico para CTEs recursivas é explorar listas de materiais (peças e subpeças):
-- Tabela de peças exemplo
CREATE TABLE parts (
part_id INT PRIMARY KEY,
part_name VARCHAR(100),
quantity INT
);
-- Tabela de lista de materiais exemplo
CREATE TABLE bom (
parent_part_id INT,
child_part_id INT,
quantity_needed INT,
PRIMARY KEY (parent_part_id, child_part_id)
);
-- Dados exemplo
INSERT INTO parts VALUES
(1, 'Bicicleta', 1),
(2, 'Quadro', 1),
(3, 'Roda', 2),
(4, 'Pneu', 1),
(5, 'Aro', 1),
(6, 'Raio', 36);
INSERT INTO bom VALUES
(1, 2, 1), -- Bicicleta precisa de 1 Quadro
(1, 3, 2), -- Bicicleta precisa de 2 Rodas
(3, 4, 1), -- Roda precisa de 1 Pneu
(3, 5, 1), -- Roda precisa de 1 Aro
(5, 6, 36); -- Aro precisa de 36 Raios
Para encontrar todas as peças necessárias para construir uma bicicleta:
WITH RECURSIVE explosao_pecas AS (
-- Âncora: Começar com o produto de nível superior
SELECT
p.part_id,
p.part_name,
1 AS quantidade,
0 AS nivel,
CAST(p.part_name AS VARCHAR(1000)) AS caminho
FROM
parts p
WHERE
p.part_name = 'Bicicleta'
UNION ALL
-- Recursivo: Explodir a BOM
SELECT
p.part_id,
p.part_name,
pe.quantidade * b.quantity_needed,
pe.nivel + 1,
CONCAT(pe.caminho, ' > ', p.part_name)
FROM
explosao_pecas pe
INNER JOIN
bom b ON pe.part_id = b.parent_part_id
INNER JOIN
parts p ON b.child_part_id = p.part_id
)
SELECT
part_id,
REPEAT(' ', nivel) || part_name AS hierarquia_pecas,
quantidade,
nivel,
caminho
FROM
explosao_pecas
ORDER BY
caminho;
Resultado:
part_id | hierarquia_pecas | quantidade | nivel | caminho
--------|------------------|------------|-------|------------------------
1 | Bicicleta | 1 | 0 | Bicicleta
2 | Quadro | 1 | 1 | Bicicleta > Quadro
3 | Roda | 2 | 1 | Bicicleta > Roda
4 | Pneu | 2 | 2 | Bicicleta > Roda > Pneu
5 | Aro | 2 | 2 | Bicicleta > Roda > Aro
6 | Raio | 72 | 3 | Bicicleta > Roda > Aro > Raio
Observe que precisamos de 72 raios no total: 2 rodas × 1 aro por roda × 36 raios por aro = 72 raios.
CTEs recursivas podem criar loops infinitos se houver referências circulares em seus dados. Aqui estão estratégias para prevenir isso:
WITH RECURSIVE recursao_limitada AS (
SELECT
category_id,
category_name,
parent_category_id,
0 AS profundidade
FROM
category
WHERE
parent_category_id IS NULL
UNION ALL
SELECT
c.category_id,
c.category_name,
c.parent_category_id,
lr.profundidade + 1
FROM
category c
INNER JOIN
recursao_limitada lr ON c.parent_category_id = lr.category_id
WHERE
lr.profundidade < 10 -- Limite máximo de profundidade
)
SELECT * FROM recursao_limitada;
WITH RECURSIVE travessia_segura AS (
SELECT
category_id,
category_name,
parent_category_id,
ARRAY[category_id] AS ids_visitados
FROM
category
WHERE
parent_category_id IS NULL
UNION ALL
SELECT
c.category_id,
c.category_name,
c.parent_category_id,
st.ids_visitados || c.category_id
FROM
category c
INNER JOIN
travessia_segura st ON c.parent_category_id = st.category_id
WHERE
NOT (c.category_id = ANY(st.ids_visitados)) -- Prevenir ciclos
)
SELECT * FROM travessia_segura;
Sempre indexe as colunas usadas em junções recursivas:
CREATE INDEX idx_employee_manager ON employee(manager_id);
CREATE INDEX idx_category_parent ON category(parent_category_id);
Use cláusulas WHERE para limitar o escopo da recursão:
WITH RECURSIVE subordinados AS (
SELECT employee_id, employee_name, manager_id, 0 AS nivel
FROM employee
WHERE employee_name = 'Bob Smith'
UNION ALL
SELECT e.employee_id, e.employee_name, e.manager_id, s.nivel + 1
FROM employee e
INNER JOIN subordinados s ON e.manager_id = s.employee_id
WHERE s.nivel < 3 -- Apenas 3 níveis de profundidade
)
SELECT * FROM subordinados;
INNER JOIN quando você quiser apenas linhas correspondentesLEFT JOIN quando você quiser incluir nós folha sem filhosUm padrão comum de aplicação web são comentários aninhados ou threads de fórum:
CREATE TABLE comments (
comment_id INT PRIMARY KEY,
parent_comment_id INT,
user_name VARCHAR(100),
comment_text TEXT,
created_at TIMESTAMP
);
WITH RECURSIVE thread_comentarios AS (
-- Âncora: Comentários de nível superior
SELECT
comment_id,
parent_comment_id,
user_name,
comment_text,
0 AS profundidade,
CAST(comment_id AS VARCHAR(1000)) AS caminho_ordenacao
FROM
comments
WHERE
parent_comment_id IS NULL
UNION ALL
-- Recursivo: Respostas aninhadas
SELECT
c.comment_id,
c.parent_comment_id,
c.user_name,
c.comment_text,
ct.profundidade + 1,
CONCAT(ct.caminho_ordenacao, '-', LPAD(c.comment_id::TEXT, 10, '0'))
FROM
comments c
INNER JOIN
thread_comentarios ct ON c.parent_comment_id = ct.comment_id
)
SELECT
REPEAT(' ', profundidade) || user_name AS usuario_indentado,
comment_text,
profundidade
FROM
thread_comentarios
ORDER BY
caminho_ordenacao;
CTEs recursivas são uma ferramenta essencial para trabalhar com dados estruturados em árvore e hierárquicos em SQL. Elas transformam operações complexas de múltiplas consultas em soluções elegantes de consulta única.
No próximo módulo, exploraremos Funções de Janela para análise avançada de dados.