skip to content

Explain how a slotted page stores variable-length rows: what sits in the page header, where do the rows live, and why is there a slot or line-pointer array in between?

level: middleimportance: must knowfreq 55%

answer

  1. header, slots forward, rows backward, gap in middle
  2. slot equals offset plus length plus status
  3. address is page number plus slot number
  4. compaction moves rows, updates slots only
  5. dead slots wait for index cleanup

basics

~20 s

A page has a header at the front, an array of slots growing forward, and row data filling from the back. Free space is the gap in the middle. Each slot holds the offset and length of one row. Rows are addressed by slot number, not byte offset, so the engine can move rows within the page to compact free space without invalidating any external reference.

solid answer

~60 s

Layout, from the two ends inward: - **Header** at the front: page identifier, page LSN, checksum, pointers to the start of free space and to the lower end of the row area, plus flags. - **Slot array** (line pointers) growing forward from just after the header. Each entry is a few bytes: the row's byte offset within the page, its length, and a status such as used, dead or redirected. - **Row data** filling backwards from the end of the page. - **Free space** is the shrinking gap between them, so a single pair of pointers describes it. The indirection is the point. A row is addressed as (page number, slot number). Because nothing outside the page stores a byte offset, the engine can shuffle rows inside the page to reclaim fragmented space and just update the slots. Index entries pointing at that page stay valid. It also allows a slot to be marked dead while its space is not yet reclaimed, and to be redirected to another slot on the same page when a row is rewritten in place.

code

text · 13 lines
text
+---------------------------------------------------+
| header: pageLSN, checksum, freeStart, freeEnd     |
| slot[0]=(off 8100, len 92, USED)                  |
| slot[1]=(off 7990, len 110, DEAD)                 |
| slot[2]=(off 7850, len 140, USED)                 |
|            <-- slots grow this way                |
|                                                   |
|                 F R E E   S P A C E               |
|                                                   |
|            rows grow this way -->                 |
| ...row2 bytes... ...row1 bytes... ...row0 bytes...|
+---------------------------------------------------+
row address = (page number, slot number)

go deeper

for a junior

Describe the three regions and say a row is found by slot number, which holds its offset and length.

for a middle

Explain why the indirection exists: rows can be moved during in-page compaction without invalidating index references, and slots carry status such as dead or redirected.

for a senior

Connect it to observable behaviour: unstable result order without ORDER BY, why dead slots linger until index cleanup, and how redirects keep row addresses valid across in-page rewrites.

for a principal

Frame the slot array as the stable-address contract between the heap and every index; discuss what that contract costs (cleanup ordering, deferred slot reuse) and what it buys (index-free page reorganisation).

## The problem Rows are variable-length: NULLs, VARCHAR, differing column counts across versions. Storing them in a fixed page requires answering three questions: how to find row k inside the page, how to reclaim space when a row is deleted, and how to let external structures (indexes) refer to a row without breaking when the page is reorganised. The slotted page answers all three with one design. ## The layout Read a page from both ends toward the middle. **1. Page header** (tens of bytes at offset zero). Typical fields: the page's own identifier or checksum, the **page LSN** used by crash recovery to decide whether a logged change is already applied, offsets marking the start and end of free space, the count of slots, and flags such as *page is full* or *page has dead rows*. **2. Slot array**, immediately after the header, growing toward higher offsets. Each slot is small, on the order of 4 bytes, and holds: the offset where the row begins in this page, the row's length, and a status. A row's address is therefore (page number, slot number), a compact identifier commonly called a row id, tuple id or ctid depending on the engine. **3. Row data**, written from the end of the page downward. **4. Free space**, the gap between the growing slot array and the descending row area. Inserting a row consumes from both ends: one slot at the top and the row's bytes at the bottom. The page is full when the gap can no longer hold both. ## Why the indirection matters If indexes stored byte offsets, the engine could never move a row within its page, because every index entry pointing at it would break. With slot numbers, moving is free: rewrite the row's bytes elsewhere in the page and update its slot's offset. Nothing outside the page notices. This enables **in-page compaction**. Deleting rows leaves holes scattered through the row area. Rather than maintaining a free list of fragments, the engine simply slides the remaining rows together when it next needs space, updating offsets in the slots. The result is contiguous free space again, and page-internal fragmentation never accumulates permanently. Slot status carries further value: - **Dead or unused** marks a slot whose row is gone but whose slot number must not be reused until every index entry referencing it has been removed, otherwise an index could point at a different row. - **Redirect** lets a slot point to another slot on the same page. Engines use this when a row is rewritten in place (for example an update that stays on the page) so old references still resolve without touching indexes. ## The row itself Inside the row area, a row begins with its own small header: visibility or version information, a NULL bitmap with one bit per column, sometimes the column count and flags such as *has off-page values*. Fixed-width columns follow, often aligned to word boundaries, then variable-length columns each preceded by a length. Reading column seven means walking the layout, which is why fixed-width columns and column ordering can affect access cost and padding waste in some engines. ## Ordering within a page is not logical ordering Slot 1 need not hold the row with the smallest key, or the oldest row. After compaction and reuse of freed slots, physical position within a page is arbitrary. This is one physical reason a query without ORDER BY has no guaranteed order: the engine returns rows in whatever order it walks slots, which changes as the page is modified. ## Interaction with indexes An index entry stores the key plus the row address (page, slot). Lookups therefore cost the index traversal plus one page read per matching row, and the address's stability requirements explain much of the engine's bookkeeping: dead slots that cannot be reused early, cleanup processes that remove index entries before freeing slots, and redirect slots that keep addresses valid across in-page rewrites. ## What an interviewer is checking That you can explain (a) the three regions and the free-space gap, (b) that a row is identified by slot number rather than byte offset, and (c) the consequence: rows can be relocated within a page for compaction without touching indexes. Everything else, headers, NULL bitmaps, redirects, is supporting detail.

  • When a row is deleted, is its space immediately available for a new row?
    Not necessarily. The slot is marked dead and the bytes become reclaimable, but the actual space usually becomes usable only when the page is compacted, and in a multi-version engine only after no transaction can still see the old row and any index entries referencing that slot have been removed. Until then the slot number itself is also held back, because reusing it early would let an index entry resolve to an unrelated row.
  • Why can two identical SELECT statements without ORDER BY return rows in different orders?
    Because the returned order reflects the physical walk of pages and slots, and that layout changes. Compaction relocates rows within a page, freed slots get reused, updates can move rows to other pages, and the planner may switch between a sequential scan and an index scan. None of that is stable, which is why order must be requested explicitly rather than relied upon.
  • What is stored in the row header itself, as opposed to the slot?
    The slot holds the page-local addressing information: offset, length and status. The row header holds per-row metadata: version or visibility information used by the concurrency mechanism, a NULL bitmap with one bit per column, sometimes a column count and flags such as whether some value is stored off-page. The slot lets you find the row; the row header lets you interpret it.

A coat-check: your ticket number never changes, but the attendant is free to rearrange the coats on the rack to make room.

saying these in an interview costs you the question

  • Saying indexes store the byte offset of a row inside its page
  • Assuming slot 1 always holds the first-inserted or lowest-key row
  • Believing a delete instantly frees usable space in the page
  • Thinking rows are fixed-length and packed contiguously with no per-row bookkeeping

context