Back to Case Studies
databasesadvanced 13 min read

Uber

Uber's Decision to Move from Postgres to MySQL

Why Uber left PostgreSQL's MVCC model for MySQL's replication architecture

Key outcome: Solved write amplification at scale
PostgreSQLMySQLDatabase InternalsMVCCReplication

The Controversial Post

In 2016, Uber Engineering published a blog post titled "Why Uber Engineering Switched from Postgres to MySQL." It was one of the most debated database engineering posts in years.

Postgres advocates responded with detailed rebuttals. MySQL advocates cited it as vindication. The actual content — a careful analysis of specific MVCC and replication internals — was often lost in the noise.

This case study examines what Uber actually said, what was technically correct, what was debatable, and what the underlying lesson is for database selection.


Uber's Original Architecture

In Uber's early days (circa 2012-2014), their backend ran on a small number of PostgreSQL databases. The data model was relatively conventional: trips, drivers, riders, locations, payments — normalized relational data with foreign keys and ACID transactions.

PostgreSQL was a reasonable choice. It had excellent support for geospatial queries via PostGIS (important for ride locations), strong ACID guarantees, and a sophisticated query planner.

As Uber scaled, they began running into specific PostgreSQL behaviors that interacted poorly with their workload. The decision to migrate to MySQL was driven by three technical concerns.


Concern 1: Write Amplification from MVCC

PostgreSQL uses Multi-Version Concurrency Control (MVCC) to allow readers and writers to proceed concurrently without locking. The way PostgreSQL implements MVCC has a specific characteristic: old row versions are stored in-place.

When you update a row in PostgreSQL:

  1. The old row version is not deleted immediately — it stays in the table with an xmax transaction ID marking it as superseded
  2. A new version of the row is inserted
  3. A background process called autovacuum eventually cleans up old versions
PostgreSQL table storage after 3 updates to a row:

[ Row v1: id=1, name="Alice", xmin=100, xmax=101 ]  ← dead (old)
[ Row v2: id=1, name="Bob",   xmin=101, xmax=102 ]  ← dead (old)
[ Row v3: id=1, name="Carol", xmin=102, xmax=NULL ]  ← live (current)
[ Row v4: id=2, name="Dave",  xmin=103, xmax=NULL ]  ← live
...

The consequence for write-heavy workloads:

  • Table bloat: dead row versions accumulate faster than autovacuum can clean them up
  • Index bloat: PostgreSQL's secondary indexes point to the physical row location (ctid) — every update creates a new ctid, requiring every secondary index to be updated with the new pointer
  • Autovacuum pressure: heavy write workloads require aggressive autovacuum tuning; without it, tables grow unboundedly and queries slow down

For Uber's ride-sharing tables — updated millions of times per second — this became significant.

MySQL InnoDB's approach is different: InnoDB uses a clustered primary key structure. The primary key index IS the table — rows are stored in primary key order. Old row versions are stored in a separate undo log (rollback segment) and cleaned up immediately after the transaction commits (no background vacuum process needed).

For Uber's specific workload (many small, frequent updates to existing rows), InnoDB's approach resulted in less write amplification.


Concern 2: Secondary Index Structure

This is the technical point that generated the most debate.

In PostgreSQL: A secondary index entry contains the column value and the physical location of the row in the heap (ctid). When a row is updated, its heap location changes — every secondary index must be updated to reflect the new ctid.

PostgreSQL secondary index on (email):
  "alice@example.com" → ctid (1, 5)   ← physical location: page 1, slot 5
  "bob@example.com"   → ctid (1, 6)

When the "alice" row is updated:
  Row written to new heap location: ctid (3, 2)
  Index entry must be updated: "alice@example.com" → ctid (3, 2)

With many secondary indexes on a frequently-updated table, each update touches many index pages.

In MySQL InnoDB: Secondary index entries contain the column value and the primary key value (not the physical location). The primary key is the clustered index — it points to the physical row.

MySQL secondary index on (email):
  "alice@example.com" → primary key: 1
  "bob@example.com"   → primary key: 2

