Diferenças entre dialetos SQL que afetam a formatação

O padrão SQL cobre quase tudo o que uma consulta típica faz, mas PostgreSQL, MySQL e MariaDB, SQL Server, Oracle, IBM Db2 e SQLite ainda diferem justamente nos pontos que importam para o layout: como os identificadores são colocados entre aspas, como as linhas são limitadas, como as strings são concatenadas, como o código procedural é delimitado e como os scripts são divididos em lotes. Este guia compara esses bancos e explica o que o SQL Format faz com cada caso.

O que o SQL Format faz com os dialetos

Antes da comparação, o resumo honesto. O SQL Format não tem seletor de dialeto. Ele formata o SQL que esses bancos de dados têm em comum: SELECT, joins, subconsultas, CTEs, CASE, INSERT, UPDATE, DELETE e comentários. Além disso, ele suporta alguns extras do PostgreSQL, descritos em formatando consultas PostgreSQL e PL/pgSQL: casts com ::, parâmetros $1 e corpos de função entre dollar quotes.

Nomes entre crases do MySQL e nomes entre colchetes do SQL Server ([bracket]) são reconhecidos como identificadores entre aspas, então são mantidos inteiros e nunca têm a caixa alterada, como mostrado abaixo. Ele não dá tratamento especial ao separador de lotes GO, ao comando DELIMITER do cliente MySQL nem a blocos procedurais BEGIN … END fora dos dollar quotes do PostgreSQL. Se o seu código usa esses recursos, as dicas práticas de cada seção abaixo mostram como trabalhar junto com o formatador, e não contra ele.

Comparação rápida

A tabela resume as versões atuais mais usadas. Versões mais antigas podem não ter alguns recursos, e vários bancos de dados têm modos de configuração que alteram os padrões mostrados aqui.

TemaPostgreSQLMySQL / MariaDBSQL ServerOracleDb2SQLite
Identificador entre aspas"name"`name`[name] ou "name""name""name""name" (também aceita os outros)
Nomes sem aspasconvertidos para minúsculasnomes de colunas não diferenciam maiúsculas de minúsculas; nomes de tabelas dependem da plataforma e da configuraçãomantidos como escritos; a comparação segue a collationconvertidos para maiúsculasconvertidos para maiúsculasnão diferenciam maiúsculas de minúsculas
Limitação de linhasLIMIT/OFFSET, FETCH FIRSTLIMIT/OFFSETTOP, OFFSET … FETCH NEXTFETCH FIRSTFETCH FIRSTLIMIT/OFFSET
Concatenação de strings||, concat()CONCAT()+, CONCAT()||||||
Linhas de volta do DMLRETURNINGnenhum no MySQL; RETURNING em alguns comandos do MariaDBOUTPUTRETURNING … INTOSELECT … FROM FINAL TABLE (…)RETURNING
Linguagem proceduralPL/pgSQL em corpos $$stored programs com BEGIN … ENDT-SQLPL/SQLSQL PLnenhuma (só corpos de trigger)
Divisão de scripts;;, DELIMITER do clienteGO nas ferramentas cliente;, / depois de blocos PL/SQL; ou um terminador personalizado no CLP;

Aspas em identificadores e conversão de caixa

Identificadores entre aspas permitem usar espaços, palavras reservadas ou maiúsculas e minúsculas misturadas nos nomes. Cada banco de dados tem o seu caractere de aspas preferido:

-- MySQL / MariaDB (default sql_mode)
SELECT `order id`, `status` FROM `orders`;

-- SQL Server (brackets; double quotes also work with QUOTED_IDENTIFIER ON)
SELECT [order id], [status] FROM [dbo].[orders];

-- PostgreSQL, Oracle, Db2, SQLite (standard double quotes)
SELECT "order id", "status" FROM "orders";

