When a stored row is rewritten at a larger size and no longer fits on its current page, what options does the storage engine have, and how does it find a page with enough free space for a new row in the first place?
answer
- free-space map: coarse, cached, approximate hint
- compact the page first, slots absorb the move
- off-page a large value to fit again
- relocate: update all indexes, or leave a forwarding pointer
- fill factor trades scan I/O against relocation
basics
~30 sFor a new row the engine consults a free-space map, a compact per-page summary of remaining space, to pick a page without scanning the file. If a rewritten row no longer fits, the engine first compacts the page; if that is still not enough it places the row on another page, either updating index entries or leaving a forwarding pointer behind, which costs an extra read on later lookups.
solid answer
~60 s**Finding space for an insert.** Engines keep a **free-space map**: a small structure with a coarse free-space figure per page, so the engine can locate a suitable page in a couple of reads instead of scanning the table. The granularity is deliberately coarse and slightly stale; it is a hint, and the engine verifies on the page itself. **A row that grew.** In order of preference: 1. **Compact the page.** Slide live rows together to merge fragmented holes; because rows are addressed by slot number, nothing outside the page changes. 2. **Move a large value off-page**, if the growth came from a big variable-length column. 3. **Place the row on another page.** Then the old address must remain meaningful, so either every index entry is updated, or a **forwarding pointer** (a redirect stub) is left at the old address. Forwarding costs one extra page read on each subsequent lookup, and chains of them are a known degradation. **Prevention.** Reserve free space per page at load time (a fill factor) so in-place growth and same-page rewrites stay local. Full pages force relocation; over-reserved pages waste I/O on every scan.
go deeper
Say the engine keeps a map of free space per page to choose where a row goes, and that a row too big for its page has to be compacted into place or moved elsewhere.
Order the options: fit in place, compact the page (slots absorb the move), push a large value off-page, relocate; and explain why relocation involves indexes.
Contrast updating all index entries against forwarding pointers, quantify the fill-factor tradeoff against scan cost, and name the production symptoms of accumulated relocation.
Turn it into physical design policy: per-table fill factor driven by update profile, schema choices that stop rows from growing after insert, and maintenance windows sized against fragmentation growth.
## Two related problems A storage engine constantly answers two placement questions: *where does this new row go?* and *what if a row no longer fits where it is?* Both come down to how free space is tracked and how much of it was left available. ## Tracking free space Scanning the table to find a page with room would be absurd, so engines maintain a **free-space map**: a compact side structure recording, per page, roughly how many bytes remain. It is compact by design, often just a few bits per page giving a bucketed figure, so that a map covering a huge table still fits in a handful of pages and stays cached. A tree or hierarchical layout over those summaries lets the engine descend to a suitable page in a couple of reads. Two properties matter: - **It is approximate.** Bucketed values under-report or over-report. The engine treats the map as a hint and confirms against the target page, falling back to another candidate if the space is gone. - **It can be stale.** Space freed by deletions typically becomes visible in the map only after background maintenance has processed the page. So a table can have plenty of dead space and still extend its file, because the engine did not know the space was reusable yet. On insert, the engine picks a page with enough room, writes the row, adds a slot, and updates the map's entry. If nothing suitable exists, the file is extended with a fresh page, which is why insert-heavy tables grow in bursts. ## When a row grows A rewrite can enlarge a row: a variable-length column gets a longer value, a previously NULL column receives data. The engine tries, in order: **1. Fit it in place.** If the new size is no larger, or the immediately following bytes are free, nothing else is needed. **2. Compact the page.** Deletes and earlier rewrites leave holes scattered through the row area. Sliding the live rows together merges those holes into one contiguous region. Because rows are addressed as (page, slot) and the slot holds the offset, this reorganisation is invisible outside the page: only slot offsets change. This is the cheapest recovery and it is why fragmentation within a page is a transient condition rather than a permanent loss. **3. Push a large value out of line.** If the growth came from a big variable-length column, moving that value to overflow storage and leaving a small pointer often shrinks the row enough to keep it on its page. **4. Relocate the row to another page.** Now the row's physical address changes, and every index entry pointing at the old address is stale. Two families of solution exist: - **Update the index entries.** Correct and keeps lookups at one hop, but expensive: every index on the table must be touched, so a table with six indexes pays six index writes for one row move. - **Leave a forwarding pointer.** The old slot becomes a stub pointing to the row's new location. Index entries stay valid, and the cost is deferred: every subsequent lookup that arrives at the old address pays one extra page read to follow the redirect. In engines that do this, accumulated *migrated rows* or *row chaining* is a recognised performance problem, detectable in statistics and cured by reorganising the table. Multi-version engines change the picture slightly: an update writes a new version rather than modifying the row in place, so the question becomes where that new version goes. Placing it on the same page enables an optimisation where indexes need not be touched at all, and placing it elsewhere forces the full index maintenance. Either way the underlying tension is identical: keeping the new image on the same page is what avoids index work. ## Prevention: reserving space Since same-page rewrites are the cheap path, engines let you reserve free space when filling pages, commonly called a **fill factor** or **pctfree**. Filling pages only to, say, 80 or 90 percent leaves room for rows to grow or for new versions to land locally. The tradeoff is direct and quantifiable. Reserving 20 percent means every sequential scan reads about 25 percent more pages forever, and the buffer pool caches 20 percent less useful data. So: - **Append-mostly tables never updated after insert** (event logs, audit trails, immutable facts): fill pages completely. Reserved space is pure waste. - **Tables with frequent in-place growth**, especially where the growing column starts NULL or short and later becomes long: reserve meaningfully, because relocation or forwarding is far more expensive than a few percent of extra scan I/O. - **Bulk loads followed by heavy updates**: load with reserved space, or accept a reorganisation later. ## Diagnosing in production Symptoms of getting this wrong: point lookups that gradually slow down without any change in data volume (accumulating forwarded rows), a table file much larger than the sum of its live row sizes (fragmentation plus reserved space plus unreclaimed dead space), and write throughput dropping as each update triggers index maintenance for relocation. The remedies are a reorganisation or rebuild to restore locality, a fill-factor change to prevent recurrence, and schema changes that stop rows from growing after insert, such as giving a column a realistic initial value rather than leaving it NULL until later.
- What is the cost of leaving a forwarding pointer instead of updating index entries?It makes the write cheap and the reads permanently more expensive: every lookup arriving at the old address reads that page, follows the redirect and reads a second page. On a heavily updated table these accumulate, so point lookups slow down over time with no change in data volume. The cure is a table reorganisation that rewrites rows into their proper places, plus a fill-factor change so it does not recur.
- When should the fill factor be left at 100 percent?For append-mostly tables whose rows are never updated after insert, such as event logs, audit trails and immutable fact tables. There is no in-place growth to accommodate, so any reserved space is dead weight that inflates every sequential scan and wastes buffer pool. Reserving space is worth it only where rows actually grow or where new row versions benefit from landing on the same page.
- A table's file keeps growing even though rows are deleted at roughly the rate they are inserted. What explains it?Freed space is not immediately visible or reusable. Background maintenance must process the pages before their space is reported in the free-space map, and in a multi-version engine the old row versions cannot be reclaimed while any transaction might still need to see them, so a long-running transaction pins them. Meanwhile inserts that find no known free page extend the file. The fix is to ensure maintenance keeps up and to eliminate long-lived transactions holding old snapshots.
A parking garage sign showing free spaces per floor: it is approximate and slightly out of date, but it saves driving every level; if your car has grown you first rearrange the floor, and only then take a space elsewhere and leave a note at your old bay.
saying these in an interview costs you the question
- Assuming an engine scans the table to find a page with free space
- Believing an updated row always stays on its original page
- Treating a forwarding pointer as free, ignoring the extra read on every later lookup
- Setting a large reserved-space percentage everywhere without accounting for the permanent scan cost
- Assuming deleted space is instantly reusable by the next insert