<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Vamsi</title>
    <description>The latest articles on DEV Community by Vamsi (@vamshi_993).</description>
    <link>https://dev.to/vamshi_993</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F2325087%2Fae29bffe-0a52-4bff-a53e-825cc4e0e622.png</url>
      <title>DEV Community: Vamsi</title>
      <link>https://dev.to/vamshi_993</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/vamshi_993"/>
    <language>en</language>
    <item>
      <title>I built an SSTable storage engine</title>
      <dc:creator>Vamsi</dc:creator>
      <pubDate>Fri, 13 Nov 2026 15:33:11 +0000</pubDate>
      <link>https://dev.to/vamshi_993/i-built-an-sstable-storage-engine-2dol</link>
      <guid>https://dev.to/vamshi_993/i-built-an-sstable-storage-engine-2dol</guid>
      <description>&lt;p&gt;Hello people!&lt;/p&gt;

&lt;p&gt;I've been working on a project called &lt;strong&gt;Stryx&lt;/strong&gt; — &lt;a href="https://github.com/vamsi200/stryx" rel="noopener noreferrer"&gt;github&lt;/a&gt;, for a couple of weeks. I don't really have a motivation for as to why I built it — I read about "Sorted String Tables" somewhere, found them cool, coincidentally at the same time I'd been itching to build something involving binary formats. So I thought it would be fun, and here we are.&lt;/p&gt;

&lt;h2&gt;
  
  
  Contents
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Part I: Background&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;What's an SSTable?&lt;/li&gt;
&lt;li&gt;Designing the Stryx Table&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Part II: The File Format&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;How Records are Stored&lt;/li&gt;
&lt;li&gt;Index&lt;/li&gt;
&lt;li&gt;Filter&lt;/li&gt;
&lt;li&gt;Footer, and How &lt;code&gt;open()&lt;/code&gt; Works&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Part III: Reading and Writing&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The Whole Flow: &lt;code&gt;get(b"cherry")&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Building a Table&lt;/li&gt;
&lt;li&gt;Reading Records&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Part IV: Behavior, Performance and Getting started&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Compression, Concurrency and Caching&lt;/li&gt;
&lt;li&gt;Benchmarks&lt;/li&gt;
&lt;li&gt;Limitations&lt;/li&gt;
&lt;li&gt;Getting Started&lt;/li&gt;
&lt;/ol&gt;




&lt;p&gt;If you'd rather see the format before reading about it, I built an interactive explorer that walks through the exact byte layout described below — &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://vamsi200.github.io/stryx/" rel="noopener noreferrer"&gt;→ Explore the Stryx table format&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Part I: Background
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. What's an SSTable?
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;It's essentially a file-based storage format that stores key-value pairs in sorted order. The records are arranged lexicographically by their keys, which makes looking up records and performing range queries efficient compared to scanning an entire file.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;SSTables are &lt;strong&gt;immutable&lt;/strong&gt;, meaning that once the table has been written and finalized, its contents aren't modified (new data is written into a new table). This makes the format relatively straightforward to read concurrently and also avoids the complexity of managing in-place updates.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;They're commonly used as building blocks in storage engines and databases, often alongside structures such as memtables. Systems like &lt;code&gt;RocksDB&lt;/code&gt; and &lt;code&gt;LevelDB&lt;/code&gt; use this general approach.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For Stryx, I wanted to explore this storage model at a lower level: how records are encoded, organized into blocks, indexed, fetched, their metadata, and so on.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Designing the Stryx Table
&lt;/h3&gt;

&lt;p&gt;A Stryx table is a single file that you write once and never touch again. Records are sorted by key, grouped into blocks, and a small amount of metadata at the end of the file describes how to find everything.&lt;/p&gt;

&lt;p&gt;The reader starts by locating the footer at the end of the file, which tells it where the other sections are (all integers are little-endian). Both the metadata section and the optional filter section are prefixed with an 8-byte little-endian length, which lets the reader seek past one to find the next.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;+------------------------------------------+
| data block 0 ... data block N-1          |  Sorted records, optionally compressed
+------------------------------------------+
| index                                    |  First key, offset, and size per block
|                                          |  Entry count + CRC32C
+------------------------------------------+
| metadata (length-prefixed)               |  Record count, block count, key range,
|                                          |  compression, file size, index location
+------------------------------------------+
| filter section (optional, len-prefixed)  |  Binary Fuse filter + CRC32C
+------------------------------------------+
| footer (29 bytes)                        |  Index offset/size, filter type, version, magic
+------------------------------------------+
 ^                                          ^
 |                                          |
