Todos os artigos
55 artigos · atualizado semanalmente Veja nossas Ferramentas
Todos os artigos
Comparativos

PostgreSQL vs MySQL: diferenças práticas para quem precisa escolher

Comparativo honesto entre PostgreSQL e MySQL: tipos nativos, JSONB vs JSON_EXTRACT, concorrência MVCC e extensões. Por que PostgreSQL como default faz sentido.

COVER · Comparativos

Na maioria dos projetos novos que começa com MySQL, a justificativa é "todo mundo usa" ou "o tutorial usava". Raramente é uma decisão informada sobre o que o sistema vai precisar. Isso funciona até o dia que você tenta fazer uma query com CTE recursivo, descobre que o JSON_EXTRACT do MySQL tem performance de tortura, ou percebe que o GROUP BY aceitava colunas que não estavam no SELECT sem reclamar por anos.

Esse post é o comparativo que eu gostaria de ter lido antes de alguns projetos que não vou mencionar.

O que os dois bancos têm em comum (e por que isso confunde a decisão)

Ambos são bancos relacionais, ambos falam SQL, ambos têm ACID, índices B-tree, replicação, e rodam bem em qualquer cloud. Para um CRUD simples — tabela de usuários, tabela de pedidos, autenticação — qualquer um dos dois funciona e você provavelmente nunca vai notar a diferença em produção.

O problema é que "CRUD simples" é a parte mais fácil. O que diferencia os dois são as arestas: conformidade com o padrão SQL, o modelo de concorrência, o suporte a tipos de dados complexos, e o ecossistema de extensões.

Se você quer o contexto mais amplo de quando usar relacional vs. NoSQL em geral, esse post sobre bancos relacionais vs. NoSQL cobre as diferenças de modelo. Este aqui foca especificamente no que muda dentro do mundo relacional.

Tipos de dados: PostgreSQL ganha sem discussão

MySQL tem INT, VARCHAR, TEXT, DATETIME, FLOAT e uns tipos menos usados. Funciona para a maioria dos casos. PostgreSQL tem tudo isso mais:

  • Arrays nativos: INTEGER[], TEXT[], UUID[]. Sem tabela de pivot só para guardar uma lista de tags.
  • JSONB: JSON binário com indexação GIN. Não é "suporte a JSON" — é um tipo de primeira classe com operadores, funções, e índices parciais.
  • UUID como tipo nativo: sem CHAR(36) com poluição de index.
  • Intervalos (tsrange, daterange, int4range): para dados que têm início e fim, com operadores de overlap, contains e adjacent nativos.
  • Tipos geométricos e PostGIS: ponto, linha, polígono, círculo — e com PostGIS você tem um banco geoespacial completo sem instalar mais nada.
  • Enums tipados: CREATE TYPE status AS ENUM ('ativo', 'inativo', 'pendente') — com validação no banco, não na aplicação.
  • HSTORE: key-value flat dentro de uma coluna, mais leve que JSONB quando o dado é simples.
-- PostgreSQL: coluna de tags sem tabela auxiliar
CREATE TABLE artigos (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  titulo TEXT NOT NULL,
  tags TEXT[],
  metadata JSONB,
  periodo tsrange
);

-- Query com array e JSON no mesmo statement
SELECT titulo
FROM artigos
WHERE 'postgresql' = ANY(tags)
  AND metadata->>'autor' = 'rafael'
  AND periodo @> NOW()::TIMESTAMPTZ;

No MySQL, esse schema exigiria tabela de tags separada, coluna JSON sem indexação eficiente no campo interno, e lógica de datas manual.

JSON: JSONB vs JSON_EXTRACT

Esse é o ponto onde a diferença fica mais evidente em projetos reais.

No MySQL, o tipo JSON armazena o documento e te dá funções como JSON_EXTRACT() e o operador ->. Funciona. Mas fazer index em campo dentro do JSON requer criar uma coluna gerada:

-- MySQL: index em campo JSON exige coluna virtual
ALTER TABLE eventos
  ADD COLUMN user_id VARCHAR(36) GENERATED ALWAYS AS (JSON_UNQUOTE(JSON_EXTRACT(payload, '$.user_id'))) STORED;
CREATE INDEX ON eventos(user_id);

No PostgreSQL, o JSONB tem índice GIN nativo que cobre queries em qualquer campo do documento, sem precisar saber de antemão quais campos você vai consultar:

