Guia de estilo SQL
Um conjunto prático de convenções de SQL para nomenclatura, caixa das palavras-chave, indentação, aliases, joins, comentários e CTEs. Cada regra vem acompanhada do motivo por trás dela, para que o seu time possa adotá-la, adaptá-la ou rejeitá-la de forma consciente.
Um guia de estilo só é útil se as pessoas realmente o seguirem. As convenções abaixo são as que mais compensam em bases de código reais: deixam as consultas mais fáceis de ler, reduzem o tamanho dos diffs e eliminam categorias inteiras de erros. Onde não existe uma única resposta certa, como nomes de tabela no singular ou no plural, o guia apresenta as duas opções e pede que você escolha uma e mantenha a consistência.
Convenções de nomenclatura
Use snake_case em minúsculas para tabelas, colunas, views, funções e constraints. Os bancos de dados não concordam sobre a caixa dos identificadores: alguns convertem nomes sem aspas para minúsculas, outros para maiúsculas, e em alguns o comportamento depende do sistema operacional ou de uma configuração de collation. Nomes com letras maiúsculas e minúsculas misturadas só sobrevivem se você colocar aspas em todos os lugares. O PostgreSQL, por exemplo, converte nomes sem aspas para minúsculas, então uma tabela criada como "CustomerOrder" precisa ficar entre aspas em todas as consultas, para sempre. O snake_case evita o problema e fica legível em qualquer ferramenta.
- Evite palavras reservadas como nomes (
user,order,group,month). Elas exigem aspas em alguns bancos e confundem quem lê em todos eles.app_useroucustomer_orderé mais claro. - Dê às colunas nomes que digam o que elas guardam, não qual é o seu tipo:
created_at,total_amount,country_code. Use o prefixois_ouhas_em booleanos:is_paid,has_discount. - Use o mesmo nome para a mesma coisa em todo lugar. Se a chave é
customer_idemcustomer, a chave estrangeira nas outras tabelas também écustomer_id, e nãocustouclient_ref. - Evite abreviações, a menos que sejam universais no seu domínio.
qtyeconomiza três caracteres e custa um segundo de reflexão a cada pessoa nova que lê o código.
Nomes de tabela no singular ou no plural
As duas convenções são comuns e as duas são defensáveis:
- Singular (
customer,customer_order) trata a tabela como a definição de uma entidade. Os nomes se combinam com naturalidade (customer_address), coincidem com os nomes de classe na maioria dos ORMs e evitam plurais irregulares comoperson/peopleoustatus/statuses. - Plural (
customers,orders) trata a tabela como uma coleção de linhas, o que soa natural numa consulta:FROM orders. Muitos frameworks geram nomes no plural por padrão.
A escolha importa muito menos do que a consistência. Um schema com customer, orders e product_list obriga todo mundo a conferir cada nome. Escolha uma regra, documente-a e aplique-a às novas tabelas. Os exemplos deste site usam principalmente nomes no plural, porque eles soam naturais em consultas curtas; nada no formatador depende dessa escolha.
Nomes de chaves e constraints
Dê um nome explícito a toda constraint e a todo índice. Os nomes gerados automaticamente variam entre bancos de dados e entre ambientes, o que transforma um simples DROP CONSTRAINT numa migration em uma investigação. Um prefixo que indica o tipo do objeto, seguido da tabela e das colunas, fica fácil de ler nas mensagens de erro:
| Objeto | Padrão | Exemplo |
|---|---|---|
| Chave primária | pk_<table> | pk_customer |
| Chave estrangeira | fk_<table>_<referenced_table> | fk_customer_order_customer |
| Unique | uq_<table>_<columns> | uq_customer_email |
| Check | ck_<table>_<column_or_rule> | ck_customer_country_code |
| Índice | ix_<table>_<columns> | ix_customer_order_customer_id |
Fique atento aos limites de tamanho: versões mais antigas do Oracle permitem 30 caracteres por identificador, o PostgreSQL 63 e o MySQL 64, então nomes longos de tabelas e colunas podem precisar de uma abreviação mais curta, combinada com o time, dentro dos nomes de constraints. Veja uma definição de tabela escrita à mão que segue essas regras:
CREATE TABLE customer (
customer_id BIGINT GENERATED ALWAYS AS IDENTITY,
email VARCHAR(255) NOT NULL,
country_code CHAR(2) NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
CONSTRAINT pk_customer PRIMARY KEY (customer_id),
CONSTRAINT uq_customer_email UNIQUE (email),
CONSTRAINT ck_customer_country_code CHECK (country_code = upper(country_code))
);
CREATE TABLE customer_order (
customer_order_id BIGINT GENERATED ALWAYS AS IDENTITY,
customer_id BIGINT NOT NULL,
total_amount NUMERIC(12,2) NOT NULL,
is_paid BOOLEAN NOT NULL DEFAULT false,
CONSTRAINT pk_customer_order PRIMARY KEY (customer_order_id),
CONSTRAINT fk_customer_order_customer
FOREIGN KEY (customer_id) REFERENCES customer (customer_id)
);
CREATE INDEX ix_customer_order_customer_id ON customer_order (customer_id);O alinhamento das colunas nesse bloco foi feito à mão. Para DDL, muitos times mantêm as definições de tabela alinhadas manualmente assim nos arquivos de migration, em vez de passá-las por um formatador. O SQL Format organiza, sim, o CREATE TABLE, com uma definição de coluna por linha e parâmetros de tipo como NUMERIC(12, 2) mantidos na mesma linha, mas não alinha as colunas numa grade.
Caixa das palavras-chave
Escreva as palavras-chave do SQL em uma caixa e os identificadores em outra, para que a estrutura do comando se destaque dos nomes que aparecem nele. A escolha tradicional é palavras-chave em maiúsculas com identificadores em minúsculas; tudo em minúsculas também é popular, principalmente em bases de código em que o SQL fica dentro do código da aplicação, com realce de sintaxe. O que prejudica a leitura é misturar as duas coisas no mesmo arquivo.
Você não precisa redigitar nada para chegar lá. A opção Palavras-chave reescreve a caixa das palavras-chave para você:
select customer_id, count(*) as order_count, sum(total_amount) as revenue from orders where status = 'shipped' and created_at >= date '2026-01-01' group by customer_id having count(*) > 5 order by revenue desc;SELECT customer_id,
COUNT(*) AS order_count,
SUM(total_amount) AS revenue
FROM orders
WHERE status = 'shipped'
AND created_at >= DATE '2026-01-01'
GROUP BY customer_id
HAVING COUNT(*) > 5
ORDER BY revenue DESC;Nesta saída, as palavras-chave e as funções comuns (COUNT, SUM, CURRENT_DATE) estão em maiúsculas, enquanto os nomes de colunas e o literal de string não foram alterados. Com case=lower, a mesma consulta sai inteiramente em minúsculas. Um detalhe importante: um identificador sem aspas e não qualificado que também é palavra-chave, como um alias chamado month, segue a caixa das palavras-chave, o que é mais um motivo para evitar palavras reservadas como nomes de colunas. Nomes entre aspas e nomes qualificados como o.month nunca têm a caixa alterada.
Indentação e alinhamento
Existem duas grandes famílias de layout:
- Cláusulas alinhadas à esquerda: cada cláusula (
SELECT,FROM,WHERE,GROUP BY,ORDER BY) começa na mesma coluna e o seu conteúdo é indentado com um número fixo de espaços. É simples de digitar e funciona em qualquer editor. - Alinhamento em "rio": as palavras-chave são alinhadas à direita, de modo que o corpo das cláusulas forma uma linha vertical reta. Fica bonito, mas cada nova palavra-chave de tamanho diferente pode obrigar você a realinhar tudo, e os editores raramente ajudam.
-- "River" style: keywords right-aligned so the clause bodies line up.
SELECT o.order_id,
c.customer_name,
o.total_amount
FROM orders AS o
JOIN customers AS c
ON c.customer_id = o.customer_id
WHERE o.status = 'open'
AND o.total_amount > 100
ORDER BY o.created_at;O SQL Format usa a primeira família. Cada cláusula principal começa numa nova linha, o primeiro item do select fica na linha do SELECT e os demais vão um por linha, indentados. (Quando o primeiro item começa com uma chamada de função, um CASE ou um parêntese, ele vai para a linha seguinte junto com os outros.) Os joins ficam indentados sob o FROM, com a condição ON indentada um nível a mais, e cada condição AND começa na sua própria linha:
select o.order_id, c.customer_name, p.product_name, oi.quantity from orders o join customers c on c.customer_id = o.customer_id join order_items oi on oi.order_id = o.order_id left join products p on p.product_id = oi.product_id where o.status = 'open';SELECT o.order_id,
c.customer_name,
p.product_name,
oi.quantity
FROM orders o
JOIN customers c
ON c.customer_id = o.customer_id
JOIN order_items oi
ON oi.order_id = o.order_id
LEFT JOIN products p
ON p.product_id = oi.product_id
WHERE o.status = 'open';Seja qual for a sua escolha, use espaços em vez de tabs, para que o layout fique igual em qualquer editor, ferramenta de revisão de código e terminal. O formatador substitui tabs por espaços, exceto dentro de literais de string e de identificadores entre aspas, conforme a configuração Espaços de indentação, que é quatro por padrão. Mantenha também as linhas com uma largura razoável; a opção Quantidade máxima de colunas quebra qualquer linha que ultrapasse o limite.
Aliases com significado
Aliases existem para economizar digitação sem perder o significado. Letras isoladas tiradas do alfabeto (a, b, c) economizam digitação e perdem todo o significado: no terceiro join, ninguém lembra mais que c é a tabela de itens do pedido.
SELECT a.id, b.name, c.qty, d.name
FROM orders a, customers b, order_items c, products d
WHERE a.cust = b.id
AND c.ord = a.id
AND d.id = c.prod
AND status = 'open';Opções melhores, em ordem de preferência:
- Iniciais do nome da tabela:
oparaorders,cparacustomers,oiparaorder_items. - Uma palavra curta quando as iniciais coincidem ou a mesma tabela aparece duas vezes:
buyeresellerpara dois joins comcustomers, ouparentechildpara um self-join. - Nenhum alias em uma consulta de uma única tabela.
Use o mesmo alias para a mesma tabela em toda a base de código, para que oi signifique sempre order_items. Nos aliases de coluna, escreva AS explicitamente. Ele é opcional na maioria dos bancos, mas sem ele uma vírgula esquecida transforma silenciosamente a coluna seguinte em alias, uma armadilha tratada em detalhes no guia de vírgulas à esquerda e à direita.
JOIN ... ON explícito em vez de joins com vírgula
A consulta com aliases ruins mostrada acima também usa a sintaxe antiga de join com vírgula: todas as tabelas listadas no FROM, separadas por vírgulas, com as condições de join misturadas no WHERE junto com os filtros de verdade. Funciona, mas tem custos reais:
- Esquecer uma condição de join gera um produto cartesiano, e nada na sintaxe aponta para a linha que está faltando.
- Condições de join e filtros ficam intercalados, então quem lê precisa descobrir qual predicado faz o quê.
- Outer joins simplesmente não podem ser expressos na sintaxe padrão com vírgula.
Com JOIN ... ON explícito, cada tabela é introduzida junto com a condição que a liga às outras, e o WHERE contém apenas filtros. O exemplo de join formatado na seção anterior mostra essa reescrita: cada join fica na sua própria linha, com a cláusula ON logo abaixo. Prefira ON a USING ou NATURAL JOIN em código compartilhado; o NATURAL JOIN, em especial, faz a junção por todas as colunas com o mesmo nome, então adicionar um inocente created_at a uma tabela pode mudar o resultado de uma consulta que nem menciona essa coluna.
Qualificando as colunas
Assim que uma consulta lê de mais de uma tabela, prefixe todas as colunas com o alias da tabela, inclusive as colunas no WHERE, no GROUP BY e no ORDER BY.
-- Which table does each column come from?
SELECT order_id, customer_name, total_amount, created_at
FROM orders
JOIN customers USING (customer_id)
WHERE status = 'open';Quem lê não tem como saber de onde vêm status ou created_at sem abrir o schema. Pior: a consulta pode quebrar mais tarde. Se alguém adicionar uma coluna created_at em customers, a referência não qualificada fica ambígua e a consulta falha. Colunas qualificadas deixam a consulta autoexplicativa e resistente a mudanças no schema. Numa consulta de uma única tabela, a qualificação é opcional.
Comentários
O SQL tem duas formas de comentário:
--vai até o fim da linha. Use-o na maioria dos comentários: um cabeçalho acima de uma consulta, uma observação acima de uma condição complicada./* ... */pode ocupar várias linhas ou ficar no meio de uma. É útil para uma observação curta dentro de uma expressão ou para desativar temporariamente um bloco, e é mais seguro que--quando o SQL pode acabar achatado numa única linha por alguma ferramenta, já que um comentário de linha engoliria o restante do comando.
Comente o porquê, não o quê. Um -- join customers acima de um join não acrescenta nada. Coisas que merecem um comentário: a regra de negócio por trás de um filtro, por que um LEFT JOIN é necessário, uma peculiaridade conhecida dos dados, o número esperado de linhas numa correção de dados, ou qual relatório ou job usa a consulta. O formatador mantém as duas formas de comentário no lugar:
-- Active customers with at least one order in the last 90 days.
-- Used by the weekly retention report.
select c.customer_id, c.email, max(o.created_at) as last_order_at from customers c join orders o on o.customer_id = c.customer_id where c.deleted_at is null /* soft-deleted rows stay in the table */ and o.created_at >= current_date - 90 group by c.customer_id, c.email;-- Active customers with at least one order in the last 90 days.
-- Used by the weekly retention report.
SELECT c.customer_id,
c.email,
MAX(o.created_at) AS last_order_at
FROM customers c
JOIN orders o
ON o.customer_id = c.customer_id
WHERE c.deleted_at IS NULL /* soft-deleted rows stay in the table */
AND o.created_at >= CURRENT_DATE - 90
GROUP BY c.customer_id,
c.email;CTEs ou subconsultas: qual é mais legível
Subconsultas aninhadas são lidas de dentro para fora: para entender a consulta externa, você precisa primeiro encontrar o SELECT mais interno e voltar a partir dele. Veja uma tabela derivada com dois níveis:
SELECT c.customer_name,
ct.total_30d
FROM (
SELECT customer_id,
SUM(total_amount) AS total_30d
FROM (
SELECT customer_id,
total_amount
FROM orders
WHERE created_at >= CURRENT_DATE - 30
) ro
GROUP BY customer_id
) ct
JOIN customers c
ON c.customer_id = ct.customer_id
WHERE ct.total_30d > 1000
ORDER BY ct.total_30d DESC;A mesma lógica escrita com common table expressions é lida de cima para baixo, e cada etapa tem um nome que documenta o seu propósito:
WITH recent_orders AS (
SELECT customer_id,
total_amount
FROM orders
WHERE created_at >= CURRENT_DATE - 30
),
customer_totals AS (
SELECT customer_id,
SUM(total_amount) AS total_30d
FROM recent_orders
GROUP BY customer_id
)
SELECT c.customer_name,
ct.total_30d
FROM customer_totals ct
JOIN customers c
ON c.customer_id = ct.customer_id
WHERE ct.total_30d > 1000
ORDER BY ct.total_30d DESC;Prefira uma CTE quando uma etapa tem um nome com significado, quando é referenciada mais de uma vez ou quando o aninhamento passa de um nível. Uma subconsulta curta numa lista IN ou uma única tabela derivada muitas vezes fica bem inline. Lembre-se de que o otimizador pode tratar CTEs e subconsultas de forma diferente dependendo do banco de dados e da versão, então confira o plano de execução quando a reescrita for sensível a desempenho. O guia de formatação de consultas complexas vai além, com cadeias de CTEs, CASE, funções de janela e UNION.
Adotando um guia de estilo no time
Documente as regras no repositório, mantenha-as curtas e deixe a parte mecânica para uma ferramenta. Layout, caixa das palavras-chave e posição das vírgulas são exatamente o tipo de regra que um formatador aplica por você; nomenclatura, aliases e comentários ainda dependem de uma pessoa. O guia de revisão de código e controle de versão explica como implantar um formato comum sem inundar o histórico com ruído, e erros comuns de legibilidade em SQL lista os hábitos que vale a pena apontar na revisão.