Erros comuns de legibilidade em SQL

Doze hábitos que deixam o SQL mais difícil de ler, revisar e confiar, cada um com um "antes" e uma reescrita mais clara. Alguns são puramente de layout, e um formatador resolve com um clique; outros exigem uma decisão humana, e alguns deles também mudam o que a consulta retorna ou a velocidade com que ela roda.

Por que vale a pena investir em legibilidade

Uma consulta é escrita uma vez e lida muitas vezes: na revisão de código, durante um incidente, quando alguém adiciona um filtro dois anos depois. A maioria dos bugs de SQL que chegam à produção não são erros de sintaxe; são consultas que fazem algo um pouco diferente do que o autor pretendia, e quem revisou não percebeu porque a consulta era difícil de ler. Os erros abaixo são os que mais aparecem em filas reais de revisão. Os erros 1 a 9 tratam de estrutura e layout; os erros 10 a 12 são problemas de legibilidade que também escondem uma armadilha de correção ou de desempenho.

Para as convenções de layout citadas aqui, veja o guia de estilo de SQL. Todos os blocos "depois" marcados como saída do formatador foram gerados pelo SQL Format com as opções padrão.

1. Joins implícitos com vírgula

Listar as tabelas no FROM separadas por vírgula e fazer o join no WHERE mistura condições de join com filtros. Com três tabelas ou mais, fica difícil ver qual condição liga o quê, e, se uma condição de join for esquecida, você ganha um produto cartesiano silencioso em vez de um erro.

SELECT c.name, o.id, o.total
FROM customers c, orders o, shipments s
WHERE o.customer_id = c.id
  AND s.order_id = o.id
  AND s.shipped_at IS NULL
  AND o.status = 'paid'

Use JOIN … ON explícito. Cada condição de join fica ao lado da tabela que ela traz, e o WHERE contém apenas os filtros. O formatador indenta cada JOIN abaixo do FROM e o seu ON um nível a mais:

Depois, formatado pelo SQL Format
Opções: Espaços de indentação 4 · Quantidade máxima de colunas 120 · Vírgulas: Depois · Palavras-chave: Maiúsculas · Parênteses em nova linha: não
SELECT c.name,
    o.id,
    o.total
FROM customers c
    JOIN orders o
        ON  o.customer_id = c.id
    JOIN shipments s
        ON  s.order_id = o.id
WHERE s.shipped_at IS NULL
AND o.status = 'paid'

2. SELECT * em código de aplicação e de views

SELECT * não tem problema num prompt interativo. Em código, ele esconde de quais colunas quem chama depende, transfere colunas que ninguém lê e muda de comportamento quando alguém adiciona ou reordena uma coluna: uma view, um INSERT … SELECT ou um mapeamento posicional do resultado pode quebrar ou, pior, continuar funcionando com os dados errados.

SELECT *
FROM orders o
WHERE o.customer_id = 42

Dê nome às colunas. O formatador mantém a primeira na linha do SELECT e coloca cada uma das outras na sua própria linha, de modo que quem revisa enxerga uma coluna adicionada ou removida como um diff de uma linha:

Depois, formatado pelo SQL Format
Opções: Espaços de indentação 4 · Quantidade máxima de colunas 120 · Vírgulas: Depois · Palavras-chave: Maiúsculas · Parênteses em nova linha: não
SELECT o.id,
    o.created_at,
    o.status,
    o.total
FROM orders o
WHERE o.customer_id = 42

3. Aliases que não significam nada

Aliases como a, b, c ou t1, t2 obrigam quem lê a voltar até o FROM toda vez que uma coluna é citada. No exemplo abaixo, c.amount parece uma coluna de cliente, mas vem de pagamentos.

SELECT a.name, b.number, c.amount
FROM customers a
JOIN invoices b ON b.customer_id = a.id
JOIN payments c ON c.invoice_id = b.id
WHERE c.received_at >= DATE '2026-01-01'

Escolha aliases que lembrem o nome da tabela. Numa consulta curta, uma inicial (o para orders) basta; quando as iniciais se repetem ou a consulta é longa, use uma palavra curta:

Depois, formatado pelo SQL Format
Opções: Espaços de indentação 4 · Quantidade máxima de colunas 120 · Vírgulas: Depois · Palavras-chave: Maiúsculas · Parênteses em nova linha: não
SELECT cust.name,
    inv.number,
    pay.amount