file start                            file end (footer starts at len - 29)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To understand how the rest of the format works, let's start with how a single record is represented.&lt;/p&gt;




&lt;h2&gt;
  
  
  Part II: The File Format
&lt;/h2&gt;

&lt;h3&gt;
  
  
  3. How Records are Stored
&lt;/h3&gt;

&lt;p&gt;For this walkthrough let's take these three records:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Key     Value
apple   red
banana  yellow
cherry  red
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;First record (encoded): key &lt;code&gt;apple&lt;/code&gt; and value &lt;code&gt;red&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;record = [key_len: u16][value_len: u32][key][value]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;th&gt;Bytes&lt;/th&gt;
&lt;th&gt;Why it exists&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;key_len&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;&lt;code&gt;05 00&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Keys are variable-length, so the reader needs to know where the key ends. A &lt;code&gt;u16&lt;/code&gt; allows keys up to 65,535 bytes.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;value_len&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;&lt;code&gt;03 00 00 00&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Same idea for the value. A &lt;code&gt;u32&lt;/code&gt; allows values up to 4,294,967,295 bytes.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;key&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;apple&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;61 70 70 6c 65&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The raw key bytes, which are compared lexicographically.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;value&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;red&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;72 65 64&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The raw value bytes. Stryx doesn't interpret them.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;That's 6 bytes of header plus 8 bytes of payload, so &lt;strong&gt;14 bytes in total&lt;/strong&gt;. Notice that the integers are little-endian: 5 as a u16 is &lt;code&gt;05 00&lt;/code&gt;, with the low byte first. Both keys and values are opaque bytes, so there is no schema and no type information in the format.&lt;/p&gt;

&lt;h3&gt;
  
  
  Records Inside a Block
&lt;/h3&gt;

&lt;p&gt;Writing records one after another is straightforward, but there's a small problem: records are variable-length, so you can't jump straight to the second record without knowing where it starts.&lt;/p&gt;

&lt;p&gt;That's why Stryx groups records into blocks and keeps an offset table for each block. The block also stores a record count and a checksum:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[record 0][record 1] ... [record N-1]
[offset 0: u32] ... [offset N-1: u32]
[record_count: u32][crc32c: u32]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here is a block holding &lt;code&gt;apple&lt;/code&gt; and &lt;code&gt;banana&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Block layout

byte 0                                      byte 32
  |                                            |
  v                                            v
  +------------------+------------------------+
  | apple record     | banana record          |
  | 14 bytes         | 18 bytes               |
  +------------------+------------------------+
  | offset 0 = 0     | offset 1 = 14          |  8 bytes
  +------------------+------------------------+
  | record_count = 2                          |  4 bytes
  +-------------------------------------------+
  | CRC32C                                    |  4 bytes
  +-------------------------------------------+

  Total block size: 48 bytes
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The record area is 32 bytes. The offset table takes another 8 bytes, followed by 4 bytes for the record count and 4 bytes for the CRC32C, making the entire block 48 bytes.&lt;/p&gt;

&lt;p&gt;Each part does the following:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Offset table&lt;/strong&gt;: lets the reader binary-search within the block. It jumps to the middle record, reads its key, compares, and repeats. Without it, a lookup would have to scan the block.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;record_count&lt;/strong&gt;: sits at a fixed position at the end, so the reader can read backwards from there and find the offset table.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;crc32c&lt;/strong&gt;: computed in &lt;code&gt;finish()&lt;/code&gt; over everything before it (records, offsets and count) and appended last. It catches accidental corruption such as bit rot or a truncated write.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  How Big Is a Block?
&lt;/h3&gt;

&lt;p&gt;Records are packed into blocks until the next record would exceed the configured &lt;code&gt;block_size&lt;/code&gt; target. A record is never split across blocks, so blocks are approximately the target size rather than exactly that size. A single record bigger than the target simply gets a block of its own.&lt;/p&gt;

&lt;p&gt;Since the records are sorted, each block can be represented in the index by its first key. That brings us to the next part of the format.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Index
&lt;/h3&gt;

