The 30 second answer
I'd switch databases only when a measured limit is built into the engine's design and can't be fixed by tuning, a newer version, or sharding. Uber left Postgres in 2016 over write amplification and replication pain, mostly on version 9.2. Instagram kept Postgres and scaled it with thousands of logical shards. So first I'd check HOT updates, logical replication and sharding, then price the migration.
The two stories, stated precisely.
- Uber (2016) moved its core storage from PostgreSQL to MySQL, under its own Schemaless layer. Its engineering post lists the reasons: write amplification, inefficient replication across data centers, a replica data-corruption bug it hit during a primary promotion, MVCC trouble on replicas, and difficult major-version upgrades. The post also says Uber's experience was mainly with PostgreSQL 9.2. That caveat matters, because several of these issues were fixed or reduced in later releases.
- Instagram (2012) stayed on PostgreSQL and sharded it. It split its data into thousands of logical shards, implemented as Postgres schemas, and mapped them onto a much smaller number of physical servers. It also built 64-bit IDs that sort by time and carry the shard number inside them: a millisecond timestamp, a logical shard ID and a per-shard sequence.
These are not two answers to the same question. The posts were written four years apart, at different scale, and for different workloads. Uber's problem was update-heavy rows with many secondary indexes, replicated across data centers. Instagram's problem was spreading a fast-growing write load across many machines.
Why Postgres can amplify writes. Postgres MVCC never edits a row in place: an UPDATE writes a new row version. Its secondary indexes point at the physical location of a row version, so a new version can mean a new entry in every index on the table, and all of that goes into the WAL that replicas receive. In MySQL's InnoDB, secondary indexes store the primary key instead, so an update that doesn't touch an indexed column doesn't change the secondary indexes.
What weakens the "every update rewrites every index" claim: HOT. Postgres has had heap-only tuple (HOT) updates since 8.3, so this predates Uber's move. The current documentation says an update can skip new index entries when no indexed column is modified (summarizing indexes such as BRIN don't count) and the page holding the old version has enough free space for the new one. Then the new version is chained from the old one on the same page. You can make HOT more likely by lowering a hot table's fillfactor (the default is 100, meaning fully packed pages). You can measure it by comparing n_tup_hot_upd with n_tup_upd in pg_stat_user_tables. Uber's case was weak for HOT because its updates changed indexed columns on tables with many indexes, so HOT couldn't apply. That's a workload fact, not a universal Postgres rule.
What weakens the upgrade argument: logical replication. In 2016 Postgres replication was physical, meaning byte-level WAL, so a replica had to run the same major version as the primary. Upgrades needed pg_upgrade and downtime. PostgreSQL 10 (2017) added built-in logical replication, which publishes row changes and can feed a subscriber running a different major version. That makes near-zero-downtime major upgrades and cross-version migrations practical.
How I decide, in order.
- Measure the pain. Name the metric, for example WAL volume per second, replica lag, p99 write latency or the HOT ratio. Then name the operation that causes it. "Postgres is slow" is not a reason.
- Try fixes inside the engine. Drop unused indexes, avoid updating indexed columns, tune
fillfactor, upgrade the version, and move to logical replication or partitioning. - Try changing the shape of the data on the same engine. Shard it (Instagram's route), split hot columns into their own table, or move append-only data to a log or event store.
- Only then price a switch. Count dual writes or change data capture (CDC), backfill, verification, rollback, rewriting queries and tooling, retraining on-call engineers, and the new engine's own failure modes. Uber didn't just swap engines: it ran MySQL underneath Schemaless, its own sharded storage layer.
- Migrate per workload. Move the one table or service with the measured problem, not "the company".
Logical sharding in detail (Instagram's pattern).
- Pick far more logical shards than you have servers today, so growth means moving shards, not re-hashing keys.
- Store the mapping from logical shard to physical host in configuration or a small directory service. Moving a shard is then copy, catch up, and flip the mapping.
- Put the shard ID inside the primary key, as Instagram's IDs do. Any service can then route a request without a lookup.
- Choose the shard key from the main access path, such as the user ID. Cross-shard queries, joins and transactions become application work, so design them out early.
What a senior answer sounds like. "I wouldn't treat Uber and Instagram as a vote on Postgres versus MySQL. Uber's post is honest that it describes 9.2 and a write pattern where HOT couldn't help. Instagram shows that the same engine scales a different workload with logical shards. Before migrating, I'd show the metric, rule out the in-engine fixes, and estimate the migration cost against the gain."
Alternate interview scenarios
"Our Postgres WAL volume tripled after we added two indexes."
Check n_tup_hot_upd against n_tup_upd. If the new indexes cover columns that change often, every update now loses HOT and writes to every index. Remove or redesign the index, or move the changing column into a narrow side table. Lowering fillfactor helps only if no indexed column changes. Switching engines is not the first fix.
"We need to upgrade Postgres from version 12 to 16 without downtime."
Set up logical replication from the old primary to a new cluster on 16. Copy the data, let it catch up, compare row counts and checksums, then switch over during a short write pause. Sequences aren't replicated by logical replication, so set them before switching. Keep the old cluster for a while so you can roll back.
"One Postgres primary can't keep up with writes. The CTO wants to move to MySQL."
Ask what MySQL would actually fix. If the limit is total write throughput on one machine, MySQL hits the same ceiling, so the real fix is sharding on either engine. Propose logical shards keyed by tenant or user ID with a shard map, then move shards to new hosts as load grows.
"We sharded by user, and now a celebrity account makes one shard hot."
Logical shards help, because you can move that shard to its own host. But a single key can still outgrow one machine. Split that user's heavy data, such as followers or feed entries, by a secondary key. Cache reads, or fan out writes asynchronously.
"Design IDs for a sharded database without a central ID service."
Use Instagram-style 64-bit IDs: a millisecond timestamp from a custom epoch, the logical shard ID, and a per-shard sequence, generated inside the database or service. They sort by time and route by themselves. Discuss clock skew and the per-millisecond sequence limit.
Sources (9)
- Uber Engineering: Why Uber Engineering switched from Postgres to MySQL (2016)uber.com
- Uber Engineering: Designing Schemaless, Uber's scalable datastore using MySQLuber.com
- Instagram Engineering: Sharding & IDs at Instagram (2012)instagram-engineering.com
- PostgreSQL documentation: Heap-only tuples (HOT)postgresql.org
- PostgreSQL documentation: CREATE TABLE storage parameters (fillfactor)postgresql.org
- PostgreSQL documentation: pg_stat_user_tables statisticspostgresql.org
- PostgreSQL 10 documentation: Logical replicationpostgresql.org
- PostgreSQL documentation: pg_upgradepostgresql.org
- PostgreSQL 10 documentation: Logical replication restrictions (sequences)postgresql.org