-- PostgreSQL: um índice cobre qualquer campo
CREATE INDEX ON eventos USING GIN (payload);

-- Essas duas queries usam o mesmo índice
SELECT * FROM eventos WHERE payload->>'user_id' = '123';
SELECT * FROM eventos WHERE payload @> '{"tipo": "compra", "valor_acima": 1000}';

Para aplicações que usam JSON extensivamente — event sourcing, schemas semi-estruturados, integração com APIs externas — JSONB com GIN é uma diferença prática, não teórica.

Concorrência: MVCC vs locking por linhas

Os dois bancos implementam MVCC (Multi-Version Concurrency Control), mas com diferenças importantes.

O PostgreSQL usa MVCC em toda operação de escrita, sem exceção. Leituras nunca bloqueiam escritas e escritas nunca bloqueiam leituras — cada transação vê um snapshot consistente do banco no momento em que começou.

O MySQL/InnoDB também usa MVCC, mas tem comportamentos que surpreendem quem vem do PostgreSQL:

  • SELECT dentro de uma transação com REPEATABLE READ (padrão no MySQL) não vê mudanças de outras transações confirmadas depois que a sua transação começou — o que parece óbvio, mas cria surpresas em algumas operações de upsert e contadores.
  • SELECT ... FOR UPDATE no MySQL faz lock de gap, não só da linha — em certas situações, pode causar deadlock onde o PostgreSQL não causaria.
  • O MySQL tem um problema histórico com o ENUM: a ordem dos valores é pela posição de criação, não alfabética, e alterar a ordem em produção requer um ALTER TABLE que pode ser lento dependendo do tamanho da tabela.

Para sistemas com alta concorrência de escrita — filas, contadores, sistemas de inventário — o comportamento do PostgreSQL é mais previsível. Não que o MySQL seja inadequado, mas o modelo de locking tem mais casos de borda.

Conformidade SQL: MySQL ainda escorrega

O MySQL historicamente foi mais permissivo que o padrão SQL — o que parece uma vantagem até você precisar portar uma query ou contar com um comportamento específico.

Exemplo clássico: por anos, o MySQL aceitava GROUP BY com colunas não-agregadas no SELECT sem reclamar, retornando um valor arbitrário para essas colunas. Isso foi "corrigido" com o modo ONLY_FULL_GROUP_BY, que é padrão desde o MySQL 5.7, mas bases legadas ainda têm esse comportamento desabilitado.

-- Essa query é inválida em SQL padrão e em PostgreSQL
-- O MySQL aceitava silenciosamente antes de 5.7
SELECT user_id, email, MAX(created_at)
FROM orders
GROUP BY user_id;
-- Qual email retorna? Qualquer um do grupo. Não determinístico.

Outras diferenças de conformidade:

  • CTEs: o PostgreSQL suporta CTEs desde a versão 8.4, com materialização controlada. O MySQL só suportou CTEs na versão 8.0 (2018), sem WITH RECURSIVE otimizado da mesma forma.
  • Window functions: disponíveis nos dois desde versões razoavelmente recentes, mas o PostgreSQL tem mais funções e comportamentos padronizados.
  • LATERAL joins: PostgreSQL suporta, MySQL tem suporte limitado.
  • RETURNING em INSERT/UPDATE/DELETE: PostgreSQL suporta, MySQL não — você precisa de uma query separada para recuperar o ID do registro inserido ou o valor atualizado.
-- PostgreSQL: INSERT com RETURNING evita round-trip
INSERT INTO usuarios (nome, email)
VALUES ('rafael', 'rafael@example.com')
RETURNING id, criado_em;

Extensões: o ecossistema do PostgreSQL

Essa é a diferença que mais pesa na escolha de longo prazo.

O PostgreSQL tem um sistema de extensões que transforma o banco em algo muito além de um RDBMS genérico:

Extensão O que faz
PostGIS Banco geoespacial completo. Distâncias, polígonos, geofencing.
pg_vector Vetores de alta dimensão para busca semântica com embeddings. Relevante para LLMs.
TimescaleDB Séries temporais sobre PostgreSQL com compressão e time-bucketing.
pgcrypto Funções criptográficas nativas: hash, criptografia simétrica/assimétrica.
pg_stat_statements Análise de queries por custo, frequência, tempo. Essencial para tuning.
Citus Sharding horizontal do PostgreSQL para escala massiva.
pg_partman Gerenciamento automático de particionamento por tempo ou range.

