Formatação de SQL para code review e controle de versão

Como uma formatação de SQL consistente mantém os diffs pequenos e as revisões de código focadas no que importa: combinar um único formato, separar a reformatação das mudanças de lógica, manter o SQL em arquivos versionados e um checklist para revisar SQL em pull requests.

O SQL costuma receber menos atenção na revisão do que o código da aplicação. Ele se esconde em strings, em pastas de migrations e em definições de relatórios, e quando aparece num pull request, o diff muitas vezes é dominado por espaços e quebras de linha. Este guia trata de deixar as mudanças em SQL tão fáceis de revisar quanto qualquer outro código, para que a atenção de quem revisa vá para a lógica e não para o layout.

Por que o ruído de formatação atrapalha as revisões

Uma ferramenta de diff compara linhas. Se um desenvolvedor escreve uma consulta em quatro linhas longas e o próximo a reformata em vinte linhas curtas enquanto também muda uma condição, o diff mostra todas as linhas como alteradas. A mudança real está em algum lugar ali, e quem revisa precisa encontrá-la lendo as duas versões inteiras. Veja um pequeno exemplo em que uma consulta foi reformatada e um valor foi alterado ao mesmo tempo:

--- a/reports/late_orders.sql
+++ b/reports/late_orders.sql
@@ -1,4 +1,7 @@
-select o.order_id, o.customer_id, o.created_at
-from orders o
-where o.status = 'open' and o.created_at < current_date - 14
-order by o.created_at
+SELECT o.order_id,
+    o.customer_id,
+    o.created_at
+FROM orders o
+WHERE o.status = 'open'
+AND o.created_at < CURRENT_DATE - 7
+ORDER BY o.created_at

Todas as linhas aparecem como alteradas, mas só uma coisa mudou de significado: o limite passou de 14 dias para 7. Numa consulta de quatro linhas, um revisor cuidadoso talvez perceba. Numa consulta de relatório de duzentas linhas, é muito provável que passe despercebido. O ruído de formatação tem outros custos também:

  • O git blame atribui todas as linhas a quem fez a reformatação, então o histórico de por que uma condição existe fica um passo mais distante.
  • Conflitos de merge ficam mais prováveis e mais difíceis de resolver, porque duas branches que mexeram na mesma consulta com estilos diferentes entram em conflito em todas as linhas.
  • Os revisores aprendem a passar o olho por cima dos diffs de SQL, que é justamente o hábito que você não quer para um código capaz de atualizar ou apagar dados.

Combinem um único formato

A solução é tornar a formatação algo sem graça: todo mundo produz o mesmo layout para a mesma consulta, então os diffs só mostram mudanças reais. Isso exige duas coisas.

Primeiro, uma decisão. O time combina a indentação, o tamanho das linhas, a posição das vírgulas, a caixa das palavras-chave e como os parênteses são organizados. As escolhas específicas importam muito menos do que tê-las feito; o guia de estilo de SQL discute os prós e contras, e vírgulas à esquerda ou à direita trata em profundidade da mais debatida delas.

Segundo, uma ferramenta que aplique a decisão de forma mecânica. Regras de estilo que dependem de as pessoas se lembrarem delas se perdem em poucas semanas. Um formatador com opções fixas produz sempre a mesma saída a partir da mesma entrada, independentemente de quem o executa, e é isso que deixa os diffs estáveis.

Usando as opções do formatador como padrão comum

O SQL Format tem cinco opções, e juntas elas definem todo o layout:

OpçãoPadrãoO que ela controla
Espaços de indentação4Espaços por nível de indentação; os tabs da entrada são substituídos por espaços, exceto dentro de literais de string e de identificadores entre aspas.
Quantidade máxima de colunas120Linhas maiores que esse valor são quebradas; 0 desativa a quebra.
VírgulasDepoisVírgulas à direita (Depois) ou à esquerda (Antes) nas listas.
Palavras-chaveMaiúsculasPalavras-chave em maiúsculas ou em minúsculas.
Parênteses em nova linhadesativadaSe um parêntese de abertura que inicia um bloco indentado vai para uma linha própria.

