What happens to a B+tree index page when its entries are deleted? Describe underflow, merging and redistribution, and explain why real implementations often do not merge pages aggressively.
answer
- Underflow → redistribute from sibling, else merge
- Merge removes a separator; can cascade to root collapse
- MVCC: dead entry survives until no snapshot needs it
- Merges are concurrency-hostile; right-links only help splits
- In practice: only fully empty pages get recycled
basics
~20 sTextbook B+trees merge or redistribute when a page drops below half full, removing a separator from the parent and possibly shrinking the tree. Real engines mostly defer: entries are marked dead, only fully empty pages are recycled, because merging needs multi-page locking and MVCC forbids early removal.
solid answer
~60 sThe **textbook** delete path: remove the entry; if the page falls below its minimum occupancy, either **redistribute** entries from an adjacent sibling (if the sibling can spare them) or **merge** the two pages into one. Merging removes a separator key from the parent, which can itself underflow and cascade; if the root ends up with a single child, that child becomes the new root and the tree loses a level. **Real implementations** are far lazier, for three reasons. First, MVCC: a deleted entry may still be needed by older snapshots, so it can only be marked dead and reclaimed later by background maintenance. Second, concurrency: a merge must lock two siblings plus the parent in a consistent order while readers are traversing, which is harder and more deadlock-prone than a split — the right-link protocols that make splits cheap for concurrent readers do not make merges cheap. Third, it usually is not worth it: the workload often refills the range. So the common behaviour is to reclaim entries within pages and put only *completely empty* pages on a free list. Half-empty pages persist.
code
text · 10 linestextbook (min occupancy 50%)
delete from P5 -> 40% full, sibling P6 at 45%
cannot borrow -> merge P5+P6 into P5, drop separator from parent
parent may underflow -> repeat upward; root with 1 child -> height - 1
production (MVCC engine)
delete -> entries marked dead, page size unchanged
background cleanup -> dead entries removed, page now 40% full
page still 40% full, still in the tree, still in the leaf chain
only a 0%-full page is unlinked and put on the free listgo deeper
Know that deleting entries can leave a page underfull, that the textbook fix is merging with a neighbour, and that in practice the index does not shrink.
Describe redistribution versus merging, the separator removal in the parent, the cascade, and root collapse reducing height.
Explain why engines defer — MVCC visibility and the concurrency cost of merges — and translate that into the operational consequence for delete-heavy tables.
Design around it: choose delete strategies (partition drop over bulk DELETE) and key designs so that reclamation is structural rather than dependent on maintenance windows.
## The delete path in the textbook B+tree Every B+tree definition specifies a minimum occupancy — classically, each non-root page must remain at least half full. Deleting an entry can violate that, which is called **underflow**. The classical repair has two forms: **Redistribution (borrowing).** If an adjacent sibling has more than the minimum, move entries across so both pages are legal again. Only one separator key in the parent needs updating to reflect the new boundary; the tree's shape is otherwise unchanged. This is the cheaper repair and is preferred when possible. **Merging (coalescing).** If the sibling cannot spare entries — because it is itself at or near the minimum — the two pages are combined into one, and the now-redundant separator key is removed from the parent. At the leaf level, the emptied page is also unlinked from the sibling chain so ordered scans remain correct. Removing a separator from the parent is itself a delete, so the parent may underflow and repeat the same procedure one level up. In the extreme the cascade reaches the root; when the root is left with a single child, that child becomes the new root and the tree's height decreases by one. That is the exact mirror of the root split that grows the tree, and it is the only way a B+tree gets shorter. ## Why production engines behave differently **Reason one: multi-version visibility.** In an MVCC engine, deleting a row does not immediately make its index entry removable. Transactions that started earlier hold snapshots in which the row version is still visible, and they may legitimately reach it through the index. So the delete path marks the entry dead and defers actual removal to background maintenance that runs once no active snapshot can see the version. Aggressive structural repair at delete time is therefore not even possible — at the moment of the delete, the entries are still needed. **Reason two: concurrency cost.** A split touches a page, a new page, and the parent, and widely used protocols make it cheap for concurrent readers: each page carries a link to its right sibling and a high key, so a reader that lands on a page which has since split simply follows the link. Merging does not enjoy the same trick. It removes a page that concurrent readers and scans may be positioned on or about to follow, and it requires holding several pages consistently at once — two siblings plus the parent — which introduces lock-ordering and deadlock concerns. Many implementations therefore restrict merging severely, perform it only through a separate maintenance path, or handle only the simple case of a page that has become completely empty. **Reason three: it often is not worth doing.** Deletes frequently come in patterns that will be refilled. A queue table deletes low keys and inserts high ones; a table with a status index moves entries between ranges constantly. Merging pages that will be repopulated shortly is wasted structural work plus wasted logging. ## What actually happens, then The typical production behaviour is: 1. Delete marks index entries dead; the page's live count falls but its size does not. 2. Background maintenance later removes dead entries, reclaiming space **inside** the page. 3. If a leaf page becomes entirely empty, it is unlinked and placed on the index's free list, from which it can be reused anywhere in the index once no scan can still be positioned on it. 4. A page that ends up 20% full simply stays 20% full, and its space is reusable only for keys in its own range. The visible consequence is that indexes shrink far less readily than they grow, and that a large range delete leaves a long stretch of sparse pages that will never refill if the key is monotonically increasing — a date, an id — because no future key will ever fall into that range. ## What this means in practice - **Do not expect deletes to shrink an index.** Reclaiming that space requires a rebuild or a compacting reorganize, or removing the segment structurally. - **Delete patterns matter more than delete volume.** Deleting scattered rows leaves modest gaps that get reused; deleting a contiguous key range on an ever-increasing key leaves permanently dead space. - **Partitioning converts the problem into a non-problem.** Dropping a partition removes its index segment outright: no dead entries, no underfull pages, no maintenance. - **Long-running transactions block step 2.** If an old snapshot is held open, dead entries cannot be reclaimed at all, so even the in-page space recovery stops happening. ## Answering the question well A strong answer separates the two layers explicitly: the classical algorithm (underflow, redistribute, merge, cascade, root collapse) as the textbook contract, and the engineering reality (deferred reclamation under MVCC, merges avoided because they are concurrency-hostile, only empty pages recycled) as what engines actually do — and then connects that gap to the operational consequence: index space is not returned by deleting rows.
- Why is merging harder to make concurrent than splitting?A split adds a page, and protocols using a right-link plus a high key let a reader that arrives at a stale page simply follow the link to the new sibling — no reader is ever left pointing at something that no longer exists. A merge removes a page that concurrent scans may be positioned on or about to traverse, and it must hold two siblings and the parent consistently at the same time, which raises lock-ordering and deadlock issues. That asymmetry is why many implementations perform splits eagerly and merges rarely or not at all.
- After deleting 80% of a large table's rows, the index size is unchanged. Is something wrong?No, that is expected behaviour. Deletes mark entries dead; background maintenance reclaims space inside pages but leaves the pages allocated to the index, and only completely emptied pages are recycled onto a free list. The file's high-water mark is unchanged. Returning the space to the filesystem requires a rebuild or compacting reorganize — or, better, deleting by dropping a partition so the segment disappears entirely.
Emptying half the folders from filing drawers: the drawers stay in the cabinet. Only a drawer that ends up completely empty gets taken out and reused elsewhere.
saying these in an interview costs you the question
- "Deleting rows shrinks the index" — pages stay allocated; only a rebuild reliably returns the space
- "Every underflow triggers a merge" — production engines defer or skip merging; the textbook rule is not what most implementations do
- "Merging is just a split in reverse, so it costs the same" — it is materially harder under concurrency because pages disappear beneath readers
- "MVCC has nothing to do with the index" — index entries are versioned by row visibility and cannot be removed while an older snapshot might need them
- "A page that empties is lost space forever" — a fully empty page is recycled onto the free list; it is the partly-empty ones that persist