O MySQL tem um ecossistema menor de plugins. Funcional, mas a distância é grande.

Quando usar MySQL

Não vou fingir que MySQL não tem casos onde faz mais sentido:

Leitura pesada com schema simples: MySQL tem reputação de ser mais rápido em workloads read-heavy com queries simples. Para um blog de alto tráfego onde a maioria das queries são SELECT * FROM posts WHERE slug = ?, a diferença de performance em favor do MySQL é mensurável.

Ecossistema legado: se você herda um sistema em MySQL com ORM, migrations, e equipe que conhece o banco, o custo de migrar raramente vale o ganho técnico. PostgreSQL melhor não é justificativa para uma migração de seis meses.

Managed databases em ambientes restritos: MySQL em algumas plataformas cloud ainda é mais barato ou mais fácil de provisionar do que PostgreSQL. Esse argumento está diminuindo, mas existe.

Compatibilidade com stacks específicas: algumas ferramentas de BI, ERPs, e sistemas legados têm suporte mais maduro para MySQL. Checar antes de decidir.

Quando usar PostgreSQL

Basicamente todo o resto. Especificamente:

  • Dados financeiros, transacionais, com necessidade de ACID robusto
  • JSON semi-estruturado com necessidade de query e indexação
  • Dados geoespaciais (PostGIS)
  • Séries temporais (TimescaleDB)
  • Embeddings e busca semântica (pg_vector)
  • Sistemas novos onde você ainda não sabe exatamente como os dados vão crescer
  • Times que vão escrever queries complexas — CTEs, window functions, subqueries laterais

Perguntas frequentes

PostgreSQL é mais lento que MySQL?

Para leituras simples com schema fixo, o MySQL pode ser marginalmente mais rápido. Para queries complexas, writes com alta concorrência, e workloads analíticos, o PostgreSQL tende a ter melhor performance. O mito de que PostgreSQL é mais pesado vem de configurações padrão conservadoras — ajustar shared_buffers, work_mem e effective_cache_size no postgresql.conf geralmente elimina qualquer diferença percebida.

Posso migrar de MySQL para PostgreSQL em produção?

Sim, mas não subestime o trabalho. As diferenças de SQL (modo ONLY_FULL_GROUP_BY, tipos de dados, funções específicas), o formato de dumps, e os comportamentos de auto-increment vs sequences exigem revisão caso a caso. Ferramentas como pgloader automatizam parte da migração, mas queries e stored procedures precisam ser revisadas manualmente.

MySQL 8 não fechou a diferença com PostgreSQL?

Fechou algumas. CTEs, window functions, e modos mais estritos de SQL foram adicionados. A distância diminuiu. Mas o ecossistema de extensões, os tipos de dados nativos (arrays, ranges), JSONB com GIN, e a conformidade SQL mais sólida ainda deixam o PostgreSQL à frente para a maioria dos casos novos.

Qual banco usar em um novo projeto SaaS?

PostgreSQL como default, sem hesitar. A única razão para escolher MySQL num projeto novo é restrição de plataforma ou preferência pessoal do time. Em qualidade de features, conformidade, e ecossistema, o PostgreSQL oferece mais — e o custo de performance para casos simples é negligenciável com configuração adequada.

PostgreSQL como default, MySQL quando há motivo concreto

Minha heurística: começa com PostgreSQL. Se aparecer um motivo concreto para MySQL — ecossistema legado, restrição de plataforma, time com expertise exclusiva — considera. Se o motivo for "é mais popular" ou "o tutorial usava", não conta.

Para explorar e formatar queries enquanto você avalia a migração ou modela o schema novo, uso o SQL Formatter — especialmente útil quando as queries começam a crescer com CTEs e subqueries que precisam ser legíveis para o próximo dev que vai manter o código.

RD
Autor
Rafael Duarte
Desenvolvedor backend com passagem por fintech e SaaS B2B — trabalhou em times que escalaram APIs de zero a milhões de requisições. Carrega cicatrizes de produção suficientes para ter opiniões fortes sobre ferramentas, padrões e decisões de arquitetura. Não é acadêmico: leu a RFC do UUID quando precisou escolher entre v4 e v7 para uma tabela de alta escrita.
Ver perfil