Self JOIN: uma tabela se juntando a si mesma

1. O Conceito de Self JOIN

O Self JOIN é uma técnica poderosa em SQL onde uma tabela é combinada com ela mesma. Diferentemente dos JOINs tradicionais que conectam duas tabelas distintas, o Self JOIN utiliza uma única tabela, mas a trata como se fossem duas instâncias separadas através do uso de aliases.

A necessidade do Self JOIN surge quando precisamos estabelecer relacionamentos dentro da mesma entidade. Os casos mais comuns envolvem estruturas hierárquicas (como um funcionário que se reporta a outro funcionário) ou comparações internas (como encontrar pares de registros que compartilham alguma característica).

A principal diferença para JOINs tradicionais é que, enquanto estes conectam tabelas diferentes (ex: clientes com pedidos), o Self JOIN explora relacionamentos que existem dentro de uma única tabela, geralmente através de uma chave estrangeira que referencia a própria chave primária.

2. Sintaxe Básica e Alias de Tabela

A sintaxe fundamental do Self JOIN segue este padrão:

SELECT A.coluna1, B.coluna2
FROM tabela AS A
JOIN tabela AS B ON A.chave = B.chave_estrangeira;

Os aliases (neste caso A e B) são obrigatórios. Sem eles, o SQL não conseguiria distinguir qual instância da tabela estamos referenciando em cada parte da consulta.

Vejamos um exemplo mínimo com uma tabela de funcionários:

CREATE TABLE funcionarios (
    id INT PRIMARY KEY,
    nome VARCHAR(100),
    gerente_id INT
);

INSERT INTO funcionarios VALUES
(1, 'Ana', NULL),
(2, 'Carlos', 1),
(3, 'Maria', 1),
(4, 'João', 2);

-- Listar funcionários com seus gerentes
SELECT 
    F.nome AS funcionario,
    G.nome AS gerente
FROM funcionarios AS F
JOIN funcionarios AS G ON F.gerente_id = G.id;

Resultado:

funcionario | gerente
Carlos      | Ana
Maria       | Ana
João        | Carlos

3. Casos de Uso Clássicos – Hierarquias

Estruturas organizacionais são o exemplo mais clássico de Self JOIN. Cada funcionário pode ter um gerente que também é um funcionário registrado na mesma tabela.

-- Estrutura organizacional completa
SELECT 
    F.nome AS funcionario,
    F.cargo,
    G.nome AS gerente,
    G.cargo AS cargo_gerente
FROM funcionarios AS F
JOIN funcionarios AS G ON F.gerente_id = G.id
ORDER BY G.nome, F.nome;

Outro caso comum são árvores de categorias:

CREATE TABLE categorias (
    id INT PRIMARY KEY,
    nome VARCHAR(100),
    categoria_pai_id INT
);

INSERT INTO categorias VALUES
(1, 'Eletrônicos', NULL),
(2, 'Computadores', 1),
(3, 'Smartphones', 1),
(4, 'Notebooks', 2),
(5, 'Tablets', 2);

-- Listar subcategorias com suas categorias pai
SELECT 
    C.nome AS subcategoria,
    P.nome AS categoria_pai
FROM categorias AS C
JOIN categorias AS P ON C.categoria_pai_id = P.id;

4. Casos de Uso Clássicos – Comparações Internas

O Self JOIN também é excelente para encontrar pares de registros que compartilham características:

-- Encontrar clientes que moram na mesma cidade
SELECT 
    A.nome AS cliente1,
    B.nome AS cliente2,
    A.cidade
FROM clientes AS A
JOIN clientes AS B ON A.cidade = B.cidade AND A.id < B.id;

A condição A.id < B.id é crucial para evitar pares espelhados (João-Maria e Maria-João) e também para evitar que um cliente seja pareado consigo mesmo.

Outro exemplo prático:

-- Filmes lançados no mesmo ano
SELECT 
    F1.titulo AS filme1,
    F2.titulo AS filme2,
    F1.ano
FROM filmes AS F1
JOIN filmes AS F2 ON F1.ano = F2.ano AND F1.id < F2.id
ORDER BY F1.ano, F1.titulo;

5. Self JOIN com LEFT JOIN

Quando trabalhamos com hierarquias, frequentemente precisamos incluir registros que não têm correspondência (como o CEO que não tem gerente):

-- Todos os funcionários, incluindo o topo da hierarquia
SELECT 
    F.nome AS funcionario,
    COALESCE(G.nome, 'Sem gerente') AS gerente
FROM funcionarios AS F
LEFT JOIN funcionarios AS G ON F.gerente_id = G.id;

A diferença entre INNER e LEFT JOIN em Self JOIN é significativa:

  • INNER JOIN: retorna apenas registros com correspondência (funcionários que têm gerente)
  • LEFT JOIN: retorna todos os registros da tabela esquerda, mesmo sem correspondência