FROM customers cust
    JOIN invoices inv
        ON  inv.customer_id = cust.id
    JOIN payments pay
        ON  pay.invoice_id = inv.id
WHERE pay.received_at >= DATE '2026-01-01'

4. Colunas não qualificadas em joins

Quando uma consulta faz join entre duas tabelas, um status ou name sem qualificação obriga quem lê a saber os dois esquemas de cor. Também é frágil: se alguém depois adicionar uma coluna status à segunda tabela, a consulta falha com um erro de "coluna ambígua", ou uma subconsulta correlacionada pode se ligar silenciosamente à tabela externa.

SELECT id, created_at, name, email
FROM orders o
JOIN customers c ON c.id = customer_id
WHERE status = 'open'
  AND country = 'BR'

Qualifique toda coluna com o seu alias assim que houver mais de uma tabela:

Depois, formatado pelo SQL Format
Opções: Espaços de indentação 4 · Quantidade máxima de colunas 120 · Vírgulas: Depois · Palavras-chave: Maiúsculas · Parênteses em nova linha: não
SELECT o.id,
    o.created_at,
    c.name,
    c.email
FROM orders o
    JOIN customers c
        ON  c.id = o.customer_id
WHERE o.status = 'open'
AND c.country = 'BR'

5. Misturar AND e OR sem parênteses

O AND tem precedência maior que o OR. A condição abaixo é avaliada como (country = 'BR' AND status = 'active') OR (status = 'trial' AND deleted_at IS NULL), então ela retorna clientes em trial de todos os países. Isso quase nunca é o que o autor queria, e o layout achatado esconde o problema.

SELECT id, name, status
FROM customers
WHERE country = 'BR' AND status = 'active' OR status = 'trial' AND deleted_at IS NULL

Coloque parênteses sempre que AND e OR aparecerem na mesma condição, mesmo quando a precedência por acaso dá a resposta certa. O formatador coloca cada condição de nível superior na sua própria linha e indenta o grupo entre parênteses, de modo que a estrutura fica visível de relance:

Depois, formatado pelo SQL Format
Opções: Espaços de indentação 4 · Quantidade máxima de colunas 120 · Vírgulas: Depois · Palavras-chave: Maiúsculas · Parênteses em nova linha: não
SELECT id,
    name,
    status
FROM customers
WHERE country = 'BR'
AND (
    status = 'active'
    OR status = 'trial'
)
AND deleted_at IS NULL

6. Subconsultas profundamente aninhadas em vez de CTEs

Tabelas derivadas aninhadas em dois ou três níveis precisam ser lidas de dentro para fora, e os seus nomes (p, t) só aparecem depois do código que as define.

SELECT c.name, t.revenue
FROM (SELECT customer_id, revenue
      FROM (SELECT customer_id, SUM(total) AS revenue
            FROM orders
            WHERE status = 'paid'
            GROUP BY customer_id) p
      WHERE revenue > 10000) t
JOIN customers c ON c.id = t.customer_id
ORDER BY t.revenue DESC

Uma common table expression (WITH) permite dar nome a cada etapa e ler a consulta de cima para baixo. Todos os bancos de dados cobertos na nossa comparação de dialetos suportam CTEs nas versões atuais. O formatador organiza o corpo de cada CTE como qualquer outra subconsulta, indentado dentro dos seus parênteses:

Depois, formatado pelo SQL Format
Opções: Espaços de indentação 4 · Quantidade máxima de colunas 120 · Vírgulas: Depois · Palavras-chave: Maiúsculas · Parênteses em nova linha: não
WITH paid_orders AS (
    SELECT customer_id,
        SUM(total) AS revenue
    FROM orders
    WHERE status = 'paid'
    GROUP BY customer_id
),
top_customers AS (
    SELECT customer_id,
        revenue
    FROM paid_orders
    WHERE revenue > 10000
)
SELECT c.name,
    t.revenue
FROM top_customers t
    JOIN customers c
        ON  c.id = t.customer_id
ORDER BY t.revenue DESC

Mais técnicas para consultas grandes estão em formatando consultas complexas.

7. Números mágicos sem explicação

Um status_id = 4 ou - 90 solto não diz nada a quem lê. Alguém precisa descobrir o que é o status 4, e ninguém sabe se 90 é uma regra de negócio ou um chute.