&lt;p&gt;A table with a million records might have thousands of blocks, and the reader needs to know which one to read. The index answers that. It is &lt;code&gt;sparse&lt;/code&gt;, with one entry per block rather than per record. Each entry holds:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;+-----------------------------+
| key_len                 u16 |
+-----------------------------+
| first_key             bytes |
+-----------------------------+
| offset                  u64 |
+-----------------------------+
| size                    u32 |
+-----------------------------+
| uncompressed_block_size u32 |
+-----------------------------+
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;                  Index
+------------------------------------------------+
| Entry 0                                        |
|                                                |
| first_key:              "apple"                |
| offset:                 0                      |
| size:                   48                     |
| uncompressed_block_size: 48                    |
|                                                |
+------------------------------------------------+
| Entry 1                                        |
|                                                |
| first_key:              "cherry"               |
| offset:                 48                     |
| size:                   27                     |
| uncompressed_block_size: 27                    |
|                                                |
+------------------------------------------------+

                    |
                    | Each entry points to a block
                    v

+----------------------+----------------------+
| Block 0              | Block 1              |
| apple ...            | cherry ...           |
+----------------------+----------------------+
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For our example table, assuming no compression:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Entry&lt;/th&gt;
&lt;th&gt;First key&lt;/th&gt;
&lt;th&gt;Offset&lt;/th&gt;
&lt;th&gt;Size&lt;/th&gt;
&lt;th&gt;Uncompressed size&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;&lt;code&gt;apple&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;48&lt;/td&gt;
&lt;td&gt;48&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;&lt;code&gt;cherry&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;48&lt;/td&gt;
&lt;td&gt;27&lt;/td&gt;
&lt;td&gt;27&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The total encoded size of an entry is &lt;code&gt;2 + key_len + 8 + 4 + 4&lt;/code&gt; bytes. The index is serialized with its entry count and a CRC32C checksum. The reader uses it to find the candidate block without having to scan all the data blocks.&lt;/p&gt;

&lt;h3&gt;
  
  
  How the Reader Finds the Block
&lt;/h3&gt;

&lt;p&gt;Each entry identifies a candidate block based on its first key. The reader finds the last index entry whose first key is less than or equal to the requested key.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Looking up cherry&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Binary-search the index for the last first-key ≤ &lt;code&gt;cherry&lt;/code&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;apple ≤ cherry&lt;/code&gt; and &lt;code&gt;cherry ≤ cherry&lt;/code&gt;, so entry 1 wins.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Read 27 bytes at offset 48. That's block 1.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Binary-search its offset table, and &lt;code&gt;cherry&lt;/code&gt; is there.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For this example, only one block is read. Two other cases follow from the same rule:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;blueberry&lt;/strong&gt; (not in the table): the last first-key ≤ &lt;code&gt;blueberry&lt;/code&gt; is &lt;code&gt;apple&lt;/code&gt;, so the reader reads block 0, searches it, finds nothing, and returns &lt;code&gt;None&lt;/code&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;aardvark&lt;/strong&gt;, which sorts before every first key: no entry qualifies, so there is no block to read and the answer is &lt;code&gt;None&lt;/code&gt; straight away.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  5. Filter
&lt;/h3&gt;

&lt;p&gt;The index can only answer "which block would hold this key?", not "is the key present?". A missing key still costs an index search and potentially a block read. The optional filter exists to skip that work.&lt;/p&gt;

&lt;p&gt;At &lt;code&gt;finalize()&lt;/code&gt;, Stryx hashes every key into a &lt;code&gt;u64&lt;/code&gt; (xxHash3 with a fixed seed), sorts and deduplicates those hashes, then builds a &lt;strong&gt;Binary Fuse filter&lt;/strong&gt; (8, 16 or 32 bits per key, from the &lt;a href="https://crates.io/crates/xorf" rel="noopener noreferrer"&gt;&lt;code&gt;xorf&lt;/code&gt;&lt;/a&gt; crate) over the distinct hashes. Before touching the index, the reader asks it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;"Definitely absent"&lt;/strong&gt; → returns &lt;code&gt;None&lt;/code&gt; immediately, without loading a data block.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;"Maybe present"&lt;/strong&gt; → carries on to the index.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A false positive only wastes an index search and potentially a block read, because the block's binary search always has the final word. A filter can never cause a wrong value to be returned. Fuse8 is the smallest, and Fuse16 and Fuse32 spend more space for fewer false positives.&lt;/p&gt;

