PostgreSQL versus MySQL is one of the oldest rivalries in software, and like most old rivalries it is mostly narrated from memory. The databases have spent twenty years trading features, and today they overlap far more than they differ: both are ACID by default, both have JSON support, both run at every scale from Raspberry Pi to planet-scale fleets. Choosing between them in 2026 is less about which one is “better” and more about which one’s strengths align with your workload — and knowing the real remaining differences, because a few of them still bite.
This is a practitioner’s comparison: storage engines and transaction internals, SQL surface, JSON handling, replication, extensions, and operational reality. No winners declared, no cheerleading — just the differences that show up in production.
Foundations: how each thinks about data
Postgres is a research pedigree turned product. Its architecture is unified: one storage engine, one query planner, one way things work, with an extensibility model (custom types, operators, index methods) that lets third parties bolt on capabilities the core team never imagined. That extensibility is why Postgres became the platform other databases get built on — timeseries (TimescaleDB), geospatial (PostGIS), document store (via JSONB), vector search (pgvector) all live inside the same engine.
MySQL is a pragmatist’s tool with a pluggable storage engine architecture. In practice everyone runs InnoDB, which is what people mean when they say “MySQL”: ACID transactions, clustered primary key index, and a design that prioritizes simple, predictable, fast reads. MySQL’s historic identity — fast, simple, ubiquitous — came from defaults that favored web workloads, and that identity still shows in its optimizer tendencies and operational tooling.
SQL surface and standards
Postgres has traditionally been the standards-compliant one, and it shows in the details: full WINDOW functions (MySQL added them in 8.0), FILTER clauses on aggregates, DISTINCT ON, transactional DDL, partial and expression indexes, EXCLUDE constraints, and richer join options like FULL OUTER JOIN without workarounds. Postgres also implements NULL sorting and some semantics closer to the standard’s letter.
MySQL closed the headline gaps years ago — window functions, CTEs (including recursive), and JSON functions arrived with 8.0 — and for typical application SQL the two are interchangeable. Where the difference persists is depth: Postgres supports index types MySQL does not (more below), constraint machinery like deferrable checks, and transactional DDL — you can roll back a CREATE TABLE. In MySQL, DDL statements implicitly commit the current transaction, which changes how safe online migrations are and why tooling like gh-ost and pt-online-schema-change exists.
Indexing and query planning
InnoDB clusters rows by primary key — the table is the primary key B-tree, secondary indexes store the PK as their row pointer. That makes PK lookups and range scans on the clustering key exceptionally cheap, and it makes primary key choice a real design decision: a random UUIDv4 PK fragments the clustered index and bloats every secondary index, which is why ordered identifiers are recommended for high-insert tables.
Postgres stores a heap — rows go wherever there is space, indexes point at them — so no index is privileged and secondary indexes all cost the same. In exchange Postgres offers index types InnoDB does not have: partial indexes (index only rows matching a predicate), expression indexes (index lower(email)), GIN for contains-queries over JSON/arrays/full-text, GiST and SP-GiST for geometry and exotic predicates, BRIN for huge append-only tables. For query shapes beyond “equality and range on columns,” Postgres simply has more moves.
The planners differ in temperament. Postgres’ planner is aggressive and cost-based, occasionally producing plans that are brilliant or catastrophically wrong (the classic bane: ANALYZE your tables, or cardinality misestimates will haunt you). MySQL’s optimizer historically leaned simpler and more predictable, at the price of occasionally missing clever plans. In 2026 both are far better than their reputations from either direction suggest.
JSON: two philosophies
Both databases store and query JSON. The philosophical difference: Postgres’ JSONB decomposes the document into a parsed binary form on write — slower ingest, but every query against it is fast and indexable via GIN. MySQL’s JSON type keeps a binary format optimized for reading dom elements, with fast paths for the common operations and function-based indexes via generated columns when you need them.
In practice, JSONB plus a GIN index is the strongest “document database inside your relational database” story available: containment queries (@>), per-key indexing, and the ability to mix columns and documents in one table. MySQL’s approach is workable and fast for read-mostly JSON, but the tooling for indexing deep into documents is clunkier.
Replication and high availability
Both ecosystems default to primary-replica replication with async followers. The differences are in the details that matter at scale:
- Postgres: WAL-shipping replication, logical replication (row-level streams between clusters — the basis of zero-downtime major version upgrades), and synchronous replication modes per-transaction. HA is composable: Patroni plus etcd is the de facto standard, and tools like pg_auto_failover make smaller setups simple.
- MySQL: binlog-based replication is famously flexible — statement, row, or mixed logging; global transaction identifiers (GTIDs) make failover and re-pointing replicas mechanical; and the ecosystem (Orchestrator’s lineage, modern proxies) is battle-tested at internet-company scale. MySQL’s replication is arguably the most operationally mature replication story in open source.
Read scaling is easy in both. Multi-master write scaling is genuinely hard in both — Galera-style synchronous multi-master exists for MySQL, BDR-style for Postgres, but the honest answer in 2026 is still: scale reads horizontally, scale writes with a bigger primary or sharding, and distrust anyone selling painless multi-master.
Extensions and ecosystem
Postgres’ extension API is its superpower. PostGIS is the reference geospatial database, period. pgvector turned Postgres into a credible vector store just as every application on earth grew embedding search. TimescaleDB does timeseries. pg_stat_statements, postgis, pgcrypto, citus for sharding — the pattern is that Postgres grows a first-class capability without changing what it is. MySQL’s plugin ecosystem is thinner, focused mainly on audit, connection pooling (ProxySQL is excellent), and authentication.
Operations: the boring differences that matter daily
- Vacuum: Postgres’ MVCC keeps old row versions until autovacuum reclaims them; misconfigured vacuum on high-churn tables is the top source of Postgres operational pain. MySQL/InnoDB purges old versions internally, and the equivalent headache is purge lag rather than bloat.
- Connection model: Postgres connections are processes — expensive, thousands max, so a pooler (PgBouncer) is standard in front. MySQL connections are threads — cheaper, tens of thousands feasible, though pooling still pays.
- Upgrades: Postgres major upgrades historically meant dump/restore or pg_upgrade; logical replication now enables near-zero-downtime paths. MySQL in-place major upgrades have long been smoother.
- Max table size / row size: both are enormous and rarely the constraint people fear; the practical limits are operational (replication lag, backup windows) not physical.
So which one?
The honest 2026 answer: for most application workloads, either is excellent and the deciding factors are team familiarity and ecosystem fit. The differentiators:
- Choose Postgres when you need advanced SQL (window analytics, CTE-heavy reporting), rich indexing (partial/expression/GIN), GIS, vector search, or you want one database to be relational-plus-documents-plus-everything. When the workload is diverse or the queries are clever, Postgres rewards you.
- Choose MySQL when the workload is classic web-shaped CRUD with simple queries at enormous scale, when your ops team knows its replication and failover tooling cold, or when the surrounding ecosystem (WordPress and the PHP world, managed offerings) tilts that way. When the workload is simple and the scale is not, MySQL is friction-free.
And if you are starting fresh in 2026 with no legacy weight: the extensibility trajectory means Postgres is the default answer more often than not — the database that keeps absorbing use cases is the one that keeps surprising you with capabilities, and the ecosystem energy behind extensions like pgvector compounds every year. But that is a default, not a verdict. The best database is the one your team can operate at 3 a.m., and both of these have been keeping real businesses alive for decades.