A maior parte da sintaxe PostgreSQL vs MySQL que você escreve todo dia é idêntica. SELECT, JOIN, WHERE, GROUP BY e INSERT básico funcionam da mesma forma em ambos. O problema são os outros 20 por cento, os cantos específicos de cada dialeto onde uma consulta que roda limpa no MySQL gera um erro de sintaxe no Postgres (e vice-versa). Este guia mapeia esses cantos com exemplos lado a lado para que você possa portar consultas sem tentativa e erro.
Se você está migrando um banco de dados, escrevendo uma consulta que precisa rodar em ambos os motores, ou apenas tentando entender por que um trecho do Stack Overflow não funciona no seu console, a resposta é quase sempre uma de um punhado de diferenças conhecidas. UPSERT, booleanos, operadores de conjunto, auto-incremento e manipulação de strings causam a grande maioria da dor na portabilidade.
PostgreSQL vs MySQL Sintaxe: Os 20% Que Realmente Quebram#
Comparações de nível de negócio entre esses dois bancos de dados focam em desempenho, licenciamento e replicação. Úteis para escolher uma stack, inúteis quando você tem uma consulta quebrada na sua frente. O que você precisa é da tabela de sintaxe.
Aqui está a versão resumida de onde os dialetos divergem. O restante deste guia expande cada linha com exemplos reais.
| Recurso | PostgreSQL | MySQL |
|---|---|---|
| UPSERT | INSERT ... ON CONFLICT ... DO UPDATE | INSERT ... ON DUPLICATE KEY UPDATE |
| Auto-incremento | SERIAL / GENERATED ... AS IDENTITY | AUTO_INCREMENT |
| Booleano | BOOLEAN nativo (true/false) | TINYINT(1) alias, armazena 0/1 |
| Operadores de conjunto | INTERSECT, EXCEPT suportados | INTERSECT/EXCEPT apenas no 8.0.31+ |
| Concatenação de strings | Operador || | CONCAT() (|| significa OR) |
| Sensibilidade a maiúsculas | identificadores convertidos para minúsculas | depende do SO e da configuração da tabela |
| Aspas | aspas duplas para identificadores | crases para identificadores |
| LIMIT/OFFSET | LIMIT n OFFSET m | LIMIT m, n ou LIMIT n OFFSET m |
Regra prática rápida: se uma consulta falhar após uma migração, verifique primeiro UPSERT, comparações booleanas e uso de aspas em identificadores. Esses três são responsáveis pela maioria das quebras.
UPSERT: ON CONFLICT vs ON DUPLICATE KEY UPDATE#
Esta é a maior pegadinha ao portar consultas, e não tem uma tradução automática limpa. Ambos os mecanismos permitem inserir uma linha e atualizá-la se uma chave já existir, mas a sintaxe é completamente diferente.
O MySQL usa ON DUPLICATE KEY UPDATE, que é acionado em qualquer colisão de chave única ou primária:
INSERT INTO users (id, email, login_count)
VALUES (1, 'a@example.com', 1)
ON DUPLICATE KEY UPDATE login_count = login_count + 1;
O PostgreSQL usa ON CONFLICT, e força você a nomear o alvo do conflito (a coluna ou restrição que você espera que colida):
INSERT INTO users (id, email, login_count)
VALUES (1, 'a@example.com', 1)
ON CONFLICT (id) DO UPDATE
SET login_count = users.login_count + 1;
Duas coisas pegam as pessoas aqui. Primeiro, o Postgres exige o alvo do conflito, enquanto o MySQL infere a partir de qualquer chave única que foi violada. Segundo, a forma de referenciar a linha recebida difere. O MySQL expõe os novos valores através de VALUES(col) (sintaxe antiga) ou um alias de linha, enquanto o Postgres os expõe através da pseudo-tabela especial EXCLUDED:
-- PostgreSQL referenciando o valor recebido
ON CONFLICT (id) DO UPDATE
SET login_count = EXCLUDED.login_count;
Se você só lembrar de uma diferença deste artigo, que seja esta. UPSERT é onde a maioria das consultas falha silenciosamente ao ser traduzida.
Auto-Incremento: SERIAL e IDENTITY vs AUTO_INCREMENT#
Chaves primárias auto-incrementadas existem em ambos os mecanismos, mas são declaradas de forma diferente.
O MySQL anexa AUTO_INCREMENT à coluna:
CREATE TABLE orders (
id INT AUTO_INCREMENT PRIMARY KEY,
total DECIMAL(10,2)
);
O PostgreSQL tem duas abordagens. A mais antiga e amplamente vista é SERIAL, que é uma abreviação que cria uma sequência nos bastidores:
CREATE TABLE orders (
id SERIAL PRIMARY KEY,
total NUMERIC(10,2)
);
A abordagem moderna e compatível com o SQL padrão (recomendada no Postgres 10 e posteriores) é GENERATED ... AS IDENTITY:
CREATE TABLE orders (
id INT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
total NUMERIC(10,2)
);
Uma diferença sutil relacionada: DECIMAL do MySQL e NUMERIC do Postgres são funcionalmente o mesmo tipo de precisão arbitrária, e ambos aceitam qualquer uma das palavras-chave. Mas NUMERIC é o nome do SQL padrão que você verá na documentação do Postgres, portanto não se surpreenda ao portar no sentido contrário.
Booleanos: BOOLEAN nativo vs TINYINT(1)#
O PostgreSQL tem um tipo BOOLEAN real que armazena true e false (e NULL). Você pode compará-lo diretamente:
SELECT * FROM users WHERE is_active = true;
SELECT * FROM users WHERE is_active; -- também válido no Postgres
O MySQL não possui booleano nativo. BOOLEAN e BOOL são apelidos para TINYINT(1), que armazena inteiros. true mapeia para 1 e false para 0. Isso funciona na maioria dos casos, mas vaza em dois lugares:
- Comparações com os literais
true/falsefuncionam porque o MySQL os trata como1/0, mas seus dados armazenados são inteiros, então umSELECTretorna1e0, nãotrueefalse. - Qualquer valor diferente de 0 é considerado verdadeiro no mundo de inteiros do MySQL, então colunas
TINYINT(1)podem tecnicamente conter2,5ou-1se sua aplicação for descuidada.
Ao migrar uma coluna booleana do Postgres para o MySQL, espere que os dados apareçam como 0/1 e ajuste qualquer código da aplicação ou mapeamento ORM que esperava as strings true/false.
Operadores de Conjunto: INTERSECT e EXCEPT#
O PostgreSQL tem suporte completo aos operadores de conjunto padrão: UNION, UNION ALL, INTERSECT e EXCEPT. Todos se comportam conforme descrito pelo padrão SQL.
SELECT id FROM tabela_a
INTERSECT
SELECT id FROM tabela_b;
O MySQL ficou atrasado nesse aspecto por anos. UNION e UNION ALL sempre funcionaram, mas INTERSECT e EXCEPT só chegaram no MySQL 8.0.31. Se você tem como alvo uma versão mais antiga do MySQL, não pode usá-los e precisa reescrever a lógica.
A solução clássica no MySQL para INTERSECT é um inner join ou uma subconsulta com IN:
-- INTERSECT reescrito para MySQL antigo
SELECT DISTINCT a.id
FROM tabela_a a
JOIN tabela_b b ON a.id = b.id;
E para EXCEPT, um padrão LEFT JOIN ... IS NULL ou NOT IN / NOT EXISTS:
-- EXCEPT reescrito para MySQL antigo
SELECT a.id
FROM tabela_a a
LEFT JOIN tabela_b b ON a.id = b.id
WHERE b.id IS NULL;
Cuidado com o tratamento de NULL.
NOT INcom uma subconsulta que retorna qualquer valor NULL pode produzir um conjunto de resultados vazio inesperadamente.NOT EXISTSe o padrãoLEFT JOIN ... IS NULLsão mais seguros quando NULLs são possíveis.
Strings, Aspas e Concatenação#
Esta categoria causa os erros mais desconcertantes porque os mesmos caracteres significam coisas diferentes.
Aspas para identificadores#
O PostgreSQL usa aspas duplas para identificadores (nomes de tabelas e colunas) e aspas simples para literais de string:
SELECT "userName" FROM "Users" WHERE name = 'Alice';
O MySQL usa crases para identificadores por padrão:
SELECT `userName` FROM `Users` WHERE name = 'Alice';
O MySQL pode ser configurado para aceitar identificadores com aspas duplas ativando o modo ANSI_QUOTES, mas, por padrão, aspas duplas em torno de uma string podem se comportar como um literal de string, o que leva a bugs confusos quando você copia SQL do Postgres para um console MySQL.
Concatenação de strings#
O PostgreSQL usa o operador padrão || para concatenar strings:
SELECT first_name || ' ' || last_name AS full_name FROM users;
No MySQL, || significa OU lógico por padrão, não concatenação. Você deve usar a função CONCAT():
SELECT CONCAT(first_name, ' ', last_name) AS full_name FROM users;
Isso é uma falha silenciosa prestes a acontecer. Uma concatenação com || copiada do Postgres não gerará erro no MySQL; ela será avaliada silenciosamente como um OU booleano e retornará 0 ou 1, o que é muito mais difícil de depurar do que um erro de sintaxe.
Sensibilidade a maiúsculas/minúsculas#
O PostgreSQL converte identificadores não citados para minúsculas, então MyTable e mytable são o mesmo objeto, a menos que você os cite. A sensibilidade a maiúsculas/minúsculas de identificadores no MySQL depende do sistema operacional e da configuração lower_case_table_names, o que significa que uma consulta pode funcionar no Mac de um desenvolvedor e falhar em um servidor de produção Linux. Em caso de dúvida, adote uma convenção de nomenclatura consistente com letras minúsculas e sublinhados, e você contorna todo o problema.
LIMIT, OFFSET e Paginação#
Ambos os mecanismos suportam LIMIT n OFFSET m, que é a forma portável a ser preferida:
SELECT * FROM products ORDER BY id LIMIT 10 OFFSET 20;
O MySQL também aceita uma abreviação com vírgula que o PostgreSQL não entende:
-- Apenas MySQL: pular 20, pegar 10
SELECT * FROM products ORDER BY id LIMIT 20, 10;
Observe que a ordem é invertida na forma com vírgula (offset primeiro, depois contagem), o que é uma fonte comum de erros de página. Mantenha a sintaxe explícita LIMIT ... OFFSET ... e suas consultas de paginação serão portáveis de forma limpa em ambas as direções.
Portando Consultas Sem Adivinhação#
Ao mover uma consulta entre esses dialetos, trabalhe nos recursos de alto risco em ordem, em vez de executá-la cegamente e ler mensagens de erro. Um checklist prático:
- Substitua a sintaxe UPSERT (
ON DUPLICATE KEY UPDATEparaON CONFLICT, ou o inverso) e corrija a referência de linha (VALUES()paraEXCLUDED). - Troque a concatenação de strings com
||porCONCAT()ao usar MySQL. - Converta a citação de identificadores (crases para aspas duplas, ou o inverso).
- Verifique colunas booleanas e espere dados
0/1no MySQL. - Substitua
INTERSECT/EXCEPTpor joins se você usar MySQL anterior à versão 8.0.31. - Normalize declarações de auto-incremento (
SERIAL/IDENTITYversusAUTO_INCREMENT).
Ler uma consulta densa para encontrar esses pontos é muito mais fácil depois que ela é formatada. Uma parede de SQL em uma linha esconde a cláusula ON CONFLICT e o || solto. Nosso formatador SQL multi-dialeto reformata e indentifica consultas tanto para PostgreSQL quanto para MySQL, para que você veja a estrutura claramente antes de começar a traduzir. Para um olhar mais aprofundado sobre como a formatação difere entre mecanismos, veja nosso guia sobre como formatar uma consulta SQL para legibilidade entre dialetos.
Se seu trabalho também envolve enviar resultados de consultas para uma API ou configuração, o mesmo princípio de legibilidade se aplica aos seus payloads. Nosso formatador JSON limpa a camada de dados, e o tutorial sobre como formatar JSON em JavaScript cobre os padrões JSON.stringify que combinam com a saída do banco de dados.
O Resumo sobre as Diferenças de Sintaxe entre PostgreSQL e MySQL#
As diferenças de sintaxe entre PostgreSQL e MySQL são concentradas, não espalhadas. SELECT, JOIN e agregações padrão são portáveis; os problemas se concentram em UPSERT, booleanos, operadores de conjunto, concatenação de strings, citação de identificadores e auto-incremento. Aprenda esses seis e você conseguirá portar a grande maioria das consultas do mundo real sem surpresas.
Mantenha esta tabela mental: quando algo falhar após uma migração, a causa quase certamente será um dos cantos de dialeto acima, não sua lógica principal. Formate a consulta primeiro, procure pelas cláusulas de alto risco, traduza-as deliberadamente e você transforma um jogo frustrante de adivinhação em uma correção rápida e mecânica.
Perguntas Frequentes#
Qual é a maior diferença de sintaxe entre PostgreSQL e MySQL?
UPSERT é a maior diferença prática. O PostgreSQL usa INSERT ... ON CONFLICT ... DO UPDATE com um alvo de conflito explícito e a pseudo-tabela EXCLUDED, enquanto o MySQL usa INSERT ... ON DUPLICATE KEY UPDATE e infere a chave. Não há uma tradução automática limpa, então é a cláusula com maior probabilidade de quebrar ao portar uma consulta.
Por que minha concatenação de strings do PostgreSQL falha no MySQL?
O PostgreSQL usa o operador || para concatenação de strings, mas no MySQL || significa OR lógico por padrão. Copiar uma consulta do Postgres para o MySQL não gerará um erro de sintaxe; ele retornará silenciosamente 0 ou 1 em vez de uma string concatenada. Use a função CONCAT() no MySQL para obter o mesmo resultado.
O MySQL tem um tipo BOOLEAN real como o PostgreSQL?
Não. O PostgreSQL tem um tipo BOOLEAN nativo que armazena true/false. No MySQL, BOOLEAN e BOOL são apelidos para TINYINT(1), que armazena inteiros, então true vira 1 e false vira 0. Ao portar colunas booleanas, espere que os dados apareçam como 0/1 e atualize qualquer código que esperava os valores de string.
Posso usar INTERSECT e EXCEPT no MySQL?
Apenas no MySQL 8.0.31 e posteriores. O PostgreSQL suporta INTERSECT e EXCEPT há muito tempo. Em versões mais antigas do MySQL, você deve reescrevê-los, usando um inner join ou subconsulta IN para INTERSECT, e um padrão LEFT JOIN ... IS NULL ou NOT EXISTS para EXCEPT, tomando cuidado com o tratamento de NULL.
A maior parte do SQL é portável entre PostgreSQL e MySQL?
Sim. Cerca de 80% do SQL do dia a dia, incluindo SELECT, JOIN, WHERE, GROUP BY e INSERT básico, é idêntico ou próximo o suficiente para rodar em ambos os motores. As diferenças se concentram em UPSERT, booleanos, operadores de conjunto, citação de identificadores, concatenação de strings e auto-incremento, por isso uma lista de verificação focada é melhor do que ler mensagens de erro uma por uma.
Qual é a maneira mais rápida de identificar diferenças de dialeto em uma consulta?
Formate a consulta primeiro para que a estrutura fique visível, depois examine as cláusulas de alto risco. Um formatador que suporta ambos os dialetos, como o formatador SQL Molixa, indentifica e alinha o SQL para que as diferenças de ON CONFLICT, || e citação apareçam em vez de se esconderem em uma única linha densa.