O padrão SQL usa aspas duplas. O MySQL trata aspas duplas como delimitadores de string, a menos que o SQL mode ANSI_QUOTES esteja ativado, e é por isso que o código MySQL é cheio de crases. O SQL Server aceita aspas duplas para identificadores quando QUOTED_IDENTIFIER está ligado, que é o padrão nas bibliotecas cliente modernas, mas os colchetes continuam sendo o estilo da casa na maior parte do código T-SQL.

A conversão de caixa é a outra metade da história. Um nome sem aspas é convertido para minúsculas no PostgreSQL e para maiúsculas no Oracle e no Db2. Assim, CREATE TABLE Orders cria orders no PostgreSQL e ORDERS no Oracle, e nos dois casos SELECT * FROM orders funciona. Já um nome criado com aspas e caixa mista precisa ser colocado entre aspas do mesmo jeito em toda consulta. O hábito portável é simples: nomes em snake_case minúsculo que não sejam palavras reservadas, nunca entre aspas.

O SQL Format reconhece os três estilos de aspas. Um nome entre aspas é copiado exatamente como foi digitado: ele nunca é dividido, e a opção Palavras-chave nunca o altera, mesmo quando a palavra dentro dele é uma palavra-chave, como [Status] ou `group`. Veja uma consulta do SQL Server e uma do MySQL formatadas com palavras-chave em minúsculas:

Antes
select o.[Order Id], o.[Status], c.[Name] from [dbo].[orders] o join [dbo].[customers] c on c.[Id] = o.[CustomerId] where o.[Status] = 'open';
select `order id`, `group`, `Status` from `orders` where `Status` = 'open'
Depois, formatado pelo SQL Format
Opções: Espaços de indentação 4 · Quantidade máxima de colunas 120 · Vírgulas: Depois · Palavras-chave: Minúsculas · Parênteses em nova linha: não
select o.[Order Id],
    o.[Status],
    c.[Name]
from [dbo].[orders] o
    join [dbo].[customers] c
        on  c.[Id] = o.[CustomerId]
where o.[Status] = 'open';
select `order id`,
    `group`,
    `Status`
from `orders`
where `Status` = 'open'

O formatador mantém os nomes entre aspas seguros, mas não os torna portáveis. Crases e colchetes não são SQL padrão, e um nome entre aspas com caixa mista precisa ser escrito entre aspas do mesmo jeito para sempre; por isso, nomes em snake_case sem aspas continuam sendo o hábito que elimina a questão por completo.

Limitação de linhas: LIMIT, TOP e FETCH FIRST

Limitar um resultado às primeiras linhas é a diferença mais visível entre os dialetos:

-- PostgreSQL, MySQL, MariaDB, SQLite
SELECT id, title FROM articles ORDER BY published_at DESC LIMIT 10 OFFSET 20;

-- SQL Server
SELECT TOP (10) id, title FROM articles ORDER BY published_at DESC;
SELECT id, title FROM articles ORDER BY published_at DESC
OFFSET 20 ROWS FETCH NEXT 10 ROWS ONLY;

-- Standard form: Oracle 12c and later, Db2, PostgreSQL
SELECT id, title FROM articles ORDER BY published_at DESC
OFFSET 20 ROWS FETCH FIRST 10 ROWS ONLY;

A forma padrão é OFFSET n ROWS FETCH FIRST m ROWS ONLY. O Oracle a suporta a partir do 12c (antes disso, ROWNUM numa subconsulta era o contorno habitual), o Db2 suporta FETCH FIRST há muito tempo e o PostgreSQL a aceita junto com LIMIT. O SQL Server suporta a forma OFFSET … FETCH NEXT apenas depois de um ORDER BY e mantém o TOP para o caso simples. MySQL e SQLite usam LIMIT; versões recentes do MariaDB também aceitam a forma padrão.

Seja qual for a sintaxe, sempre combine a limitação de linhas com um ORDER BY único, por exemplo acrescentando a chave primária como última coluna de ordenação. Sem isso, "as dez primeiras linhas" não está definido, e as páginas podem repetir ou pular linhas.

Concatenação de strings

