The first thing you will probably learn about Postgres, if you follow people who don't like Postgres, is that MVCC is bad. The 40-year-old design mistake. It's signatures are everywhere. Bloated tables that double in size, 32-bit transaction counter limit, the never ending struggle with VACCUM, dead tuples nightmares. It comes with credentials, too: Uber measured the write amplification in 2016 and left for MySQL over it; Andy Pavlo's database group called MVCC the part of PostgreSQL they hate the most. It's a real thing. Postgres is as bad as it gets.
While none of this is exaggerated, it comes down to a real design choice. The bloat, the amplified writes, the vacuum babysitting: every charge traces to a decision, not a defect, and we reproduce each one below on a live PostgreSQL 19 beta2 instance, so you can watch the damage happen yourself. But the verdict that spreads from community to community always stops one question early: compared to what? What does every other engine do instead, and what does that cost?
Because MVCC is not optional. Any database that wants readers to not block writers has to keep multiple versions of rows somewhere, and every engine that does so answers the same four questions:
Where do old versions live? In the table itself, or in a separate structure? Which way do version chains point? From old to new, or new to old? What do indexes point at? A physical row location, or a logical key? Who cleans up, and when? A background process later, or the transaction itself?
PostgreSQL's answers: in the table, old to new, physical location, background process later. Every cost the critics list follows from those four answers. And every alternative is a different set of answers with the bill sent to someone else, the writer, the reader of history, tempdb, the cache, the compactor. One of them spent years of engineering to buy the one property PostgreSQL's design has had for free since day one. All of them fail, differently, when a transaction stays open over lunch.
The charges against PostgreSQL
If you want the full mechanism, PostgreSQL MVCC, Byte by Byte walks through it with pageinspect . The short version: an UPDATE in PostgreSQL never modifies a row. It writes a complete new copy of the row into the heap, stamps the old version's t_xmax , and leaves both versions sitting on disk. Visibility is decided at read time, tuple by tuple. Cleanup is somebody else's problem, specifically VACUUM's.
Four charges follow, reproduced one at a time below.
Charge 1: write amplification
The heart of Uber's complaint. Because every index on the table points at the row's physical location (a page number plus a slot, the ctid), and an UPDATE creates a new physical row, every index needs a new entry pointing at the new location. Even indexes on columns you didn't touch.
... continue reading