Skip to content

Step 1 of 3 · Reading · ~3 min

Handling Large Values Across Pages

Page-Based Storage

Overflow Pages: Values Bigger Than a Page

A page holds a few thousand bytes, but a row can hold a megabyte of text. If a big value were stored in its page, one row would fill the page and the B-tree would hold one row per node. After this lesson your store keeps a prefix of each value in its cell and puts the remainder in a chain of overflow pages, reads it back by following the chain, and recycles freed pages.

One cell, a prefix, a chain

Two sizes control the layout (CONFIG <local> <chunk>; the defaults are 8 and 16 so tests stay readable):

  • local: how many bytes of the value stay inside the cell itself
  • chunk: how many bytes each overflow page carries

A value of length n is split like this:

value:  The quick brown fox jumps over the lazy dog      (43 bytes, local 8, chunk 16)

cell:        "The quic"                          first overflow page: 1
page 1:      next=2   "k brown fox jumps"   ... 16 bytes
page 2:      next=3   16 bytes
page 3:      next=0   the last 3 bytes

If n <= local there is no chain at all: CHAIN prints (inline) and no page is used. Otherwise the remaining n - local bytes need ceil((n - local) / chunk) pages. Each page stores its piece of the value and the number of the next page; 0 ends the chain. The cell remembers only the first page. Notice the boundaries: a remainder of exactly chunk bytes uses one page, and one byte more needs a second.

Reading

GET starts with the inline part, then follows next pointers from the first overflow page, appending each page's data until it reaches 0. The order of the pointers is the order of the bytes: that is the invariant to protect when you allocate.

Allocating and recycling pages

Pages are numbered from 1. A new page takes the lowest-numbered free page if there is one; otherwise it takes the next unused number (the file grows by one page). A value's pages are allocated in chain order, so the first piece gets the first page allocated.

DROP frees every page of the chain and removes the cell. A freed page is not erased: it joins the free list, to be reused by a later store. That is how real files avoid growing forever (SQLite keeps a free-page list in its file header). Because the free list is consulted lowest-first, pages freed in any order come back in ascending order. A chain built from reused pages can therefore jump around: 1 -> 2 -> 5. That is normal, and exactly why every page records its own next.

Commands

CommandOutput
CONFIG <local> <chunk>OK (both 1 to 1000); empties the store
STORE <key> <value>OK pages=<n> (the value is the rest of the line, spaces included), or ERR key exists
GET <key>the whole value, or NOT_FOUND
CHAIN <key>page numbers joined by ->, or (inline), or NOT_FOUND
DROP <key>OK freed=<n> or NOT_FOUND
PAGE <n>next=<m> data="<text>", free for a freed page, NOT_FOUND for a number never allocated
STATSkeys=<n> pages=<pages ever allocated> free=<pages on the free list>

Common mistakes

  • Off by one at the boundary. A value of local + chunk bytes needs one overflow page, not two.
  • Linking in the wrong order. If page numbers come from the free list, the chain order is the order of allocation, not numeric order.
  • Forgetting to free the last page of a chain, so STATS free is one too low.
  • Taking the highest free page instead of the lowest.

Your exercise

The starter does the parsing and printing. You write the five storage functions: alloc_page, free_page, store_value, chain_of, read_value and drop_value. The mistake to avoid: giving each piece the next page number instead of the next page allocated.

Up nextDisk Serialization: The SQLite Record FormatPage-Based Storage

Discussion

Ask a question, share an insight, or help someone who’s stuck.

Sign in to post a comment or reply.

Loading…