-- Comparando INNER vs LEFT
SELECT 'INNER' AS tipo, F.nome, G.nome AS gerente
FROM funcionarios AS F
JOIN funcionarios AS G ON F.gerente_id = G.id
UNION ALL
SELECT 'LEFT', F.nome, COALESCE(G.nome, 'NULL')
FROM funcionarios AS F
LEFT JOIN funcionarios AS G ON F.gerente_id = G.id;

6. Self JOIN com Filtros e Condições Avançadas

Condições mais sofisticadas permitem análises interessantes:

-- Encontrar pares de produtos com preços próximos (diferença menor que 10%)
SELECT 
    P1.nome AS produto1,
    P2.nome AS produto2,
    P1.preco,
    P2.preco,
    ROUND(ABS(P1.preco - P2.preco) / P1.preco * 100, 2) AS diferenca_percentual
FROM produtos AS P1
JOIN produtos AS P2 
    ON P1.categoria_id = P2.categoria_id 
    AND P1.id < P2.id
    AND ABS(P1.preco - P2.preco) / P1.preco < 0.10
ORDER BY diferenca_percentual;

A combinação de múltiplas condições no JOIN evita a necessidade de WHERE e pode melhorar o desempenho:

-- Funcionários que ganham mais que seus gerentes
SELECT 
    F.nome AS funcionario,
    F.salario,
    G.nome AS gerente,
    G.salario AS salario_gerente
FROM funcionarios AS F
JOIN funcionarios AS G ON F.gerente_id = G.id
WHERE F.salario > G.salario;

7. Self JOIN vs. Subconsultas e CTEs

Para hierarquias simples de um nível, o Self JOIN é geralmente mais eficiente e legível que subconsultas correlacionadas:

-- Self JOIN (mais eficiente)
SELECT F.nome, G.nome AS gerente
FROM funcionarios F
JOIN funcionarios G ON F.gerente_id = G.id;

-- Subconsulta correlacionada (menos eficiente)
SELECT 
    F.nome,
    (SELECT nome FROM funcionarios WHERE id = F.gerente_id) AS gerente
FROM funcionarios F;

Para hierarquias de múltiplos níveis, CTEs recursivas são mais adequadas:

-- Hierarquia completa com Self JOIN (apenas 1 nível)
SELECT F.nome, G.nome AS gerente
FROM funcionarios F
LEFT JOIN funcionarios G ON F.gerente_id = G.id;

-- Hierarquia completa com CTE recursiva (todos os níveis)
WITH RECURSIVE hierarquia AS (
    SELECT id, nome, gerente_id, 1 AS nivel
    FROM funcionarios
    WHERE gerente_id IS NULL
    UNION ALL
    SELECT F.id, F.nome, F.gerente_id, H.nivel + 1
    FROM funcionarios F
    JOIN hierarquia H ON F.gerente_id = H.id
)
SELECT * FROM hierarquia;

A escolha depende da profundidade da hierarquia: Self JOIN para um nível, CTE recursiva para múltiplos níveis.

8. Armadilhas e Boas Práticas

Armadilha 1: Junções Cartesianas Acidentais

-- PERIGO: Isso gera um produto cartesiano!
SELECT A.nome, B.nome
FROM funcionarios A, funcionarios B;

-- CORRETO: Sempre especifique a condição de JOIN
SELECT A.nome, B.nome
FROM funcionarios A
JOIN funcionarios B ON A.gerente_id = B.id;

Armadilha 2: Pares Espelhados e Auto-relacionamento

-- Evite pares espelhados e auto-relacionamento
SELECT A.nome, B.nome
FROM clientes A
JOIN clientes B ON A.cidade = B.cidade AND A.id < B.id;
-- A condição A.id < B.id é essencial!

Boas Práticas:

  1. Índices são cruciais: Crie índices nas colunas usadas na condição de JOIN
CREATE INDEX idx_gerente ON funcionarios(gerente_id);
  1. Nomenclatura clara de aliases: Use letras que façam sentido
-- Bom
FROM funcionarios AS func
JOIN funcionarios AS ger ON func.gerente_id = ger.id

-- Melhor ainda em tabelas grandes
FROM funcionarios AS subordinado
JOIN funcionarios AS superior ON subordinado.gerente_id = superior.id
  1. Sempre teste com dados pequenos primeiro antes de aplicar em produção

  2. Documente o propósito do Self JOIN em consultas complexas

O Self JOIN é uma ferramenta indispensável no arsenal de qualquer profissional de banco de dados. Dominá-lo permite modelar relacionamentos hierárquicos e realizar comparações internas com elegância e eficiência, transformando uma única tabela em uma fonte rica de insights relacionais.

Referências