SELECT id, name
FROM employees
WHERE status_id = 4
  AND hired_at < CURRENT_DATE - 90

Faça join com a tabela de domínio e filtre por um código legível, e coloque um comentário ao lado de qualquer constante que represente uma regra. O formatador preserva os comentários no lugar em que estão:

Depois, formatado pelo SQL Format
Opções: Espaços de indentação 4 · Quantidade máxima de colunas 120 · Vírgulas: Depois · Palavras-chave: Maiúsculas · Parênteses em nova linha: não
SELECT e.id,
    e.name
FROM employees e
    JOIN employee_status s
        ON  s.id = e.status_id
WHERE s.code = 'active'
-- probation period is 90 days (HR policy)
AND e.hired_at < CURRENT_DATE - INTERVAL '90 days'

8. Caixa inconsistente nas palavras-chave

SELECT numa linha, from na seguinte e order BY depois disso não está errado, mas torna mais difícil distinguir palavras-chave de identificadores e transforma cada edição em ruído no diff. Escolha uma convenção para o time e deixe uma ferramenta aplicá-la. Com a opção Palavras-chave definida como Maiúsculas, o formatador reescreve as palavras-chave e mantém identificadores como Name e Customers do jeito que foram digitados:

Antes
SELECT id, Name, email
from Customers
Where status = 'active'
order BY created_at DESC
Depois, formatado pelo SQL Format
Opções: Espaços de indentação 4 · Quantidade máxima de colunas 120 · Vírgulas: Depois · Palavras-chave: Maiúsculas · Parênteses em nova linha: não
SELECT id,
    Name,
    email
FROM Customers
WHERE status = 'active'
ORDER BY created_at DESC

9. A consulta inteira numa linha só

Consultas copiadas de logs, ORMs ou chats costumam chegar como uma única linha de várias centenas de caracteres. Revisá-las significa rolar para os lados, e um diff nessa linha mostra a consulta inteira como alterada. Este é o caso em que um formatador mais ajuda:

Antes
select p.sku, p.name, sum(i.quantity) as units, sum(i.quantity * i.unit_price) as revenue from order_items i join products p on p.id = i.product_id join orders o on o.id = i.order_id where o.status = 'paid' and o.created_at >= date '2026-01-01' group by p.sku, p.name having sum(i.quantity) > 100 order by revenue desc
Depois, formatado pelo SQL Format
Opções: Espaços de indentação 4 · Quantidade máxima de colunas 120 · Vírgulas: Depois · Palavras-chave: Maiúsculas · Parênteses em nova linha: não
SELECT p.sku,
    p.name,
    SUM(i.quantity) AS units,
    SUM(i.quantity * i.unit_price) AS revenue
FROM order_items i
    JOIN products p
        ON  p.id = i.product_id
    JOIN orders o
        ON  o.id = i.order_id
WHERE o.status = 'paid'
AND o.created_at >= DATE '2026-01-01'
GROUP BY p.sku,
    p.name
HAVING SUM(i.quantity) > 100
ORDER BY revenue DESC

Se algumas linhas continuarem longas demais depois da organização do layout, a opção Quantidade máxima de colunas as quebra num espaço.

10. ORDER BY e GROUP BY por posição

ORDER BY 1, 3 é rápido de digitar, mas obriga quem lê a contar os itens do select, e muda de significado silenciosamente quando alguém insere uma coluna no começo da lista do select. O GROUP BY posicional não é suportado em todo lugar, e a coluna calculada abaixo nem sequer tem nome.

SELECT region, product_line, SUM(amount)
FROM sales
GROUP BY 1, 2
ORDER BY 1, 3 DESC

Agrupe pelas expressões e ordene por nomes de coluna ou aliases:

Depois, formatado pelo SQL Format
Opções: Espaços de indentação 4 · Quantidade máxima de colunas 120 · Vírgulas: Depois · Palavras-chave: Maiúsculas · Parênteses em nova linha: não
SELECT region,
    product_line,
    SUM(amount) AS revenue
FROM sales
GROUP BY region,
    product_line
ORDER BY region,
    revenue DESC

11. NOT IN com uma subconsulta que pode retornar NULL

Este parece inofensivo e é um bug de correção. x NOT IN (a, b, NULL) significa x <> a AND x <> b AND x <> NULL, e x <> NULL nunca é verdadeiro: é desconhecido. Então, se a subconsulta retornar um único NULL, o predicado inteiro nunca é verdadeiro e a consulta não retorna nenhuma linha.