&lt;h3&gt;
  
  
  Filter Variants
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Variant&lt;/th&gt;
&lt;th&gt;Bits per key&lt;/th&gt;
&lt;th&gt;False-positive rate (approx.)&lt;/th&gt;
&lt;th&gt;Memory for 1M keys&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;BinaryFuse8&lt;/td&gt;
&lt;td&gt;~9&lt;/td&gt;
&lt;td&gt;~1 in 256&lt;/td&gt;
&lt;td&gt;~1.1 MB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;BinaryFuse16&lt;/td&gt;
&lt;td&gt;~18&lt;/td&gt;
&lt;td&gt;~1 in 65,000&lt;/td&gt;
&lt;td&gt;~2.2 MB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;BinaryFuse32&lt;/td&gt;
&lt;td&gt;~36&lt;/td&gt;
&lt;td&gt;~1 in 4 billion&lt;/td&gt;
&lt;td&gt;~4.5 MB&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  The Guarantee
&lt;/h3&gt;

&lt;p&gt;To make the guarantee concrete, consider looking up &lt;code&gt;aardvark&lt;/code&gt;, which sorts before every key. The filter has never seen it, so it returns "definitely absent" and the reader returns &lt;code&gt;None&lt;/code&gt; without touching the index at all. If the filter returns "maybe present" for a key that isn't actually there, the reader falls through to the normal index-and-block lookup and still correctly returns &lt;code&gt;None&lt;/code&gt;. The filter can save work on misses, but it can never change an answer.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Performance Trade-off
&lt;/h3&gt;

&lt;p&gt;The performance trade-off is a little counterintuitive: hits get slower, because every successful lookup pays for a filter check first, while misses get faster because most of them are rejected before any index or block work happens. In my benchmarks, hits go from ~80 µs to ~97–99 µs per 1,024 lookups, and misses drop from ~12 µs to ~9.2–9.5 µs. That's roughly a 20% improvement on misses.&lt;/p&gt;

&lt;p&gt;My guess is that on a warm, memory-mapped file a miss without a filter is already cheap, since it's only an index search and a block search with no real disk I/O. A filter should matter more when blocks have to come from disk, and I haven't measured that.&lt;/p&gt;

&lt;h3&gt;
  
  
  6. Footer, and How open() Works
&lt;/h3&gt;

&lt;p&gt;The index and filter are useful only if the reader can find them. The footer is what makes that possible. It is a fixed &lt;strong&gt;29 bytes&lt;/strong&gt; at the very end of the file, at a known position:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Type&lt;/th&gt;
&lt;th&gt;Bytes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;index_offset&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;u64&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;index_size&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;u64&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;filter_type&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;u8&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;version&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;u32&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;magic&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;bytes&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  What &lt;code&gt;TableReader::open()&lt;/code&gt; Does
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Read the footer.&lt;/strong&gt; Seek to &lt;code&gt;file_len - 29&lt;/code&gt; and read 29 bytes.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Validate the magic.&lt;/strong&gt; If it doesn't match, this isn't a Stryx file, and the reader stops here.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Validate the version.&lt;/strong&gt; The format is versioned but not yet stable, so a file from an incompatible version is rejected cleanly rather than misread.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Locate the sections.&lt;/strong&gt; &lt;code&gt;index_offset&lt;/code&gt; and &lt;code&gt;index_size&lt;/code&gt; say exactly where the index is. The metadata and filter sit after the index (the order is index, metadata, filter, footer). The metadata begins with an 8-byte little-endian length, and the optional filter section (when &lt;code&gt;filter_type != None&lt;/code&gt;) is also length-prefixed, so both can be skipped or bounded precisely.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Load index, metadata and filter.&lt;/strong&gt; These are read into memory. The index and optional filter have their CRC32C checksums verified. The metadata is validated structurally (bounds and field consistency) rather than via a checksum.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Prepare for lookups.&lt;/strong&gt; The index, filter, metadata and memory-mapped file are put behind an &lt;code&gt;Arc&lt;/code&gt;. Cloning a reader for another thread shares the table state while each reader has its own scratch buffers.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Why Opening Is Cheap
&lt;/h3&gt;

&lt;p&gt;The data blocks are &lt;strong&gt;not&lt;/strong&gt; eagerly read into memory during &lt;code&gt;open()&lt;/code&gt;. The file is memory-mapped, and blocks are accessed on demand. Compressed blocks are decompressed when needed. That's why opening doesn't require reading the entire table: about &lt;strong&gt;15 µs&lt;/strong&gt; for 10,000 records, and roughly &lt;strong&gt;740 µs&lt;/strong&gt; for a million. These are from my current benchmark run.&lt;/p&gt;