Escolha os valores uma vez e registre-os no repositório, por exemplo no README, num guia de contribuição ou num pequeno bloco de comentário num arquivo ao lado do seu SQL:

-- SQL formatting standard for this repository
-- Formatter: sql-format.io (paste, format, commit the result)
--   Indent spaces             = 4
--   Maximum columns           = 120
--   Commas                    = after (trailing)
--   Keywords                  = upper
--   Parentheses on new line   = off
-- Reformatting-only changes go in their own commit.

Com os valores registrados, qualquer pessoa consegue reproduzir o layout padrão: cole o SQL no formatador, ajuste as opções documentadas, formate e cole o resultado de volta. Como a saída depende apenas da entrada e das opções, duas pessoas que formatam a mesma consulta obtêm o mesmo texto. O SQL Format é uma página web, não uma ferramenta de linha de comando nem um plugin de build, então esse passo é feito à mão antes do commit, e a revisão confere se ele foi feito. Para SQL com lógica sensível, pode ser útil saber que o formatador não armazena o SQL que você envia; a política de privacidade traz os detalhes.

Reformate num commit separado

O hábito mais eficaz de todos: nunca misture reformatação e mudanças de lógica no mesmo commit. Se um arquivo precisa ser reformatado antes de você alterá-lo, faça isso em dois passos:

  1. Reformate o arquivo e faça o commit com uma mensagem como "Reformata open_orders.sql, sem mudança de lógica". Quem revisa consegue confirmar rapidamente que a mudança é só de layout, ou pular essa revisão por completo se o time confia no formatador.
  2. Faça a mudança de lógica e o commit separadamente. O diff agora mostra apenas as linhas que importam.

Ao adotar um formato numa base de código existente, a mesma regra vale em escala maior. Reformate tudo num pull request dedicado, faça o merge rapidamente para minimizar conflitos com as branches abertas e, a partir daí, exija SQL formatado. O Git também pode ignorar esses commits ao calcular o blame: liste os hashes deles num arquivo como .git-blame-ignore-revs e aponte a configuração blame.ignoreRevsFile para ele. Algumas plataformas de hospedagem leem esse arquivo automaticamente na visualização de blame.

A formatação muda o layout, nunca o significado. Mesmo assim, depois de reformatar um arquivo grande, rode as consultas ou a suíte de testes uma vez antes do commit. É uma verificação barata de que nada foi colado no lugar errado.

Mantenha migrations e views em arquivos

SQL que existe apenas no banco de dados, como uma view criada à mão em produção, ou numa página de wiki, não tem histórico nem revisão. Mantenha toda mudança de schema e toda view, função e consulta de relatório importante num arquivo sob controle de versão:

  • Migrations: um arquivo por mudança, com um nome que fique em ordem quando classificado (por exemplo, um prefixo com número de versão, como o Flyway usa). Nunca edite uma migration que já foi aplicada num ambiente compartilhado; escreva uma nova.
  • Views e funções: mantenha a definição atual num arquivo, de preferência como CREATE OR REPLACE quando o seu banco suportar, para que uma mudança apareça como um diff normal da definição.
  • Consultas de relatórios e de processos batch: uma consulta por arquivo é mais fácil de revisar, comparar e testar do que consultas montadas a partir de strings dentro do código da aplicação.

Uma definição de view mantida num arquivo e formatada uma vez gera diffs limpos sempre que a definição muda:

Antes
create or replace view open_order_summary as select c.customer_id, c.customer_name, count(o.order_id) as open_orders, sum(o.total_amount) as open_amount from customers c join orders o on o.customer_id = c.customer_id where o.status = 'open' group by c.customer_id, c.customer_name;
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
CREATE OR REPLACE VIEW open_order_summary AS
SELECT c.customer_id,
    c.customer_name,
    COUNT(o.order_id) AS open_orders,
    SUM(o.total_amount) AS open_amount
