skip to content

A table has a column that sometimes holds a multi-megabyte value, far larger than a single 8 KB page. How do storage engines physically store such values, and what does that mean for queries that do not select that column?

level: middleimportance: should knowfreq 40%

answer

  1. row must fit a page, so compress then move out of line
  2. chunks in overflow structure, pointer stays inline
  3. lazy fetch: unselected column costs nothing
  4. predicates over the value force fetch and decompress
  5. cannot be an index key; hash or full-text instead

basics

~20 s

The value is moved out of the row into overflow storage: chopped into chunks held in a separate structure, often compressed first, with only a short pointer left inline. The main row therefore stays small, so scans and queries that do not select the column never read the large data at all; touching it costs extra I/O.

solid answer

~60 s

A row must fit within a page, so when it does not, the engine performs **off-page (overflow) storage**. Typical sequence: try compressing the largest variable-length values in place; if the row still does not fit, move value chunks out to a dedicated overflow area, splitting them into page-sized pieces chained or keyed by an identifier, and leave a small pointer inline. Consequences worth naming: - **Narrow queries stay fast.** A scan that does not select the large column reads only the main pages; the overflow chunks are never touched. This is a concrete reason to avoid SELECT star on tables with large columns. - **Touching the column costs extra random I/O**, one or more reads per row, plus decompression CPU. - **Predicates on the column** (a LIKE over a big text value) force the value to be fetched and expanded, so they are far more expensive than they look. - **Indexes** generally cannot hold such values; engines hash or truncate the key, or refuse the index. - Deleting or updating the row must also release or rewrite the overflow chunks, so the write is bigger than it appears.

go deeper

for a junior

Say the value is stored outside the row in chunks with a pointer left behind, so the row stays small and unselected columns are not read.

for a middle

Add compression-before-move, lazy fetch on access, and the cost of predicates or projections that touch the column.

for a senior

Cover indexing limits, write amplification, deferred cleanup and the effect on backup and replication volume; explain observed plan-versus-reality slowness.

for a principal

Weigh in-database payloads against an object store: transactional consistency and unified access control versus database size, backup windows and streaming access, and name the reconciliation cost of the split design.

## The constraint that forces the mechanism A row is stored contiguously within one page, and pages are typically 8 or 16 KB. A 5 MB document therefore cannot be stored inline by definition. Even a 3 KB value is problematic, because putting only two rows per page would wreck scan efficiency for the whole table. Engines resolve this with off-page storage, known by various vendor names, but with a common shape. ## How it works When a row is about to be written and exceeds a threshold (often a fraction of the page, such as a quarter), the engine works through its variable-length columns largest first: 1. **Compress in place.** Many large values are text or JSON and compress well. If compressing brings the row under the threshold, it stays inline and no separate structure is needed. 2. **Move out of line.** Otherwise the value is split into chunks sized to fit pages of a separate overflow structure: a companion table, a dedicated segment, or a chain of overflow pages linked from the row. Each chunk is stored with an identifier and a sequence number so the value can be reassembled in order. 3. **Leave a pointer.** The main row keeps a small descriptor: the value identifier, the total length, and whether it is compressed. That descriptor is a few dozen bytes instead of megabytes. The main row is now small again, and the table's rows-per-page stays high. ## Reading behaviour, and why it matters Off-page values are fetched **lazily**. The engine reads the main row, and only if the query actually needs that column does it follow the pointer, read the chunks and decompress them. Three consequences follow directly. **Not selecting the column is genuinely free.** `SELECT id, created_at FROM docs WHERE ...` scans only the main pages. The multi-megabyte payloads sit untouched in overflow. This is the sharpest practical argument against SELECT star: on a table with large columns, the difference between selecting three small columns and selecting everything can be one or two orders of magnitude in I/O. **Selecting the column costs extra I/O per row, and it is scattered.** Each large value may require several page reads from the overflow structure, and those reads are not sequential with the table scan. Returning 10,000 large values is a very different operation from returning 10,000 small ones. **Predicates on the column are expensive.** A filter such as `WHERE body LIKE '%needle%'` cannot be evaluated without fetching and decompressing each value. The plan may look like a simple scan while actually performing an enormous amount of hidden work. Where such searches are a real requirement, the answer is a purpose-built index (full-text or trigram style) or an extracted, materialised summary column, not a scan over the payload. ## Other consequences to know - **Aggregates over length or presence.** Asking whether the column is NULL is answered from the row header and the pointer, so it is cheap, whereas computing its length may or may not require fetching, depending on whether the length is kept in the descriptor. - **Indexes.** Index entries must fit in an index page, so a multi-megabyte value cannot be an index key. Engines either refuse the index, truncate, or index a hash or an expression of the value. Uniqueness on a large value is usually implemented as uniqueness on a hash, which changes the semantics you must reason about. - **Write amplification.** Inserting a row with a large value writes the main row plus every chunk, all of it logged for durability. Updating any column of that row may or may not rewrite the chunks, depending on whether the engine can leave the untouched off-page value in place; the good implementations do leave it alone, which is why updating a small column of a row with a huge payload is normally cheap. - **Deletion and cleanup.** Removing the row must eventually free the chunks, which is deferred work that shows up in background maintenance. - **Replication and backup volume** track the chunks, so a table of large blobs dominates log traffic and backup size disproportionately to its row count. ## The design question behind the interview question Given all this, the recurring judgment call is whether large payloads belong in the database at all. Keeping them in the database buys transactional consistency with the metadata, unified backup, and access control in one place. Keeping them in an object store and storing a reference buys smaller databases, cheaper storage, faster backups and streaming reads, at the cost of losing atomicity between the two systems, which then has to be recovered with an outbox or a reconciliation job. Either answer is defensible; not knowing the tradeoff is not.

  • Does updating an unrelated small column of a row that has a multi-megabyte off-page value rewrite that value?
    In well-implemented engines, no. The off-page chunks are addressed by an identifier stored in the row, so a new row version can reference the same existing chunks and only the main row is rewritten. That keeps such updates cheap. The chunks are rewritten only when the large column itself is modified, and they are released when no row version references them any more.
  • Why can a query with a LIKE filter on a large text column be far slower than its plan suggests?
    Because the plan shows a scan of the main table, but evaluating the predicate requires following each row's pointer into overflow storage, reading several pages and decompressing the value. That hidden per-row cost dwarfs the visible scan. The fix is a purpose-built text index, or extracting the searchable part into a small materialised column that can be indexed normally.
  • When would you keep large binary payloads outside the database entirely?
    When the payloads are large and numerous relative to the metadata, when backups and replication volume become the operational bottleneck, or when clients need streaming or direct signed-URL access. You store a reference plus a checksum in the database and accept that the two systems can diverge, so you add an outbox or a reconciliation job to clean up orphans. If atomic consistency with the metadata matters more than volume, keeping them inline is the simpler correct choice.

A library catalogue card stays thin because it names a shelf location rather than reprinting the book; you only walk to the shelf if you actually need the text.

saying these in an interview costs you the question

  • Believing a large value is stored inline in the row and simply spans multiple pages
  • Assuming SELECT star costs the same as selecting a few small columns on such a table
  • Thinking a large text column can be used as an ordinary index key
  • Claiming updating any column of the row rewrites the whole off-page payload

context