&lt;p&gt;Now that the file layout and lookup structures are established, let's follow an actual read to see how all these pieces work together.&lt;/p&gt;




&lt;h2&gt;
  
  
  Part III: Reading and Writing
&lt;/h2&gt;

&lt;h3&gt;
  
  
  7. The Whole Flow: get(b"cherry")
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Requested key: cherry
        |
        v
Filter check ........ "definitely absent"? -&amp;gt; return None
        |  "maybe present"
        v
Index lookup ........ last first-key &amp;lt;= "cherry"  -&amp;gt;  entry 1
        |
        v
Locate data block ... offset 48, size 27
        |
        v
Load the block ...... from the block cache if present, otherwise access it through mmap
        |              decompress if needed, CRC-check on first use
        v
Binary search ....... offset table -&amp;gt; compare keys -&amp;gt; find the record
        |
        v
Decode the record ... read key_len, value_len, then the value bytes
        |
        v
Return Some(b"red")
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  8. Building a Table
&lt;/h3&gt;

&lt;p&gt;Everything so far describes the finished file. This chapter covers how &lt;code&gt;TableBuilder&lt;/code&gt; produces it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="kd"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;table&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;TableBuilder&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"fruit.stryx"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;table&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="s2"&gt;"apple"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="s2"&gt;"red"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;table&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="s2"&gt;"banana"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="s2"&gt;"yellow"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;table&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="s2"&gt;"cherry"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="s2"&gt;"red"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;metadata&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;stats&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;table&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;finalize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Key Ordering
&lt;/h3&gt;

&lt;p&gt;The format requires sorted keys, so the builder has to get them sorted somehow. &lt;code&gt;key_order&lt;/code&gt; picks how:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Presorted&lt;/strong&gt;: you promise the input is already in order. The builder checks each key against the previous one and rejects anything out of order. This is the cheap path, and records can be streamed straight into blocks without being held in memory.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Lexicographic&lt;/strong&gt;: you can hand over records in any order, and Stryx routes them through an external spill-sorter (64 MiB in-memory budget by default) before writing. This lets arbitrary input become sorted output without holding the entire dataset in memory.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Accumulating Records Into Blocks
&lt;/h3&gt;

&lt;p&gt;Internally, a block builder holds two growing buffers: the encoded records, and a list of each record's offset. Each &lt;code&gt;add()&lt;/code&gt; call:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Encodes the record as &lt;code&gt;[key_len][value_len][key][value]&lt;/code&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Notes its offset, which is the current length of the data buffer.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Appends it to the buffer.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Remembers the key if this is the block's first record, since that becomes the index entry.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Sealing a Block
&lt;/h3&gt;

&lt;p&gt;When the block fills up, the builder seals it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;finish()&lt;/code&gt; appends the offset table, then &lt;code&gt;record_count&lt;/code&gt;, then the CRC32C of everything before it.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The bytes are compressed if the table uses LZ4 or Zstd.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;They are written to the file.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The block's first key, file offset and size are pushed onto the index.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The buffers are cleared and the next block starts.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is also why the index records each block's size. Compressed blocks have different lengths, so the reader can't work out where the next block starts from the block size alone.&lt;/p&gt;

&lt;h3&gt;
  
  
  What &lt;code&gt;finalize()&lt;/code&gt; Does
&lt;/h3&gt;

&lt;p&gt;The last block is usually only partly full, and the file doesn't have its index, metadata or footer yet. &lt;code&gt;finalize()&lt;/code&gt; finishes the job. Steps 1–2 build everything in memory; steps 3–6 write the file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1. Flush the last partial block         -&amp;gt; data blocks done

2. Build the filter in memory           -&amp;gt; Binary Fuse over the sorted, deduped key hashes, plus CRC32C
   (skipped when filter_type is None)

3. Encode and write the index           -&amp;gt; one entry per block, then entry count + CRC32C

4. Write the metadata                   -&amp;gt; 8-byte length prefix + encoded metadata

5. Write the filter section (if any)    -&amp;gt; 8-byte length prefix + filter data + CRC32C

