Cabins

v3.0.0

How a Cabin serves non-key equality lookups for the values queries actually ask for, and why dropping it never changes a result.

In this article

A Cabin is a per-(table, column) store that speeds up column = value lookups for the values queries have actually asked for. For each such value, called an observed value, it knows the primary key of every matching row, and it is authoritative: an observed value with no matching rows answers “no rows” without reading the table. For any other value, the query scans as it would without a Cabin. Writes pay only when they touch an observed value, so a Cabin suits columns where a few values are looked up often.

The idea

A secondary index pays on every write for every value, including values nobody queries. A Cabin pays only for the values that were asked for, and it holds authority only there. The rule the whole design rests on is:

Observed means complete. For every observed value, the Cabin’s entry set contains every row visible to any active snapshot with that value. Extra entries are allowed; a missing one is not.

Two consequences follow:

  • An observed value’s empty set is an authoritative “no rows”. No advisory structure can give a negative answer.
  • Forgetting a value is always legal. If a value is dropped from the Cabin, queries for it go back to the scan. That is slower, never wrong. This is what lets an authoritative structure be evicted value by value.

At the limit, a Cabin that has observed every value of its column is a secondary index built lazily, one value at a time.

How it works

Reading a value

WHERE name = 'Kim Minjun'
        |
  is 'Kim Minjun' observed?
   |                         |
  yes (hit)                 no (miss)
   |                         |
  for each entry:           scan the table as usual,
   try the location hint     recording every matching primary key;
   (page, epoch, slot);      when the scan completes, 'Kim Minjun'
   on a miss, look up        becomes observed
   the primary key
   |
  check visibility, re-check name = 'Kim Minjun',
  drop duplicates, emit in primary-key order
  • Entries store primary keys. Each 24-byte entry holds the row’s primary key plus a location hint: the page, the page’s epoch and the slot where the row was last seen. The primary key is the authority. The hint is advisory: the reader checks it with the same verifier that Waystone trails use, falls back to a primary-key lookup when it fails, and repairs the hint in place.
  • The first query for a value pays nothing extra in big-O. It was going to scan anyway; recording the matches is a side effect.
  • When a value becomes observed. A Cabin you declare (with CREATE CABIN or CABIN in CREATE TABLE) records a value on its first lookup. For a join, where each outer row probes a value nobody wrote in the statement, a value is recorded on its second lookup and served from its third, declared or not.

Writing

The write hook is what “observed means complete” costs. Every action is an append. Removing an entry on the write path is forbidden, because an older snapshot may still need it.

WriteCabin action
INSERTIf the new value is observed, append the primary key.
DELETENothing.
UPDATE, column unchangedNothing.
UPDATE, value v to v2If v2 is observed, append to v2’s set. Leave v’s set alone.

Surplus entries cost a little read time. The read removes them by checking visibility and re-checking the value.

When a scan is not allowed to record

A scan can only record a set it can prove complete. If any transaction anywhere in the instance is in flight when the scan’s snapshot is taken, or the scan runs inside a transaction, it declines to record. The query still returns the right rows from its scan; the value just stays unobserved. Before the scan, the Cabin marks the value as being built, so writes that land during the scan are appended to the set.

The cost is that a Cabin may fill slowly, or never, on a busy instance. One session idling inside an open BEGIN stops every Cabin from recording. SHOW CABINS counts these declines as unbankable_views=.

Who may create a Cabin

Each column in CREATE TABLE can say who decides whether it carries a Cabin:

WrittenMeaning
CABINCreate a Cabin now.
CABIN AUTOThe engine may create one. The physical optimizer’s Cabin controller acts on it only when cabin_optimizer is on; it is off by default.
NO CABINNever, by any route. CREATE CABIN is refused.
(nothing)Treated as CABIN AUTO, but stored separately.

A policy on the primary-key column is refused: the primary-key tree is that column’s Cabin.

Example

Run against the sample passengers table, where exactly one row has name 'Kim Minjun' and no row has name 'Seo Jian':

