trilha

Tecnologias/05 - Comparações7 min

PostgreSQL vs MySQL vs Oracle

Conceito

Os três falam SQL, implementam ACID, têm índice B-tree, MVCC, replicação e transação. Se a comparação parar aí, os três parecem intercambiáveis — e é exatamente por isso que projetos de migração estouram o prazo.

A diferença real está em decisões internas de arquitetura que vazam para o seu código e para a sua operação. São seis, e vale conhecer cada uma pelo nome.

Panorama

PostgreSQL MySQL (InnoDB) Oracle
Tese correção e extensibilidade simplicidade de operação completude e garantia
Conexão processo (~5–10 MB) thread processo/thread dedicado
Layout da tabela heap + índices índice clusterizado pela PK heap (ou IOT, opcional)
Versões antigas (MVCC) na própria tabela undo log undo tablespace
Limpeza VACUUM / autovacuum reciclagem do undo reciclagem do undo
Isolamento padrão READ COMMITTED REPEATABLE READ READ COMMITTED
READ UNCOMMITTED não implementa (aceita a sintaxe) implementa não existe
DDL transacional sim não (commit implícito) não (commit implícito)
Índice parcial / por expressão sim / sim não / functional index (8.0) não / function-based index
Tipos além do básico array, jsonb, range, hstore, geo JSON, VECTOR (9.0+) XMLType, JSON, VECTOR (26ai)
RETURNING sim não via RETURNING INTO (PL/SQL)
Extensões de terceiros PostGIS, pgvector, Timescale… limitado opções pagas da própria Oracle
Particionamento nativo, incluso nativo, incluso opção paga na EE
Failover automático externo (Patroni, RDS) InnoDB Cluster / Group Replication Data Guard / RAC
Cluster ativo-ativo não Group Replication (multi-primário, com ressalvas) RAC (disco compartilhado)
Viagem no tempo PITR pelo WAL PITR pelo binlog Flashback (query, tabela, banco)
Licença PostgreSQL (permissiva) GPL + comercial (Oracle) proprietária, por core + opções
Ciclo de versões 1 major/ano, 5 anos de suporte LTS ~2 anos + Innovation trimestral LTS de longa duração (19c, 26ai)
Versão atual (set/2026) 18 (18.6) 9.7 LTS / 26.7 Innovation 26ai (19c até 2032)

As 6 diferenças que realmente mudam seu trabalho

1. Onde ficam as versões antigas → define como o banco é operado

No Postgres, um UPDATE grava uma nova versão da linha na própria tabela e marca a antiga como morta. No MySQL e no Oracle, a linha é alterada no lugar e a versão anterior vai para o undo.

Postgres MySQL / Oracle
Custo do UPDATE escreve linha nova + atualiza todos os índices altera no lugar; índice só se a coluna indexada mudou
Problema crônico bloat: tabela e índice incham undo crescendo; ORA-01555 / history list
Rollback quase instantâneo (descarta a versão nova) precisa desfazer pelo undo (pode demorar)
Ferramenta de convivência monitorar autovacuum, pg_repack monitorar undo, encurtar transações longas

Uma transação idle in transaction esquecida no Postgres impede o VACUUM de limpar qualquer coisa mais nova que ela. No Oracle, ela causa snapshot too old. Mesmo pecado, punição diferente.

2. Heap vs índice clusterizado → define a chave primária

No InnoDB a tabela é a árvore da PK (1. MySQL). No Postgres e no Oracle a tabela é um heap e a PK é só mais um índice.

PK aleatória (UUID v4):
  MySQL   → insere no meio da árvore: page splits, I/O aleatório, índices secundários gordos
  Postgres → insere no fim do heap; só o índice sofre um pouco

Prático: UUID v4 como PK é um problema de performance no MySQL e quase um não-problema no Postgres. No MySQL, use AUTO_INCREMENT ou UUID v7 em BINARY(16). Nos dois últimos, uuidv7() (Postgres 18) ainda é uma boa ideia por localidade de índice, mas não é emergência.

3. DDL transacional → define como você faz migração

BEGIN;
  ALTER TABLE a ADD COLUMN x int;
  ALTER TABLE b ADD COLUMN y int;
ROLLBACK;
  • Postgres: desfaz as duas. Migração é atômica; se falhar no meio, não aconteceu.
  • MySQL / Oracle: cada ALTER faz commit implícito. Falhar na segunda deixa a primeira aplicada.

Isso muda a estratégia, não só a ferramenta: em MySQL e Oracle toda migração precisa ser escrita para ser idempotente e retomável, porque um estado intermediário é possível. É a origem do ecossistema gh-ost / pt-online-schema-change no MySQL.

4. Isolamento padrão → o mesmo código dá resultados diferentes

O padrão do MySQL é REPEATABLE READ; dos outros dois, READ COMMITTED.

-- Sessão A                       -- Sessão B
BEGIN;
SELECT saldo FROM contas WHERE id=1;  -- 100
                                  UPDATE contas SET saldo=200 WHERE id=1; COMMIT;
SELECT saldo FROM contas WHERE id=1;
--  MySQL:    100  (snapshot do início da transação)
--  Postgres: 200  (vê o commit de B)
--  Oracle:   200

Nenhum está errado — mas um relatório que soma em duas queries dentro da mesma transação é consistente no MySQL e pode ser inconsistente no Postgres. E vice-versa: lógica que depende de "ler o valor mais recente" para decidir quebra no MySQL. Além disso o REPEATABLE READ do MySQL traz gap locks, que produzem deadlocks que simplesmente não existem nos outros. Ver 3. MVCC e Isolamento na Prática.

