You would hate PostgreSQL if you have an update-heavy use case because every update writes a brand new tuple, and the old one does not go away.
This is MVCC (Multi-Version Concurrency Control) - when you update a row, PostgreSQL does not modify it in place. It creates a new tuple with the updated values, sets the old tuple’s xmax field to the current transaction ID, and leaves the old row sitting on disk.
The old tuple is now “dead” - invisible to new transactions but still consuming physical space. Thus, a single logical update effectively doubles the storage for that row until vacuum reclaims it.
For read-heavy or mixed workloads, autovacuum handles this quietly in the background. But for update-heavy workloads - think session stores, order pipelines, leaderboards, or counters - dead tuples can pile up faster than autovacuum can clean them.
The default autovacuum triggers after roughly 20% of a table has changed. On a table with 500 million rows, that means you could have 100 million dead tuples accumulating before cleanup even starts.
The performance impact is not just storage. Bloated tables also affect sequential scans, which now have to read significantly more pages from disk. Index scans slow down because index entries still point to dead tuples and must be resolved.
By the way, there is an escape hatch for this called Heap-Only Tuple updates. When the updated row fits on the same page, and the changed columns are not indexed, PostgreSQL can skip creating new index entries. Cheaper, but the dead tuple on the heap still builds up.
If you are running a genuinely update-heavy workload on PostgreSQL, just make sure you tune your autovacuum really well.