trilha

Tecnologias/05 - Comparações5 min

Dialetos SQL na Prática

Conceito

Nota de referência: a mesma operação escrita nos três dialetos. Serve para consulta durante migração, code review e para calibrar a expectativa de portabilidade — que é mais baixa do que parece.

O padrão ANSI SQL cobre o núcleo (SELECT, JOIN, GROUP BY, transação). Tudo o que um sistema real precisa — paginação, upsert, identidade, data, JSON, agregação de string — foi implementado antes ou fora do padrão, e por isso divergiu.

Paginação

-- PostgreSQL / MySQL
SELECT * FROM pedidos ORDER BY id LIMIT 20 OFFSET 40;

-- Oracle 12c+ (padrão ANSI, também funciona no Postgres)
SELECT * FROM pedidos ORDER BY id OFFSET 40 ROWS FETCH FIRST 20 ROWS ONLY;

-- Oracle antigo (você vai encontrar isso em código legado)
SELECT * FROM (
  SELECT p.*, ROWNUM rn FROM (SELECT * FROM pedidos ORDER BY id) p
   WHERE ROWNUM <= 60
) WHERE rn > 40;
OFFSET grande é lento nos três

OFFSET 100000 faz o banco ler e descartar 100 mil linhas. Para listas longas, use paginação por cursor: WHERE id > :ultimo_id ORDER BY id LIMIT 20. Vale nos três e é O(log n) em vez de O(n).

Upsert

-- PostgreSQL
INSERT INTO t (k, v) VALUES ('a', 1)
ON CONFLICT (k) DO UPDATE SET v = EXCLUDED.v;

-- MySQL
INSERT INTO t (k, v) VALUES ('a', 1)
ON DUPLICATE KEY UPDATE v = VALUES(v);   -- 8.0.20+: v = new.v

-- Oracle
MERGE INTO t USING (SELECT 'a' k, 1 v FROM dual) s ON (t.k = s.k)
WHEN MATCHED THEN UPDATE SET t.v = s.v
WHEN NOT MATCHED THEN INSERT (k, v) VALUES (s.k, s.v);

Identidade / auto-incremento

-- PostgreSQL
id bigserial PRIMARY KEY                        -- forma histórica
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY  -- forma padrão, preferível

-- MySQL
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY

-- Oracle 12c+
id NUMBER GENERATED ALWAYS AS IDENTITY PRIMARY KEY
-- Oracle antigo: CREATE SEQUENCE + trigger BEFORE INSERT

Retorno da linha inserida

Como
PostgreSQL INSERT ... RETURNING id, criado_em
MySQL não existe; SELECT LAST_INSERT_ID() (só a PK, só auto-increment)
Oracle INSERT ... RETURNING id INTO :out (PL/SQL ou bind de saída no driver)

É uma das diferenças mais irritantes na prática: no Postgres você resolve "inserir e devolver o objeto criado" em uma ida ao banco; no MySQL, em duas — com uma janela de concorrência no meio.

Tabela de equivalências

Operação PostgreSQL MySQL Oracle
Concatenar a || b CONCAT(a,b) (|| é OR!) a || b ou CONCAT
Agora now(), CURRENT_TIMESTAMP NOW() SYSDATE, SYSTIMESTAMP
Data sem hora date DATE DATE inclui hora (use TRUNC)
Booleano boolean TINYINT(1) / BOOL NUMBER(1) ou CHAR(1); BOOLEAN em SQL só no 23ai+
Texto ilimitado text TEXT/LONGTEXT CLOB (VARCHAR2 até 4000/32767)
Aspas em identificador "tabela" `tabela` "TABELA" (vira MAIÚSCULA se sem aspas)
String vazia '' ≠ NULL '' ≠ NULL '' = NULL
Agregar strings string_agg(x, ',') GROUP_CONCAT(x) LISTAGG(x, ',')
Primeiro não-nulo coalesce coalesce/IFNULL coalesce/NVL
Casting x::int ou CAST CAST(x AS SIGNED) CAST / TO_NUMBER
Tabela dummy SELECT 1 SELECT 1 SELECT 1 FROM dual
Somar dias d + interval '7 day' DATE_ADD(d, INTERVAL 7 DAY) d + 7
Diferença de datas a - b (interval) DATEDIFF(a,b) a - b (em dias, número)
JSON: extrair j->>'campo' j->>'$.campo' JSON_VALUE(j, '$.campo')
Random random() RAND() DBMS_RANDOM.VALUE
Regex ~, regexp_replace REGEXP_LIKE REGEXP_LIKE
Fila com skip FOR UPDATE SKIP LOCKED FOR UPDATE SKIP LOCKED (8.0+) FOR UPDATE SKIP LOCKED
Limitar UPDATE não suporta LIMIT UPDATE ... LIMIT 100 não suporta (use ROWNUM na subquery)
Ver plano EXPLAIN (ANALYZE, BUFFERS) EXPLAIN ANALYZE (8.0.18+) EXPLAIN PLAN + DBMS_XPLAN / AUTOTRACE