kdbs> CREATE CABIN ON passengers(name)
CREATED CABIN on=passengers.name cabin_id=1 column=1 observed=0
WARN no recorded access has filtered 'passengers.name'; this cabin will stay empty until traffic probes that column by equality (SHOW ACCESS lists what has been recorded)
kdbs> ANALYZE SELECT * FROM passengers WHERE name = 'Kim Minjun'
...
step 0 CabinProbe passengers opens=1 examined=8 matched=1 sel=12% pages=1 cabin_misses=1 cabin_recorded=1
kdbs> ANALYZE SELECT * FROM passengers WHERE name = 'Kim Minjun'
...
step 0 CabinProbe passengers opens=1 examined=1 matched=1 sel=100% pages=2 cabin_hits=1 cabin_entries=1 hint_hits=1
kdbs> ANALYZE SELECT * FROM passengers WHERE name = 'Seo Jian'
...
step 0 CabinProbe passengers opens=1 examined=8 matched=0 sel=0% pages=1 cabin_misses=1 cabin_recorded=1
kdbs> ANALYZE SELECT * FROM passengers WHERE name = 'Seo Jian'
...
step 0 CabinProbe passengers opens=1 examined=0 matched=0 cabin_hits=1
kdbs> SHOW CABINS
cabins=1 unbankable_views=0 cap_refusals=0
cabin_id=1 rel=passengers column=name origin=user status=active observed=2 entries=1 hits=2 misses=2

(... marks ANALYZE lines left out.) The first lookup of each value scanned all eight rows and recorded the matches. The second lookup of 'Kim Minjun' read one row through its hint. The second lookup of 'Seo Jian' returned zero rows and read no rows (examined=0), because the observed empty set is authoritative.

What it guarantees

A Cabin is the third rung of the trust ladder:

StructureAuthorityWrite costDroppableA failed entry means
Primary-key B+ treeAlways authoritativeEvery writeNeverCorruption
Secondary indexAuthoritative, full coverageAppend on every writeWhole (DROP INDEX)Statement failure
CabinAuthoritative for observed valuesProbe and append, observed values onlyPer valueForget the value, then scan
Waystone trailNever authoritativeNoneWholeFall back to the authoritative path

Turning Cabins off (cabins = off), dropping a Cabin, or forgetting any of its values never changes a query result. The engine’s contract tests compare replies byte for byte with Cabins on and off.

A Cabin step is still a search, not a lookup, so a Waystone trail never replaces it. In a join such as tickets JOIN flights ON tickets.flight_id = flights.id WHERE tickets.seat = ?, the Cabin serves the seat step and the primary-key step on flights can be served from a trail. Each layer falls back on its own.

What it costs

  • One in-memory directory probe per Cabin on every write to the table. A table with no Cabin pays one integer test.
  • Memory for the entry sets: 24 bytes per entry.
  • The first lookup of each value runs the full scan.
  • On a multi-core instance, one in-flight transaction anywhere declines every Cabin build (see above).

Durability

A Cabin created by CREATE CABIN, a CABIN column or the Cabin controller is unlogged authoritative. Its entry sets live only in memory. A restart treats every value as unobserved, and traffic fills the Cabin again; only the catalog row survives. This keeps the write hook off the log.

The Cabin that backs an assertion, the Bound Cabin, is a different class: it covers every row from the start, is logged and pinned in memory, can never be evicted, and fails the statement if maintenance fails. The rest of this page is about the ordinary, observational class.

How to observe it

  • SHOW CABINS: per Cabin, origin= (user or auto), status=, observed= (values), entries=, hits=, misses=; and per instance, unbankable_views= and cap_refusals=.
  • ANALYZE <select>: the CabinProbe step with cabin_hits, cabin_misses, cabin_recorded, cabin_entries and hint_hits. A probe served by a Cabin the controller manages is marked cabin_optimizer=true.
  • SHOW ACCESS: CabinProbe shapes per table and column.
  • DESCRIBE <table>: each column’s cabin= policy.

Configuration

KeyDefaultEffect
cabinsonWhether Cabins may be built and served. Replies are identical either way.
cabin_max_values4096Observed values per Cabin. Past the cap a new value is not observed; it scans. 0 observes nothing.
cabin_max_entries_per_value4096Entries per value. A value that would exceed it is not observed. Caps refuse to observe; they never truncate a set.
cabin_optimizeroffLets the engine create and drop Cabins on CABIN AUTO columns. See Physical optimizer.

Limitations

  • One column per Cabin, equality only. No multi-column, range or expression Cabins.
  • Not allowed on the primary key or on a NO CABIN column.
  • Entry sets do not survive a restart.
  • If a secondary index exists on the same column, the index is used and the Cabin is not probed.
  • A function comparison such as DATE(ts) = '2026-10-02' never becomes a Cabin probe.
  • On a single-core instance, every Cabin probe, append and serve still takes an uncontended lock (known gap, cost unmeasured).
  • A probe compiled before a DROP TABLE can leave a set for the dropped table in memory until a restart. Only a statement compiled before the drop can reach it.

See also