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_atTodas 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 blameatribui 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ção | Padrão | O que ela controla |
|---|---|---|
| Espaços de indentação | 4 | Espaç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 colunas | 120 | Linhas maiores que esse valor são quebradas; 0 desativa a quebra. |
| Vírgulas | Depois | Vírgulas à direita (Depois) ou à esquerda (Antes) nas listas. |
| Palavras-chave | Maiúsculas | Palavras-chave em maiúsculas ou em minúsculas. |
| Parênteses em nova linha | desativada | Se 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:
- 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.
- 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 REPLACEquando 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:
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;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 deINSERTsem 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.