-- returns no rows at all if any blocked_emails.email is NULL
SELECT c.id, c.name
FROM customers c
WHERE c.email NOT IN (SELECT email FROM blocked_emails)

Use NOT EXISTS, que compara linha a linha e não é afetado por valores NULL na outra tabela. Ele também expressa a intenção, "clientes sem nenhum e-mail bloqueado correspondente", de forma mais direta. O formatador mantém NOT EXISTS ( na linha do WHERE e indenta a subconsulta correlacionada logo abaixo:

Depois, formatado pelo SQL Format
Opções: Espaços de indentação 4 · Quantidade máxima de colunas 120 · Vírgulas: Depois · Palavras-chave: Maiúsculas · Parênteses em nova linha: não
SELECT c.id,
    c.name
FROM customers c
WHERE NOT EXISTS (
    SELECT 1
    FROM blocked_emails b
    WHERE b.email = c.email
)

Um anti-join com LEFT JOIN … IS NULL dá o mesmo resultado e é questão de preferência do time; teste a coluna que não pode ser NULL numa linha correspondente, geralmente a chave do join:

Depois, formatado pelo SQL Format
Opções: Espaços de indentação 4 · Quantidade máxima de colunas 120 · Vírgulas: Depois · Palavras-chave: Maiúsculas · Parênteses em nova linha: não
SELECT c.id,
    c.name
FROM customers c
    LEFT JOIN blocked_emails b
        ON  b.email = c.email
WHERE b.email IS NULL

12. Funções sobre colunas indexadas no WHERE

Envolver uma coluna numa função, como em EXTRACT(YEAR FROM created_at) = 2026, parece natural, mas geralmente impede o banco de dados de usar um índice simples nessa coluna, porque o índice armazena created_at, não o ano. Diz-se que o predicado é non-sargable, e a consulta acaba fazendo uma varredura da tabela. Também é mais difícil de ler do que parece: duas condições separadas descrevem o que na verdade é um único intervalo de datas.

-- an index on created_at cannot be used for this range
SELECT id, total
FROM orders
WHERE EXTRACT(YEAR FROM created_at) = 2026
  AND EXTRACT(MONTH FROM created_at) = 3

Escreva o intervalo sobre a coluna pura, com início inclusivo e fim exclusivo. Isso funciona para qualquer tipo de coluna, inclusive timestamps com parte de hora:

Depois, formatado pelo SQL Format
Opções: Espaços de indentação 4 · Quantidade máxima de colunas 120 · Vírgulas: Depois · Palavras-chave: Maiúsculas · Parênteses em nova linha: não
SELECT id,
    total
FROM orders
WHERE created_at >= DATE '2026-03-01'
AND created_at < DATE '2026-04-01'

Um índice de expressão pode deixar a forma com função rápida de novo, mas o intervalo é mais simples e não depende de um índice que alguém pode apagar.

O que um formatador corrige e o que não corrige

Um formatador corrige o layout: os erros 8 e 9 por completo, e o lado visual dos outros, como uma coluna por linha e uma condição por linha. Ele não muda o significado da sua consulta, então não vai adicionar parênteses, substituir NOT IN nem renomear aliases. Formatar primeiro ainda ajuda nesses casos, porque uma consulta bem organizada deixa os problemas restantes mais fáceis de enxergar. Para manter isso consistente em todo o time, veja formatação de SQL na revisão de código e no controle de versão.

Perguntas frequentes

SELECT * está sempre errado?

Não. Ele não tem problema em explorações ad hoc e dentro de subconsultas EXISTS, em que a lista do select é ignorada. O problema é código de aplicação, views e comandos INSERT … SELECT que dependem silenciosamente da lista atual de colunas.

Joins implícitos com vírgula são mais lentos que JOIN explícito?

Para inner joins, os otimizadores mais usados geralmente tratam as duas formas da mesma maneira. O motivo para preferir JOIN explícito é legibilidade e segurança: uma condição de join esquecida fica visível em vez de produzir um produto cartesiano.

O NOT IN alguma vez se comporta corretamente?

Sim, quando nem a lista nem a subconsulta podem produzir NULL, por exemplo uma lista de literais ou uma coluna declarada NOT NULL. Como as constraints podem mudar depois, NOT EXISTS é o hábito mais seguro para subconsultas.