skip to content

questions

5

In a database that uses multi-version concurrency control (MVCC), deleting a million rows usually frees no disk space immediately. Why does the engine keep the old row versions, and what eventually removes them?

level: juniorimportance: must knowfreq 60%

answer

  1. MVCC = readers see a snapshot, so old versions must survive
  2. DELETE marks expired, does not free
  3. Undo-based purge vs heap vacuum — same idea
  4. Space reused inside the object, not returned to OS
  5. Rollback also leaves garbage

basics

~20 s

Under MVCC an UPDATE or DELETE does not overwrite data; it marks the old version dead so transactions with older snapshots can still read it. The space is only reclaimable once no running transaction can see that version, and a background cleanup process (vacuum or undo purge) reclaims it later.

solid answer

~50 s

MVCC gives readers a consistent snapshot without blocking writers, and it does that by keeping **multiple versions** of a row. An `UPDATE` writes a new version and marks the old one as expired; a `DELETE` only marks the row expired. Neither frees anything at once. The old version must survive while any transaction whose snapshot predates the change might still read it — that is the whole point. Only when no live transaction can see it does it become garbage. Cleanup is asynchronous and comes in two flavours. In **in-place-update engines with an undo log** (InnoDB, Oracle), the current version lives in the table and old versions live in undo/rollback segments; a purge thread truncates undo once it is no longer needed. In **append-style engines** (PostgreSQL-style heaps), all versions live in the table itself and `VACUUM` marks dead tuples' space reusable, plus cleans the corresponding index entries. In both cases the space is normally returned to the table for reuse, not to the operating system.

go deeper

for a junior

State that MVCC keeps old versions so concurrent readers see a consistent snapshot, that DELETE only marks rows dead, and that background cleanup reclaims the space later for reuse.

for a middle

Distinguish undo-based purge from heap vacuum, explain that updates produce versions too, and that space is reused rather than returned.

for a senior

Add the operational angle: cleanup as a background service that competes for I/O and can be stalled by old snapshots; prefer partition drops over bulk deletes.

for a principal

Frame lifecycle management as a schema decision — partitioning and retention design so reclamation is a metadata operation rather than a maintenance burden.

## Why versions exist at all The classic problem: a long report reads a table while other sessions modify it. Locking readers against writers is correct but kills concurrency. **MVCC** solves it by never destroying data in place. Each transaction gets a **snapshot** — a definition of which versions are visible to it — and reads the version that was current as of that snapshot. Readers do not block writers and writers do not block readers. The price is that a row is not one thing. Each version carries bookkeeping (creating transaction id, expiring transaction id, or a pointer to the previous version) so visibility can be decided per version, per transaction. ## What UPDATE and DELETE actually do - **UPDATE**: a new version of the row becomes current, and the previous version is marked expired by this transaction. Whether the new version is written into the table (append-style) or the old one is moved to undo (in-place style) is an engine design choice. - **DELETE**: no new version. The existing one is stamped with the deleting transaction id. It is invisible to new transactions after commit, but still physically present and still readable by older snapshots. - **ROLLBACK**: the new version is the one that becomes garbage instead. Note that rolled-back work also leaves debris — an aborted bulk load still consumed space. This is why `DELETE FROM t` leaves the file exactly as large as before, and why the row count dropping to zero does not shrink the database. ## Two engine families **Undo-based / in-place update** (InnoDB, Oracle). The table holds the newest version. Older versions are reconstructed by walking undo records in rollback segments. Consequences: the table stays compact, but the undo log grows with the amount of change that old snapshots might still need. A **purge** process truncates undo once no transaction needs it, and also removes index entries for delete-marked rows. **Append-style / heap versions** (PostgreSQL-style). Every version is a tuple in the table's pages, with expired ones sitting there as **dead tuples**. `VACUUM` scans, confirms which tuples are invisible to everyone, marks their space free for reuse within the page, and removes matching index pointers. Consequences: no separate undo store, but the table itself grows and must be cleaned. The vocabulary differs; the concept — asynchronous reclamation of versions nobody can see — is identical. ## Space is reused, not returned After cleanup, freed space is normally recorded as **reusable inside the object**. New rows fill it. The file does not shrink and the operating system sees no change. This is deliberate: shrinking a file requires moving live rows and an exclusive lock, which is far too disruptive for routine maintenance. So a table that held 100 GB, then had 90% of its rows deleted, will still occupy roughly 100 GB but will absorb the next 90 GB of inserts without growing. Actually returning space needs an explicit rewrite operation (a full vacuum, a table rebuild, `OPTIMIZE TABLE`, dropping a partition), all of which are expensive and locking. The practical takeaway is that **`DELETE` is not how you reclaim disk**. Deleting a partition or truncating a table drops whole files and returns space immediately; row-by-row deletion does not. ## Why this matters to an application developer Three everyday consequences: 1. **Deletes are expensive writes.** A `DELETE` writes to the row, writes to the write-ahead log, and creates future cleanup work in the table and in every index on it. Bulk deletion is often slower and heavier than the insert that created the rows. 2. **Updates behave like insert + delete for space purposes.** A table updated constantly grows and needs cleanup even if its row count never changes. A row of counters updated once a second produces 86,400 dead versions a day. 3. **Cleanup is a background service you can starve.** It competes for I/O, and — critically — it can be blocked entirely by a single long-running or idle-in-transaction session, because that session's snapshot may still need the old versions. That is the most common cause of runaway table growth in MVCC systems. ## Quick model to remember Write paths create versions; snapshots keep versions alive; a background collector frees them once no snapshot can see them; freed space is reused in place. Every operational problem in this area is one of those four steps going wrong.

  • You deleted 90% of a 100 GB table but disk usage did not drop. Why, and how do you actually reclaim the space?
    Cleanup marks the space reusable inside the table rather than returning it to the filesystem, because shrinking a file requires relocating live rows under an exclusive lock. The table will absorb future inserts without growing. To truly return space you need a rewrite — a full vacuum, table rebuild or OPTIMIZE-style operation — or a design where you drop a partition instead of deleting rows.
  • Does an UPDATE that changes one column of a wide row create a whole new version?
    In append-style engines, yes: the whole tuple is rewritten as a new version, which is why updating a single counter column on a wide row is far more expensive than it looks, and why every index entry may need updating too unless the engine can apply an in-page optimization. In undo-based engines the row is modified in place and only the before-image of the changed part goes to undo, so the cost profile differs, but old versions are still produced.