A concatenação parece trivial e é uma fonte frequente de bugs em migrações entre bancos:

-- PostgreSQL, Oracle, Db2, SQLite
SELECT first_name || ' ' || last_name AS full_name FROM customers;

-- SQL Server
SELECT first_name + ' ' + last_name AS full_name FROM customers;

-- MySQL / MariaDB (|| means OR unless PIPES_AS_CONCAT is set)
SELECT CONCAT(first_name, ' ', last_name) AS full_name FROM customers;

No MySQL, || significa OR lógico, a menos que o SQL mode PIPES_AS_CONCAT esteja definido, então 'a' || 'b' não retorna ab. No SQL Server, + concatena strings mas soma números, então misturar tipos exige casts explícitos. O tratamento de NULL também difere: no PostgreSQL, 'a' || NULL é NULL, enquanto o Oracle trata NULL como string vazia na concatenação e retorna 'a'. Quando o resultado importa, envolva as colunas que aceitam nulo com COALESCE.

Expressões portáveis como ||, COALESCE e CASE são formatadas da mesma maneira, não importa onde vão rodar. Os argumentos de funções são quebrados um por linha e o CASE recebe o seu próprio layout:

Antes
select c.id, c.first_name || ' ' || c.last_name as full_name, coalesce(c.phone, c.email) as contact, case when c.vip then 'VIP' else 'regular' end as tier from customers c where c.country = 'BR' order by c.last_name, c.first_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
SELECT c.id,
    c.first_name || ' ' || c.last_name AS full_name,
    COALESCE(
        c.phone,
        c.email
    ) AS contact,
    CASE WHEN c.vip THEN
        'VIP'
    ELSE
        'regular'
    END AS tier
FROM customers c
WHERE c.country = 'BR'
ORDER BY c.last_name,
    c.first_name

Recebendo linhas de volta: RETURNING e OUTPUT

Ler chaves geradas ou valores atualizados no mesmo comando economiza uma ida ao banco e evita condições de corrida. A sintaxe varia mais do que em qualquer outra área:

-- PostgreSQL, SQLite (and MariaDB for some statements)
INSERT INTO tags (name) VALUES ('urgent') RETURNING id;

-- SQL Server
INSERT INTO tags (name) OUTPUT inserted.id VALUES ('urgent');

-- Oracle, inside PL/SQL
INSERT INTO tags (name) VALUES ('urgent') RETURNING id INTO v_id;

-- Db2
SELECT id FROM FINAL TABLE (INSERT INTO tags (name) VALUES ('urgent'));

PostgreSQL e SQLite usam uma cláusula RETURNING no final. O SQL Server usa OUTPUT, colocado antes do VALUES num INSERT, com as pseudotabelas inserted e deleted. O RETURNING … INTO do Oracle grava em variáveis, então ele aparece em PL/SQL ou com bind variables. O Db2 envolve a alteração de dados em FINAL TABLE e faz o select a partir dela. O MySQL não tem equivalente e depende de funções como LAST_INSERT_ID().

Separadores de lote e terminadores

Parte do que parece SQL num script é, na verdade, uma instrução para a ferramenta cliente:

-- SQL Server (sqlcmd / SSMS): GO ends a batch, it is not T-SQL
CREATE TABLE audit_log (id int IDENTITY PRIMARY KEY, msg nvarchar(400));
GO
CREATE PROCEDURE log_msg @msg nvarchar(400) AS
BEGIN
    INSERT INTO audit_log (msg) VALUES (@msg);
END
GO

-- MySQL client: DELIMITER changes the statement terminator
DELIMITER //
CREATE PROCEDURE log_msg(IN p_msg VARCHAR(400))
BEGIN
    INSERT INTO audit_log (msg) VALUES (p_msg);
END //
DELIMITER ;