6. Write the footer (29 bytes)          -&amp;gt; index_offset, index_size, filter_type, version, magic
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The footer comes last because it needs the offsets of the sections before it. The filter is built here, not earlier, because a Binary Fuse filter is constructed from the complete set of keys in one go — it can't be extended key by key. &lt;code&gt;finalize()&lt;/code&gt; returns the table's metadata and write statistics.&lt;/p&gt;

&lt;h3&gt;
  
  
  9. Reading Records
&lt;/h3&gt;

&lt;p&gt;Every read operation uses the index to pick candidate blocks and the offset table to search inside them.&lt;/p&gt;

&lt;h3&gt;
  
  
  Point Lookups
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;get()&lt;/code&gt; follows the lookup path from earlier (filter, index, block, binary search) and returns an owned &lt;code&gt;Vec&amp;lt;u8&amp;gt;&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="kd"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;reader&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;TableReader&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"fruit.stryx"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;reader&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="s2"&gt;"apple"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;   &lt;span class="c1"&gt;// Some(Vec&amp;lt;u8&amp;gt;)&lt;/span&gt;
&lt;span class="nf"&gt;assert_eq!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;Some&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="s2"&gt;"red"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to_vec&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="n"&gt;missing&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;reader&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="s2"&gt;"durian"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nf"&gt;assert_eq!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;missing&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; &lt;code&gt;contains_key()&lt;/code&gt; follows the same path through &lt;code&gt;get()&lt;/code&gt; and therefore materializes the value before discarding it. If you only need presence, calling &lt;code&gt;get()&lt;/code&gt; directly is no more expensive than &lt;code&gt;contains_key()&lt;/code&gt; — but &lt;code&gt;contains_key()&lt;/code&gt; is &lt;em&gt;not&lt;/em&gt; a cheaper check than &lt;code&gt;get()&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;
&lt;/blockquote&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="nf"&gt;assert!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;reader&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;contains_key&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="s2"&gt;"banana"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;   &lt;span class="c1"&gt;// true&lt;/span&gt;
&lt;span class="nf"&gt;assert!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;reader&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;contains_key&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="s2"&gt;"durian"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  &lt;span class="c1"&gt;// false&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you only need the answer as &lt;code&gt;Bytes&lt;/code&gt; rather than a &lt;code&gt;Vec&amp;lt;u8&amp;gt;&lt;/code&gt;, &lt;code&gt;get_in_bytes()&lt;/code&gt; returns the same lookup result without the extra copy.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;Option&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Bytes&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;reader&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_in_bytes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="s2"&gt;"cherry"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Batch Lookups
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;get_many()&lt;/code&gt; is for looking up multiple keys together. The reader sorts the requested keys, then walks them against the index in a single pass. Keys that land in the same block can be answered from one block load, and results are returned in the caller's original order.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="n"&gt;keys&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kt"&gt;u8&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="s2"&gt;"cherry"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="s2"&gt;"apple"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="s2"&gt;"durian"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="n"&gt;results&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;reader&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_many&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;keys&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nf"&gt;assert_eq!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;Some&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="s2"&gt;"red"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to_vec&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;     &lt;span class="c1"&gt;// cherry&lt;/span&gt;
&lt;span class="nf"&gt;assert_eq!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;Some&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="s2"&gt;"red"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to_vec&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;     &lt;span class="c1"&gt;// apple&lt;/span&gt;
&lt;span class="nf"&gt;assert_eq!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;                      &lt;span class="c1"&gt;// durian&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;get_many_in_bytes()&lt;/code&gt; does the same thing but returns &lt;code&gt;Vec&amp;lt;Option&amp;lt;Bytes&amp;gt;&amp;gt;&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;Vec&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;Option&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Bytes&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;reader&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_many_in_bytes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;keys&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This can avoid repeatedly traversing the index and loading the same block, although sorting the requested keys also has a cost.&lt;/p&gt;

&lt;h3&gt;
  
  
  Range and Prefix Queries
&lt;/h3&gt;