When the "alice" row is updated:
  Row is updated in-place in the clustered index
  Secondary index entry: "alice@example.com" → primary key: 1
  (unchanged — primary key didn't change)

For tables with many secondary indexes and frequent updates, MySQL's approach results in fewer secondary index updates per row write.

The trade-off: MySQL's secondary index lookups require two index traversals — first the secondary index to get the primary key, then the clustered index to get the actual row data. PostgreSQL's direct heap lookup is more efficient for index reads.

Uber's workload was write-heavy on these tables, so MySQL's approach suited them better.


Concern 3: Replication Architecture

Uber's third concern was about their specific replication setup.

PostgreSQL's physical replication (streaming replication, the most common setup at the time) sends WAL (Write-Ahead Log) records — low-level byte changes to data pages. This is efficient but has a significant constraint: the replica must run the same major version of PostgreSQL as the primary.

Upgrading PostgreSQL major versions (e.g., from 9.3 to 9.4) required:

  1. Setting up a new replica with the new version
  2. Doing a full base backup onto the new replica
  3. Cutting over to the new replica
  4. Repeating for all replicas

For Uber's topology (multiple regions, many replicas), major version upgrades were operationally painful.

MySQL's statement-based or row-based replication sends logical operations — the SQL statements or the row changes. Replicas with different MySQL minor versions can follow a primary. Major version upgrades can be done by upgrading replicas first and then doing a primary cutover — a much more graceful process.

Uber also used a custom replication topology with multiple levels (primary → regional replica → local replica), which worked more cleanly with MySQL's logical replication protocol.


What the Critics Said

The PostgreSQL community's response raised valid counterpoints:

HOT Updates

PostgreSQL 8.3 introduced Heap-Only Tuple (HOT) updates. If a row is updated and the updated columns are NOT indexed, PostgreSQL can update the row in-place without touching any secondary indexes. For rows where only non-indexed columns change, there's no secondary index write amplification.

Uber's critics argued that proper indexing strategy would have reduced the write amplification significantly.

Autovacuum Tuning

Autovacuum's default configuration is conservative. For write-heavy tables, aggressive autovacuum tuning (lower thresholds, more workers) can keep table bloat under control. Many PostgreSQL deployments at Uber's scale run fine with proper tuning.

Logical Replication Existed

PostgreSQL has had logical replication (pglogical, later built-in as of PG10 in 2017) that addresses the replication upgrade concern. Uber's post predated the built-in support, but third-party solutions existed.

The General Point

The critics' general point: Uber encountered specific operational challenges with PostgreSQL that were solvable with proper tuning and configuration. The migration to MySQL was a valid response to those challenges at the time and with the team's expertise, but it wasn't the only solution.


What's Actually True

Both databases are excellent, general-purpose relational databases. The technical differences are real but nuanced:

| Dimension | PostgreSQL | MySQL InnoDB | |-----------|-----------|-------------| | MVCC old versions | In-heap (table bloat risk) | Undo log (no vacuum needed) | | Secondary index lookup | Direct heap (faster reads) | Via primary key (extra hop) | | Secondary index updates | ctid-based (more updates on write-heavy tables) | PK-based (fewer updates for non-PK rows) | | Replication type | Physical WAL (strict version matching) | Logical row/statement (flexible) | | JSON support | JSONB (excellent) | JSON (decent, improving) | | Extensions | PostGIS, TimescaleDB, etc. (strong) | Limited | | SQL standards | More compliant | Less strict | | Query planner | More sophisticated | Improving |

For Uber's specific workload in 2015-2016:

  • Many small writes to existing rows
  • Many secondary indexes per table
  • Complex replication topology
  • Frequent schema migrations required
  • Team had more MySQL operational expertise

...MySQL InnoDB was a reasonable choice.


The Lesson

Database selection is workload-specific, not absolute.

PostgreSQL is generally considered the better database for:

  • Complex analytical queries
  • Geospatial data (PostGIS)
  • JSON document storage alongside relational data
  • Strong SQL compliance requirements
  • Rich extension ecosystem
  • Read-heavy workloads with complex queries

MySQL InnoDB can be a better fit for:

  • High-throughput OLTP write workloads on tables with many secondary indexes
  • Teams with strong MySQL operational expertise
  • Workloads that benefit from clustered primary key storage

The dangerous lesson to take from Uber's post is "MySQL is better than PostgreSQL." The correct lesson: understand your workload's characteristics, understand the trade-offs of each database's internal architecture, and make a data-driven decision.

Both Notion (PostgreSQL at scale) and Uber (MySQL at scale) succeeded. Their database choices reflected their workloads and teams — not a universal truth about which database is better.


Further Reading

  • Uber Engineering Blog: "Why Uber Engineering Switched from Postgres to MySQL" (2016)
  • Craig Kerstiens's rebuttal: "Why I Disagree with Uber's Rebuttal of Postgres" (2016)
  • PostgreSQL documentation: HOT updates, autovacuum
  • Course: Database internals and selection

Related Case Studies

Go Deeper

Case studies teach the "what". Our courses teach the "how" — the patterns behind these decisions, built up from first principles.

Explore Courses