Crossing out a line in a ledger instead of erasing it: everyone reading an earlier photocopy still needs the original text, and the page only gets reused after every such reader has finished.

saying these in an interview costs you the question

  • Believing DELETE frees disk space immediately
  • Thinking MVCC means there is only ever one copy of a row
  • Assuming a rollback leaves no trace on disk
  • Saying updates are cheaper than inserts because the row count does not change
  • Expecting routine cleanup to shrink files back to the operating system

context

open as a page

How does an MVCC engine decide that a particular old row version is safe to reclaim? Explain the role of the oldest still-running transaction.

level: middleimportance: must knowfreq 52%

basics

~20 s

The engine computes a horizon: the oldest snapshot any active transaction could use. A version is reclaimable only if it was expired by a committed transaction older than that horizon, so nobody could still see it. One old transaction holds the horizon back and blocks cleanup for the whole database.

open as a page

A reporting query has been running for six hours, and another session has sat idle inside an open transaction all morning. What does that do to version cleanup, undo or rollback-segment growth, and table size, and how would you diagnose and prevent it?

level: seniorimportance: must knowfreq 48%

basics

~20 s

Both pin the cleanup horizon, so no version expired since they started can be reclaimed anywhere. Undo segments grow, tables and indexes bloat, version chains lengthen and reads slow down. Diagnose by finding the oldest transaction and its state; prevent with idle-in-transaction and statement timeouts, chunked batches, and monitoring oldest-transaction age.

open as a page

What is table and index bloat in an MVCC database, what symptoms does it produce, and what actually returns the space — to the object for reuse versus to the operating system?

level: seniorimportance: should knowfreq 42%

basics

~20 s

Bloat is space allocated to dead versions and half-empty pages rather than live data. Symptoms: files far larger than live data, scans reading mostly garbage, cache polluted, plans degrading. Ordinary cleanup frees space for reuse inside the object; only a rewrite or dropping a partition returns it to the operating system.

open as a page

You own a queue-like table in an MVCC database where rows are inserted and then updated to a completed state thousands of times per second, and background cleanup can never keep up. How would you redesign around dead-version accumulation?

level: principalimportance: should knowfreq 30%

basics

~20 s

Reduce versions produced and make removal a metadata operation. Narrow the churned row, cut updates per item, partition by time or state so completed work is dropped as a partition rather than deleted, tune cleanup to be far more aggressive on that table, and keep transactions short so the horizon never stalls.

open as a page