&lt;p&gt;Sorted storage is also what makes range queries possible. For a range, the reader uses the index to find the block that could contain the start key, searches the block's offset table for the first record at or after the start, walks records forward (moving to the next block when one runs out), and stops at the exclusive end bound. Only the blocks that overlap the range need to be read, and they sit next to each other in the file, so access is mostly sequential.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;record&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;reader&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="s2"&gt;"apple"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="s2"&gt;"cherry"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="n"&gt;record&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;record&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nf"&gt;println!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"{:?} -&amp;gt; {:?}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;record&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;record&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="c1"&gt;// apple  -&amp;gt; red&lt;/span&gt;
&lt;span class="c1"&gt;// banana -&amp;gt; yellow&lt;/span&gt;
&lt;span class="c1"&gt;// (cherry is excluded: the end bound is exclusive)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;An inverted range is rejected rather than silently returning nothing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="nf"&gt;assert!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;reader&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="s2"&gt;"cherry"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="s2"&gt;"apple"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;is_err&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  &lt;span class="c1"&gt;// InvalidRange&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;prefix()&lt;/code&gt; provides a dedicated prefix iterator:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;record&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;reader&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;prefix&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="s2"&gt;"ban"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="n"&gt;record&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;record&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nf"&gt;println!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"{:?}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;record&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="c1"&gt;// banana&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Iteration
&lt;/h3&gt;

&lt;p&gt;A full scan doesn't need to search the index or the offset tables for individual keys. It decodes each block's records one after another, using &lt;code&gt;key_len&lt;/code&gt; and &lt;code&gt;value_len&lt;/code&gt; to move from record to record, then moves to the next block.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;record&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;reader&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;iter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="n"&gt;record&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;record&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nf"&gt;println!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"{:?} -&amp;gt; {:?}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;record&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;record&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="c1"&gt;// apple  -&amp;gt; red&lt;/span&gt;
&lt;span class="c1"&gt;// banana -&amp;gt; yellow&lt;/span&gt;
&lt;span class="c1"&gt;// cherry -&amp;gt; red&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Part IV: Behavior and Performance
&lt;/h2&gt;

&lt;h3&gt;
  
  
  10. Compression, Concurrency and Caching
&lt;/h3&gt;

&lt;h3&gt;
  
  
  Compression
&lt;/h3&gt;

&lt;p&gt;Compression is chosen once per table: None, LZ4 (fast) or Zstd (smaller). It's applied to each block separately rather than to the file as a whole. If the whole file were one compressed stream, reading one key could mean decompressing a much larger portion of the file; with per-block compression, the index points at a block and the reader decompresses that block alone. Smaller files cost CPU time on every uncached compressed block read; the block cache can avoid repeated decompression.&lt;/p&gt;

&lt;h3&gt;
  
  
  Concurrency
&lt;/h3&gt;

&lt;p&gt;Opening a table doesn't read it all into memory. Only the small structures at the end of the file are loaded: the index, the metadata and the filter. The data blocks are accessed through the memory mapping when needed, and the OS manages which mapped pages are resident.&lt;/p&gt;