FROM customers c
    JOIN orders o
        ON  o.customer_id = c.customer_id
WHERE o.status = 'open'
GROUP BY c.customer_id,
    c.customer_name;

Com uma coluna por linha, adicionar uma coluna a esta view gera um diff de uma ou duas linhas, dependendo do estilo de vírgula. Um arquivo de migration também deve se explicar sozinho: um comentário de cabeçalho dizendo o que a mudança faz e como as linhas existentes são tratadas poupa muito trabalho de adivinhação a quem revisa.

-- V42__add_currency_code_to_orders.sql
-- Adds the order currency. Existing rows are backfilled with 'USD'.

ALTER TABLE orders ADD COLUMN currency_code CHAR(3);

UPDATE orders
SET currency_code = 'USD'
WHERE currency_code IS NULL;

ALTER TABLE orders ALTER COLUMN currency_code SET NOT NULL;

O checklist de formatação de migrations repassa os pontos a verificar antes de fazer o merge de uma migration.

Um checklist de pull request para SQL

A formatação remove o ruído; estas são as coisas que um revisor deve procurar depois disso. Elas se aplicam igualmente a migrations, correções de dados e consultas da aplicação.

  • Listas de colunas explícitas. Nada de SELECT * no código da aplicação ou em views, e nada de INSERT sem lista de colunas. Os dois mudam o comportamento silenciosamente quando uma coluna é adicionada ou reordenada.
  • Uma cláusula WHERE em todo UPDATE e DELETE. Confira se ela está lá e se corresponde exatamente às linhas pretendidas. Em correções de dados, peça a contagem de linhas esperada e a consulta usada para obtê-la.
  • Impacto nos índices. Novos filtros, joins e ordenações em tabelas grandes podem precisar de um índice. Novos índices também têm custo: eles deixam as escritas mais lentas, e criar um índice numa tabela grande pode bloqueá-la ou levar muito tempo, dependendo do banco de dados e das opções usadas.
  • Limites das transações. Comandos que precisam ter sucesso ou falhar juntos pertencem a uma única transação. Comandos demorados dentro de uma transação seguram locks durante toda a sua duração. Saiba se a sua ferramenta de migration envolve cada arquivo numa transação e se o seu banco de dados permite DDL dentro de uma.
  • Locks e volume das alterações de dados. Um update de milhões de linhas num único comando pode bloquear outros trabalhos e fazer o log de transações crescer. Backfills grandes costumam ser mais seguros em lotes.
  • Tratamento de NULL. Condições como column <> 'x' não retornam as linhas em que a coluna é NULL. Confira se essa é a intenção.
  • Formatação e nomenclatura. O SQL segue as opções documentadas, e as novas tabelas, colunas e constraints seguem as convenções de nomenclatura.

Uma correção de dados escrita para ser revisável traz as suas próprias evidências: uma contagem das linhas afetadas, a alteração em si com um WHERE preciso e uma transação explícita, para que quem a executa possa comparar as contagens antes do commit.

BEGIN;

-- Expected: about 1,200 rows (checked with the SELECT below on a recent copy of production)
SELECT count(*)
FROM orders
WHERE status = 'pending'
AND created_at < DATE '2026-09-01';

UPDATE orders
SET status = 'cancelled',
    updated_at = now()
WHERE status = 'pending'
AND created_at < DATE '2026-09-01';

-- Compare the reported row count with the SELECT above, then:
COMMIT;

Resumo

  • Combinem um único formato e registrem os valores das cinco opções no repositório.
  • Formate o SQL antes do commit, para que os diffs só mostrem mudanças reais.
  • Mantenha a reformatação num commit próprio, separado das mudanças de lógica.
  • Mantenha migrations, views e consultas importantes em arquivos versionados.
  • Revise o SQL em busca de listas de colunas, cláusulas WHERE, impacto nos índices e limites das transações, não de layout.

Para o layout das consultas grandes que aparecem nas revisões, veja como formatar consultas complexas e, para os hábitos que os revisores mais apontam, erros comuns de legibilidade em SQL.