5. Custo da conexão → define a arquitetura do app

Custo por conexão Teto prático Consequência
Postgres processo, ~5–10 MB centenas pooler é obrigatório (PgBouncer)
MySQL thread milhares pool no app costuma bastar
Oracle processo dedicado ou shared server depende da configuração connection pooling (DRCP/UCP) é padrão

Junte isso com goroutines baratas (2. Goroutines e Channels) e o padrão MaxOpenConns ilimitado do database/sql: no Postgres, um pico de tráfego derruba o banco; no MySQL, provavelmente só fica lento. Ver 5. Go em Produção.

6. Dialeto e semântica → define quanto seu SQL é portável

  • Oracle: '' é NULL; concatenação ignora NULL; FROM dual; VARCHAR2; NUMBER para tudo; sem BOOLEAN em SQL antes do 23ai.
  • MySQL: || significa OR (não concatenação, salvo PIPES_AS_CONCAT); BOOLEAN é TINYINT(1); identificadores em `crase`; sensibilidade a maiúsculas em nome de tabela depende do sistema de arquivos.
  • Postgres: identificadores em "aspas duplas"; sem RETURNING você não vive depois de ter usado.

O catálogo completo está em 2. Dialetos SQL na Prática.

Trade-offs

Portabilidade custa features. Escrever SQL que roda nos três significa abrir mão de jsonb, índice parcial, RETURNING, SKIP LOCKED com dialeto próprio, tipos específicos — ou seja, abrir mão exatamente do que faz um banco valer a escolha. ORM (GORM, sqlc, Hibernate) esconde parte disso e vaza justamente onde dói: tipos, lock, paginação, migração.

A escolha "certa" na maioria dos casos é chata. Em 2026, para um serviço novo sem requisito institucional, PostgreSQL é a escolha padrão defensável: mais recursos, licença permissiva, extensões e gerenciados em toda nuvem. MySQL vence quando a operação é o gargalo, quando a carga é dominada por leitura por PK, ou quando o time já opera MySQL há dez anos. Oracle não é uma escolha nova — é uma herança a administrar, e o trabalho normalmente é migrar dele.

O trade-off central: o modelo relacional é o mesmo nos três; o que você compra é o modo de operar. Postgres pede vigilância e devolve recursos. MySQL pede pouco e devolve pouco. Oracle pede dinheiro e devolve garantia.

Exemplo prático

Como escolher, em ordem de peso das perguntas:

Existe requisito institucional (contrato, certificação, ERP, PL/SQL legado)?
  └─ sim → Oracle. A discussão é de migração, não de escolha.

Preciso de tipos ricos, jsonb, geo, vetor, índice parcial, DDL transacional?
  └─ sim → PostgreSQL.

Minha carga é 95% leitura por chave primária, com time que já opera MySQL?
  └─ sim → MySQL. É onde o índice clusterizado brilha.

Preciso escalar escrita além de um nó?
  └─ nenhum dos três resolve sozinho: [[Sharding]], [[Federation]] ou outro modelo
     ([[1. MongoDB]], [[DynamoDB]]).

Nenhuma das anteriores?
  └─ PostgreSQL. Escolha padrão por eliminação — e a menos arrependível.

O mesmo UPSERT nos três

-- PostgreSQL
INSERT INTO estoque (sku, qtd) VALUES ('A1', 5)
ON CONFLICT (sku) DO UPDATE SET qtd = estoque.qtd + EXCLUDED.qtd
RETURNING qtd;                              -- devolve o resultado de graça

-- MySQL
INSERT INTO estoque (sku, qtd) VALUES ('A1', 5)
ON DUPLICATE KEY UPDATE qtd = qtd + VALUES(qtd);
-- sem RETURNING: precisa de um SELECT depois (e de cuidado com concorrência)

-- Oracle
MERGE INTO estoque e
USING (SELECT 'A1' sku, 5 qtd FROM dual) s ON (e.sku = s.sku)
WHEN MATCHED THEN UPDATE SET e.qtd = e.qtd + s.qtd
WHEN NOT MATCHED THEN INSERT (sku, qtd) VALUES (s.sku, s.qtd);

Três sintaxes, três comportamentos de lock diferentes sob concorrência, e um RETURNING que existe em um só. Este bloco resume a nota inteira: a operação é a mesma, o contrato não é.

Aprofundamento aplicado

Comparar a mesma regra sob as mesmas exigências

Use uma reserva de estoque como unidade de comparação: mesma distribuição de dados, mesma concorrência, saldo inicial e comportamento esperado após falhas. A consulta que vence um benchmark isolado pode perder quando inclui locks, índices e durabilidade.

Dimensão Evidência útil
Correção A reserva nunca confirma além do estoque
Desempenho Throughput e percentis de latência sob concorrência
Recuperação Resultado de queda durante transação e confirmação
Operação Trabalho de índices, versões antigas, backups e migrações
Compatibilidade Comportamento dos comandos realmente usados

Defaults são parte da diferença

O mesmo código pode encontrar snapshots e locks diferentes conforme banco, engine e isolamento da sessão. Declare essas opções no experimento. Nome parecido para um nível de isolamento não demonstra implementação ou anomalias idênticas.

Exercício: execute a regra em duas sessões, com ambos tentando reservar o último item. Resultado esperado: documente espera, erro ou retry e como o aplicativo chega a exatamente uma confirmação. A escolha final deve incluir a operação que o time consegue manter, além da medição.

Referências: PostgreSQL, InnoDB e Oracle.

Relacionado


Parte de MOC Comparações · postgresql.org/docs · dev.mysql.com/doc · docs.oracle.com/database

Buscar

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