Sensibilidade a maiúsculas — a diferença silenciosa

Nome de tabela Comparação de texto (padrão)
PostgreSQL case-sensitive; sem aspas vira minúscula sensível: 'A' <> 'a'
MySQL depende do sistema de arquivos (Linux sim, macOS/Windows não) insensível (collation _ci)
Oracle sem aspas vira MAIÚSCULA sensível (salvo collation nova)

É a origem de dois bugs clássicos: o código que funciona no Mac do dev e quebra no Linux da produção (MySQL), e o login por e-mail que aceita Joao@x.com no MySQL e recusa no Postgres. No Postgres, a solução é índice por expressão — CREATE UNIQUE INDEX ON users (lower(email)) — ou o tipo citext.

Trade-offs

ORM não resolve, adia. Um ORM traduz SELECT/INSERT básico e entrega ao driver as partes divergentes: tipos (data, boolean, JSON), locks, paginação, upsert e migração. O teste honesto: rode a suíte contra o banco real, no dialeto real, num contêiner (Testcontainers). SQLite em memória "porque é mais rápido" é a forma mais eficiente de descobrir a diferença em produção. Ver 2. Teste de Integração.

SQL portável é SQL pobre. Se a regra do projeto é "tem que rodar em qualquer banco", você abriu mão de jsonb, índice parcial, RETURNING, tipos de intervalo e particionamento — e provavelmente reimplementou uma parte disso na aplicação, pior.

O trade-off central: portabilidade de banco é um requisito que raramente se concretiza e que sempre cobra. Só pague por ele se de fato existirem dois bancos no seu contrato — vender software instalado no cliente, por exemplo.

Exemplo prático

A mesma pergunta de negócio — "os 3 maiores pedidos de cada cliente no último mês" — escrita de forma quase idêntica nos três, porque window function é padrão e os três suportam (MySQL desde o 8.0):

WITH ranqueado AS (
  SELECT cliente_id, id, total,
         ROW_NUMBER() OVER (PARTITION BY cliente_id ORDER BY total DESC) AS rn
    FROM pedidos
   WHERE criado_em >= /* aqui o dialeto entra */
)
SELECT cliente_id, id, total
  FROM ranqueado
 WHERE rn <= 3
 ORDER BY cliente_id, rn;

Só a data divergiu:

-- PostgreSQL: now() - interval '1 month'
-- MySQL:      DATE_SUB(NOW(), INTERVAL 1 MONTH)
-- Oracle:     ADD_MONTHS(SYSDATE, -1)

A lição é útil: o SQL analítico moderno (CTE, window function, FILTER, GROUPING SETS) é a parte mais portável do SQL. O que diverge é a borda — data, JSON, identidade, upsert, lock. Concentre a portabilidade onde ela é barata e aceite dialeto no resto. Ver 2. Agregações.

Aprofundamento aplicado

Paginação precisa de desempate estável

Ordenar só por data não define a ordem entre linhas com a mesma data. Um cursor deve conter os valores que determinam a ordenação completa. Exemplo de predicado para ordem crescente por criado_em, id, usando parâmetros nomeados ilustrativos:

WHERE criado_em > :ultima_data
   OR (criado_em = :ultima_data AND id > :ultimo_id)
ORDER BY criado_em, id

A cláusula de limite e a sintaxe de parâmetros devem seguir o banco e o driver. O cursor evita descartar todas as páginas anteriores quando há acesso indexado adequado; ele não fixa automaticamente um snapshot entre requisições HTTP.

Portabilidade inclui semântica

Testes de migração devem incluir NULL, string vazia, precisão decimal, comparação de texto, timezone e ordenação. Um SQL que compila pode devolver um conjunto diferente por causa desses detalhes. Converta datas e números de maneira deliberada, sem depender de formato regional implícito.

Exercício: crie três linhas com o mesmo timestamp e navegue com páginas de duas. Resultado esperado: cada ID aparece uma vez com o desempate, sem depender da ordem física da tabela.

Referência: ordenação de resultados no PostgreSQL.

Relacionado


Parte de MOC Comparações · Comandos SQL (PostgreSQL)

Buscar

Busca por título, seção e texto das notas