&lt;p&gt;The shared state sits behind an &lt;code&gt;Arc&lt;/code&gt;:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Shared across all readers (&lt;code&gt;Arc&lt;/code&gt;)&lt;/th&gt;
&lt;th&gt;Private to each reader&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;The mapped file&lt;/td&gt;
&lt;td&gt;Scratch buffers (decompression, decoding)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Index&lt;/td&gt;
&lt;td&gt;Read statistics (&lt;code&gt;readstats&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Filter&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Metadata&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Block cache (if enabled)&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Lookups take &lt;code&gt;&amp;amp;mut self&lt;/code&gt;, so one &lt;code&gt;TableReader&lt;/code&gt; can't be used from many threads at once. You open the table once and &lt;code&gt;clone()&lt;/code&gt; a reader per thread. A clone only needs its own scratch buffers and an increment of the &lt;code&gt;Arc&lt;/code&gt; reference count, so it's very cheap.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="n"&gt;reader&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;TableReader&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"data.stryx"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="n"&gt;std&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;thread&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;scope&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="kd"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;reader&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;clone&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;spawn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;move&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="s2"&gt;"some-key"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  The Block Cache
&lt;/h3&gt;

&lt;p&gt;The cache is off by default. When enabled, it keeps decoded (decompressed and verified) blocks and is shared between all clones, with a size you set in bytes. A block only enters the cache on its second load, so blocks that are read once never pay for an insert. Two threads can each decode the same cold block once before it lands in the cache, and a uniform random workload over a table much bigger than the cache gets little benefit from it.&lt;/p&gt;

&lt;h3&gt;
  
  
  11. Benchmarks
&lt;/h3&gt;

&lt;p&gt;Single-machine microbenchmarks comparing Stryx's own configurations; I make no claims about other libraries. Point-lookup times are per 1,024 lookups.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Environment:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Intel i5-12450H, Linux 6.18.38-1-lts, Rust 1.96.0, Criterion.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Point lookups:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Operation&lt;/th&gt;
&lt;th&gt;Config&lt;/th&gt;
&lt;th&gt;Time&lt;/th&gt;
&lt;th&gt;Throughput&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;hit&lt;/td&gt;
&lt;td&gt;no filter&lt;/td&gt;
&lt;td&gt;79.9 µs&lt;/td&gt;
&lt;td&gt;12.8 Melem/s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;hit&lt;/td&gt;
&lt;td&gt;Fuse8 / 16 / 32&lt;/td&gt;
&lt;td&gt;97.0 / 97.5 / 99.3 µs&lt;/td&gt;
&lt;td&gt;~10.3–10.6 Melem/s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;miss&lt;/td&gt;
&lt;td&gt;no filter&lt;/td&gt;
&lt;td&gt;12.0 µs&lt;/td&gt;
&lt;td&gt;85.5 Melem/s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;miss&lt;/td&gt;
&lt;td&gt;Fuse8 / 16 / 32&lt;/td&gt;
&lt;td&gt;9.5 / 9.2 / 9.4 µs&lt;/td&gt;
&lt;td&gt;~107–111 Melem/s&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Throughput, by configuration:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Workload&lt;/th&gt;
&lt;th&gt;Config&lt;/th&gt;
&lt;th&gt;Throughput&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Sequential scan, 4 KiB blocks&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;643 MiB/s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sequential scan, 4 KiB blocks&lt;/td&gt;
&lt;td&gt;LZ4&lt;/td&gt;
&lt;td&gt;461 MiB/s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sequential scan, 4 KiB blocks&lt;/td&gt;
&lt;td&gt;Zstd level 1&lt;/td&gt;
&lt;td&gt;306 MiB/s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Concurrent point lookups&lt;/td&gt;
&lt;td&gt;1 thread&lt;/td&gt;
&lt;td&gt;11.0 Melem/s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Concurrent point lookups&lt;/td&gt;
&lt;td&gt;2 threads&lt;/td&gt;
&lt;td&gt;24.8 Melem/s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Concurrent point lookups&lt;/td&gt;
&lt;td&gt;4 threads&lt;/td&gt;
&lt;td&gt;40.0 Melem/s&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Full tables — including scan times for 16 KiB blocks, opening latency at 10K/100K/1M records, and block-cache results across compression settings — are in the &lt;a href="https://github.com/vamsi200/stryx" rel="noopener noreferrer"&gt;&lt;code&gt;cargo bench&lt;/code&gt; harness&lt;/a&gt; and on the &lt;a href="https://crates.io/crates/stryx" rel="noopener noreferrer"&gt;crate page&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;A filter adds cost to successful lookups but helps reject missing keys — the same trade-off discussed in Filter. Four threads gave roughly 3.6× single-thread throughput on this machine and workload.&lt;/p&gt;

&lt;h3&gt;
  
  
  12. Limitations
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Pre-1.0 API.&lt;/strong&gt; The format and Rust API may change between minor versions. Version is validated on open, so an incompatible file is rejected cleanly rather than misread — but expect breakage.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Single-writer, immutable tables.&lt;/strong&gt; There's no in-place update or delete. New records mean a new table; merging tables is left to the caller.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;No compression tuning per workload.&lt;/strong&gt; Compression is chosen once per table and applied per block; there's no adaptive policy, and no measurement yet on cold-cache (disk-bound) reads.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  13. Getting Started
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;cargo add stryx
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or from source:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/vamsi200/stryx
&lt;span class="nb"&gt;cd&lt;/span&gt; stryx
cargo &lt;span class="nb"&gt;test&lt;/span&gt;
cargo bench
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For more details:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href="https://docs.rs/stryx/latest/stryx/" rel="noopener noreferrer"&gt;docs.rs/stryx&lt;/a&gt;&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href="https://crates.io/crates/stryx" rel="noopener noreferrer"&gt;crates.io/crates/stryx&lt;/a&gt;&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href="https://github.com/vamsi200/stryx" rel="noopener noreferrer"&gt;github.com/vamsi200/stryx&lt;/a&gt;&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;And that's it — that's the whole design and inner workings of Stryx. I've omitted a few details, but this is mostly it. Thank you for reading!&lt;/p&gt;

</description>
      <category>showdev</category>
      <category>rust</category>
      <category>database</category>
    </item>
  </channel>
</rss>
