skip to content

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%

answer

  1. Bloat = allocated minus live, dead versions + fragmentation
  2. Indexes bloat worse: splits never merge back
  3. Symptoms indirect: I/O per row, cache pollution
  4. Cleanup = reuse in place; rewrite = return to OS
  5. Prevent: partition drops, short transactions

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.

solid answer

~60 s

**Bloat** is the gap between the space an object occupies and the space its live rows need. It comes from dead row versions left by updates, deletes and rollbacks, and from pages left partly empty after cleanup — including index pages, which bloat independently and often worse than the heap. Symptoms are indirect: a table many times larger than `live rows × row width`; sequential scans that read mostly dead tuples so I/O per useful row rises; a buffer pool full of garbage pages, which pushes out useful data and raises cache misses; index scans touching more pages per lookup; backups and maintenance taking longer. Routine cleanup marks space **reusable inside the object**. Files do not shrink; new rows fill the holes. That is intentional, since shrinking requires relocating live rows. Returning space to the filesystem needs a rewrite — a full vacuum, a table/index rebuild, an OPTIMIZE-style operation — or dropping a partition. Rewrites need roughly a second copy's worth of free space and take strong locks unless the engine offers a concurrent variant. The durable fix is preventing bloat: bound transaction lifetime and design deletes as partition drops.

go deeper

for a junior

Explain that bloat is space taken by dead versions rather than live rows, and that cleanup frees it for reuse rather than shrinking the file.

for a middle

Add fragmentation and index bloat as distinct sources, and name the rewrite operations that actually return space.

for a senior

Quantify with an allocated-versus-live ratio, connect bloat to I/O per useful row and cache pollution, and sequence remediation after fixing the cause, preferring online rebuilds.

for a principal

Design it away: partitioned retention so deletion is a partition drop, enforced transaction lifetime limits, narrow tables for volatile columns, and bloat ratio as a tracked capacity metric.

## Defining bloat Measure two things: the object's **allocated size** on disk, and the **live data size** — roughly live row count times average row width, plus index overhead. Bloat is the difference. A little is healthy: free space in pages absorbs future updates without splitting or relocating rows, which is exactly what a fill factor reserves. A lot means most of your I/O buys nothing. Bloat has two sources: 1. **Dead versions** that cleanup has not yet reclaimed, or cannot reclaim because the visibility horizon is pinned by an old snapshot. 2. **Fragmentation**: pages left partly empty after reclamation, index pages that split and never re-merge, and rows relocated by updates leaving holes behind. Even with perfect cleanup, a heavily churned object does not repack itself. ## Index bloat deserves separate attention Every version of a row generally needs index entries, and expired entries linger until cleaned. B-tree pages split under insert pressure and typically do not merge back when entries are removed, so an index can end up half empty and stay that way. It is routine to find indexes several times larger than necessary while the heap looks acceptable — and since index lookups touch a path of pages, bloat there directly raises the page count per lookup. ## Symptoms, and why they are indirect Nothing reports 'you have bloat'. You infer it: - **Size versus expectation.** Live rows × average width ≪ allocated size. - **I/O per useful row rises.** A scan that used to read 1 GB to return 5 million rows now reads 4 GB for the same result. - **Cache pollution.** The buffer pool holds pages that are largely dead versions, so the effective cache is smaller and miss rates climb — often felt as latency on *unrelated* queries. - **Degrading index performance** despite unchanged data volumes. - **Slower maintenance**: backups, rebuilds and statistics gathering scale with allocated size, not live size. - **Estimates drift**: statistics derived from sampling bloated pages can skew plans. ## Reuse versus return: the crucial distinction Routine cleanup (vacuum, purge) makes dead space **available for reuse within the object**. The file stays the same size, and subsequent inserts and updates consume the freed space rather than extending the file. For a table in steady state this is exactly right — the space would only be re-allocated anyway, and repacking would cost far more than it saves. So after a one-off deletion of 90% of a huge table, the correct expectation is: the file stays big, the table stops growing, and it will absorb roughly that much new data for free. Actually returning space to the operating system requires **rewriting the object**: - a full vacuum or `OPTIMIZE`/`ALTER TABLE … REBUILD` style operation, which writes a fresh compact copy and swaps it in; - rebuilding indexes, often the larger win since indexes fragment most; - dropping a partition or truncating the table, which unlinks whole files and is instant. Rewrites carry real costs. They typically need free disk roughly equal to the object's live size (you hold both copies briefly), they are I/O-heavy, and unless the engine offers a concurrent/online variant they take an exclusive lock for the duration — which on a large table is an outage. Concurrent rebuild variants exist on most engines and are the right default; they trade extra I/O and time for availability. ## Deciding whether to act A useful sequence: 1. **Quantify** the ratio rather than reacting to absolute size. Two-times allocated versus live on a hot, constantly-updated table is often just steady-state working space. 2. **Ask whether it will be reused.** If the table's insert rate will refill the space within days, a rewrite buys nothing but I/O. 3. **Fix the cause first.** If the horizon is pinned by a long transaction, a rewrite is treating a symptom — the table will bloat straight back. 4. **Prefer online/concurrent rebuilds**, and prefer rebuilding indexes to rewriting the whole table when indexes are the bloated part. 5. **Schedule around load** and verify you have the free space for a second copy before starting. ## Prevention beats remediation The designs that avoid bloat entirely: - **Partition by time** and drop old partitions instead of deleting rows. A drop is a metadata operation that returns space instantly with no cleanup work. - **Bound transaction lifetime** so the horizon stays young and cleanup can keep up. - **Do not store hot counters in wide rows.** Frequent updates on a wide table generate large dead versions; move volatile columns into a narrow companion table. - **Batch bulk changes** in bounded transactions. - **Tune cleanup aggressiveness** on high-churn tables so it triggers on a smaller fraction of changed rows rather than a global default suited to quiet tables. - **Trend allocated versus live size** in monitoring, so bloat is visible as a slope long before it becomes a disk alert.

  • When would you rebuild a bloated table, and when would you leave it alone?
    Leave it alone when the object is in steady state and will refill the freed space soon, since a rewrite only spends I/O to reach the same size again. Rebuild after a genuine one-off shrinkage — a large archival delete or a retired feature — or when bloat is so severe that scan and cache costs are measurably hurting queries. Always fix the cause first, use a concurrent/online variant, and confirm you have free space for a second copy.
  • Why do indexes often bloat more than the table itself?
    Every row version generally requires index entries, and removed entries leave gaps that B-tree pages usually do not merge back, so pages stay half empty after churn. Random insert patterns also cause splits that never consolidate. Because each lookup traverses a path of pages, that emptiness directly raises pages touched per lookup, which is why rebuilding indexes is often the cheapest large win.

A warehouse where emptied crates are left on the shelves: staff can restack new goods into them (reuse), but the building only gets smaller if you physically consolidate everything into fewer aisles — a disruptive operation you do rarely.

saying these in an interview costs you the question

  • Expecting routine vacuum or purge to shrink files back to the operating system
  • Reacting to absolute table size instead of the allocated-versus-live ratio
  • Rebuilding a table while a long transaction still pins the cleanup horizon
  • Forgetting that a full rewrite needs free space for a second copy and takes a strong lock
  • Ignoring index bloat because the heap looks fine

context