GO não é um comando T-SQL. O sqlcmd e o SQL Server Management Studio dividem o script nele e enviam cada lote separadamente, o que é necessário porque comandos como CREATE PROCEDURE precisam ser os primeiros do seu lote. O comando DELIMITER do cliente MySQL muda o terminador de comandos para que os pontos e vírgulas dentro do corpo de uma procedure não encerrem o CREATE PROCEDURE antes da hora. O SQL*Plus e o SQLcl do Oracle executam um bloco PL/SQL quando encontram uma / sozinha numa linha, e o command line processor do Db2 pode ser configurado para usar outro terminador, como @, pelo mesmo motivo.

O SQL Format divide a entrada apenas nos pontos e vírgulas. Para scripts que usam GO ou DELIMITER, formate os comandos entre os separadores e mantenha os próprios separadores nas suas próprias linhas ao remontar o script.

Blocos procedurais

Todos os principais bancos de dados, exceto o SQLite, têm uma linguagem procedural, e cada um a envolve de um jeito diferente:

-- Oracle PL/SQL anonymous block (SQL*Plus runs it after the "/")
DECLARE
    v_total NUMBER;
BEGIN
    SELECT SUM(total) INTO v_total FROM orders;
    DBMS_OUTPUT.PUT_LINE(v_total);
END;
/

-- PostgreSQL PL/pgSQL: the body is a dollar-quoted string
DO $$
DECLARE
    v_total numeric;
BEGIN
    SELECT SUM(total) INTO v_total FROM orders;
    RAISE NOTICE '%', v_total;
END
$$;

O PL/SQL do Oracle usa blocos DECLARE/BEGIN/EXCEPTION/END, e o PL/pgSQL do PostgreSQL segue o mesmo formato dentro de uma string entre dollar quotes. O SQL PL do Db2 e os stored programs do MySQL ou MariaDB usam comandos compostos BEGIN … END, com as declarações no topo e DECLARE … HANDLER para erros. O T-SQL mistura comandos procedurais diretamente com SQL e trata erros com BEGIN TRY/BEGIN CATCH. O SQLite não tem stored procedures; só os corpos de trigger usam BEGIN … END.

Desses, o SQL Format só aplica layout de bloco ao PL/pgSQL dentro de $$. Para T-SQL, PL/SQL e SQL PL, ele é mais útil nas consultas individuais dentro do bloco: cole um SELECT ou UPDATE longo, formate-o e indente o resultado no nível do bloco. As mesmas regras de layout do guia de estilo de SQL valem dentro de uma procedure como em qualquer outro lugar.

Hábitos que deixam o SQL portável e fácil de formatar

  • Use nomes em snake_case minúsculo, sem aspas, que não sejam palavras reservadas.
  • Prefira a sintaxe padrão quando todos os bancos de destino a suportarem: JOIN explícito, CASE, COALESCE, CTEs e FETCH FIRST onde estiver disponível.
  • Mantenha os comandos das ferramentas cliente (GO, DELIMITER, /) nas suas próprias linhas, para que fiquem fáceis de identificar na revisão.
  • Formate com um único conjunto documentado de opções, para que os diffs mostrem mudanças de lógica e não de estilo. O checklist de migrations trata disso para alterações de esquema.

Perguntas frequentes

Posso escolher um dialeto no SQL Format?

Não. Existe um único formatador para o SQL que os bancos de dados têm em comum, com suporte extra a casts, parâmetros, operadores e corpos PL/pgSQL entre dollar quotes do PostgreSQL. Nomes entre crases do MySQL e entre colchetes do SQL Server são mantidos inteiros.

Qual sintaxe de limitação de linhas é a mais portável?

FETCH FIRST n ROWS ONLY é o padrão SQL e funciona no PostgreSQL, no Oracle, no Db2 e em versões recentes do MariaDB; o SQL Server exige uma cláusula OFFSET antes do FETCH e um ORDER BY. MySQL e SQLite ainda exigem LIMIT, então não existe uma sintaxe única que rode em todos.

O GO faz parte do SQL do SQL Server?

Não. Ele é um separador de lotes entendido por ferramentas cliente como o sqlcmd e o SQL Server Management Studio. O servidor nunca o vê.