<script data-pm-proxy="intercept"></script><?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:itunes="http://www.itunes.com/dtds/podcast-1.0.dtd" xmlns:googleplay="http://www.google.com/schemas/play-podcasts/1.0"><channel><title><![CDATA[Bytes & B-Tree's]]></title><description><![CDATA[Bytes & B-trees is a weekly newsletter for software engineers who want to understand databases from first principles — storage physics to distributed consensus. One production concept per issue, with working SQL scripts and demos in the GitHub vault.]]></description><link>https://bytesbtrees.substack.com</link><image><url>https://substackcdn.com/image/fetch/$s_!nL3X!,w_256,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0693b673-c070-4b18-911a-1c08a170def3_608x608.png</url><title>Bytes &amp; B-Tree&apos;s</title><link>https://bytesbtrees.substack.com</link></image><generator>Substack</generator><lastBuildDate>Tue, 01 Sep 2026 10:54:43 GMT</lastBuildDate><atom:link href="/__u/bytesbtrees.substack.com/feed" rel="self" type="application/rss+xml"/><copyright><![CDATA[System Design Roadmap]]></copyright><language><![CDATA[en-gb]]></language><webMaster><![CDATA[bytesbtrees@substack.com]]></webMaster><itunes:owner><itunes:email><![CDATA[bytesbtrees@substack.com]]></itunes:email><itunes:name><![CDATA[System Design Roadmap]]></itunes:name></itunes:owner><itunes:author><![CDATA[System Design Roadmap]]></itunes:author><googleplay:owner><![CDATA[bytesbtrees@substack.com]]></googleplay:owner><googleplay:email><![CDATA[bytesbtrees@substack.com]]></googleplay:email><googleplay:author><![CDATA[System Design Roadmap]]></googleplay:author><itunes:block><![CDATA[Yes]]></itunes:block><item><title><![CDATA[Issue-018: B-trees — the balanced tree on disk]]></title><description><![CDATA[Every Postgres table has at least one B-tree index &#8212; the primary key.]]></description><link>https://bytesbtrees.substack.com/p/issue-018-b-trees-the-balanced-tree</link><guid isPermaLink="false">https://bytesbtrees.substack.com/p/issue-018-b-trees-the-balanced-tree</guid><dc:creator><![CDATA[System Design Roadmap]]></dc:creator><pubDate>Sun, 30 Aug 2026 02:30:33 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!9eQD!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa4199200-bbfe-4c1c-ba4f-891bb6beb92f_3000x2400.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<blockquote><p><em>Every Postgres table has at least one B-tree index &#8212; the primary key. Understanding the physical layout of that tree &#8212; pages, keys, pointers, splits &#8212; explains every index tuning decision you will ever make. This is the issue most engineers say they should have read first.</em></p></blockquote><div><hr></div><blockquote><p>An engineer asked me why their query was slow after they added an index. EXPLAIN showed the planner was using the index, the query was returning 1 row, and yet it took 40ms on a table with 2 million rows. The answer was on the B-tree&#8217;s fourth level: the table had been created with a UUID primary key, which caused random page splits on every insert and bloated the index to four levels with poor fill rates. Understanding this required understanding what a B-tree page split actually is.</p></blockquote><div><hr></div><h2><strong>What a B-tree index physically is</strong></h2><blockquote><p>A B-tree index is a file of 8KB pages, structured as a tree. The file lives alongside the heap file in the Postgres data directory. Every page in the index file is one of three types:</p><p><strong>Root page:</strong> The entry point. Every index has exactly one root. A new index starts as a single root page that is also a leaf. The root&#8217;s location is stored in the index&#8217;s meta page.</p><p><strong>Internal pages:</strong> Non-leaf pages containing keys and pointers to child pages. Each internal page can hold roughly 340 entries for an integer key (8KB page &#247; ~24 bytes per entry). The more entries an internal page holds, the shorter the tree.</p><p><strong>Leaf pages:</strong> The bottom level. Each leaf page entry contains the indexed key value and a ctid &#8212; the heap tuple physical address <code>(page_number, line_pointer)</code>. Leaf pages are linked in key order via forward and backward pointers. This linking is what makes B+ trees (what Postgres actually uses) efficient for range scans.</p></blockquote><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="/__u/substackcdn.com/image/fetch/$s_!9eQD!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa4199200-bbfe-4c1c-ba4f-891bb6beb92f_3000x2400.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="/__u/substackcdn.com/image/fetch/$s_!9eQD!, /__u/bytesbtrees.substack.com/w_424, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_webp, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa4199200-bbfe-4c1c-ba4f-891bb6beb92f_3000x2400.png 424w, /__u/substackcdn.com/image/fetch/$s_!9eQD!, /__u/bytesbtrees.substack.com/w_848, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_webp, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa4199200-bbfe-4c1c-ba4f-891bb6beb92f_3000x2400.png 848w, /__u/substackcdn.com/image/fetch/$s_!9eQD!, /__u/bytesbtrees.substack.com/w_1272, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_webp, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa4199200-bbfe-4c1c-ba4f-891bb6beb92f_3000x2400.png 1272w, /__u/substackcdn.com/image/fetch/$s_!9eQD!, /__u/bytesbtrees.substack.com/w_1456, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_webp, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa4199200-bbfe-4c1c-ba4f-891bb6beb92f_3000x2400.png 1456w" sizes="100vw"><img src="/__u/substackcdn.com/image/fetch/$s_!9eQD!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa4199200-bbfe-4c1c-ba4f-891bb6beb92f_3000x2400.png" width="627" height="501.6861263736264" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/a4199200-bbfe-4c1c-ba4f-891bb6beb92f_3000x2400.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:1165,&quot;width&quot;:1456,&quot;resizeWidth&quot;:627,&quot;bytes&quot;:481077,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://bytesbtrees.substack.com/i/211143245?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa4199200-bbfe-4c1c-ba4f-891bb6beb92f_3000x2400.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="/__u/substackcdn.com/image/fetch/$s_!9eQD!, /__u/bytesbtrees.substack.com/w_424, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_auto, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa4199200-bbfe-4c1c-ba4f-891bb6beb92f_3000x2400.png 424w, /__u/substackcdn.com/image/fetch/$s_!9eQD!, /__u/bytesbtrees.substack.com/w_848, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_auto, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa4199200-bbfe-4c1c-ba4f-891bb6beb92f_3000x2400.png 848w, /__u/substackcdn.com/image/fetch/$s_!9eQD!, /__u/bytesbtrees.substack.com/w_1272, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_auto, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa4199200-bbfe-4c1c-ba4f-891bb6beb92f_3000x2400.png 1272w, /__u/substackcdn.com/image/fetch/$s_!9eQD!, /__u/bytesbtrees.substack.com/w_1456, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_auto, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa4199200-bbfe-4c1c-ba4f-891bb6beb92f_3000x2400.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div>
      <p>
          <a href="/__u/bytesbtrees.substack.com/p/issue-018-b-trees-the-balanced-tree">
              Read more
          </a>
      </p>
   ]]></content:encoded></item><item><title><![CDATA[Issue- 017: Row vs columnar storage — when each wins]]></title><description><![CDATA[Postgres stores your data row-by-row.]]></description><link>https://bytesbtrees.substack.com/p/issue-017-row-vs-columnar-storage</link><guid isPermaLink="false">https://bytesbtrees.substack.com/p/issue-017-row-vs-columnar-storage</guid><dc:creator><![CDATA[System Design Roadmap]]></dc:creator><pubDate>Sun, 23 Aug 2026 02:30:41 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!RQDr!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F42a5ca19-0a90-4ed6-81e9-21edfb7addb7_3200x2320.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<blockquote><p><em>Postgres stores your data row-by-row. DuckDB, Redshift, and ClickHouse store it column-by-column. Same data, fundamentally different I/O cost for different query types. This is the single most consequential storage design decision in modern database engineering.</em></p></blockquote><div><hr></div><blockquote><p>A data analyst runs <code>SELECT SUM(revenue) FROM sales</code> on a 10-million-row table. The <code>sales</code> table has 40 columns. <code>revenue</code> is one of them &#8212; 8 bytes each, totalling 80MB for all 10 million values.</p><p>In Postgres, that query reads approximately 800MB from disk. All 40 columns of all 10 million rows must be loaded &#8212; 8KB pages containing complete rows &#8212; even though only one column is needed.</p><p>In a columnar store like DuckDB or Redshift, the same query reads approximately 80MB &#8212; just the <code>revenue</code> column. Ten times less I/O.</p><p>This is not a Postgres bug or limitation. It is the fundamental consequence of the row storage format, and understanding <em>why</em> makes the right database choice obvious for any given workload.</p></blockquote><div><hr></div><h2><strong>Row storage: optimised for rows</strong></h2><blockquote><p>Postgres stores each row&#8217;s columns contiguously on the same 8KB page. When a query reads row #4,827, Postgres loads the page containing that row and has immediate access to all its columns &#8212; <code>id</code>, <code>customer_name</code>, <code>product_id</code>, <code>revenue</code>, <code>discount</code>, <code>timestamp</code>, all 40 of them &#8212; in a single page read.</p></blockquote><blockquote><p>This is perfect for OLTP workloads: <code>SELECT * FROM orders WHERE id = 42</code> loads one page and returns one complete row. <code>UPDATE orders SET status='shipped' WHERE id = 42</code> modifies one tuple on one page and writes one WAL record. The row is the atomic unit of access.</p></blockquote><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="/__u/substackcdn.com/image/fetch/$s_!RQDr!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F42a5ca19-0a90-4ed6-81e9-21edfb7addb7_3200x2320.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="/__u/substackcdn.com/image/fetch/$s_!RQDr!, /__u/bytesbtrees.substack.com/w_424, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_webp, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F42a5ca19-0a90-4ed6-81e9-21edfb7addb7_3200x2320.png 424w, /__u/substackcdn.com/image/fetch/$s_!RQDr!, /__u/bytesbtrees.substack.com/w_848, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_webp, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F42a5ca19-0a90-4ed6-81e9-21edfb7addb7_3200x2320.png 848w, /__u/substackcdn.com/image/fetch/$s_!RQDr!, /__u/bytesbtrees.substack.com/w_1272, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_webp, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F42a5ca19-0a90-4ed6-81e9-21edfb7addb7_3200x2320.png 1272w, /__u/substackcdn.com/image/fetch/$s_!RQDr!, /__u/bytesbtrees.substack.com/w_1456, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_webp, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F42a5ca19-0a90-4ed6-81e9-21edfb7addb7_3200x2320.png 1456w" sizes="100vw"><img src="/__u/substackcdn.com/image/fetch/$s_!RQDr!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F42a5ca19-0a90-4ed6-81e9-21edfb7addb7_3200x2320.png" width="1456" height="1056" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/42a5ca19-0a90-4ed6-81e9-21edfb7addb7_3200x2320.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:1056,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:707183,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://bytesbtrees.substack.com/i/210314119?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F42a5ca19-0a90-4ed6-81e9-21edfb7addb7_3200x2320.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="/__u/substackcdn.com/image/fetch/$s_!RQDr!, /__u/bytesbtrees.substack.com/w_424, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_auto, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F42a5ca19-0a90-4ed6-81e9-21edfb7addb7_3200x2320.png 424w, /__u/substackcdn.com/image/fetch/$s_!RQDr!, /__u/bytesbtrees.substack.com/w_848, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_auto, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F42a5ca19-0a90-4ed6-81e9-21edfb7addb7_3200x2320.png 848w, /__u/substackcdn.com/image/fetch/$s_!RQDr!, /__u/bytesbtrees.substack.com/w_1272, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_auto, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F42a5ca19-0a90-4ed6-81e9-21edfb7addb7_3200x2320.png 1272w, /__u/substackcdn.com/image/fetch/$s_!RQDr!, /__u/bytesbtrees.substack.com/w_1456, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_auto, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F42a5ca19-0a90-4ed6-81e9-21edfb7addb7_3200x2320.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div>
      <p>
          <a href="/__u/bytesbtrees.substack.com/p/issue-017-row-vs-columnar-storage">
              Read more
          </a>
      </p>
   ]]></content:encoded></item><item><title><![CDATA[Issue-016: TOAST — variable-length columns & the hidden storage layer]]></title><description><![CDATA[Postgres has a second table you never created.]]></description><link>https://bytesbtrees.substack.com/p/issue-016-toast-variable-length-columns</link><guid isPermaLink="false">https://bytesbtrees.substack.com/p/issue-016-toast-variable-length-columns</guid><dc:creator><![CDATA[System Design Roadmap]]></dc:creator><pubDate>Sun, 16 Aug 2026 02:30:47 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!3jLa!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb3cf4921-8086-4eca-b45d-e5bcd0bbfcc0_3200x2400.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><em>Postgres has a second table you never created. It stores your large column values and is queried every time you read them &#8212; but it never appears in EXPLAIN. This is TOAST, and understanding it explains some of the most puzzling query performance patterns.</em></p><div><hr></div><p>An engineer optimised every aspect of a query &#8212; added the right index, reduced column count, ran ANALYZE. The query still took three times longer than the EXPLAIN estimated. The culprit: a <code>JSONB</code> column averaging 15KB per row. Every row read from the heap was a TOAST pointer, triggering an invisible join to a second table for the actual value.</p><p>TOAST (The Oversized-Attribute Storage Technique) is Postgres&#8217;s mechanism for storing values larger than approximately 2KB out-of-line, in a separate <code>pg_toast</code> table. It&#8217;s transparent &#8212; you never write TOAST explicitly &#8212; but its performance implications are very much not transparent.</p><div><hr></div><h2><strong>Why TOAST exists: the 8KB page constraint</strong></h2><p>From Issue #015: Postgres reads and writes in 8KB pages. A page header uses 24 bytes. Line pointers use ~4 bytes each. That leaves roughly 8,100 bytes for actual row data. If a single column value is 50KB, it cannot fit on a page.</p><p>The solution TOAST uses: if a row would exceed roughly 2KB (the <code>TOAST_TUPLE_THRESHOLD</code>), Postgres compresses and/or moves the large values out of the heap page and into a dedicated TOAST table. What remains in the heap is an 18-byte TOAST pointer: a reference containing the TOAST table OID, a chunk ID, and the original value size.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="/__u/substackcdn.com/image/fetch/$s_!3jLa!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb3cf4921-8086-4eca-b45d-e5bcd0bbfcc0_3200x2400.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="/__u/substackcdn.com/image/fetch/$s_!3jLa!, /__u/bytesbtrees.substack.com/w_424, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_webp, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb3cf4921-8086-4eca-b45d-e5bcd0bbfcc0_3200x2400.png 424w, /__u/substackcdn.com/image/fetch/$s_!3jLa!, /__u/bytesbtrees.substack.com/w_848, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_webp, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb3cf4921-8086-4eca-b45d-e5bcd0bbfcc0_3200x2400.png 848w, /__u/substackcdn.com/image/fetch/$s_!3jLa!, /__u/bytesbtrees.substack.com/w_1272, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_webp, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb3cf4921-8086-4eca-b45d-e5bcd0bbfcc0_3200x2400.png 1272w, /__u/substackcdn.com/image/fetch/$s_!3jLa!, /__u/bytesbtrees.substack.com/w_1456, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_webp, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb3cf4921-8086-4eca-b45d-e5bcd0bbfcc0_3200x2400.png 1456w" sizes="100vw"><img src="/__u/substackcdn.com/image/fetch/$s_!3jLa!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb3cf4921-8086-4eca-b45d-e5bcd0bbfcc0_3200x2400.png" width="582" height="436.5" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/b3cf4921-8086-4eca-b45d-e5bcd0bbfcc0_3200x2400.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:1092,&quot;width&quot;:1456,&quot;resizeWidth&quot;:582,&quot;bytes&quot;:526841,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://bytesbtrees.substack.com/i/209619667?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb3cf4921-8086-4eca-b45d-e5bcd0bbfcc0_3200x2400.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="/__u/substackcdn.com/image/fetch/$s_!3jLa!, /__u/bytesbtrees.substack.com/w_424, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_auto, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb3cf4921-8086-4eca-b45d-e5bcd0bbfcc0_3200x2400.png 424w, /__u/substackcdn.com/image/fetch/$s_!3jLa!, /__u/bytesbtrees.substack.com/w_848, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_auto, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb3cf4921-8086-4eca-b45d-e5bcd0bbfcc0_3200x2400.png 848w, /__u/substackcdn.com/image/fetch/$s_!3jLa!, /__u/bytesbtrees.substack.com/w_1272, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_auto, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb3cf4921-8086-4eca-b45d-e5bcd0bbfcc0_3200x2400.png 1272w, /__u/substackcdn.com/image/fetch/$s_!3jLa!, /__u/bytesbtrees.substack.com/w_1456, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_auto, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb3cf4921-8086-4eca-b45d-e5bcd0bbfcc0_3200x2400.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div>
      <p>
          <a href="/__u/bytesbtrees.substack.com/p/issue-016-toast-variable-length-columns">
              Read more
          </a>
      </p>
   ]]></content:encoded></item><item><title><![CDATA[Issue-015:-Pages, heaps & row storage anatomy]]></title><description><![CDATA[Every Postgres read and write moves exactly one 8,192-byte unit between disk and memory &#8212; regardless of how small the row you&#8217;re looking for.]]></description><link>https://bytesbtrees.substack.com/p/issue-015-pages-heaps-and-row-storage</link><guid isPermaLink="false">https://bytesbtrees.substack.com/p/issue-015-pages-heaps-and-row-storage</guid><dc:creator><![CDATA[System Design Roadmap]]></dc:creator><pubDate>Sun, 09 Aug 2026 02:30:34 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!GcSq!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe02e079c-c686-4e0a-89d8-f13383584a4c_3200x2720.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><em>Every Postgres read and write moves exactly one 8,192-byte unit between disk and memory &#8212; regardless of how small the row you&#8217;re looking for. Understanding this changes how you think about schema design, index decisions, and every query you write.</em></p><div><hr></div><p>A senior engineer I work with discovered something that permanently changed how they designed schemas. They were profiling a slow endpoint and found the database was reading 14,000 pages to serve a query returning 200 rows. Each page is 8KB. That is 112MB of I/O for a 200-row result.</p><p>The table had 47 columns. The query selected 3 of them. But since row storage puts all columns of a row on the same page, reading those 3 columns forced Postgres to load every page containing the 200 matching rows &#8212; each page packed with all 47 columns of all rows on that page.</p><p>The fix was a covering index. But understanding <em>why</em> the covering index helped &#8212; and why the original query was expensive &#8212; required understanding what a page actually is.</p><div><hr></div><h2><strong>The 8KB page: Postgres&#8217;s atomic storage unit</strong></h2><p>Postgres never reads or writes individual rows. It reads and writes <strong>pages</strong> &#8212; fixed-size blocks of 8,192 bytes (8KB). Every heap table, every index, every system catalog is a sequence of these pages. When you execute <code>SELECT * FROM employees WHERE id = 42</code>, Postgres locates the page containing that row and loads the entire 8KB page into shared_buffers, even if the row itself is only 200 bytes.</p><p>This is the <strong>page size contract</strong>: the operating system&#8217;s unit of I/O aligns with Postgres&#8217;s page size, meaning each read maps to one or a small number of OS operations. Making pages smaller would mean more I/O operations for sequential scans; making them larger would waste memory for small tables. 8KB is the default, compiled-in constant.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="/__u/substackcdn.com/image/fetch/$s_!GcSq!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe02e079c-c686-4e0a-89d8-f13383584a4c_3200x2720.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="/__u/substackcdn.com/image/fetch/$s_!GcSq!, /__u/bytesbtrees.substack.com/w_424, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_webp, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe02e079c-c686-4e0a-89d8-f13383584a4c_3200x2720.png 424w, /__u/substackcdn.com/image/fetch/$s_!GcSq!, /__u/bytesbtrees.substack.com/w_848, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_webp, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe02e079c-c686-4e0a-89d8-f13383584a4c_3200x2720.png 848w, /__u/substackcdn.com/image/fetch/$s_!GcSq!, /__u/bytesbtrees.substack.com/w_1272, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_webp, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe02e079c-c686-4e0a-89d8-f13383584a4c_3200x2720.png 1272w, /__u/substackcdn.com/image/fetch/$s_!GcSq!, /__u/bytesbtrees.substack.com/w_1456, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_webp, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe02e079c-c686-4e0a-89d8-f13383584a4c_3200x2720.png 1456w" sizes="100vw"><img src="/__u/substackcdn.com/image/fetch/$s_!GcSq!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe02e079c-c686-4e0a-89d8-f13383584a4c_3200x2720.png" width="1456" height="1238" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/e02e079c-c686-4e0a-89d8-f13383584a4c_3200x2720.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:1238,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:546698,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://bytesbtrees.substack.com/i/208929245?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe02e079c-c686-4e0a-89d8-f13383584a4c_3200x2720.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="/__u/substackcdn.com/image/fetch/$s_!GcSq!, /__u/bytesbtrees.substack.com/w_424, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_auto, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe02e079c-c686-4e0a-89d8-f13383584a4c_3200x2720.png 424w, /__u/substackcdn.com/image/fetch/$s_!GcSq!, /__u/bytesbtrees.substack.com/w_848, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_auto, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe02e079c-c686-4e0a-89d8-f13383584a4c_3200x2720.png 848w, /__u/substackcdn.com/image/fetch/$s_!GcSq!, /__u/bytesbtrees.substack.com/w_1272, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_auto, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe02e079c-c686-4e0a-89d8-f13383584a4c_3200x2720.png 1272w, /__u/substackcdn.com/image/fetch/$s_!GcSq!, /__u/bytesbtrees.substack.com/w_1456, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_auto, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe02e079c-c686-4e0a-89d8-f13383584a4c_3200x2720.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div>
      <p>
          <a href="/__u/bytesbtrees.substack.com/p/issue-015-pages-heaps-and-row-storage">
              Read more
          </a>
      </p>
   ]]></content:encoded></item><item><title><![CDATA[Issue-014: Build It Yourself #1 — Part 3: Checkpointing and fast startup]]></title><description><![CDATA[Parts 1 and 2 fixed correctness and read performance.]]></description><link>https://bytesbtrees.substack.com/p/issue-014-build-it-yourself-1-part</link><guid isPermaLink="false">https://bytesbtrees.substack.com/p/issue-014-build-it-yourself-1-part</guid><dc:creator><![CDATA[System Design Roadmap]]></dc:creator><pubDate>Sun, 02 Aug 2026 02:31:18 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!AZO_!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4b13008d-1fa4-42f5-8237-09c2175438a2_3600x2080.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><em>Parts 1 and 2 fixed correctness and read performance. One problem remains: every restart scans the entire log to rebuild the index. For a 300,000-record log, that&#8217;s 600ms before the database can serve a query. Postgres, Redis, and Bitcask all solve this the same way &#8212; periodic checkpoints. Today we implement it in 80 lines.</em></p><div><hr></div><p>There is a pattern in storage systems so fundamental that once you see it, you see it everywhere:</p><ol><li><p><strong>Keep an append-only log.</strong> Never modify history.</p></li><li><p><strong>Periodically snapshot the current state.</strong> Call this a checkpoint.</p></li><li><p><strong>On startup, load the snapshot, then replay only what came after it.</strong></p></li></ol><p>Postgres does this with checkpoints and WAL replay. Redis does this with RDB snapshots and AOF replay. Bitcask does this with its &#8220;keydir&#8221; snapshot and log replay. The specific names differ. The pattern is identical. This issue implements it in about 80 new lines on top of Part 2.</p><p>After a checkpoint on a 300,000-record log, startup drops from 613ms to 202ms &#8212; a 3&#215; speedup. More importantly, the startup time no longer scales with total log history. It scales only with records written since the last checkpoint. Checkpoint every 10,000 writes and startup is bounded regardless of how long the system has been running.</p><div><hr></div><h2><strong>What a checkpoint stores</strong></h2><pre><code><code>Snapshot file format:
  [MAGIC: 4 bytes]           "BBKV" &#8212; identifies this as our format
  [checkpoint_offset: 8 bytes]  byte position in log when snapshot was taken
  [entry_count: 4 bytes]     number of index entries
  for each entry:
    [key_len: 4 bytes]
    [key bytes]
    [value_offset: 8 bytes]  same as what's in self._index
    [value_len: 4 bytes]
</code></code></pre><p>It&#8217;s a complete serialisation of <code>self._index</code> plus the log byte offset at the moment <code>checkpoint()</code> was called. That offset is the crucial piece &#8212; it tells us exactly where to seek in the log on startup to replay only the new records.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="/__u/substackcdn.com/image/fetch/$s_!AZO_!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4b13008d-1fa4-42f5-8237-09c2175438a2_3600x2080.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="/__u/substackcdn.com/image/fetch/$s_!AZO_!, /__u/bytesbtrees.substack.com/w_424, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_webp, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4b13008d-1fa4-42f5-8237-09c2175438a2_3600x2080.png 424w, /__u/substackcdn.com/image/fetch/$s_!AZO_!, /__u/bytesbtrees.substack.com/w_848, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_webp, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4b13008d-1fa4-42f5-8237-09c2175438a2_3600x2080.png 848w, /__u/substackcdn.com/image/fetch/$s_!AZO_!, /__u/bytesbtrees.substack.com/w_1272, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_webp, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4b13008d-1fa4-42f5-8237-09c2175438a2_3600x2080.png 1272w, /__u/substackcdn.com/image/fetch/$s_!AZO_!, /__u/bytesbtrees.substack.com/w_1456, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_webp, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4b13008d-1fa4-42f5-8237-09c2175438a2_3600x2080.png 1456w" sizes="100vw"><img src="/__u/substackcdn.com/image/fetch/$s_!AZO_!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4b13008d-1fa4-42f5-8237-09c2175438a2_3600x2080.png" width="1456" height="841" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/4b13008d-1fa4-42f5-8237-09c2175438a2_3600x2080.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:841,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:644745,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://bytesbtrees.substack.com/i/208418511?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4b13008d-1fa4-42f5-8237-09c2175438a2_3600x2080.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="/__u/substackcdn.com/image/fetch/$s_!AZO_!, /__u/bytesbtrees.substack.com/w_424, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_auto, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4b13008d-1fa4-42f5-8237-09c2175438a2_3600x2080.png 424w, /__u/substackcdn.com/image/fetch/$s_!AZO_!, /__u/bytesbtrees.substack.com/w_848, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_auto, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4b13008d-1fa4-42f5-8237-09c2175438a2_3600x2080.png 848w, /__u/substackcdn.com/image/fetch/$s_!AZO_!, /__u/bytesbtrees.substack.com/w_1272, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_auto, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4b13008d-1fa4-42f5-8237-09c2175438a2_3600x2080.png 1272w, /__u/substackcdn.com/image/fetch/$s_!AZO_!, /__u/bytesbtrees.substack.com/w_1456, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_auto, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4b13008d-1fa4-42f5-8237-09c2175438a2_3600x2080.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div>
      <p>
          <a href="/__u/bytesbtrees.substack.com/p/issue-014-build-it-yourself-1-part">
              Read more
          </a>
      </p>
   ]]></content:encoded></item><item><title><![CDATA[Issue 013: Build It Yourself #1 — Part 2: Adding a hash index]]></title><description><![CDATA[In Part 1 we built a crash-safe key-value store.]]></description><link>https://bytesbtrees.substack.com/p/issue-013-build-it-yourself-1-part</link><guid isPermaLink="false">https://bytesbtrees.substack.com/p/issue-013-build-it-yourself-1-part</guid><dc:creator><![CDATA[System Design Roadmap]]></dc:creator><pubDate>Sun, 26 Jul 2026 02:30:17 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!V6WV!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9fa36da9-1148-4643-bdb9-a8fb43ddfa4a_4050x2160.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<blockquote><p><em>In Part 1 we built a crash-safe key-value store. get(&#8221;user:42&#8221;) worked correctly and survived process kills. It was also catastrophically slow &#8212; O(n) for every lookup. Today we fix that with a 12-line structural change that makes reads O(1). No new disk format. No new files. Just a dict.</em></p></blockquote><div><hr></div><p>Part 1 ended with a working, crash-safe key-value store that stored records in an append-only log and recovered from crashes by scanning for the last valid CRC. The correctness story was solid. The performance story was not. To look up a single key in a 20,000-record log, the store read every record from the beginning to find the latest match. 500 random lookups took 15.5 seconds.</p><p>The fix is embarrassingly simple in hindsight, which is exactly why it&#8217;s worth implementing yourself before reading about it: keep a dictionary in memory that maps every live key to the byte offset where its value lives in the log. A lookup becomes <code>offset, length = index[key]</code> followed by one <code>file.seek(offset)</code> and one <code>file.read(length)</code>. No scan. No iteration. One dict lookup and one seek.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="/__u/substackcdn.com/image/fetch/$s_!V6WV!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9fa36da9-1148-4643-bdb9-a8fb43ddfa4a_4050x2160.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="/__u/substackcdn.com/image/fetch/$s_!V6WV!, /__u/bytesbtrees.substack.com/w_424, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_webp, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9fa36da9-1148-4643-bdb9-a8fb43ddfa4a_4050x2160.png 424w, /__u/substackcdn.com/image/fetch/$s_!V6WV!, /__u/bytesbtrees.substack.com/w_848, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_webp, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9fa36da9-1148-4643-bdb9-a8fb43ddfa4a_4050x2160.png 848w, /__u/substackcdn.com/image/fetch/$s_!V6WV!, /__u/bytesbtrees.substack.com/w_1272, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_webp, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9fa36da9-1148-4643-bdb9-a8fb43ddfa4a_4050x2160.png 1272w, /__u/substackcdn.com/image/fetch/$s_!V6WV!, /__u/bytesbtrees.substack.com/w_1456, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_webp, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9fa36da9-1148-4643-bdb9-a8fb43ddfa4a_4050x2160.png 1456w" sizes="100vw"><img src="/__u/substackcdn.com/image/fetch/$s_!V6WV!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9fa36da9-1148-4643-bdb9-a8fb43ddfa4a_4050x2160.png" width="1456" height="777" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/9fa36da9-1148-4643-bdb9-a8fb43ddfa4a_4050x2160.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:777,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:560141,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://bytesbtrees.substack.com/i/207897555?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9fa36da9-1148-4643-bdb9-a8fb43ddfa4a_4050x2160.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="/__u/substackcdn.com/image/fetch/$s_!V6WV!, /__u/bytesbtrees.substack.com/w_424, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_auto, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9fa36da9-1148-4643-bdb9-a8fb43ddfa4a_4050x2160.png 424w, /__u/substackcdn.com/image/fetch/$s_!V6WV!, /__u/bytesbtrees.substack.com/w_848, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_auto, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9fa36da9-1148-4643-bdb9-a8fb43ddfa4a_4050x2160.png 848w, /__u/substackcdn.com/image/fetch/$s_!V6WV!, /__u/bytesbtrees.substack.com/w_1272, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_auto, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9fa36da9-1148-4643-bdb9-a8fb43ddfa4a_4050x2160.png 1272w, /__u/substackcdn.com/image/fetch/$s_!V6WV!, /__u/bytesbtrees.substack.com/w_1456, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_auto, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9fa36da9-1148-4643-bdb9-a8fb43ddfa4a_4050x2160.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div>
      <p>
          <a href="/__u/bytesbtrees.substack.com/p/issue-013-build-it-yourself-1-part">
              Read more
          </a>
      </p>
   ]]></content:encoded></item><item><title><![CDATA[Issue #012 — Build It Yourself #1, Part 1: A Crash-Safe Key-Value Store]]></title><description><![CDATA[Track: Build It Yourself | Vault: kv_store_part1.py + crash_test.py]]></description><link>https://bytesbtrees.substack.com/p/issue-012-build-it-yourself-1-part</link><guid isPermaLink="false">https://bytesbtrees.substack.com/p/issue-012-build-it-yourself-1-part</guid><dc:creator><![CDATA[System Design Roadmap]]></dc:creator><pubDate>Sun, 19 Jul 2026 02:30:31 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!1xmu!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0d5bba6f-df93-4693-bca6-b87e3a94fe25_1334x800.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><span>Here is something that will change how you read every piece of database documentation: the most important primitive in database engineering is not a B-tree, not a WAL, and not MVCC. It is a crash-safe append. Three steps. One guarantee.</span></p><p><span>In 2012, engineers at a startup discovered their config store had been silently corrupting data for three months. The code wrote a JSON file on every config update. It worked perfectly across 4 million writes in staging. On the first power cut in production, the file was half-written. JSON parse failed. Service refused to start. Every engineer on the team had read about durability. None of them had built the primitive that makes it real.</span></p><p><span>By the end of this issue you will understand exactly what went wrong, why the fix is a single design pattern, and how that pattern underlies every durable storage system you have ever used &#8212; from SQLite to Postgres to RocksDB.</span></p><div><hr></div><h2><strong><span>The problem with writing directly to a file</span></strong></h2><p><span>When your code calls write() on a file and the call returns success, the data is not on disk. It is in the Linux kernel&#8217;s page cache &#8212; a pool of RAM managed by the OS, separate from your process memory. The kernel decides when to flush it to actual storage. Usually this happens within seconds. But if the machine loses power before that flush, your data is gone. The OS confirmed the write succeeded. The disk never saw it.</span></p><p><span>This is not a bug. It is how write() is specified. Performance would collapse if every write() forced a physical disk write.</span></p><p><span>Databases need stronger guarantees. They use fsync() &#8212; a system call that blocks until the OS confirms the data has physically reached the storage device. Expensive: 500&#181;s&#8211;5ms on NVMe, 10&#8211;30ms on spinning disk. But it is the only way to guarantee durability.</span></p><p><span>fsync() solves the write-to-cache problem. But it introduces a new problem: what if the machine crashes halfway through writing a record? You now have a partially-written file on disk. Corrupted. Unrecoverable.</span></p>
      <p>
          <a href="/__u/bytesbtrees.substack.com/p/issue-012-build-it-yourself-1-part">
              Read more
          </a>
      </p>
   ]]></content:encoded></item><item><title><![CDATA[Issue #011: NULL and Three-Valued Logic: The SQL Traps That Cost You Silently]]></title><description><![CDATA[A payments company ran a daily reconciliation query for eight months before anyone noticed it was wrong.]]></description><link>https://bytesbtrees.substack.com/p/issue-011-null-and-three-valued-logic</link><guid isPermaLink="false">https://bytesbtrees.substack.com/p/issue-011-null-and-three-valued-logic</guid><dc:creator><![CDATA[System Design Roadmap]]></dc:creator><pubDate>Sun, 28 Jun 2026 02:30:36 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!ImKA!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd71c9f25-c559-4e5c-b970-2f264cb24bd0_1044x579.gif" length="0" type="image/jpeg"/><content:encoded><![CDATA[<blockquote><p>A payments company ran a daily reconciliation query for eight months before anyone noticed it was wrong. The query aggregated transaction totals by region and compared them against bank statements. Every day it produced numbers. Every day the finance team signed off on them. Every day it was silently excluding transactions from three regions.</p></blockquote><blockquote><p>The bug was a single NULL in the <code>region_id</code> column of a lookup table &#8212; a row that had been inserted by a data migration script eighteen months earlier. The query used <code>NOT IN</code> with a subquery. The NULL made the entire <code>NOT IN</code> clause evaluate to UNKNOWN for every transaction. The finance team was reconciling against numbers that excluded roughly 4% of daily transaction volume.</p></blockquote><p>Eight months of wrong numbers. One NULL. Zero errors. Zero warnings.</p><blockquote><p>This is not a Postgres quirk or a MySQL gotcha. It is the mathematically correct behaviour of SQL&#8217;s three-valued logic &#8212; a system where comparisons involving NULL produce neither TRUE nor FALSE but a third value called UNKNOWN. Understanding why this happens, and where it happens, is the difference between writing queries that work and writing queries that look like they work.</p></blockquote><div><hr></div><h2>Why NULL exists at all</h2><p>CODD introduced NULL in his relational model to represent information that is missing or inapplicable. A customer with no phone number on file. An employee whose salary hasn&#8217;t been set yet. An order with no shipping date because it hasn&#8217;t shipped.</p><p>Two-valued logic &#8212; TRUE and FALSE &#8212; has no way to represent this. If you store 0 for a missing salary, you&#8217;ve confused &#8220;no salary&#8221; with &#8220;zero salary.&#8221; If you store an empty string for a missing email, you&#8217;ve confused &#8220;no email&#8221; with &#8220;an empty email address.&#8221; NULL is a different thing: it is the explicit representation of &#8220;we don&#8217;t know.&#8221;</p><p>CODD&#8217;s decision was correct and mathematically elegant. But it means SQL operates on three truth values, not two:</p><ul><li><p><strong>TRUE</strong> &#8212; the proposition is known to be true</p></li><li><p><strong>FALSE</strong> &#8212; the proposition is known to be false</p></li><li><p><strong>UNKNOWN</strong> &#8212; the proposition involves at least one NULL; we cannot determine its truth</p></li></ul><p>Every WHERE clause filter, every JOIN condition, every CHECK constraint evaluates to one of these three values. Rows are included in results only when the predicate evaluates to TRUE. UNKNOWN rows are discarded &#8212; silently, identically to FALSE.</p><p>This is the mechanism behind the reconciliation bug. The <code>NOT IN</code> predicate evaluated to UNKNOWN for every row. UNKNOWN rows are discarded. Result: empty.</p><div><hr></div><h2>The five places NULL behaves differently</h2><p>The challenge is that NULL&#8217;s semantics are not consistent across SQL contexts. The same value &#8212; NULL &#8212; is handled differently depending on where it appears.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="/__u/substackcdn.com/image/fetch/$s_!ImKA!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd71c9f25-c559-4e5c-b970-2f264cb24bd0_1044x579.gif" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="/__u/substackcdn.com/image/fetch/$s_!ImKA!, /__u/bytesbtrees.substack.com/w_424, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_webp, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd71c9f25-c559-4e5c-b970-2f264cb24bd0_1044x579.gif 424w, /__u/substackcdn.com/image/fetch/$s_!ImKA!, /__u/bytesbtrees.substack.com/w_848, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_webp, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd71c9f25-c559-4e5c-b970-2f264cb24bd0_1044x579.gif 848w, /__u/substackcdn.com/image/fetch/$s_!ImKA!, /__u/bytesbtrees.substack.com/w_1272, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_webp, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd71c9f25-c559-4e5c-b970-2f264cb24bd0_1044x579.gif 1272w, /__u/substackcdn.com/image/fetch/$s_!ImKA!, /__u/bytesbtrees.substack.com/w_1456, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_webp, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd71c9f25-c559-4e5c-b970-2f264cb24bd0_1044x579.gif 1456w" sizes="100vw"><img src="/__u/substackcdn.com/image/fetch/$s_!ImKA!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd71c9f25-c559-4e5c-b970-2f264cb24bd0_1044x579.gif" width="1044" height="579" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/d71c9f25-c559-4e5c-b970-2f264cb24bd0_1044x579.gif&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:579,&quot;width&quot;:1044,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:123583,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/gif&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://bytesbtrees.substack.com/i/203050471?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd71c9f25-c559-4e5c-b970-2f264cb24bd0_1044x579.gif&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="/__u/substackcdn.com/image/fetch/$s_!ImKA!, /__u/bytesbtrees.substack.com/w_424, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_auto, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd71c9f25-c559-4e5c-b970-2f264cb24bd0_1044x579.gif 424w, /__u/substackcdn.com/image/fetch/$s_!ImKA!, /__u/bytesbtrees.substack.com/w_848, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_auto, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd71c9f25-c559-4e5c-b970-2f264cb24bd0_1044x579.gif 848w, /__u/substackcdn.com/image/fetch/$s_!ImKA!, /__u/bytesbtrees.substack.com/w_1272, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_auto, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd71c9f25-c559-4e5c-b970-2f264cb24bd0_1044x579.gif 1272w, /__u/substackcdn.com/image/fetch/$s_!ImKA!, /__u/bytesbtrees.substack.com/w_1456, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_auto, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd71c9f25-c559-4e5c-b970-2f264cb24bd0_1044x579.gif 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>The diagram builds the five contexts in sequence, showing both the behaviour and the most common trap in each.</p><p><strong>Comparisons ( = , != , &gt; , &lt; ):</strong> Any comparison involving NULL produces UNKNOWN. <code>NULL = NULL</code> is UNKNOWN, not TRUE. <code>NULL = 5</code> is UNKNOWN. <code>NULL != 5</code> is UNKNOWN. The only operators that handle NULL specially are <code>IS NULL</code> and <code>IS NOT NULL</code> &#8212; these are not comparisons, they&#8217;re existence tests, and they always return TRUE or FALSE.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;sql&quot;,&quot;nodeId&quot;:&quot;14dae8db-ebbd-4deb-a9d2-30c7704b4fa2&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-sql">-- This never matches any row, even rows where col IS NULL:
SELECT * FROM orders WHERE status = NULL;

-- This correctly finds NULL rows:
SELECT * FROM orders WHERE status IS NULL;</code></pre></div><p><strong>GROUP BY:</strong> NULL is grouped together. All rows with NULL in the GROUP BY column land in the same group, as if NULL equals NULL. This is the opposite of comparison behaviour, where NULL does not equal NULL. The inconsistency is intentional &#8212; grouping requires partitioning rows; without this rule, every NULL would be its own group.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;sql&quot;,&quot;nodeId&quot;:&quot;e2774206-ea16-419b-a25b-fba0682315e5&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-sql">SELECT region_id, COUNT(*) FROM orders GROUP BY region_id;
-- Rows with NULL region_id appear as a single group with region_id = NULL</code></pre></div><p><strong>UNIQUE constraints:</strong> Multiple NULLs are allowed in a UNIQUE column. The reasoning: since NULL does not equal NULL, two NULLs are not considered duplicates. A column with a UNIQUE constraint can contain many NULL rows. If you want to prevent this, add <code>NOT NULL</code> alongside <code>UNIQUE</code>.</p><p><strong>ORDER BY:</strong> NULL sorts LAST in ascending order, FIRST in descending order by default. Control this explicitly with <code>NULLS FIRST</code> or <code>NULLS LAST</code>:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;sql&quot;,&quot;nodeId&quot;:&quot;8d3596d7-734e-4a59-936f-9f58c85edbf1&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-sql">ORDER BY salary ASC NULLS LAST   -- 1, 2, 3, NULL
ORDER BY salary DESC NULLS LAST  -- 3, 2, 1, NULL
ORDER BY salary ASC NULLS FIRST  -- NULL, 1, 2, 3</code></pre></div><p><strong>Aggregate functions:</strong> Every aggregate function except <code>COUNT(*)</code> silently ignores NULL values. <code>SUM</code>, <code>AVG</code>, <code>MIN</code>, <code>MAX</code>, <code>COUNT(column)</code> &#8212; all of them exclude NULL rows from their computation without warning. <code>COUNT(*)</code> counts all rows including those with NULLs. <code>COUNT(salary)</code> counts only rows where salary is not NULL.</p><div><hr></div><h2>The AND and OR truth tables</h2><p>Before the traps, the mechanics. Three-valued logic has precise rules for combining UNKNOWN with TRUE and FALSE:</p><p>ABA AND BA OR BTRUETRUETRUETRUETRUEFALSEFALSETRUETRUEUNKNOWN<strong>UNKNOWNTRUE</strong>FALSEUNKNOWN<strong>FALSEUNKNOWN</strong>UNKNOWNUNKNOWNUNKNOWNUNKNOWN</p><p>Two entries are surprising:</p><p><code>FALSE AND UNKNOWN = FALSE</code> &#8212; regardless of what UNKNOWN resolves to, ANDing with FALSE gives FALSE. So the result is definitively FALSE.</p><p><code>TRUE OR UNKNOWN = TRUE</code> &#8212; regardless of what UNKNOWN resolves to, ORing with TRUE gives TRUE.</p><p>These rules matter because SQL WHERE clauses are boolean expressions. A predicate like <code>salary &gt; 80000 AND department_id = 5</code> evaluates both sides and combines them with AND. If either side is UNKNOWN, the combined result follows the AND table above.</p><p>And NOT: <code>NOT UNKNOWN = UNKNOWN</code>. Negating an unknown truth is still unknown.</p><div><hr></div><h2>The trap every engineer hits: complementary predicates that don&#8217;t cover everything</h2><p>Here is a query that looks like it returns all rows:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;sql&quot;,&quot;nodeId&quot;:&quot;ef5d79d9-e0a1-4c10-bccd-b67ae39bd6fa&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-sql">SELECT * FROM employees
WHERE salary &gt; 50000 OR salary &lt;= 50000;</code></pre></div><p>It doesn&#8217;t. Employees with NULL salary satisfy neither predicate &#8212; both evaluate to UNKNOWN, <code>UNKNOWN OR UNKNOWN = UNKNOWN</code>, and UNKNOWN rows are excluded. An employee with no recorded salary is invisible to this query.</p><p>The correct version: <code>WHERE salary &gt; 50000 OR salary &lt;= 50000 OR salary IS NULL</code>.</p><p>This is why filtering on a nullable column requires explicit NULL handling. The complement of <code>salary &gt; 50000</code> is not <code>salary &lt;= 50000</code>. It is <code>salary &lt;= 50000 OR salary IS NULL</code>.</p><div><hr></div><h2>Try this now</h2><p>Run this on any Postgres database. It demonstrates all four anomalous truth table entries in one query:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;sql&quot;,&quot;nodeId&quot;:&quot;f11cdb15-e180-42b2-b013-8977f4c91b29&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-sql">SELECT
    TRUE  AND UNKNOWN AS "T AND U",   -- UNKNOWN (surprising)
    FALSE AND UNKNOWN AS "F AND U",   -- FALSE   (surprising)
    TRUE  OR  UNKNOWN AS "T OR U",    -- TRUE    (surprising)
    FALSE OR  UNKNOWN AS "F OR U",    -- UNKNOWN (surprising)
    NOT UNKNOWN       AS "NOT U";     -- UNKNOWN</code></pre></div><p>Postgres returns the values directly &#8212; no table needed. Notice the two entries that most engineers get wrong: <code>FALSE AND UNKNOWN</code> returns FALSE (not UNKNOWN), and <code>TRUE OR UNKNOWN</code> returns TRUE (not UNKNOWN).</p><p>The paid section covers the production traps that follow directly from these rules &#8212; including the reconciliation bug from the opening and exactly why <code>NOT IN</code> with a NULL subquery empties your result set.</p><div><hr></div><blockquote><p><strong>What&#8217;s in this issue&#8217;s paid section</strong></p><p>&#10022; The <code>NOT IN</code> / NULL trap &#8212; the full mechanism, the reconciliation bug explained, and the exact fix<br>&#10022; NULL in aggregate functions &#8212; the silent denominator problem with <code>AVG</code>, <code>COUNT</code>, and <code>SUM</code><br>&#10022; The four NULL-safe operators: <code>COALESCE</code>, <code>NULLIF</code>, <code>IS DISTINCT FROM</code>, <code>NULLS FIRST/LAST</code><br>&#10022; <code>GROUP BY NULL</code> behaviour and when it produces wrong business metrics<br>&#10022; Full hands-on experiments &#8212; 4 reproducible SQL demos with annotated output<br>&#10022; Production NULL audit checklist &#8212; 7 SQL queries to find NULL bugs in your existing schema<br>&#10022; Query Lab annotated solution from Issue #010<br>&#10022; Downloadable cheat sheet: NULL behaviour reference card</p><p><strong>$15/month &#183; $120/year &#183; cancel anytime</strong></p></blockquote><div><hr></div><h2>The NOT IN / NULL trap &#8212; the full mechanism</h2><p>This is the bug that emptied eight months of reconciliation reports. Here it is in minimal form:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;sql&quot;,&quot;nodeId&quot;:&quot;f04bb0d3-96cf-4378-b892-429ebdba7bbc&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-sql">CREATE TEMP TABLE regions (id INT, region_id INT);
INSERT INTO regions VALUES (1, 10), (2, 20), (3, NULL);

CREATE TEMP TABLE transactions (id INT, region_id INT, amount NUMERIC);
INSERT INTO transactions VALUES (1, 10, 500), (2, 20, 300), (3, 30, 200);

-- Intended: find transactions in regions not marked active
SELECT * FROM transactions
WHERE region_id NOT IN (SELECT region_id FROM regions);
-- Returns: (0 rows)</code></pre></div><p>The subquery returns <code>{10, 20, NULL}</code>. For each transaction row, SQL expands <code>NOT IN</code> as:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;sql&quot;,&quot;nodeId&quot;:&quot;6836d214-7c88-46ea-89b9-8dc13ef83ca9&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-sql">-- For transaction with region_id = 10:
10 NOT IN (10, 20, NULL)
= 10 != 10 AND 10 != 20 AND 10 != NULL
= FALSE AND TRUE AND UNKNOWN
= FALSE
-- Row excluded (correct &#8212; region 10 is in the list)

-- For transaction with region_id = 30:
30 NOT IN (10, 20, NULL)
= 30 != 10 AND 30 != 20 AND 30 != NULL
= TRUE AND TRUE AND UNKNOWN
= UNKNOWN
-- Row excluded (WRONG &#8212; should be included, region 30 is not in the list)</code></pre></div><p>Every row where the match against NULL is the deciding factor becomes UNKNOWN and is excluded. The query returns nothing even when it should return rows.</p><p><strong>Fix 1 &#8212; Filter NULL from subquery:</strong></p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;sql&quot;,&quot;nodeId&quot;:&quot;1301f9ef-7471-412d-83cf-d60fccbc365d&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-sql">SELECT * FROM transactions
WHERE region_id NOT IN (
    SELECT region_id FROM regions WHERE region_id IS NOT NULL
);
-- Returns: (1 row) &#8212; transaction with region_id = 30</code></pre></div><p><strong>Fix 2 &#8212; Use NOT EXISTS (immune to NULL, often better plan):</strong></p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;sql&quot;,&quot;nodeId&quot;:&quot;cd6ccd4c-891f-4e6b-bf31-e2248988ae86&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-sql">SELECT t.* FROM transactions t
WHERE NOT EXISTS (
    SELECT 1 FROM regions r WHERE r.region_id = t.region_id
);
-- Returns: (1 row) &#8212; correct, and the planner uses an anti-join</code></pre></div><p><code>NOT EXISTS</code> works because it tests for the existence of matching rows, not equality. A NULL in the subquery cannot match any value from the outer query&#8217;s row.</p><p><strong>Always prefer </strong><code>NOT EXISTS</code><strong> over </strong><code>NOT IN</code><strong> when the subquery touches nullable columns.</strong> It is safer, and the query planner often generates a more efficient plan (anti-join vs nested loop with filter).</p><div><hr></div><h2>NULL in aggregate functions &#8212; the silent denominator</h2><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="/__u/substackcdn.com/image/fetch/$s_!uacb!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe28bd76a-f93f-4d24-b76e-39103aa949a5_982x579.gif" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="/__u/substackcdn.com/image/fetch/$s_!uacb!, /__u/bytesbtrees.substack.com/w_424, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_webp, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe28bd76a-f93f-4d24-b76e-39103aa949a5_982x579.gif 424w, /__u/substackcdn.com/image/fetch/$s_!uacb!, /__u/bytesbtrees.substack.com/w_848, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_webp, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe28bd76a-f93f-4d24-b76e-39103aa949a5_982x579.gif 848w, /__u/substackcdn.com/image/fetch/$s_!uacb!, /__u/bytesbtrees.substack.com/w_1272, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_webp, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe28bd76a-f93f-4d24-b76e-39103aa949a5_982x579.gif 1272w, /__u/substackcdn.com/image/fetch/$s_!uacb!, /__u/bytesbtrees.substack.com/w_1456, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_webp, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe28bd76a-f93f-4d24-b76e-39103aa949a5_982x579.gif 1456w" sizes="100vw"><img src="/__u/substackcdn.com/image/fetch/$s_!uacb!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe28bd76a-f93f-4d24-b76e-39103aa949a5_982x579.gif" width="982" height="579" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/e28bd76a-f93f-4d24-b76e-39103aa949a5_982x579.gif&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:579,&quot;width&quot;:982,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:178116,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/gif&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://bytesbtrees.substack.com/i/203050471?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe28bd76a-f93f-4d24-b76e-39103aa949a5_982x579.gif&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="/__u/substackcdn.com/image/fetch/$s_!uacb!, /__u/bytesbtrees.substack.com/w_424, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_auto, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe28bd76a-f93f-4d24-b76e-39103aa949a5_982x579.gif 424w, /__u/substackcdn.com/image/fetch/$s_!uacb!, /__u/bytesbtrees.substack.com/w_848, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_auto, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe28bd76a-f93f-4d24-b76e-39103aa949a5_982x579.gif 848w, /__u/substackcdn.com/image/fetch/$s_!uacb!, /__u/bytesbtrees.substack.com/w_1272, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_auto, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe28bd76a-f93f-4d24-b76e-39103aa949a5_982x579.gif 1272w, /__u/substackcdn.com/image/fetch/$s_!uacb!, /__u/bytesbtrees.substack.com/w_1456, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_auto, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe28bd76a-f93f-4d24-b76e-39103aa949a5_982x579.gif 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>The diagram shows a 7-row employee table where 2 employees have NULL salary. It builds the aggregate results one by one &#8212; <code>COUNT(*)</code>, <code>COUNT(salary)</code>, <code>SUM</code>, <code>AVG</code>, <code>MAX</code>, <code>MIN</code> &#8212; showing which NULLs are included and which are silently excluded.</p><p>The dangerous one is <code>AVG</code>. Given salaries of {90k, 85k, NULL, 92k, 78k, NULL, 72k}:</p><ul><li><p><code>COUNT(*)</code> = 7 (counts all rows)</p></li><li><p><code>COUNT(salary)</code> = 5 (excludes 2 NULL rows)</p></li><li><p><code>SUM(salary)</code> = 417,000 (excludes NULLs)</p></li><li><p><code>AVG(salary)</code> = 417,000 &#247; 5 = <strong>83,400</strong> &#8212; not 417,000 &#247; 7 = 59,571</p></li></ul><p>The <code>AVG</code> denominator is the count of non-NULL rows, not total rows. If you mean &#8220;average salary across all employees, treating unrecorded salaries as zero,&#8221; you must write:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;sql&quot;,&quot;nodeId&quot;:&quot;212106bf-aee6-4f95-bec4-7f50c70a14d6&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-sql">-- Treating NULL salary as 0:
SELECT SUM(COALESCE(salary, 0)) / COUNT(*) AS true_avg FROM employees;
-- = 417,000 / 7 = 59,571

-- Or equivalently:
SELECT AVG(COALESCE(salary, 0)) FROM employees;</code></pre></div><p>If you mean &#8220;average salary among employees with a recorded salary,&#8221; then <code>AVG(salary)</code> is correct &#8212; but document this intent explicitly in a comment, because the next engineer to read the query will assume it covers all rows.</p><p><strong>The GROUP BY NULL problem:</strong></p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;sql&quot;,&quot;nodeId&quot;:&quot;26a28314-c287-4c2f-9d19-85de7f78df72&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-sql">SELECT region_id, SUM(amount) FROM orders GROUP BY region_id;</code></pre></div><p>Orders with NULL <code>region_id</code> form their own group, reported as:</p><pre><code><code>region_id | sum
----------+------
       10 | 5000
       20 | 3200
     NULL | 1800   &#8592; orders with no region assigned</code></code></pre><p>If the business intent is &#8220;all orders belong to exactly one region,&#8221; this NULL group is data quality problem. If the intent is &#8220;some orders have no region,&#8221; this group is correct. The query cannot tell you which &#8212; you must know the business semantics.</p><p>Audit: <code>SELECT COUNT(*) FROM orders WHERE region_id IS NULL</code> &#8212; if this returns anything unexpected, you have unassigned orders silently accumulating in your aggregate totals.</p><div><hr></div><h2>The four NULL-safe operators</h2><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="/__u/substackcdn.com/image/fetch/$s_!nPeE!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffa441aa0-234b-4527-9ac6-326226c98bf7_907x579.gif" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="/__u/substackcdn.com/image/fetch/$s_!nPeE!, /__u/bytesbtrees.substack.com/w_424, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_webp, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffa441aa0-234b-4527-9ac6-326226c98bf7_907x579.gif 424w, /__u/substackcdn.com/image/fetch/$s_!nPeE!, /__u/bytesbtrees.substack.com/w_848, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_webp, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffa441aa0-234b-4527-9ac6-326226c98bf7_907x579.gif 848w, /__u/substackcdn.com/image/fetch/$s_!nPeE!, /__u/bytesbtrees.substack.com/w_1272, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_webp, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffa441aa0-234b-4527-9ac6-326226c98bf7_907x579.gif 1272w, /__u/substackcdn.com/image/fetch/$s_!nPeE!, /__u/bytesbtrees.substack.com/w_1456, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_webp, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffa441aa0-234b-4527-9ac6-326226c98bf7_907x579.gif 1456w" sizes="100vw"><img src="/__u/substackcdn.com/image/fetch/$s_!nPeE!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffa441aa0-234b-4527-9ac6-326226c98bf7_907x579.gif" width="907" height="579" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/fa441aa0-234b-4527-9ac6-326226c98bf7_907x579.gif&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:579,&quot;width&quot;:907,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:97160,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/gif&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://bytesbtrees.substack.com/i/203050471?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffa441aa0-234b-4527-9ac6-326226c98bf7_907x579.gif&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="/__u/substackcdn.com/image/fetch/$s_!nPeE!, /__u/bytesbtrees.substack.com/w_424, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_auto, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffa441aa0-234b-4527-9ac6-326226c98bf7_907x579.gif 424w, /__u/substackcdn.com/image/fetch/$s_!nPeE!, /__u/bytesbtrees.substack.com/w_848, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_auto, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffa441aa0-234b-4527-9ac6-326226c98bf7_907x579.gif 848w, /__u/substackcdn.com/image/fetch/$s_!nPeE!, /__u/bytesbtrees.substack.com/w_1272, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_auto, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffa441aa0-234b-4527-9ac6-326226c98bf7_907x579.gif 1272w, /__u/substackcdn.com/image/fetch/$s_!nPeE!, /__u/bytesbtrees.substack.com/w_1456, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_auto, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffa441aa0-234b-4527-9ac6-326226c98bf7_907x579.gif 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p><code>COALESCE(val, fallback, ...)</code> &#8212; Returns the first non-NULL argument. The standard way to replace NULL with a default:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;sql&quot;,&quot;nodeId&quot;:&quot;5c13a92d-2bf6-4601-8c32-4b9c89eb6c58&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-sql">COALESCE(salary, 0)        -- 0 when salary is NULL
COALESCE(phone, email, 'no contact')  -- first non-NULL contact method</code></pre></div><p><code>NULLIF(val, other)</code> &#8212; Returns NULL if <code>val = other</code>, otherwise returns <code>val</code>. Prevents division-by-zero and converts sentinel values to NULL:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;sql&quot;,&quot;nodeId&quot;:&quot;2d276b4f-1bc8-49b6-a15e-fb600a6eb0d3&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-sql">100 / NULLIF(divisor, 0)        -- NULL instead of division-by-zero error
NULLIF(status, 'unknown')       -- NULL when status = 'unknown'</code></pre></div><p><code>IS DISTINCT FROM</code><strong> / </strong><code>IS NOT DISTINCT FROM</code> &#8212; NULL-safe equality comparison. Unlike <code>=</code>, these return TRUE or FALSE even when NULL is involved:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;sql&quot;,&quot;nodeId&quot;:&quot;43398b9c-cf2a-4749-b74e-53571223c48b&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-sql">NULL IS DISTINCT FROM NULL     -- FALSE  (same unknown = not distinct)
NULL IS DISTINCT FROM 5        -- TRUE   (NULL differs from 5)
5 IS NOT DISTINCT FROM 5       -- TRUE   (same value)
5 IS NOT DISTINCT FROM NULL    -- FALSE  (5 differs from unknown)</code></pre></div><p>Use this when you need to compare two nullable columns and want NULL = NULL to be TRUE &#8212; the opposite of regular <code>=</code> behaviour.</p><p><code>NULLS FIRST</code><strong> / </strong><code>NULLS LAST</code> &#8212; Explicit control over NULL sort position. Never rely on the default &#8212; it varies by sort direction. Always specify explicitly when NULLs appear in columns you sort by:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;sql&quot;,&quot;nodeId&quot;:&quot;e377f51e-9ab5-4430-9310-e7f8c35434f8&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-sql">ORDER BY salary ASC  NULLS LAST   -- 72k, 78k, 85k, 90k, 92k, NULL, NULL
ORDER BY salary DESC NULLS LAST   -- 92k, 90k, 85k, 78k, 72k, NULL, NULL</code></pre></div>
      <p>
          <a href="/__u/bytesbtrees.substack.com/p/issue-011-null-and-three-valued-logic">
              Read more
          </a>
      </p>
   ]]></content:encoded></item><item><title><![CDATA[The Vault — What’s inside the private GitHub repository]]></title><description><![CDATA[Every paid subscription includes access to bytes-and-btrees-labs on GitHub &#8212; a private repository of production-ready code, SQL, and configuration files that go with every issue. Here&#8217;s exactly what&#8217;s]]></description><link>https://bytesbtrees.substack.com/p/the-vault-whats-inside-the-private</link><guid isPermaLink="false">https://bytesbtrees.substack.com/p/the-vault-whats-inside-the-private</guid><dc:creator><![CDATA[System Design Roadmap]]></dc:creator><pubDate>Wed, 24 Jun 2026 08:30:54 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!nL3X!,w_256,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0693b673-c070-4b18-911a-1c08a170def3_608x608.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<blockquote><p><span>When you upgrade to a paid subscription, you&#8217;re added to the </span><strong><span>bytes-and-btrees-labs</span></strong><span> GitHub organisation within a few minutes. You&#8217;ll see every repository listed below &#8212; past and future &#8212; with full read access.</span></p><p><span>The vault grows by 3&#8211;4 assets every month. Founding Members get permanent access at today&#8217;s price, regardless of future price changes.</span></p></blockquote><div><hr></div><h2><span>database-internals-vault</span></h2><p><span>One folder per issue. Each folder contains the production scripts and SQL files referenced in the paid section of that issue.</span></p><p><strong><a href="https://github.com/bytes-and-btrees-labs/database-internals-vault/tree/main/issue-007-atomicity"><span>Issue #007 &#8212; Atomicity at the byte level</span></a></strong></p><ul><li><p><span>wal_demo.py &#8212; Simulates the WAL write sequence during COMMIT, ROLLBACK, and crash recovery. Zero dependencies. Shows exactly what&#8217;s in memory vs on disk at each step.</span></p></li><li><p><span>fsync_benchmark.sh &#8212; Measures fsync latency on your actual hardware using pg_test_fsync or a pure bash fallback. Tells you your hardware&#8217;s commit latency and recommends the right wal_sync_method.</span></p></li></ul><p><strong><a href="https://github.com/bytes-and-btrees-labs/database-internals-vault/tree/main/issue-008-durability/issue-008-durability"><span>Issue #008 &#8212; Durability &amp; fsync</span></a></strong></p><ul><li><p><span>verify_write_cache.sh &#8212; Audits every block device for dangerous write cache configuration. Reports which drives are misconfigured and the exact fix command.</span></p></li><li><p><span>durability_checklist.md &#8212; 8-item durability checklist with verify commands and fix commands for every Postgres durability setting.</span></p></li></ul><p><strong><a href="https://github.com/bytes-and-btrees-labs/database-internals-vault/tree/main/issue-008-durability/issue-008-durability"><span>Issue #009 &#8212; Isolation levels</span></a></strong></p><ul><li><p><span>isolation_demo.sql &#8212; All four isolation anomalies (dirty read, non-repeatable read, phantom read, write skew) reproduced in one file. Copy-paste each section into psql.</span></p></li><li><p><span>ssi_lock_inspector.sql &#8212; Observe Postgres&#8217;s SIREAD locks during active SERIALIZABLE transactions. Shows SSI&#8217;s internal dependency tracking.</span></p></li></ul><p><strong><a href="https://github.com/bytes-and-btrees-labs/database-internals-vault/tree/main/issue-008-durability/issue-008-durability"><span>Issue #010 &#8212; SQL &amp; relational algebra</span></a></strong></p><ul><li><p><span>query_equivalence_proofs.sql &#8212; Four algebraic equivalences demonstrated with EXPLAIN output. Shows the planner&#8217;s selection pushdown rewrite in action.</span></p></li><li><p><span>null_trap_demos.sql &#8212; All four NULL traps (NOT IN null subquery, complement gaps, AVG denominator, LEFT JOIN WHERE) reproduced with minimal test data and fixes.</span></p></li></ul><p><strong><a href="https://github.com/bytes-and-btrees-labs/database-internals-vault/tree/main/issue-011-nulls/issue-011-nulls"><span>Issue #011 &#8212; NULLs &amp; three-valued logic</span></a></strong></p><ul><li><p><span>null_audit.sql &#8212; 7-query audit suite that finds NULL bugs in your production schema. Read-only, safe to run in production. Requires pg_stat_statements for full results.</span></p></li></ul><div><hr></div><h2><span>production-checklists</span></h2><p><span>Standalone checklists that don&#8217;t depend on a specific issue. Run these on any Postgres server.</span></p><ul><li><p><span>postgres-durability-checklist.md &#8212; 8 items covering fsync, synchronous_commit, wal_sync_method, full_page_writes, write cache, WAL storage, checkpoint tuning, and replication durability.</span></p></li><li><p><span>isolation-level-audit.sql &#8212; Finds active transactions and their isolation levels, long-running transactions causing MVCC bloat, lock-holding queries, and tables missing integrity constraints.</span></p></li><li><p><span>index-health-check.sql &#8212; Finds unused indexes, bloated indexes, missing indexes (via seq_scan ratio), duplicate indexes, and calculates index-to-table size ratios.</span></p></li></ul><div><hr></div><h2><span>query-lab-solutions</span></h2><p><span>Every Query Lab puzzle solution in annotated SQL format. Each solution file includes:</span></p><ul><li><p><span>The puzzle restated</span></p></li><li><p><span>2&#8211;3 valid approaches with explanations</span></p></li><li><p><span>Which approach is preferred and why</span></p></li><li><p><span>The specific concept from the issue it tests</span></p></li></ul><div><hr></div><h2><span>build-it-yourself </span><em><span>(coming with Issue #012&#8211;014)</span></em></h2><p><span>The Build It Yourself series ships line-by-line annotated implementations:</span></p><ul><li><p><span>BIY #1: A crash-safe key-value store (487 lines of Python) &#8212; WAL, hash index, crash recovery</span></p></li><li><p><span>BIY #2: A minimal SQL query engine &#8212; AST parser, logical plan, hash join executor</span></p></li><li><p><span>BIY #3: A replication log with primary and replica</span></p></li></ul><div><hr></div><h2><span>How to get access</span></h2><p><span>Subscribe at $15/month or $120/year. Within a few minutes you&#8217;ll receive an email asking for your GitHub username. Enter it and you&#8217;re added to the organisation automatically.</span></p><p><span>To claim permanent access at today&#8217;s price: upgrade to Founding Member ($240/year). 50 spots available.</span></p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://bytesbtrees.substack.com/subscribe&quot;,&quot;text&quot;:&quot;Subscribe to access the vault&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="/__u/bytesbtrees.substack.com/subscribe"><span>Subscribe to access the vault</span></a></p><div><hr></div><p><em><span>&#169; Bytes &amp; B-trees &#183; Substack &#183; </span><a href="/__u/bytesbtrees.substack.com/"><span>bytesbtrees.substack.com</span></a></em></p>]]></content:encoded></item><item><title><![CDATA[Start Here — What Bytes & B-trees is and how to get the most out of it]]></title><description><![CDATA[If you just subscribed, this is the first thing to read. It takes 4 minutes and will save you from reading issues in the wrong order.]]></description><link>https://bytesbtrees.substack.com/p/start-here-what-bytes-and-b-trees</link><guid isPermaLink="false">https://bytesbtrees.substack.com/p/start-here-what-bytes-and-b-trees</guid><dc:creator><![CDATA[System Design Roadmap]]></dc:creator><pubDate>Wed, 24 Jun 2026 03:51:22 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!nL3X!,w_256,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0693b673-c070-4b18-911a-1c08a170def3_608x608.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<blockquote><p><span>Welcome. This newsletter is a structured 3-year curriculum on how databases actually work &#8212; not how to use them, but how they&#8217;re built, why they make the decisions they make, and what happens inside the engine when your queries run.</span></p></blockquote><blockquote><p><span>Every issue goes deep on exactly one concept. Not a roundup. Not bullet points. One concept, from first principles, with working code, animated diagrams, and production context.</span></p></blockquote><blockquote><p><span>Here&#8217;s what you need to know to get the most out of it.</span></p></blockquote><div><hr></div><h2><span>Who this is for</span></h2><blockquote><p><span>Engineers who use databases daily and want to understand what&#8217;s happening underneath. The sweet spot is senior backend engineers, architects, and SREs who can write complex queries but couldn&#8217;t confidently explain why a particular query plan was chosen, what MVCC is actually doing, or when to use SERIALIZABLE isolation.</span></p></blockquote><p><span>If that&#8217;s you, you&#8217;re in the right place.</span></p><p><span>If you&#8217;re completely new to databases, start with Issue #001 and work forward. The curriculum is designed to be progressive &#8212; each issue builds on the ones before it.</span></p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://bytesbtrees.substack.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en-gb&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Bytes &amp; B-Tree's is a reader-supported publication. To receive new posts and support my work, consider becoming a free or paid subscriber.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><div><hr></div><h2><span>Free vs paid</span></h2><p><strong><span>Free subscribers</span></strong><span> get the full core concept every issue &#8212; typically 1,200&#8211;2,000 words explaining the mechanism, one animated diagram, and a Query Lab puzzle. This is genuinely complete. You&#8217;ll understand the concept.</span></p><p><strong><span>Paid subscribers</span></strong><span> ($15/month &#183; $120/year) get the full article including:</span></p><ul><li><p><span>Production failure modes &#8212; the specific ways the concept breaks in real systems</span></p></li><li><p><span>3&#8211;4 hands-on experiments with annotated output</span></p></li><li><p><span>A configuration checklist with verify and fix commands</span></p></li><li><p><span>Annotated Query Lab solution from the previous issue</span></p></li><li><p><span>Access to the private GitHub vault (see below)</span></p></li></ul><p><span>The difference is the gap between </span><em><span>knowing what something is</span></em><span> and </span><em><span>being able to use it safely in production</span></em><span>.</span></p><div><hr></div><h2><span>The GitHub vault</span></h2><p><span>Every paid issue ships a vault asset: a Python script, SQL file, or configuration guide that goes into a private GitHub repository. When you subscribe, you&#8217;re invited to the </span><strong><span>bytes-and-btrees-labs</span></strong><span> GitHub organisation and get access to everything in the vault &#8212; including all past assets, not just this week&#8217;s.</span></p><p><strong><span>What&#8217;s currently in the vault:</span></strong></p><ul><li><p><span>Issue #007: WAL write sequence demo (Python, zero deps) + fsync benchmark script</span></p></li><li><p><span>Issue #008: Write cache verification script + 8-item durability checklist</span></p></li><li><p><span>Issue #009: All 4 isolation anomalies reproducible in one SQL file + SSI lock inspector</span></p></li><li><p><span>Issue #010: Query equivalence proofs + NULL trap demonstrations</span></p></li><li><p><span>Issue #011: 7-query NULL audit suite (find NULL bugs in your production schema)</span></p></li><li><p><span>Production checklists: index health check + isolation level audit + durability checklist</span></p></li></ul><p><span>The vault grows by 3&#8211;4 assets every month.</span></p><div><hr></div><h2><span>Start with these five free issues</span></h2><p><span>If you&#8217;re not sure where to begin, these five issues are the most referenced and the most shared:</span></p><ol><li><p><strong><span>Issue #001</span></strong><span> &#8212; Data, storage &amp; memory: first principles. Starts at the bottom of the stack and explains why disk access speed shapes every database design decision.</span></p></li></ol><ol start="2"><li><p><strong><span>Issue #007</span></strong><span> &#8212; Atomicity at the byte level. Traces a COMMIT from application to disk write by write. Explains what the WAL actually does and why ROLLBACK doesn&#8217;t need to touch disk.</span></p></li></ol><ol start="3"><li><p><strong><span>Issue #008</span></strong><span> &#8212; Durability: what fsync guarantees. The three ways your database&#8217;s durability claim can be silently false &#8212; and how to verify yours isn&#8217;t.</span></p></li></ol><ol start="4"><li><p><strong><span>Issue #009</span></strong><span> &#8212; Isolation levels visualised. All four anomalies, animated, with the exact SQL that reproduces each one and the isolation level that prevents it.</span></p></li></ol><ol start="5"><li><p><strong><span>Issue #010</span></strong><span> &#8212; SQL and relational algebra. Why the planner can safely rewrite your queries, and the algebraic reason NULL behaves the way it does.</span></p></li></ol><div><hr></div><h2><span>The Query Lab</span></h2><p><span>Every issue ends with a puzzle that requires applying the week&#8217;s concept to answer. The puzzle is free &#8212; post your answer in the comments. The annotated solution (with 2&#8211;3 valid approaches explained) is in the following issue&#8217;s paid section.</span></p><p><span>If you want to check your reasoning on past puzzles, the full solution archive is in the vault.</span></p><div><hr></div><h2><span>One thing that would help me</span></h2><p><span>If you know exactly one engineer who would find this useful, forward them this issue. That&#8217;s genuinely the only growth mechanism this publication uses. No paid ads. No sponsor deals. Just word of mouth.</span></p><p><span>See you Sunday.</span></p><div><hr></div><p><em><span>&#169; Bytes &amp; B-trees &#183; Substack &#183; </span><a href="/__u/bytesbtrees.substack.com/"><span>bytesbtrees.substack.com</span></a></em></p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://bytesbtrees.substack.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en-gb&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Bytes &amp; B-Tree's is a reader-supported publication. To receive new posts and support my work, consider becoming a free or paid subscriber.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div>]]></content:encoded></item><item><title><![CDATA[Issue #010: SQL and Relational Algebra — the formal foundation behind every query you've ever written]]></title><description><![CDATA[SQL looks like English. Underneath it is mathematics. Understanding the math is what separates engineers who write queries from engineers]]></description><link>https://bytesbtrees.substack.com/p/sql-and-relational-algebra-the-formal</link><guid isPermaLink="false">https://bytesbtrees.substack.com/p/sql-and-relational-algebra-the-formal</guid><dc:creator><![CDATA[System Design Roadmap]]></dc:creator><pubDate>Sun, 21 Jun 2026 02:31:03 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!rsNN!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe73fe8b2-fcb6-43d9-9884-dabf62a61ff1_917x599.gif" length="0" type="image/jpeg"/><content:encoded><![CDATA[<blockquote><p>A senior engineer at a fintech company spent two days debugging a reporting discrepancy. Their query looked right. The table had data. The filters made sense. But the result was consistently empty. No error. No warning. Just zero rows.</p></blockquote><p>The query was this:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;sql&quot;,&quot;nodeId&quot;:&quot;00da9373-5190-4f3a-9c66-305177b9e39a&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-sql">SELECT name FROM customers
WHERE region_id NOT IN (
    SELECT region_id FROM regions WHERE active = false
);</code></pre></div><p>One region had a NULL in <code>region_id</code>. That single NULL made the entire NOT IN clause evaluate to UNKNOWN for every customer row &#8212; returning nothing. Two days of debugging for one NULL in a subquery that nobody knew was there.</p><p>This isn&#8217;t a bug. It&#8217;s the mathematically correct behaviour of SQL&#8217;s three-valued logic &#8212; a direct consequence of how NULL is defined in relational algebra. The engineer didn&#8217;t have a query problem. They had a knowledge gap: they&#8217;d learned SQL syntax without the algebra underneath it.</p><blockquote><p>That algebra is what this issue covers. Not because you need to write mathematical notation &#8212; you don&#8217;t. But because when you understand the six operations that SQL is built from, queries become transparent. You&#8217;ll see immediately why that NOT IN query returns nothing, why the planner moves your WHERE clause before the JOIN, and why some &#8220;optimisations&#8221; your colleagues suggest are algebraically guaranteed to give the same result.</p></blockquote><div><hr></div><h2>What relational algebra actually is</h2><p>CODD didn&#8217;t design SQL. He designed the relational algebra &#8212; a set of six operations on relations (tables) that can express any fact retrievable from relational data. SQL was designed later as a human-readable syntax that maps onto those operations.</p><p>The six operations are:</p><p><strong>&#963; (sigma) &#8212; Selection.</strong> Filter rows based on a predicate. <code>&#963;_salary &gt; 80000 (employee)</code> is every row where salary exceeds 80,000. In SQL: <code>WHERE salary &gt; 80000</code>.</p><p><strong>&#960; (pi) &#8212; Projection.</strong> Choose which columns to include. <code>&#960;_name, salary (employee)</code> returns only name and salary. In SQL: <code>SELECT name, salary</code>.</p><p><strong>&#8904; &#8212; Join.</strong> Combine two relations on a matching condition. In SQL: <code>JOIN ... ON</code>.</p><p><strong>&#8746; &#8212; Union.</strong> Combine two result sets with identical schemas. In SQL: <code>UNION</code>.</p><p><strong>&#8722; &#8212; Difference.</strong> Rows in one relation that don&#8217;t appear in another. In SQL: <code>EXCEPT</code>.</p><p><strong>&#961; (rho) &#8212; Rename.</strong> Give a relation or attribute a new name. In SQL: <code>AS</code>.</p><p>That&#8217;s the complete vocabulary. Every SQL query you&#8217;ve ever written &#8212; no matter how complex, with subqueries, CTEs, window functions, and recursive joins &#8212; is a composition of these six operations. The query planner translates your SQL into an algebra tree and then finds the most efficient physical execution of that tree.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="/__u/substackcdn.com/image/fetch/$s_!rsNN!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe73fe8b2-fcb6-43d9-9884-dabf62a61ff1_917x599.gif" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="/__u/substackcdn.com/image/fetch/$s_!rsNN!, /__u/bytesbtrees.substack.com/w_424, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_webp, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe73fe8b2-fcb6-43d9-9884-dabf62a61ff1_917x599.gif 424w, /__u/substackcdn.com/image/fetch/$s_!rsNN!, /__u/bytesbtrees.substack.com/w_848, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_webp, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe73fe8b2-fcb6-43d9-9884-dabf62a61ff1_917x599.gif 848w, /__u/substackcdn.com/image/fetch/$s_!rsNN!, /__u/bytesbtrees.substack.com/w_1272, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_webp, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe73fe8b2-fcb6-43d9-9884-dabf62a61ff1_917x599.gif 1272w, /__u/substackcdn.com/image/fetch/$s_!rsNN!, /__u/bytesbtrees.substack.com/w_1456, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_webp, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe73fe8b2-fcb6-43d9-9884-dabf62a61ff1_917x599.gif 1456w" sizes="100vw"><img src="/__u/substackcdn.com/image/fetch/$s_!rsNN!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe73fe8b2-fcb6-43d9-9884-dabf62a61ff1_917x599.gif" width="917" height="599" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/e73fe8b2-fcb6-43d9-9884-dabf62a61ff1_917x599.gif&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:599,&quot;width&quot;:917,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:103587,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/gif&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://bytesbtrees.substack.com/i/197315676?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe73fe8b2-fcb6-43d9-9884-dabf62a61ff1_917x599.gif&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="/__u/substackcdn.com/image/fetch/$s_!rsNN!, /__u/bytesbtrees.substack.com/w_424, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_auto, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe73fe8b2-fcb6-43d9-9884-dabf62a61ff1_917x599.gif 424w, /__u/substackcdn.com/image/fetch/$s_!rsNN!, /__u/bytesbtrees.substack.com/w_848, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_auto, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe73fe8b2-fcb6-43d9-9884-dabf62a61ff1_917x599.gif 848w, /__u/substackcdn.com/image/fetch/$s_!rsNN!, /__u/bytesbtrees.substack.com/w_1272, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_auto, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe73fe8b2-fcb6-43d9-9884-dabf62a61ff1_917x599.gif 1272w, /__u/substackcdn.com/image/fetch/$s_!rsNN!, /__u/bytesbtrees.substack.com/w_1456, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_auto, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe73fe8b2-fcb6-43d9-9884-dabf62a61ff1_917x599.gif 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>The diagram shows each operation applied to an employee table, with results appearing alongside. The key observation in the last stage: operations compose. <code>&#960; name (&#963; salary &gt; 80000 (employee))</code> applies selection first, then projection &#8212; which is exactly what <code>SELECT name FROM employee WHERE salary &gt; 80000</code> does. The planner sees your SQL, builds this algebra expression, and then decides whether to apply the selection before or after loading the full table.</p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://bytesbtrees.substack.com/subscribe&quot;,&quot;text&quot;:&quot;Get Access to GitHub Link&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="/__u/bytesbtrees.substack.com/subscribe"><span>Get Access to GitHub Link</span></a></p><div><hr></div><h2>Why the algebra explains query optimisation</h2><p>Here is the insight that makes database query planning make sense: <strong>algebraic equivalences let the planner legally rewrite your query into a faster form.</strong></p><p>Two algebra expressions are equivalent if they always produce the same result on any valid input. The planner maintains a library of these equivalences &#8212; hundreds of rewrite rules &#8212; and applies them before choosing a physical execution strategy.</p><p>The most important one for everyday SQL:</p><p><strong>Selection pushdown:</strong> <code>&#963;_condition(R &#8904; S)</code> is equivalent to <code>&#963;_condition(R) &#8904; S</code></p><p>In English: filtering rows before joining is mathematically the same as filtering rows after joining &#8212; but filtering first means fewer rows enter the join, making the join dramatically cheaper.</p><p>This is why the planner moves <code>WHERE</code> clauses before <code>JOIN</code> operations. Not as a heuristic. Not as a &#8220;best practice.&#8221; As an algebraic guarantee. The result is identical either way, and one path is faster.</p><p>Other equivalences the planner applies:</p><pre><code><code>-- These two queries are algebraically identical:
SELECT * FROM orders WHERE status = 'shipped' AND amount &gt; 100;
SELECT * FROM orders WHERE amount &gt; 100 AND status = 'shipped';
-- Predicate order in WHERE is irrelevant. Planner reorders for index use.

-- These are also equivalent:
SELECT DISTINCT customer_id FROM orders;
SELECT customer_id FROM orders GROUP BY customer_id;
-- Both express &#960; with duplicate elimination. Planner may choose either plan.

-- Subquery to join rewrite (planner does this automatically):
SELECT name FROM customers
WHERE id IN (SELECT customer_id FROM orders WHERE amount &gt; 500);
-- Equivalent to:
SELECT DISTINCT c.name FROM customers c
JOIN orders o ON c.id = o.customer_id
WHERE o.amount &gt; 500;</code></code></pre><p>When you understand that these rewrites are algebraic equivalences &#8212; not approximations &#8212; you stop worrying whether the planner will &#8220;do the right thing.&#8221; It will, because the right thing is mathematically defined.</p><div><hr></div><h2>The fundamental unit of relational algebra: the relation</h2><p>A relation is not just &#8220;a table.&#8221; Formally, a relation is a <strong>set of tuples</strong> &#8212; and sets have no duplicates and no ordering. These two properties have direct SQL consequences:</p><p><strong>No ordering:</strong> A bare <code>SELECT</code> has no guaranteed row order. This surprises engineers who see consistent ordering in testing and assume it&#8217;s guaranteed. It isn&#8217;t. Without <code>ORDER BY</code>, a query can return rows in any order the planner chooses &#8212; and that order can change when you add an index, update statistics, or upgrade Postgres. Always use <code>ORDER BY</code> when order matters.</p><p><strong>No duplicates:</strong> A true relation has no duplicate rows. SQL allows duplicates (it&#8217;s a multiset, technically) &#8212; which is why <code>SELECT DISTINCT</code> exists, and why CODD&#8217;s Rule 12 is violated by SQL at a theoretical level. For most practical purposes this doesn&#8217;t matter. For queries that depend on set semantics (<code>UNION</code> vs <code>UNION ALL</code>, <code>EXCEPT</code> vs <code>EXCEPT ALL</code>), it matters significantly.</p><div><hr></div><h2>How the query lifecycle uses algebra</h2><p>Every SQL query passes through this pipeline before a single row is read:</p><p><strong>1. Parse</strong> &#8212; Your SQL string is tokenised and parsed into a parse tree. Syntax errors are caught here.</p><p><strong>2. Analyse</strong> &#8212; Names are resolved: <code>employee</code> &#8594; relation OID 16385, <code>salary</code> &#8594; attribute 4 of that relation. Type checking happens. Semantic errors are caught here.</p><p><strong>3. Rewrite</strong> &#8212; View definitions are inlined. Rule-based rewrites are applied. If your query references a view, the view&#8217;s definition is substituted into the query tree here.</p><p><strong>4. Plan (logical)</strong> &#8212; The parse tree is converted into a logical algebra tree. Equivalence rewrites are applied &#8212; selection pushdown, join reordering, subquery flattening. The planner generates multiple candidate trees.</p><p><strong>5. Plan (physical)</strong> &#8212; Each logical algebra operation is assigned a physical implementation: should this join use hash join, merge join, or nested loop? Should this selection use an index scan or sequential scan? Should this sort use an explicit sort node or an index scan that returns rows pre-sorted? The planner estimates costs using table statistics and picks the cheapest physical plan.</p><p><strong>6. Execute</strong> &#8212; The physical plan runs. Rows are returned.</p><p>Steps 4 and 5 are where the algebra matters. The planner can only rewrite the logical tree safely because the rewrites are algebraic equivalences. If they weren&#8217;t, the planner would need to ask you for permission before each rewrite &#8212; and SQL would be far less usable.</p><div><hr></div><h2>Try this right now</h2><p>Run this on any Postgres database with data:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;sql&quot;,&quot;nodeId&quot;:&quot;0e7c4e21-13a3-4501-9ef5-c0848405a352&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-sql">EXPLAIN (FORMAT TEXT)
SELECT e.name, d.dept_name
FROM   employee e
JOIN   department d ON e.dept_id = d.id
WHERE  e.salary &gt; 80000;</code></pre></div><p>Look at the output. Find the line that says <code>Filter: (salary &gt; 80000)</code>. Note whether that filter appears above or below the join node. In almost every case, Postgres will have pushed the filter below the join &#8212; applying it to the employee table before the join happens.</p><p>Now try:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;sql&quot;,&quot;nodeId&quot;:&quot;c881fa20-8d0b-4ba5-b440-23bb75339289&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-sql">EXPLAIN (FORMAT TEXT)
SELECT e.name, d.dept_name
FROM   (SELECT * FROM employee WHERE salary &gt; 80000) e
JOIN   department d ON e.dept_id = d.id;</code></pre></div><p>Compare the plans. They&#8217;re identical. Postgres recognises that both SQL forms produce the same algebra tree after selection pushdown &#8212; and generates the same plan for both. That&#8217;s the algebra at work.</p><p>The paid section explains what the other nodes in this plan mean, how to read the cost estimates, and &#8212; importantly &#8212; the three cases where Postgres <em>cannot</em> push selection below a join, why, and how to rewrite your query to help it.</p><div><hr></div><blockquote><p><strong>What&#8217;s in this issue&#8217;s paid section</strong></p><p>&#10022; Three-valued logic and NULL &#8212; the complete truth tables with every trap explained<br>&#10022; The SQL algebra tree diagram &#8212; how Postgres builds and rewrites your query plan<br>&#10022; The five equivalence rules you can apply yourself to rewrite slow queries<br>&#10022; When selection pushdown fails &#8212; three cases and how to fix them<br>&#10022; Full hands-on experiments: query equivalences, NULL behaviour, EXPLAIN analysis<br>&#10022; Production checklist: 7 NULL-related bugs to audit in your existing queries<br>&#10022; Query Lab annotated solution from Issue #009<br>&#10022; Downloadable cheat sheet: SQL &#8596; relational algebra reference card</p><p><strong>$15/month &#183; $120/year &#183; cancel anytime</strong> Subscribe &#8594; bytesandbrees.substack.com</p></blockquote><div><hr></div><h2>Three-valued logic &#8212; where the fintech bug lives</h2><p>SQL has three truth values: TRUE, FALSE, and UNKNOWN. UNKNOWN arises whenever NULL is involved in a comparison. This isn&#8217;t a Postgres quirk &#8212; it&#8217;s the formal definition of NULL in relational theory, designed to represent &#8220;unknown or inapplicable information.&#8221;</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="/__u/substackcdn.com/image/fetch/$s_!yz7z!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F10ccdea9-839c-4ec0-99a4-d54066a3695f_901x579.gif" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="/__u/substackcdn.com/image/fetch/$s_!yz7z!, /__u/bytesbtrees.substack.com/w_424, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_webp, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F10ccdea9-839c-4ec0-99a4-d54066a3695f_901x579.gif 424w, /__u/substackcdn.com/image/fetch/$s_!yz7z!, /__u/bytesbtrees.substack.com/w_848, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_webp, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F10ccdea9-839c-4ec0-99a4-d54066a3695f_901x579.gif 848w, /__u/substackcdn.com/image/fetch/$s_!yz7z!, /__u/bytesbtrees.substack.com/w_1272, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_webp, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F10ccdea9-839c-4ec0-99a4-d54066a3695f_901x579.gif 1272w, /__u/substackcdn.com/image/fetch/$s_!yz7z!, /__u/bytesbtrees.substack.com/w_1456, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_webp, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F10ccdea9-839c-4ec0-99a4-d54066a3695f_901x579.gif 1456w" sizes="100vw"><img src="/__u/substackcdn.com/image/fetch/$s_!yz7z!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F10ccdea9-839c-4ec0-99a4-d54066a3695f_901x579.gif" width="901" height="579" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/10ccdea9-839c-4ec0-99a4-d54066a3695f_901x579.gif&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:579,&quot;width&quot;:901,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:94023,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/gif&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://bytesbtrees.substack.com/i/197315676?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F10ccdea9-839c-4ec0-99a4-d54066a3695f_901x579.gif&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="/__u/substackcdn.com/image/fetch/$s_!yz7z!, /__u/bytesbtrees.substack.com/w_424, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_auto, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F10ccdea9-839c-4ec0-99a4-d54066a3695f_901x579.gif 424w, /__u/substackcdn.com/image/fetch/$s_!yz7z!, /__u/bytesbtrees.substack.com/w_848, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_auto, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F10ccdea9-839c-4ec0-99a4-d54066a3695f_901x579.gif 848w, /__u/substackcdn.com/image/fetch/$s_!yz7z!, /__u/bytesbtrees.substack.com/w_1272, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_auto, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F10ccdea9-839c-4ec0-99a4-d54066a3695f_901x579.gif 1272w, /__u/substackcdn.com/image/fetch/$s_!yz7z!, /__u/bytesbtrees.substack.com/w_1456, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_auto, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F10ccdea9-839c-4ec0-99a4-d54066a3695f_901x579.gif 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>The diagram builds the AND, OR, and NOT truth tables for three-valued logic, then reveals the three traps in sequence.</p><p><strong>The AND table has one surprising entry:</strong> <code>FALSE AND UNKNOWN = FALSE</code>. Why? Because regardless of what UNKNOWN resolves to (true or false), anding with FALSE gives FALSE. So <code>FALSE AND UNKNOWN</code> is definitely FALSE. But <code>TRUE AND UNKNOWN = UNKNOWN</code> &#8212; because if the unknown part turns out to be false, the whole expression is false.</p><p><strong>The OR table has the symmetric surprising entry:</strong> <code>TRUE OR UNKNOWN = TRUE</code>. Because regardless of what UNKNOWN resolves to, oring with TRUE gives TRUE.</p><p>These rules are internally consistent and mathematically correct. They&#8217;re also the source of bugs that have been in production codebases for years.</p><h3>Trap 1: NULL = NULL is UNKNOWN, not TRUE</h3><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;sql&quot;,&quot;nodeId&quot;:&quot;bd8e458e-6ec4-47af-bd0a-696baa8f11fc&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-sql">SELECT * FROM orders WHERE cancelled_at = NULL;  -- always 0 rows
SELECT * FROM orders WHERE cancelled_at IS NULL;  -- correct</code></pre></div><p>The equality operator applied to NULL produces UNKNOWN. SQL&#8217;s WHERE clause only returns rows where the predicate evaluates to TRUE &#8212; UNKNOWN rows are excluded, same as FALSE. <code>IS NULL</code> is the correct predicate; it&#8217;s a separate operator that specifically tests for null.</p><p>This trap is so common that some SQL clients warn you. Many don&#8217;t.</p><h3>Trap 2: NOT IN with a NULL in the subquery</h3><p>This is the bug from the opening story. Recall:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;sql&quot;,&quot;nodeId&quot;:&quot;2f52d27d-c66d-47dd-b733-006717c355e5&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-sql">SELECT name FROM customers
WHERE region_id NOT IN (
    SELECT region_id FROM regions WHERE active = false
);</code></pre></div><p>If any row in the subquery returns NULL for <code>region_id</code>, the NOT IN predicate evaluates to UNKNOWN for every row in customers &#8212; because:</p><pre><code><code>region_id NOT IN (1, 2, NULL)
= region_id != 1 AND region_id != 2 AND region_id != NULL
= region_id != 1 AND region_id != 2 AND UNKNOWN
= UNKNOWN (for any value of region_id)</code></code></pre><p>The fix: either explicitly exclude NULLs from the subquery, or use NOT EXISTS instead (which is immune to NULL):</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;sql&quot;,&quot;nodeId&quot;:&quot;410c9b5e-f1e3-4976-94c3-61ab5585fe81&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-sql">-- Fix 1: exclude NULLs from subquery
SELECT name FROM customers
WHERE region_id NOT IN (
    SELECT region_id FROM regions
    WHERE active = false AND region_id IS NOT NULL
);

-- Fix 2: NOT EXISTS (preferred &#8212; immune to NULL, often better plan)
SELECT name FROM customers c
WHERE NOT EXISTS (
    SELECT 1 FROM regions r
    WHERE r.region_id = c.region_id
      AND r.active = false
);</code></pre></div><p>The NOT EXISTS version is also frequently faster because the planner can use an anti-join, which short-circuits as soon as it finds one matching row.</p><h3>Trap 3: COUNT(*) vs COUNT(column)</h3><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;sql&quot;,&quot;nodeId&quot;:&quot;34964f2b-1378-4e81-844e-c3379d09d51d&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-sql">SELECT COUNT(*),       -- counts all rows, including NULLs
       COUNT(salary),  -- counts only non-NULL salary values
       AVG(salary)     -- NULLs excluded from average denominator
FROM employees;</code></pre></div><p>If 3 of 100 employees have NULL salary, <code>COUNT(*)</code> returns 100, <code>COUNT(salary)</code> returns 97, and <code>AVG(salary)</code> divides by 97 &#8212; not 100. This makes AVG behave like &#8220;average salary among employees who have a recorded salary&#8221; not &#8220;average salary across all employees.&#8221; If you mean the latter, use <code>AVG(COALESCE(salary, 0))</code>.</p><div><hr></div><h2>The SQL algebra tree &#8212; what the planner actually builds</h2><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="/__u/substackcdn.com/image/fetch/$s_!-viB!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8eb1205d-e5a6-48f0-b4a1-612a11f01c6b_868x599.gif" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="/__u/substackcdn.com/image/fetch/$s_!-viB!, /__u/bytesbtrees.substack.com/w_424, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_webp, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8eb1205d-e5a6-48f0-b4a1-612a11f01c6b_868x599.gif 424w, /__u/substackcdn.com/image/fetch/$s_!-viB!, /__u/bytesbtrees.substack.com/w_848, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_webp, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8eb1205d-e5a6-48f0-b4a1-612a11f01c6b_868x599.gif 848w, /__u/substackcdn.com/image/fetch/$s_!-viB!, /__u/bytesbtrees.substack.com/w_1272, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_webp, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8eb1205d-e5a6-48f0-b4a1-612a11f01c6b_868x599.gif 1272w, /__u/substackcdn.com/image/fetch/$s_!-viB!, /__u/bytesbtrees.substack.com/w_1456, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_webp, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8eb1205d-e5a6-48f0-b4a1-612a11f01c6b_868x599.gif 1456w" sizes="100vw"><img src="/__u/substackcdn.com/image/fetch/$s_!-viB!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8eb1205d-e5a6-48f0-b4a1-612a11f01c6b_868x599.gif" width="868" height="599" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/8eb1205d-e5a6-48f0-b4a1-612a11f01c6b_868x599.gif&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:599,&quot;width&quot;:868,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:110472,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/gif&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://bytesbtrees.substack.com/i/197315676?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8eb1205d-e5a6-48f0-b4a1-612a11f01c6b_868x599.gif&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="/__u/substackcdn.com/image/fetch/$s_!-viB!, /__u/bytesbtrees.substack.com/w_424, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_auto, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8eb1205d-e5a6-48f0-b4a1-612a11f01c6b_868x599.gif 424w, /__u/substackcdn.com/image/fetch/$s_!-viB!, /__u/bytesbtrees.substack.com/w_848, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_auto, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8eb1205d-e5a6-48f0-b4a1-612a11f01c6b_868x599.gif 848w, /__u/substackcdn.com/image/fetch/$s_!-viB!, /__u/bytesbtrees.substack.com/w_1272, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_auto, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8eb1205d-e5a6-48f0-b4a1-612a11f01c6b_868x599.gif 1272w, /__u/substackcdn.com/image/fetch/$s_!-viB!, /__u/bytesbtrees.substack.com/w_1456, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_auto, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8eb1205d-e5a6-48f0-b4a1-612a11f01c6b_868x599.gif 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>The diagram builds the algebra tree for a JOIN query with a WHERE clause and ORDER BY, one node at a time &#8212; base tables at the leaves, operations stacking toward the root. The critical annotation appears at step 3: the selection node (&#963; salary &gt; 80,000) is shown at the leaves, <em>below</em> the join &#8212; the result of selection pushdown.</p><p>The planner builds the initial tree with selection at the top (where your WHERE clause is syntactically), then applies the pushdown rewrite. This single rewrite can make a query 10&#8211;100x faster when the filtered table is large.</p><h3>Three cases where pushdown fails</h3><p>The planner cannot push selection below a join in these cases:</p><p><strong>Case 1 &#8212; The predicate references both tables:</strong></p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;sql&quot;,&quot;nodeId&quot;:&quot;747eaed7-065a-4c55-be5c-dd2d63940a35&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-sql">-- Cannot push: predicate uses columns from both tables
SELECT * FROM employee e JOIN department d ON e.dept_id = d.id
WHERE e.salary &gt; d.avg_salary;
-- The join must happen first; you can't filter on d.avg_salary before joining</code></pre></div><p><strong>Case 2 &#8212; The predicate uses a volatile function:</strong></p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;sql&quot;,&quot;nodeId&quot;:&quot;db7b34bb-2420-479a-9dec-451592cc2881&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-sql">-- Cannot push: random() is evaluated per-row and must stay above the join
SELECT * FROM employee e JOIN department d ON e.dept_id = d.id
WHERE e.score &gt; random() * 100;
-- If pushed below, each table side would get different random() values</code></pre></div><p><strong>Case 3 &#8212; OUTER JOIN semantics:</strong></p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;sql&quot;,&quot;nodeId&quot;:&quot;91d50581-e27a-4d7d-8171-ff15d904d83d&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-sql">-- Cannot always push: LEFT JOIN null-fills right side; WHERE on right side
-- effectively converts to an INNER JOIN, which changes semantics
SELECT e.name, d.dept_name
FROM employee e LEFT JOIN department d ON e.dept_id = d.id
WHERE d.budget &gt; 100000;
-- This already is an inner join in effect; Postgres converts it
-- But if you meant to keep employees without departments, write it differently</code></pre></div><p>Understanding these cases lets you write queries that give the planner room to optimise &#8212; and lets you diagnose cases where a plan isn&#8217;t as fast as expected.</p><div><hr></div>
      <p>
          <a href="/__u/bytesbtrees.substack.com/p/sql-and-relational-algebra-the-formal">
              Read more
          </a>
      </p>
   ]]></content:encoded></item><item><title><![CDATA[Issue #009: Isolation levels visualised — from dirty reads to serialisable anomalies]]></title><description><![CDATA[Consider a banking application running a nightly report that sums all account balances.]]></description><link>https://bytesbtrees.substack.com/p/issue-009-isolation-levels-visualised</link><guid isPermaLink="false">https://bytesbtrees.substack.com/p/issue-009-isolation-levels-visualised</guid><dc:creator><![CDATA[System Design Roadmap]]></dc:creator><pubDate>Sun, 14 Jun 2026 02:31:04 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!pMis!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F71133435-29d3-4d59-9006-0ba9c159bd40_919x579.gif" length="0" type="image/jpeg"/><content:encoded><![CDATA[<blockquote><p>Consider a banking application running a nightly report that sums all account balances. The report runs as a single long read transaction. While it runs, thousands of transfer transactions are committing &#8212; debiting one account, crediting another. If the report reads account A before its transfer is processed and account B after, it captures an inconsistent snapshot: A&#8217;s old balance plus B&#8217;s new balance. The sum is wrong. No data was corrupted. No transaction failed. The isolation level just wasn&#8217;t strong enough for the workload.</p></blockquote><p>This is a real class of bug that appears in production systems regularly. It&#8217;s not dramatic &#8212; it doesn&#8217;t destroy data, it just produces wrong numbers that take days or weeks to notice. And it&#8217;s completely preventable once you understand what isolation levels actually control.</p><blockquote><p>There are four isolation levels in the SQL standard. Each one makes a specific set of anomalies impossible. Each one costs more in locking or tracking overhead. Most applications are running at the wrong level for their workload &#8212; either too weak (producing wrong results) or stronger than necessary (throttling concurrency without benefit). This issue maps every anomaly, every level, and the precise mechanism Postgres uses to implement the strongest one.</p></blockquote><div><hr></div><h2>What isolation actually means</h2><p>Before anomalies, let&#8217;s be precise about what isolation is trying to achieve.</p><p>Ideally, concurrent transactions would behave as if they ran one at a time in some serial order. If transaction T1 and T2 run concurrently, the result should be identical to either &#8220;T1 then T2&#8221; or &#8220;T2 then T1.&#8221; When this holds, we say the execution is <strong>serialisable</strong>.</p><p>Full serialisability is achievable but expensive &#8212; it requires the database to track enough information to detect when two concurrent transactions would produce a result that&#8217;s impossible in any serial order, and abort one of them. The SQL standard defines weaker levels that relax this guarantee in exchange for better concurrency.</p><p>The four levels, from weakest to strongest:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="/__u/substackcdn.com/image/fetch/$s_!pMis!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F71133435-29d3-4d59-9006-0ba9c159bd40_919x579.gif" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="/__u/substackcdn.com/image/fetch/$s_!pMis!, /__u/bytesbtrees.substack.com/w_424, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_webp, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F71133435-29d3-4d59-9006-0ba9c159bd40_919x579.gif 424w, /__u/substackcdn.com/image/fetch/$s_!pMis!, /__u/bytesbtrees.substack.com/w_848, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_webp, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F71133435-29d3-4d59-9006-0ba9c159bd40_919x579.gif 848w, /__u/substackcdn.com/image/fetch/$s_!pMis!, /__u/bytesbtrees.substack.com/w_1272, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_webp, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F71133435-29d3-4d59-9006-0ba9c159bd40_919x579.gif 1272w, /__u/substackcdn.com/image/fetch/$s_!pMis!, /__u/bytesbtrees.substack.com/w_1456, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_webp, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F71133435-29d3-4d59-9006-0ba9c159bd40_919x579.gif 1456w" sizes="100vw"><img src="/__u/substackcdn.com/image/fetch/$s_!pMis!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F71133435-29d3-4d59-9006-0ba9c159bd40_919x579.gif" width="919" height="579" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/71133435-29d3-4d59-9006-0ba9c159bd40_919x579.gif&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:579,&quot;width&quot;:919,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:92609,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/gif&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://bytesbtrees.substack.com/i/201705910?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F71133435-29d3-4d59-9006-0ba9c159bd40_919x579.gif&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="/__u/substackcdn.com/image/fetch/$s_!pMis!, /__u/bytesbtrees.substack.com/w_424, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_auto, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F71133435-29d3-4d59-9006-0ba9c159bd40_919x579.gif 424w, /__u/substackcdn.com/image/fetch/$s_!pMis!, /__u/bytesbtrees.substack.com/w_848, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_auto, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F71133435-29d3-4d59-9006-0ba9c159bd40_919x579.gif 848w, /__u/substackcdn.com/image/fetch/$s_!pMis!, /__u/bytesbtrees.substack.com/w_1272, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_auto, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F71133435-29d3-4d59-9006-0ba9c159bd40_919x579.gif 1272w, /__u/substackcdn.com/image/fetch/$s_!pMis!, /__u/bytesbtrees.substack.com/w_1456, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_auto, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F71133435-29d3-4d59-9006-0ba9c159bd40_919x579.gif 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>The matrix builds up row by row &#8212; each anomaly appearing alongside which levels prevent it. Reading it column by column: READ UNCOMMITTED prevents nothing. READ COMMITTED (Postgres&#8217;s default) prevents only dirty reads. REPEATABLE READ prevents dirty reads and non-repeatable reads. SERIALISABLE prevents all four anomaly classes, including the subtle write skew.</p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://bytesbtrees.substack.com/subscribe&quot;,&quot;text&quot;:&quot;Get Access to GitHub Link&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="/__u/bytesbtrees.substack.com/subscribe"><span>Get Access to GitHub Link</span></a></p><div><hr></div><h2>Anomaly #1: The dirty read</h2><p>A dirty read occurs when transaction T1 reads data written by transaction T2, and T2 subsequently rolls back. T1 has read data that, from the database&#8217;s perspective, never officially existed.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="/__u/substackcdn.com/image/fetch/$s_!74v_!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe174f0b2-4bf4-443a-8877-6f28423ed566_875x579.gif" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="/__u/substackcdn.com/image/fetch/$s_!74v_!, /__u/bytesbtrees.substack.com/w_424, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_webp, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe174f0b2-4bf4-443a-8877-6f28423ed566_875x579.gif 424w, /__u/substackcdn.com/image/fetch/$s_!74v_!, /__u/bytesbtrees.substack.com/w_848, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_webp, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe174f0b2-4bf4-443a-8877-6f28423ed566_875x579.gif 848w, /__u/substackcdn.com/image/fetch/$s_!74v_!, /__u/bytesbtrees.substack.com/w_1272, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_webp, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe174f0b2-4bf4-443a-8877-6f28423ed566_875x579.gif 1272w, /__u/substackcdn.com/image/fetch/$s_!74v_!, /__u/bytesbtrees.substack.com/w_1456, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_webp, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe174f0b2-4bf4-443a-8877-6f28423ed566_875x579.gif 1456w" sizes="100vw"><img src="/__u/substackcdn.com/image/fetch/$s_!74v_!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe174f0b2-4bf4-443a-8877-6f28423ed566_875x579.gif" width="875" height="579" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/e174f0b2-4bf4-443a-8877-6f28423ed566_875x579.gif&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:579,&quot;width&quot;:875,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:168322,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/gif&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://bytesbtrees.substack.com/i/201705910?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe174f0b2-4bf4-443a-8877-6f28423ed566_875x579.gif&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="/__u/substackcdn.com/image/fetch/$s_!74v_!, /__u/bytesbtrees.substack.com/w_424, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_auto, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe174f0b2-4bf4-443a-8877-6f28423ed566_875x579.gif 424w, /__u/substackcdn.com/image/fetch/$s_!74v_!, /__u/bytesbtrees.substack.com/w_848, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_auto, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe174f0b2-4bf4-443a-8877-6f28423ed566_875x579.gif 848w, /__u/substackcdn.com/image/fetch/$s_!74v_!, /__u/bytesbtrees.substack.com/w_1272, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_auto, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe174f0b2-4bf4-443a-8877-6f28423ed566_875x579.gif 1272w, /__u/substackcdn.com/image/fetch/$s_!74v_!, /__u/bytesbtrees.substack.com/w_1456, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_auto, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe174f0b2-4bf4-443a-8877-6f28423ed566_875x579.gif 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>The animated diagram shows a clean example:</p><ol><li><p>T2 begins and updates <code>balance = 900</code> (was 1,000). T2 has not committed.</p></li><li><p>T1 reads balance &#8212; sees 900.</p></li><li><p>T2 rolls back. Balance is officially 1,000 again.</p></li><li><p>T1 proceeds having operated on a value that was never committed.</p></li></ol><p>The damage depends on what T1 did with that read. If it just displayed balance = 900 and the user immediately refreshed, the worst outcome is mild confusion. If T1 used the dirty value to calculate a transfer amount, approve a loan, or write an audit record, the consequences compound.</p><p><strong>The fix:</strong> READ COMMITTED. At this level, a transaction can only read rows where the writing transaction has committed. T1&#8217;s read of T2&#8217;s row will either wait for T2 to commit or rollback, or will see the pre-T2 version &#8212; never the dirty intermediate state.</p><p><strong>Postgres behaviour:</strong> Postgres never allows dirty reads &#8212; even at READ UNCOMMITTED, Postgres internally behaves as READ COMMITTED. This is standards-compliant: the SQL standard says READ UNCOMMITTED <em>may</em> allow dirty reads, not that it must. Postgres chose to eliminate dirty reads at all levels.</p><div><hr></div><h2>Anomaly #2: The non-repeatable read</h2><p>A non-repeatable read occurs when T1 reads the same row twice within a single transaction and gets different values &#8212; because T2 updated and committed that row between T1&#8217;s two reads.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="/__u/substackcdn.com/image/fetch/$s_!eJ15!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffb0fb4c7-a6ee-4fc1-b596-1bf937de3c1f_875x579.gif" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="/__u/substackcdn.com/image/fetch/$s_!eJ15!, /__u/bytesbtrees.substack.com/w_424, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_webp, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffb0fb4c7-a6ee-4fc1-b596-1bf937de3c1f_875x579.gif 424w, /__u/substackcdn.com/image/fetch/$s_!eJ15!, /__u/bytesbtrees.substack.com/w_848, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_webp, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffb0fb4c7-a6ee-4fc1-b596-1bf937de3c1f_875x579.gif 848w, /__u/substackcdn.com/image/fetch/$s_!eJ15!, /__u/bytesbtrees.substack.com/w_1272, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_webp, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffb0fb4c7-a6ee-4fc1-b596-1bf937de3c1f_875x579.gif 1272w, /__u/substackcdn.com/image/fetch/$s_!eJ15!, /__u/bytesbtrees.substack.com/w_1456, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_webp, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffb0fb4c7-a6ee-4fc1-b596-1bf937de3c1f_875x579.gif 1456w" sizes="100vw"><img src="/__u/substackcdn.com/image/fetch/$s_!eJ15!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffb0fb4c7-a6ee-4fc1-b596-1bf937de3c1f_875x579.gif" width="875" height="579" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/fb0fb4c7-a6ee-4fc1-b596-1bf937de3c1f_875x579.gif&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:579,&quot;width&quot;:875,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:168322,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/gif&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://bytesbtrees.substack.com/i/201705910?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffb0fb4c7-a6ee-4fc1-b596-1bf937de3c1f_875x579.gif&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="/__u/substackcdn.com/image/fetch/$s_!eJ15!, /__u/bytesbtrees.substack.com/w_424, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_auto, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffb0fb4c7-a6ee-4fc1-b596-1bf937de3c1f_875x579.gif 424w, /__u/substackcdn.com/image/fetch/$s_!eJ15!, /__u/bytesbtrees.substack.com/w_848, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_auto, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffb0fb4c7-a6ee-4fc1-b596-1bf937de3c1f_875x579.gif 848w, /__u/substackcdn.com/image/fetch/$s_!eJ15!, /__u/bytesbtrees.substack.com/w_1272, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_auto, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffb0fb4c7-a6ee-4fc1-b596-1bf937de3c1f_875x579.gif 1272w, /__u/substackcdn.com/image/fetch/$s_!eJ15!, /__u/bytesbtrees.substack.com/w_1456, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_auto, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffb0fb4c7-a6ee-4fc1-b596-1bf937de3c1f_875x579.gif 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>The diagram shows a reporting transaction:</p><ol><li><p>T1 begins and reads <code>balance = 1,000</code> for account 42.</p></li><li><p>T2 updates balance to 800 and commits.</p></li><li><p>T1 reads balance again &#8212; sees 800.</p></li></ol><p>T1 is internally inconsistent. If this is a financial report, it captured account 42 at two different times. The report&#8217;s totals are wrong even though no transaction committed invalid data.</p><p>This is surprisingly common in real applications. An ORM that lazy-loads relationships can hit this: the same entity is fetched twice in the same request (once directly, once via a join), and the second fetch reflects an update that happened between the two loads.</p><p><strong>The fix:</strong> REPEATABLE READ. At this level, T1&#8217;s snapshot is taken at the start of the transaction and held fixed. Every read within T1 sees the same version of every row &#8212; the version that existed when T1 began &#8212; regardless of commits that happen during T1&#8217;s lifetime.</p><p><strong>Postgres implementation:</strong> Postgres implements REPEATABLE READ using MVCC snapshot isolation. When T1 begins, Postgres records the current transaction ID as T1&#8217;s snapshot boundary. Any row modified by a transaction with a higher ID (i.e., committed after T1 began) is invisible to T1. The old row version is kept alive in the heap specifically so T1 can read it. This is why VACUUM must avoid removing row versions that active transactions might still need &#8212; long transactions prevent cleanup of old versions and cause table bloat.</p><div><hr></div><h2>Anomaly #3: The phantom read</h2><p>A phantom read is subtler than a non-repeatable read. Instead of the same row returning different values, a query returns a different set of rows when run twice.</p><p>The canonical example:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;sql&quot;,&quot;nodeId&quot;:&quot;59a6b674-2cd0-4005-a425-c5fed98184e3&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-sql">-- T1: find all accounts with balance &gt; 500
SELECT * FROM accounts WHERE balance &gt; 500;
-- Returns: {Alice: 1000, Carol: 750}

-- T2: INSERT INTO accounts VALUES ('Dave', 600); COMMIT;

-- T1: same query again
SELECT * FROM accounts WHERE balance &gt; 500;
-- Returns: {Alice: 1000, Carol: 750, Dave: 600}  &#8592; phantom!</code></pre></div><p>T1 ran the same query twice. The second result set has a row that didn&#8217;t exist in the first. Dave is the phantom &#8212; a row that appeared from nowhere, inserted by a committed concurrent transaction.</p><p>REPEATABLE READ prevents the non-repeatable read anomaly (re-reading the same row), but in many databases it does <strong>not</strong> prevent phantom reads &#8212; because the newly inserted row was never read by T1 before, so there&#8217;s no &#8220;previously seen version&#8221; to lock it to.</p><p><strong>Postgres is unusual here:</strong> Postgres&#8217;s REPEATABLE READ implementation actually prevents phantom reads too, because Postgres uses snapshot isolation rather than traditional range locking. T1&#8217;s snapshot was taken before Dave&#8217;s INSERT &#8212; Dave&#8217;s row doesn&#8217;t exist in T1&#8217;s snapshot, and T1 will never see it, even on a re-query. This is stricter than the SQL standard requires for REPEATABLE READ and is one reason Postgres&#8217;s REPEATABLE READ is considered particularly strong.</p><p><strong>Traditional databases (MySQL InnoDB):</strong> MySQL prevents phantom reads at REPEATABLE READ using <strong>next-key locks</strong> &#8212; range locks that prevent other transactions from inserting into the range covered by a query. This is correct but more restrictive than Postgres&#8217;s approach, because it locks not just the rows you&#8217;ve read but the gaps between them.</p><div><hr></div><h2>Anomaly #4: Write skew &#8212; the hardest one</h2><p>This is the anomaly that most engineers haven&#8217;t encountered by name, even when they&#8217;ve been bitten by it. It doesn&#8217;t involve reading stale data or phantom rows. Both transactions read correct, committed, up-to-date data. Both transactions commit successfully. And yet the result is impossible if either transaction had run alone.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="/__u/substackcdn.com/image/fetch/$s_!vaJE!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb24b9832-f13d-4e00-8ace-9e1f11fa711d_887x619.gif" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="/__u/substackcdn.com/image/fetch/$s_!vaJE!, /__u/bytesbtrees.substack.com/w_424, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_webp, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb24b9832-f13d-4e00-8ace-9e1f11fa711d_887x619.gif 424w, /__u/substackcdn.com/image/fetch/$s_!vaJE!, /__u/bytesbtrees.substack.com/w_848, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_webp, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb24b9832-f13d-4e00-8ace-9e1f11fa711d_887x619.gif 848w, /__u/substackcdn.com/image/fetch/$s_!vaJE!, /__u/bytesbtrees.substack.com/w_1272, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_webp, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb24b9832-f13d-4e00-8ace-9e1f11fa711d_887x619.gif 1272w, /__u/substackcdn.com/image/fetch/$s_!vaJE!, /__u/bytesbtrees.substack.com/w_1456, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_webp, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb24b9832-f13d-4e00-8ace-9e1f11fa711d_887x619.gif 1456w" sizes="100vw"><img src="/__u/substackcdn.com/image/fetch/$s_!vaJE!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb24b9832-f13d-4e00-8ace-9e1f11fa711d_887x619.gif" width="887" height="619" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/b24b9832-f13d-4e00-8ace-9e1f11fa711d_887x619.gif&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:619,&quot;width&quot;:887,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:182954,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/gif&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://bytesbtrees.substack.com/i/201705910?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb24b9832-f13d-4e00-8ace-9e1f11fa711d_887x619.gif&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="/__u/substackcdn.com/image/fetch/$s_!vaJE!, /__u/bytesbtrees.substack.com/w_424, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_auto, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb24b9832-f13d-4e00-8ace-9e1f11fa711d_887x619.gif 424w, /__u/substackcdn.com/image/fetch/$s_!vaJE!, /__u/bytesbtrees.substack.com/w_848, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_auto, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb24b9832-f13d-4e00-8ace-9e1f11fa711d_887x619.gif 848w, /__u/substackcdn.com/image/fetch/$s_!vaJE!, /__u/bytesbtrees.substack.com/w_1272, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_auto, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb24b9832-f13d-4e00-8ace-9e1f11fa711d_887x619.gif 1272w, /__u/substackcdn.com/image/fetch/$s_!vaJE!, /__u/bytesbtrees.substack.com/w_1456, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_auto, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb24b9832-f13d-4e00-8ace-9e1f11fa711d_887x619.gif 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>The animated diagram shows the hospital on-call scenario:</p><p><strong>Business rule:</strong> At least one doctor must be on call at all times.</p><p><strong>State:</strong> Alice and Bob are both on call (count = 2).</p><p><strong>T1 (Alice&#8217;s client):</strong> Reads count of on-call doctors &#8594; 2. Since count &#8805; 2, safe to go off call. Deletes Alice from on-call table. Commits.</p><p><strong>T2 (Bob&#8217;s client):</strong> Reads count of on-call doctors &#8594; 2. Since count &#8805; 2, safe to go off call. Deletes Bob from on-call table. Commits.</p><p><strong>Result:</strong> Zero doctors on call. Business rule violated. Both transactions read correct data. Both committed without error. No dirty reads. No phantom rows. And yet the combined result is wrong.</p><p>The key insight is timing: both T1 and T2 read the count before either deletion was committed. Each saw 2 and independently concluded it was safe to proceed. Neither could see what the other was about to do.</p><p>If the transactions had run serially &#8212; Alice first, then Bob &#8212; Bob&#8217;s read would have seen count = 1 and would have blocked or rejected the deletion. The serial result is safe. The concurrent result is not. This is the definition of a serialisation anomaly: a result that&#8217;s impossible in any serial ordering.</p><p><strong>Other write skew examples that appear in real systems:</strong></p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;sql&quot;,&quot;nodeId&quot;:&quot;623108f0-08bd-4494-b252-6327594fab14&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-sql">-- Meeting room double-booking:
-- T1: SELECT count(*) FROM bookings WHERE room=5 AND time='09:00' -- returns 0
-- T2: SELECT count(*) FROM bookings WHERE room=5 AND time='09:00' -- returns 0
-- T1: INSERT INTO bookings(room, time, user) VALUES (5, '09:00', 'Alice')
-- T2: INSERT INTO bookings(room, time, user) VALUES (5, '09:00', 'Bob')
-- Both commit &#8594; room 5 double-booked at 09:00

-- Username uniqueness with application-level checks:
-- T1: SELECT count(*) FROM users WHERE username='alice' -- returns 0
-- T2: SELECT count(*) FROM users WHERE username='alice' -- returns 0
-- T1: INSERT INTO users(username) VALUES ('alice')
-- T2: INSERT INTO users(username) VALUES ('alice')
-- Both commit &#8594; two users named 'alice' (if no UNIQUE constraint)</code></pre></div><p>The second example is exactly why UNIQUE constraints must live in the database (CODD Rule 10), not in application code. A database UNIQUE constraint is enforced atomically. Application-level uniqueness checks are vulnerable to write skew.</p><p><strong>The fix:</strong> SERIALISABLE isolation. At this level, the database detects that T1 and T2 have a dependency cycle (T1&#8217;s decision to proceed depended on data that T2 was about to modify, and vice versa) and aborts one of them. The application retries the aborted transaction, which now sees the committed result of the other and makes the correct decision.</p><div><hr></div><h2>How Postgres implements SERIALISABLE: SSI</h2><p>Postgres uses an algorithm called <strong>Serialisable Snapshot Isolation (SSI)</strong>, introduced in PostgreSQL 9.1. SSI is remarkable because it provides full serialisability without the coarse locking that traditional serialisable implementations require.</p><p>Traditional serialisable isolation works by locking everything that&#8217;s read, preventing concurrent writers from touching it. This is safe but severely limits concurrency &#8212; a long-running read-only transaction blocks all writers on the rows it has read.</p><p>SSI works differently. It lets transactions run concurrently using Postgres&#8217;s normal MVCC snapshot isolation, but it tracks <strong>read-write dependencies</strong> between transactions. When it detects a cycle of dependencies that would make the execution non-serialisable, it aborts one of the involved transactions (the &#8220;pivot&#8221; transaction).</p><p>The dependency tracking uses <strong>SIREAD locks</strong> &#8212; a new kind of lock that doesn&#8217;t block other transactions but records &#8220;transaction X has read this row.&#8221; These locks are much lighter than write locks and allow concurrent reads without blocking.</p><p><strong>What SSI tracks:</strong></p><pre><code><code>If T1 reads a row that T2 later writes (rw-antidependency T1&#8594;T2)
And T2 reads a row that T1 later writes (rw-antidependency T2&#8594;T1)
&#8594; This is a dangerous cycle &#8594; abort one transaction</code></code></pre><p>In the hospital example:</p><ul><li><p>T1 reads the on-call count (will be modified by T2&#8217;s DELETE)</p></li><li><p>T2 reads the on-call count (will be modified by T1&#8217;s DELETE)</p></li><li><p>Both have read-write antidependencies on each other</p></li><li><p>SSI detects the cycle and aborts one</p></li><li><p>The other completes successfully</p></li><li><p>The retried transaction sees count = 1 and correctly refuses to go off call</p></li></ul><p><strong>The cost of SSI:</strong> SIREAD locks consume memory. A long-running SERIALISABLE transaction that reads millions of rows can accumulate millions of SIREAD locks. Postgres manages this with lock summarisation &#8212; consolidating many per-row locks into a per-page or per-relation lock &#8212; which may produce false positives (unnecessary aborts) but never false negatives (missed violations).</p><p>The <code>max_pred_locks_per_transaction</code> parameter controls the lock table size. If SERIALISABLE transactions are aborting more than expected, check whether lock summarisation is causing overly aggressive conflict detection.</p><div><hr></div><h2>Choosing the right isolation level</h2><p>The practical question: what level should your application use?</p><p><strong>READ COMMITTED (Postgres default)</strong> &#8212; Correct for most OLTP workloads where each transaction is short, operates on a small number of rows, and doesn&#8217;t have read-modify-write logic that depends on other concurrent transactions. The reporting anomaly at the top of this issue is the sign you need to go higher.</p><p><strong>REPEATABLE READ</strong> &#8212; Correct for long-running reads that must see a consistent snapshot: financial reports, data exports, analytics queries running against OLTP data. Also correct when you have explicit optimistic locking &#8212; read a row, check a version column, update only if the version hasn&#8217;t changed. In Postgres, REPEATABLE READ is surprisingly strong (it also prevents phantoms) and rarely causes performance issues.</p><p><strong>SERIALISABLE</strong> &#8212; Necessary when your application has read-then-write logic that depends on the reads being stable: the on-call problem, meeting room bookings, maintaining invariants across multiple rows. If your application implements business rules by checking a condition before writing, and that condition involves rows that other transactions might also be checking and writing, you need SERIALISABLE.</p><p>The common mistake is using READ COMMITTED with application-level uniqueness or business-rule checks. That&#8217;s vulnerable to write skew. The correct fix is either:</p><ol><li><p>Move the constraint into the database (UNIQUE, CHECK, FOREIGN KEY constraints are write-skew-safe)</p></li><li><p>Use SERIALISABLE isolation</p></li><li><p>Use explicit locking (<code>SELECT ... FOR UPDATE</code>) to prevent concurrent modification of the rows the decision depends on</p></li></ol><p><strong>Per-transaction isolation in Postgres:</strong></p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;sql&quot;,&quot;nodeId&quot;:&quot;9aac2c8a-9e14-40e4-a89e-be4983407993&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-sql">-- Set isolation for a single transaction:
BEGIN TRANSACTION ISOLATION LEVEL SERIALIZABLE;

-- Or set the session default:
SET SESSION CHARACTERISTICS AS TRANSACTION ISOLATION LEVEL REPEATABLE READ;

-- Or set for the whole database:
ALTER DATABASE mydb SET default_transaction_isolation = 'serializable';</code></pre></div><div><hr></div><h2>The phantom that Postgres prevents but the standard doesn&#8217;t require</h2><p>One specific clarification worth making explicit before the next issue:</p><p>The SQL standard says REPEATABLE READ <em>may</em> allow phantom reads. It doesn&#8217;t say it must. Postgres&#8217;s REPEATABLE READ prevents phantoms because its snapshot isolation implementation is stricter than the standard&#8217;s minimum.</p><p>This creates a subtle portability issue: if you write an application that relies on REPEATABLE READ preventing phantoms, it will work correctly on Postgres but may exhibit phantom reads on MySQL InnoDB (which uses range locking rather than snapshot isolation for REPEATABLE READ). MySQL&#8217;s range locking does prevent phantoms through a different mechanism, so in practice both databases are safe &#8212; but the <em>reason</em> each is safe differs.</p><p>When writing cross-database code or switching databases, verify the isolation semantics, not just the level name.</p><div class="callout-block" data-callout="true"><p>Vault assets for this issue: isolation_demo.py &#8212; accessible in GitHub vault</p></div>
      <p>
          <a href="/__u/bytesbtrees.substack.com/p/issue-009-isolation-levels-visualised">
              Read more
          </a>
      </p>
   ]]></content:encoded></item><item><title><![CDATA[Issue #008 — Durability & fsync]]></title><description><![CDATA[When Postgres calls fsync() after writing a WAL record, it trusts the storage stack to confirm that the data is physically durable. That trust is usually warranted.]]></description><link>https://bytesbtrees.substack.com/p/durability-what-fsync-actually-guarantees</link><guid isPermaLink="false">https://bytesbtrees.substack.com/p/durability-what-fsync-actually-guarantees</guid><dc:creator><![CDATA[System Design Roadmap]]></dc:creator><pubDate>Sun, 07 Jun 2026 02:30:32 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!MbAD!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F33ef57dc-6be9-4899-8fbc-61605f890592_919x579.gif" length="0" type="image/jpeg"/><content:encoded><![CDATA[<blockquote><p>In March 2018, a widely-read blog post from PostgreSQL contributor Tomas Vondra described something alarming: on Linux, if a page is evicted from the OS page cache while dirty, and then a subsequent write to that page fails, Postgres might not learn about the failure. The page might be silently discarded. And the next fsync() on that file might return success &#8212; because from the kernel&#8217;s perspective, there was nothing left to sync.</p></blockquote><blockquote><p>The PostgreSQL project spent months investigating. The conclusion was that Postgres, like most database software, had been making an assumption about fsync() semantics that was only partially true on Linux. A design change was made in PostgreSQL 12. But the investigation surfaced something more general and more unsettling: <strong>fsync() returning success is not the same as data being physically durable</strong>. The gap between those two things is where data loss lives.</p></blockquote><p>This issue maps that gap precisely. We&#8217;ll cover the five levels of the durability stack between your application&#8217;s <code>write()</code> call and actual non-volatile storage, the three specific failure modes that cause fsync() to lie, and how to configure your database server to close each one.</p><div><hr></div><h2>What fsync() actually guarantees</h2><p>Let&#8217;s start from the specification. The POSIX definition of <code>fsync()</code> states:</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://bytesbtrees.substack.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en-gb&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Thanks for reading Bytes &amp; B-Tree's! Subscribe for free to receive new posts and support my work.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><blockquote><p><em>The fsync() function shall request that all data for the open file descriptor named by fildes is to be transferred to the storage device associated with the file described by fildes.</em></p></blockquote><p>&#8220;Transferred to the storage device associated with the file.&#8221; Not &#8220;written to non-volatile cells.&#8221; Not &#8220;protected from power failure.&#8221; Transferred to the storage device &#8212; meaning it has left the OS&#8217;s control and been handed to the device.</p><p>What the device does with it is not part of the POSIX specification.</p><p>This is the gap. fsync() guarantees that data has left the OS page cache and been sent to the storage device. Whether the storage device has written it to actual non-volatile storage &#8212; NAND flash cells, magnetic platter domains &#8212; depends on the device&#8217;s internal behaviour, which varies by hardware and configuration.</p><div><hr></div><h2>The durability ladder</h2><p>Before the three lies, we need the full picture of where data can live between a <code>write()</code> call and non-volatile storage.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="/__u/substackcdn.com/image/fetch/$s_!MbAD!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F33ef57dc-6be9-4899-8fbc-61605f890592_919x579.gif" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="/__u/substackcdn.com/image/fetch/$s_!MbAD!, /__u/bytesbtrees.substack.com/w_424, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_webp, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F33ef57dc-6be9-4899-8fbc-61605f890592_919x579.gif 424w, /__u/substackcdn.com/image/fetch/$s_!MbAD!, /__u/bytesbtrees.substack.com/w_848, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_webp, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F33ef57dc-6be9-4899-8fbc-61605f890592_919x579.gif 848w, /__u/substackcdn.com/image/fetch/$s_!MbAD!, /__u/bytesbtrees.substack.com/w_1272, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_webp, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F33ef57dc-6be9-4899-8fbc-61605f890592_919x579.gif 1272w, /__u/substackcdn.com/image/fetch/$s_!MbAD!, /__u/bytesbtrees.substack.com/w_1456, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_webp, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F33ef57dc-6be9-4899-8fbc-61605f890592_919x579.gif 1456w" sizes="100vw"><img src="/__u/substackcdn.com/image/fetch/$s_!MbAD!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F33ef57dc-6be9-4899-8fbc-61605f890592_919x579.gif" width="919" height="579" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/33ef57dc-6be9-4899-8fbc-61605f890592_919x579.gif&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:579,&quot;width&quot;:919,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:141829,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/gif&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://bytesbtrees.substack.com/i/196646206?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F33ef57dc-6be9-4899-8fbc-61605f890592_919x579.gif&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="/__u/substackcdn.com/image/fetch/$s_!MbAD!, /__u/bytesbtrees.substack.com/w_424, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_auto, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F33ef57dc-6be9-4899-8fbc-61605f890592_919x579.gif 424w, /__u/substackcdn.com/image/fetch/$s_!MbAD!, /__u/bytesbtrees.substack.com/w_848, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_auto, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F33ef57dc-6be9-4899-8fbc-61605f890592_919x579.gif 848w, /__u/substackcdn.com/image/fetch/$s_!MbAD!, /__u/bytesbtrees.substack.com/w_1272, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_auto, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F33ef57dc-6be9-4899-8fbc-61605f890592_919x579.gif 1272w, /__u/substackcdn.com/image/fetch/$s_!MbAD!, /__u/bytesbtrees.substack.com/w_1456, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_auto, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F33ef57dc-6be9-4899-8fbc-61605f890592_919x579.gif 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>The animated diagram builds the five levels from most volatile to most durable, with a &#8220;power failure kills everything above this line&#8221; marker placed after level four.</p><p><strong>Level 1 &#8212; Application memory:</strong> Your application&#8217;s heap. Lost on process crash, OOM kill, or SIGKILL. Data that has been <code>write()</code>&#8216;d has left this level.</p><p><strong>Level 2 &#8212; OS page cache:</strong> Kernel-managed RAM. Survives a process crash but not a kernel panic or power failure. <code>write()</code> returns success when data reaches this level. The OS will flush this to disk eventually, but &#8220;eventually&#8221; is not &#8220;now.&#8221;</p><p><strong>Level 3 &#8212; Drive write cache (volatile DRAM):</strong> Many storage devices &#8212; HDDs and consumer SSDs &#8212; have a small internal RAM buffer where they stage writes before actually committing them to persistent storage. This speeds up write throughput dramatically (the drive can acknowledge the write instantly while scheduling the actual physical write for later). If power is cut while data is in this buffer, it&#8217;s gone.</p><p><strong>Level 4 &#8212; Drive write cache (capacitor-backed or flash-backed):</strong> Enterprise SSDs and RAID controllers with a Battery Backup Unit (BBU) or supercapacitor-protected write cache store data in a non-volatile buffer that can survive a power outage long enough to flush to persistent storage on the next power cycle. This is &#8220;mostly safe&#8221; &#8212; but the BBU can degrade, the capacitor can fail, and the controller can crash.</p><p><strong>Level 5 &#8212; Non-volatile storage cells:</strong> Data physically written to NAND flash cells or magnetic platter domains. Survives power loss. This is true durability. Getting data here is the entire point of the fsync() call.</p><p>The power failure boundary sits between levels 3 and 5 depending on your hardware. fsync() guarantees that data has left level 2. Whether it reaches level 5 before power can cut it is what the three lies are about.</p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://bytesbtrees.substack.com/subscribe&quot;,&quot;text&quot;:&quot;Get Access to GitHub Link&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="/__u/bytesbtrees.substack.com/subscribe"><span>Get Access to GitHub Link</span></a></p><div><hr></div><h2>Lie #1: Disk write cache enabled without power-loss protection</h2><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="/__u/substackcdn.com/image/fetch/$s_!yJaL!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa85e0b0f-d0be-4878-a6e9-38cfab4ee192_831x619.gif" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="/__u/substackcdn.com/image/fetch/$s_!yJaL!, /__u/bytesbtrees.substack.com/w_424, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_webp, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa85e0b0f-d0be-4878-a6e9-38cfab4ee192_831x619.gif 424w, /__u/substackcdn.com/image/fetch/$s_!yJaL!, /__u/bytesbtrees.substack.com/w_848, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_webp, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa85e0b0f-d0be-4878-a6e9-38cfab4ee192_831x619.gif 848w, /__u/substackcdn.com/image/fetch/$s_!yJaL!, /__u/bytesbtrees.substack.com/w_1272, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_webp, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa85e0b0f-d0be-4878-a6e9-38cfab4ee192_831x619.gif 1272w, /__u/substackcdn.com/image/fetch/$s_!yJaL!, /__u/bytesbtrees.substack.com/w_1456, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_webp, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa85e0b0f-d0be-4878-a6e9-38cfab4ee192_831x619.gif 1456w" sizes="100vw"><img src="/__u/substackcdn.com/image/fetch/$s_!yJaL!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa85e0b0f-d0be-4878-a6e9-38cfab4ee192_831x619.gif" width="831" height="619" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/a85e0b0f-d0be-4878-a6e9-38cfab4ee192_831x619.gif&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:619,&quot;width&quot;:831,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:214310,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/gif&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://bytesbtrees.substack.com/i/196646206?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa85e0b0f-d0be-4878-a6e9-38cfab4ee192_831x619.gif&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="/__u/substackcdn.com/image/fetch/$s_!yJaL!, /__u/bytesbtrees.substack.com/w_424, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_auto, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa85e0b0f-d0be-4878-a6e9-38cfab4ee192_831x619.gif 424w, /__u/substackcdn.com/image/fetch/$s_!yJaL!, /__u/bytesbtrees.substack.com/w_848, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_auto, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa85e0b0f-d0be-4878-a6e9-38cfab4ee192_831x619.gif 848w, /__u/substackcdn.com/image/fetch/$s_!yJaL!, /__u/bytesbtrees.substack.com/w_1272, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_auto, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa85e0b0f-d0be-4878-a6e9-38cfab4ee192_831x619.gif 1272w, /__u/substackcdn.com/image/fetch/$s_!yJaL!, /__u/bytesbtrees.substack.com/w_1456, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_auto, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa85e0b0f-d0be-4878-a6e9-38cfab4ee192_831x619.gif 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>Consumer-grade HDDs and SSDs ship with write caching enabled by default. When the OS calls fsync() and the device acknowledges it, the device may be reporting that the data has reached its own internal RAM buffer &#8212; not that it has been written to persistent cells.</p><p>The acknowledgement is technically truthful at one level: the data has left the OS page cache and is in the drive&#8217;s control. But it is not in non-volatile storage. A sudden power cut will discard it.</p><p><strong>How to verify this is happening on your system:</strong></p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;bash&quot;,&quot;nodeId&quot;:&quot;af26c93a-94a8-4dd4-b125-f667edd6f772&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-bash"># Check if write cache is enabled on your drive:
sudo hdparm -I /dev/sda | grep -i "write cache"
# Output: * Write cache

# Check if it's actually enabled:
sudo hdparm -W /dev/sda
# Output: write-caching = 1 (on) -- this is the dangerous state</code></pre></div><p><strong>How to fix it:</strong></p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;bash&quot;,&quot;nodeId&quot;:&quot;2127719e-a305-492a-80a7-801c6785d52e&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-bash"># Disable the write cache (survives reboots only with hdparm.conf):
sudo hdparm -W 0 /dev/sda

# Verify:
sudo hdparm -W /dev/sda
# write-caching = 0 (off) -- fsync() now waits for actual cell writes</code></pre></div><p>The performance cost is real. Disabling the write cache means every fsync() blocks until the storage device has physically written the data. On a consumer SSD, the difference is often 2&#8211;5x slower for write-heavy workloads. On an HDD, the difference can be 10&#8211;20x.</p><p>This is why enterprise SSDs matter for database servers: they have capacitor-backed write caches that are non-volatile. The write cache can remain enabled (for performance), and the data is still safe if power is cut &#8212; the capacitor provides enough energy to complete the flush.</p><p><strong>For virtual machines and cloud instances:</strong> AWS EBS volumes, GCP Persistent Disks, and Azure Managed Disks all provide durable write semantics &#8212; they handle the write cache properly at the storage layer. You don&#8217;t need to disable write caching on cloud block storage. But you should verify the documentation of any storage provider before assuming this.</p><div><hr></div><h2>Lie #2: RAID controller with degraded or failed BBU</h2><p>RAID controllers sit between the OS and the physical drives. When a write comes in, a RAID controller typically acknowledges it to the OS once the data is in the controller&#8217;s own cache &#8212; not once it&#8217;s been distributed to the drives. This dramatically improves write performance (the application isn&#8217;t waiting for multiple drives to write) and provides some protection (the controller can retry failed drive writes from its cache).</p><p>The battery backup unit (BBU) &#8212; or supercapacitor-based equivalent &#8212; is what makes this cache non-volatile. If the BBU is healthy and the power cuts, the controller keeps power long enough to flush its cache to the drives.</p><p>The problem: BBUs degrade. A RAID controller battery rated for three years may lose significant capacity by year two. When the BBU is degraded, the controller&#8217;s firmware detects this and switches to &#8220;write-through&#8221; mode &#8212; acknowledging writes only after they&#8217;ve been committed to the drives, not the cache. This is the correct safe behaviour, but it&#8217;s also 3&#8211;5x slower, and in some firmware implementations, it happens silently without alerting the DBA.</p><p>Even worse: some older RAID controllers, when the BBU is charging after a replacement, temporarily switch to write-through mode. A DBA replaces a degraded battery, and for the 24&#8211;48 hours the new battery charges, database write performance suddenly drops dramatically.</p><p><strong>How to monitor BBU health:</strong></p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;bash&quot;,&quot;nodeId&quot;:&quot;7003665b-e958-40fb-8ecf-d62cfee2b73d&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-bash"># For MegaRAID controllers:
sudo megacli -AdpBbuCmd -GetBbuStatus -aALL | grep -E "Battery State|Relative Charge"

# For HPE Smart Array:
sudo ssacli ctrl all show status | grep -A5 "Cache"

# In your monitoring stack, alert on:
# - BBU charge below 85%
# - Write-through mode enabled
# - BBU replacement needed</code></pre></div><p><strong>What to do when BBU fails:</strong> Either replace it immediately and accept temporary performance degradation in write-through mode, or configure the database with <code>synchronous_commit = off</code> temporarily to reduce fsync frequency &#8212; acknowledging the increased (small) data loss window during the transition.</p><div><hr></div><h2>Lie #3: The Linux page cache eviction bug (and its relatives)</h2><p>This is the most subtle lie, discovered in the Postgres investigation mentioned at the top of this issue.</p><p>On Linux, the OS page cache is not just a write buffer &#8212; it also caches file data for reads. When a write comes in for a page that&#8217;s already in the page cache, the write updates the cached page. When memory pressure is high, the kernel may evict dirty pages from the page cache to reclaim memory &#8212; before they&#8217;ve been written to disk.</p><p>Under normal operation, this is fine. The kernel tracks dirty pages and ensures they&#8217;re written to disk (via pdflush/kworker) before eviction, or marks them as needing a write and tracks them.</p><p>But here is the precise failure scenario that the Postgres investigation exposed:</p><ol><li><p>Postgres writes dirty page to OS page cache (dirty)</p></li><li><p>Memory pressure causes the kernel to decide to evict this page</p></li><li><p>The kernel attempts to write the page to disk as part of eviction</p></li><li><p>The disk write fails (e.g., transient I/O error, storage temporarily unavailable)</p></li><li><p>On some Linux kernel versions: the page is evicted from the cache. The in-memory dirty state is discarded. The error is recorded in the page state &#8212; but not necessarily surfaced to any process that subsequently calls <code>fsync()</code> on that file</p></li><li><p>Postgres calls <code>fsync()</code> on the WAL file or data file</p></li><li><p>fsync() returns success &#8212; because from the kernel&#8217;s current view, there are no dirty pages for this file to sync</p></li><li><p>Postgres has been told the data is durable. The data was never written.</p></li></ol><p>This was a kernel-level behaviour that PostgreSQL&#8217;s WAL code had implicitly depended on not occurring. PostgreSQL 12 added explicit checks: Postgres now marks files with pending writes and will re-issue writes if it detects the kernel lost track of them. The kernel behaviour was also addressed in Linux kernel patches.</p><p><strong>Why this matters beyond the specific bug:</strong> It illustrates that the contract between database software and the OS storage stack is not as airtight as engineers assume. Every layer in the durability stack makes assumptions about the layers below it. When any assumption breaks &#8212; hardware caches data that it shouldn&#8217;t, kernels lose track of dirty pages, virtual storage layers introduce unexpected buffering &#8212; data can be silently lost even when every API call returned success.</p><p><strong>The defence:</strong> Use <code>O_DIRECT</code> (bypasses the page cache entirely), monitor disk I/O errors at the OS level, and keep your Linux kernel updated. For critical data, verify that your storage stack is end-to-end correct using tools like Jepsen (which we&#8217;ll cover in issue #143).</p><div class="callout-block" data-callout="true"><p>Vault assets for this issue: durability_checklist.md + verify_write_cache.sh &#8212; accessible in GitHub vault</p></div>
      <p>
          <a href="/__u/bytesbtrees.substack.com/p/durability-what-fsync-actually-guarantees">
              Read more
          </a>
      </p>
   ]]></content:encoded></item><item><title><![CDATA[Issue #007 — Atomicity at the Byte Level]]></title><description><![CDATA[&#8220;Atomic&#8221; comes from the Greek word for indivisible.]]></description><link>https://bytesbtrees.substack.com/p/atomicity-at-the-byte-level-what</link><guid isPermaLink="false">https://bytesbtrees.substack.com/p/atomicity-at-the-byte-level-what</guid><dc:creator><![CDATA[System Design Roadmap]]></dc:creator><pubDate>Sun, 31 May 2026 02:30:58 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!-Bwo!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd02cd80b-201d-40ee-955e-a23785069916_912x560.gif" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><em>&#8220;Atomic&#8221; comes from the Greek word for indivisible. In physics, atoms turned out to be very much divisible &#8212; protons, neutrons, electrons, quarks. But in databases, atomicity holds. A transaction either commits completely or leaves no trace whatsoever. This issue explains exactly how that guarantee is built from write operations that are individually not atomic at all.</em></p><div><hr></div><p>Here is a fact that should immediately raise a question: a write() system call is not atomic. You can write 8KB to a file and have the system crash after the first 4KB have been persisted. The storage hardware itself is not atomic &#8212; it has no concept of &#8220;complete this set of writes or undo all of them.&#8221; And yet, when Postgres tells you a transaction committed, you can be absolutely certain that either all of its changes are reflected in the database or none of them are. Not most of them. Not the first three of five updates. All, or none.</p><p>How is a non-atomic property &#8212; atomicity &#8212; built from hardware primitives that aren&#8217;t atomic?</p><blockquote><p>The answer is one of the most elegant pieces of systems engineering in all of database design. It doesn&#8217;t require any hardware support beyond what we described in issue #001. It doesn&#8217;t require locking data pages. It doesn&#8217;t require any special hardware atomic instructions. It requires only the write-ahead log, two types of information stored in each log record, and a clear protocol for which writes happen in which order.</p></blockquote><p>This issue traces a transaction from BEGIN to COMMIT (and from BEGIN to ROLLBACK) at the byte level &#8212; every write, every fsync, every piece of state that must exist for the guarantee to hold.</p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://bytesbtrees.substack.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe now&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="/__u/bytesbtrees.substack.com/subscribe"><span>Subscribe now</span></a></p><div><hr></div><h2>The transaction state machine</h2><p>Before we go to bytes, we need the conceptual model. Every transaction moves through a precise sequence of states. The guarantee of atomicity is that <strong>no intermediate state is ever observable to another transaction</strong>.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="/__u/substackcdn.com/image/fetch/$s_!-Bwo!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd02cd80b-201d-40ee-955e-a23785069916_912x560.gif" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="/__u/substackcdn.com/image/fetch/$s_!-Bwo!, /__u/bytesbtrees.substack.com/w_424, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_webp, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd02cd80b-201d-40ee-955e-a23785069916_912x560.gif 424w, /__u/substackcdn.com/image/fetch/$s_!-Bwo!, /__u/bytesbtrees.substack.com/w_848, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_webp, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd02cd80b-201d-40ee-955e-a23785069916_912x560.gif 848w, /__u/substackcdn.com/image/fetch/$s_!-Bwo!, /__u/bytesbtrees.substack.com/w_1272, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_webp, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd02cd80b-201d-40ee-955e-a23785069916_912x560.gif 1272w, /__u/substackcdn.com/image/fetch/$s_!-Bwo!, /__u/bytesbtrees.substack.com/w_1456, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_webp, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd02cd80b-201d-40ee-955e-a23785069916_912x560.gif 1456w" sizes="100vw"><img src="/__u/substackcdn.com/image/fetch/$s_!-Bwo!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd02cd80b-201d-40ee-955e-a23785069916_912x560.gif" width="912" height="560" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/d02cd80b-201d-40ee-955e-a23785069916_912x560.gif&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:560,&quot;width&quot;:912,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:117379,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/gif&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://bytesbtrees.substack.com/i/196646003?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd02cd80b-201d-40ee-955e-a23785069916_912x560.gif&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="/__u/substackcdn.com/image/fetch/$s_!-Bwo!, /__u/bytesbtrees.substack.com/w_424, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_auto, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd02cd80b-201d-40ee-955e-a23785069916_912x560.gif 424w, /__u/substackcdn.com/image/fetch/$s_!-Bwo!, /__u/bytesbtrees.substack.com/w_848, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_auto, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd02cd80b-201d-40ee-955e-a23785069916_912x560.gif 848w, /__u/substackcdn.com/image/fetch/$s_!-Bwo!, /__u/bytesbtrees.substack.com/w_1272, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_auto, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd02cd80b-201d-40ee-955e-a23785069916_912x560.gif 1272w, /__u/substackcdn.com/image/fetch/$s_!-Bwo!, /__u/bytesbtrees.substack.com/w_1456, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_auto, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd02cd80b-201d-40ee-955e-a23785069916_912x560.gif 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>The animated diagram builds the state machine step by step:</p><p><strong>ACTIVE</strong> &#8212; The transaction is executing SQL statements. It&#8217;s reading and writing rows. Everything is happening in memory (shared_buffers) and in WAL buffers. Nothing is on disk yet. From the outside, none of these changes are visible.</p><p><strong>PARTIALLY COMMITTED</strong> &#8212; The transaction has executed its last statement. All WAL records for the transaction have been written to the WAL buffer in memory. The system is about to fsync.</p><p><strong>COMMITTED</strong> &#8212; The WAL has been fsynced to disk. The COMMIT record exists in durable storage. This is the point of no return. The transaction&#8217;s changes will survive any subsequent crash. And only now &#8212; not before &#8212; are the changes visible to other transactions.</p><p><strong>ABORTED</strong> &#8212; Either the application issued ROLLBACK, or an error occurred, or the fsync failed. The transaction is being unwound. No changes from this transaction should be visible to anyone, ever.</p><p><strong>DONE</strong> &#8212; Resources released, locks dropped, transaction ID freed for reuse.</p><p>The critical insight is the gap between PARTIALLY COMMITTED and COMMITTED. In that gap, the transaction has finished executing, but its changes are not yet durable and not yet visible. The fsync is what bridges the gap. Before the fsync, the transaction is in a liminal state &#8212; it&#8217;s done from the application&#8217;s perspective, but the database is still making it permanent.</p><p>The application blocks at COMMIT until the fsync completes and the state transition to COMMITTED is confirmed. This is why commit latency on an HDD is 10&#8211;30ms: the application is waiting for physical confirmation from the storage device.</p><div><hr></div><h2>What is actually written to the WAL</h2><p>The WAL is a sequential append-only file. Every change to every data page generates one or more WAL records. The records are written in strict order. Let&#8217;s look at exactly what&#8217;s inside one.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="/__u/substackcdn.com/image/fetch/$s_!YFG9!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8eb74917-c67a-4e61-86e4-7d28bf450503_874x599.gif" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="/__u/substackcdn.com/image/fetch/$s_!YFG9!, /__u/bytesbtrees.substack.com/w_424, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_webp, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8eb74917-c67a-4e61-86e4-7d28bf450503_874x599.gif 424w, /__u/substackcdn.com/image/fetch/$s_!YFG9!, /__u/bytesbtrees.substack.com/w_848, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_webp, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8eb74917-c67a-4e61-86e4-7d28bf450503_874x599.gif 848w, /__u/substackcdn.com/image/fetch/$s_!YFG9!, /__u/bytesbtrees.substack.com/w_1272, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_webp, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8eb74917-c67a-4e61-86e4-7d28bf450503_874x599.gif 1272w, /__u/substackcdn.com/image/fetch/$s_!YFG9!, /__u/bytesbtrees.substack.com/w_1456, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_webp, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8eb74917-c67a-4e61-86e4-7d28bf450503_874x599.gif 1456w" sizes="100vw"><img src="/__u/substackcdn.com/image/fetch/$s_!YFG9!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8eb74917-c67a-4e61-86e4-7d28bf450503_874x599.gif" width="874" height="599" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/8eb74917-c67a-4e61-86e4-7d28bf450503_874x599.gif&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:599,&quot;width&quot;:874,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:163329,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/gif&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://bytesbtrees.substack.com/i/196646003?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8eb74917-c67a-4e61-86e4-7d28bf450503_874x599.gif&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="/__u/substackcdn.com/image/fetch/$s_!YFG9!, /__u/bytesbtrees.substack.com/w_424, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_auto, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8eb74917-c67a-4e61-86e4-7d28bf450503_874x599.gif 424w, /__u/substackcdn.com/image/fetch/$s_!YFG9!, /__u/bytesbtrees.substack.com/w_848, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_auto, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8eb74917-c67a-4e61-86e4-7d28bf450503_874x599.gif 848w, /__u/substackcdn.com/image/fetch/$s_!YFG9!, /__u/bytesbtrees.substack.com/w_1272, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_auto, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8eb74917-c67a-4e61-86e4-7d28bf450503_874x599.gif 1272w, /__u/substackcdn.com/image/fetch/$s_!YFG9!, /__u/bytesbtrees.substack.com/w_1456, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_auto, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8eb74917-c67a-4e61-86e4-7d28bf450503_874x599.gif 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p><strong>LSN (Log Sequence Number)</strong> &#8212; 8 bytes. A monotonically increasing identifier for this position in the WAL stream. LSNs are how Postgres tracks &#8220;how far has this replica replicated?&#8221; and &#8220;from which point does recovery need to replay?&#8221; Every WAL record has a unique LSN. You can query the current WAL LSN in Postgres with SELECT pg_current_wal_lsn().</p><p><strong>Transaction ID (XID)</strong> &#8212; 4 bytes. Which transaction produced this record. Postgres uses 32-bit transaction IDs, which is why transaction ID wraparound (the famous &#8220;XID wraparound&#8221; issue) is a real operational concern &#8212; running out of 32-bit XIDs would mean the database could no longer distinguish old from new.</p><p><strong>Record type</strong> &#8212; 1 byte. HEAP_INSERT, HEAP_UPDATE, HEAP_DELETE, XLOG_XACT_COMMIT, XLOG_XACT_ABORT, and others. The record type tells the recovery process what operation to replay or undo.</p><p><strong>Relation OID and block number</strong> &#8212; Which table (by object identifier) and which 8KB page within that table was modified.</p><p><strong>Before-image (old tuple)</strong> &#8212; The complete state of the row before this change. Variable length. This is the undo information. If the transaction must be rolled back, Postgres applies this image to restore the row to its pre-transaction state.</p><p><strong>After-image (new tuple)</strong> &#8212; The complete state of the row after this change. Variable length. This is the redo information. If the system crashes after the WAL record is written but before the data page is written to disk, recovery replays this image to reconstruct the correct data page state.</p><p><strong>CRC</strong> &#8212; A 32-bit checksum over the entire record. On recovery, Postgres verifies the CRC before replaying any record. A corrupted WAL record stops recovery rather than applying corrupt data.</p><p>The presence of <strong>both</strong> before-image and after-image in the same record is the key to atomicity. The after-image enables redo (crash recovery forward). The before-image enables undo (rollback). Both directions of atomicity &#8212; completing and cancelling &#8212; are possible from a single log record.</p><div><hr></div><h2>The exact commit sequence: what happens in what order</h2><p>Let&#8217;s trace a specific transaction to its individual write operations. The transaction:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;sql&quot;,&quot;nodeId&quot;:&quot;f3f3c123-af60-4fc1-9165-cbc9e9977798&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-sql">BEGIN;

UPDATE accounts SET balance = balance - 100 WHERE id = 42;

UPDATE accounts SET balance = balance + 100 WHERE id = 99;

COMMIT;</code></pre></div><p>Here is every write, in order, with nothing omitted:</p><p><strong>Step 1 &#8212; BEGIN received.</strong> A transaction ID (XID) is allocated from shared memory. A BEGIN WAL record is written to the in-memory WAL buffer. The data pages for accounts 42 and 99 are loaded into shared_buffers from disk (if not already cached). Nothing has been written to disk.</p><p><strong>Step 2 &#8212; First UPDATE executes.</strong> Postgres reads the current tuple for account 42 from shared_buffers. It creates a new tuple in the same or a nearby page with balance = old_balance - 100. Both tuples now exist in the page (this is MVCC &#8212; the old version for readers, the new version for this transaction). A WAL record is written to the in-memory WAL buffer: before-image is the old account 42 tuple, after-image is the new account 42 tuple.</p><p><strong>Step 3 &#8212; Second UPDATE executes.</strong> Same process for account 99. WAL record written to buffer.</p><p><strong>Step 4 &#8212; COMMIT received.</strong> This is where atomicity is established:</p><p>a) Write COMMIT WAL record to WAL buffer</p><p>b) Flush WAL buffer to WAL file on disk (write() + fsync())</p><p>   &#8592; THIS IS THE BLOCKING OPERATION. Application waits here.</p><p>c) fsync() returns: storage device confirms data is durable</p><p>d) COMMIT returns to the application: &#8220;Transaction committed&#8221;</p><p>The data pages for accounts 42 and 99 in shared_buffers are still dirty &#8212; they haven&#8217;t been written to the data files on disk. The background writer process will eventually flush them, but not in the commit path.</p><p><strong>Step 5 &#8212; Data pages flushed asynchronously.</strong> The background writer process or the checkpoint process flushes the dirty pages to their data files. This happens after the commit and is not visible to the committing transaction. It may happen milliseconds or minutes after the commit, depending on load.</p><p>Now here is the critical question: if the machine crashes between Step 4 and Step 5 &#8212; after the WAL is durable but before the data pages are written to disk &#8212; what happens on recovery?</p><p>Postgres opens the WAL file, finds the COMMIT record for XID 7421, sees that there is a committed transaction whose data page changes are not yet reflected in the data files, and replays the after-images from the WAL records for each UPDATE. The data pages end up in exactly the state they would have been in had the machine not crashed. The transaction is complete. Nothing is lost.</p><p>This is redo recovery. The WAL is always written before the data pages. This ordering &#8212; WAL before data &#8212; is the core invariant of write-ahead logging. It&#8217;s why the mechanism is called write-ahead: the log is always written <em>ahead</em> of the data.</p><div><hr></div><h2>How ROLLBACK works without a second disk write</h2><p>Now let&#8217;s trace a rollback:</p><p>BEGIN;</p><p>UPDATE accounts SET balance = balance - 100 WHERE id = 42;</p><p>-- Something goes wrong</p><p>ROLLBACK;</p><p>After the UPDATE, the situation is:</p><ul><li><p>shared_buffers has the modified page for account 42 (balance = 900 in the new tuple)</p></li><li><p>The WAL buffer has a WAL record with before-image (1000) and after-image (900)</p></li><li><p>The data file on disk still has the original value (balance = 1000) &#8212; the async write hasn&#8217;t happened yet</p></li></ul><p>When ROLLBACK is issued:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="/__u/substackcdn.com/image/fetch/$s_!XEGO!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F957acc1c-70c2-4c82-9857-e786b71e04d9_919x579.gif" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="/__u/substackcdn.com/image/fetch/$s_!XEGO!, /__u/bytesbtrees.substack.com/w_424, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_webp, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F957acc1c-70c2-4c82-9857-e786b71e04d9_919x579.gif 424w, /__u/substackcdn.com/image/fetch/$s_!XEGO!, /__u/bytesbtrees.substack.com/w_848, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_webp, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F957acc1c-70c2-4c82-9857-e786b71e04d9_919x579.gif 848w, /__u/substackcdn.com/image/fetch/$s_!XEGO!, /__u/bytesbtrees.substack.com/w_1272, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_webp, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F957acc1c-70c2-4c82-9857-e786b71e04d9_919x579.gif 1272w, /__u/substackcdn.com/image/fetch/$s_!XEGO!, /__u/bytesbtrees.substack.com/w_1456, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_webp, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F957acc1c-70c2-4c82-9857-e786b71e04d9_919x579.gif 1456w" sizes="100vw"><img src="/__u/substackcdn.com/image/fetch/$s_!XEGO!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F957acc1c-70c2-4c82-9857-e786b71e04d9_919x579.gif" width="919" height="579" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/957acc1c-70c2-4c82-9857-e786b71e04d9_919x579.gif&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:579,&quot;width&quot;:919,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:114300,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/gif&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://bytesbtrees.substack.com/i/196646003?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F957acc1c-70c2-4c82-9857-e786b71e04d9_919x579.gif&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="/__u/substackcdn.com/image/fetch/$s_!XEGO!, /__u/bytesbtrees.substack.com/w_424, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_auto, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F957acc1c-70c2-4c82-9857-e786b71e04d9_919x579.gif 424w, /__u/substackcdn.com/image/fetch/$s_!XEGO!, /__u/bytesbtrees.substack.com/w_848, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_auto, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F957acc1c-70c2-4c82-9857-e786b71e04d9_919x579.gif 848w, /__u/substackcdn.com/image/fetch/$s_!XEGO!, /__u/bytesbtrees.substack.com/w_1272, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_auto, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F957acc1c-70c2-4c82-9857-e786b71e04d9_919x579.gif 1272w, /__u/substackcdn.com/image/fetch/$s_!XEGO!, /__u/bytesbtrees.substack.com/w_1456, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_auto, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F957acc1c-70c2-4c82-9857-e786b71e04d9_919x579.gif 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p><strong>Step 1</strong> &#8212; Postgres writes an ABORT WAL record to the WAL buffer. This marks the transaction as aborted. It does <strong>not</strong> need to be fsynced immediately (though it will be written to disk eventually) &#8212; an uncommitted transaction&#8217;s changes are never visible to other transactions, so there&#8217;s no data loss if the ABORT record is lost in a crash.</p><p><strong>Step 2</strong> &#8212; Postgres reads the before-image from the WAL buffer (in memory &#8212; no disk read required). For account 42, the before-image is balance = 1000.</p><p><strong>Step 3</strong> &#8212; Postgres writes the before-image back into the shared_buffers page, overwriting the new tuple&#8217;s data. The page now has balance = 1000 again &#8212; the pre-transaction state.</p><p><strong>Step 4</strong> &#8212; The new tuple (balance = 900) is marked as dead. VACUUM will eventually reclaim its space.</p><p>That&#8217;s it. ROLLBACK is fast because:</p><ol><li><p>The data file was never touched &#8212; there&#8217;s nothing to undo there</p></li><li><p>The before-image is in the WAL buffer, which is in memory</p></li><li><p>Restoring the before-image is a single memory write</p></li></ol><p>No disk read. No disk write (beyond the ABORT WAL record). No seeking through the data file. Rollback&#8217;s cost is proportional to the number of rows modified, not to the size of the table.</p><p><strong>The misconception about ROLLBACK:</strong> Many engineers assume ROLLBACK &#8220;reverses&#8221; the writes to disk. It doesn&#8217;t. It reverses the writes to shared_buffers. The disk was never involved. The WAL&#8217;s before-image is the mechanism that makes this possible &#8212; it stores the undo information alongside the redo information in the same record, so both directions of recovery are available without any additional state.</p><div><hr></div><h2>Why the WAL must be fsynced before data pages</h2><p>There is a precise ordering requirement in write-ahead logging that, if violated, destroys the atomicity guarantee. Let&#8217;s examine what would happen if data pages were written to disk before the WAL was fsynced.</p><p>Scenario: Postgres writes the modified account 42 data page to disk (balance = 900), then crashes before the WAL is fsynced.</p><p>On recovery: Postgres reads the data file. It sees balance = 900 for account 42. It looks at the WAL for any un-replayed transactions. It finds... nothing (the WAL wasn&#8217;t fsynced). Postgres concludes: &#8220;This data is consistent &#8212; there are no WAL records to replay.&#8221; But the data is not consistent. Account 42&#8217;s balance was changed by a transaction that was never committed. The change is permanent, and there&#8217;s no record of what the original value was.</p><p>This is a violation of atomicity: a transaction&#8217;s changes are visible even though the transaction was never committed.</p><p>The only way to prevent this is the WAL-before-data ordering:</p><ol><li><p>WAL records for the transaction are written and fsynced</p></li><li><p>Only after the WAL is durable can data pages be written to disk</p></li></ol><p>Postgres enforces this through a protocol called <strong>WAL-logged page writes</strong>. Before any dirty page is flushed to disk, Postgres checks that all WAL records that describe changes to that page have been written to disk. This is tracked per page using the page&#8217;s LSN: the page stores the LSN of the most recent WAL record that describes a change to it. The background writer will not flush a page to disk until the WAL has been flushed to at least that page&#8217;s LSN.</p><p>The full_page_writes parameter in Postgres tightens this further: after a checkpoint, the first modification to a page writes the entire page image to the WAL (not just the change). This is the full-page write. It ensures that even if a page is torn during writing (half-written to disk, then crash), the WAL contains the complete before-and-after state needed to reconstruct the page correctly.</p><div><hr></div><h2>Savepoints: atomicity within atomicity</h2><p>SQL supports savepoints &#8212; named points within a transaction to which you can partially roll back:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;sql&quot;,&quot;nodeId&quot;:&quot;a53e7e94-fcf7-4f5f-9e16-40cffd3a50d3&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-sql">BEGIN;

UPDATE inventory
SET quantity = quantity - 10
WHERE sku = 'WIDGET';

SAVEPOINT after_inventory;

UPDATE orders
SET status = 'confirmed'
WHERE id = 9901;

-- This fails with a foreign key violation

ROLLBACK TO SAVEPOINT after_inventory;

-- The inventory update survives; the orders update is undone

COMMIT;</code></pre></div><p>Savepoints are implemented exactly like full transaction rollback, but scoped. When a savepoint is created, Postgres records the current WAL LSN as the savepoint boundary. If a rollback to savepoint is requested, Postgres applies before-images from WAL records written after that LSN &#8212; only those records, not the ones written before the savepoint.</p><p>The WAL&#8217;s sequential structure makes this straightforward: records before the savepoint LSN are left alone; records after the savepoint LSN are undone using their before-images. The transaction continues as if the rolled-back portion never happened.</p><p>This is also why long transactions that modify many rows are expensive to roll back &#8212; Postgres must apply before-images for every WAL record written during the aborted portion. The cost is proportional to the work done, not to the table size, but for a transaction that has modified a million rows, that&#8217;s a million before-image applications.</p><div><hr></div><h2>The synchronous_commit lever</h2><p>There&#8217;s a performance escape hatch that&#8217;s worth understanding in this context: synchronous_commit.</p><p>When synchronous_commit = on (the default), Postgres waits for the WAL fsync before returning COMMIT to the application. This is the full guarantee: committed transactions survive any crash.</p><p>When synchronous_commit = off, Postgres returns COMMIT to the application before the WAL is fsynced. The WAL will be fsynced shortly afterward (within wal_writer_delay, default 200ms), but the application gets its commit confirmation first.</p><p>The risk: if the machine crashes in the window between the application receiving COMMIT and the WAL being fsynced, that transaction&#8217;s changes are lost. The database will not be corrupted &#8212; the WAL is consistent, just missing the very recent transactions. But those transactions are gone, despite the application having been told they committed.</p><p>For workloads where occasional data loss is acceptable (logging, metrics, analytics ingestion), synchronous_commit = off can increase write throughput dramatically &#8212; it turns what would be 1,000 sequential fsyncs per second (each blocked, each waiting for storage confirmation) into a much smaller number of batched fsyncs.</p><p>For any workload involving financial data, user-visible state, or any operation where &#8220;committed&#8221; must mean durably committed, leave synchronous_commit = on.</p><p>The parameter can even be set per-session or per-transaction, letting you mix durability levels within a single application:</p><p>-- For this session only: high-durability mode</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;sql&quot;,&quot;nodeId&quot;:&quot;f70e5aa3-3b50-41e7-b755-9611d00728bb&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-sql">SET synchronous_commit = on;

BEGIN;

INSERT INTO payments VALUES (...);

COMMIT;  -- blocks until fsynced

-- For this session only: high-throughput mode

SET synchronous_commit = off;

BEGIN;

INSERT INTO page_views VALUES (...);

COMMIT;  -- returns immediately, fsync happens within 200ms</code></pre></div><div><hr></div><h2>The practical mental model for atomicity</h2><p>Atomicity is not a property of storage hardware. It&#8217;s a property of protocol.</p><p>The WAL protocol achieves atomicity through three rules:</p><ol><li><p>Write WAL records before data pages</p></li><li><p>Write COMMIT to WAL (and fsync) before acknowledging commit to the application</p></li><li><p>On recovery, replay all WAL records for committed transactions; apply before-images for all uncommitted transactions</p></li></ol><p>If these three rules are followed, any crash at any point leaves the database in a state where every visible transaction is fully committed and every non-visible transaction has left no trace. The atomicity guarantee holds regardless of when the crash occurs &#8212; before the WAL is written, mid-WAL write, after the WAL but before the data pages, mid-data-page write, at any point.</p><p>This is why &#8220;add a WAL&#8221; is not just a database feature &#8212; it&#8217;s the engineering solution to the fundamental impossibility of atomic writes on hardware that doesn&#8217;t support them natively. And it&#8217;s why almost every storage system that needs durability &#8212; databases, distributed file systems, operating system journals, even the internal state machines of SSDs &#8212; uses some variant of this pattern.</p>
      <p>
          <a href="/__u/bytesbtrees.substack.com/p/atomicity-at-the-byte-level-what">
              Read more
          </a>
      </p>
   ]]></content:encoded></item><item><title><![CDATA[CODD's 12 rules — the complete definition of what "relational" actually means]]></title><description><![CDATA[In 1985, fifteen years after publishing the relational model, Edgar CODD watched vendors label their products "relational" while implementing the concept halfway]]></description><link>https://bytesbtrees.substack.com/p/codds-12-rules-the-complete-definition</link><guid isPermaLink="false">https://bytesbtrees.substack.com/p/codds-12-rules-the-complete-definition</guid><dc:creator><![CDATA[System Design Roadmap]]></dc:creator><pubDate>Mon, 25 May 2026 03:30:53 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!Kk8F!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F70b94f15-4c38-4030-9cf5-64fdc2f44c15_919x560.gif" length="0" type="image/jpeg"/><content:encoded><![CDATA[<blockquote><p>By the early 1980s, relational databases were commercially successful. Oracle was selling software. IBM had DB2. Ingres had a growing user base. SQL was on its way to standardisation. CODD&#8217;s 1970 paper had won.</p></blockquote><p>And then the marketing departments got involved.</p><p>Software vendors who had built non-relational systems &#8212; or partially relational systems &#8212; began advertising them as &#8220;relational databases&#8221; because the term had acquired commercial cachet. A system that supported SQL queries but stored data in hierarchical structures. A system that had foreign keys but no support for joins. A system that was relational in spirit but navigational in implementation. The word &#8220;relational&#8221; was being diluted to the point of meaninglessness.</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://bytesbtrees.substack.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en-gb&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Thanks for reading Bytes &amp; B-Tree's! Subscribe for free to receive new posts and support my work.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><blockquote><p>In 1985, CODD published a two-part article in Computerworld magazine under the headline &#8220;Is your DBMS really relational?&#8221; He laid out 12 rules &#8212; numbered 0 through 12 &#8212; that define what a system must do to legitimately call itself a relational database. The rules are not aspirational guidelines. They are a minimum specification. A system that violates any of them, CODD argued, is selling something that isn&#8217;t what it claims to be.</p></blockquote><p>Reading the rules today is illuminating not because every database satisfies all of them &#8212; none do &#8212; but because the ones that get broken reveal exactly where the relational model turns out to be genuinely hard to implement, and what the industry decided to sacrifice.</p><div><hr></div><h2>Rule 0: The foundation rule</h2><p><em>A system advertised as a relational database management system must manage data through its relational capabilities exclusively.</em></p><p>Rule 0 is the meta-rule. It says that if a system calls itself relational, it must use the relational model for everything &#8212; not just for some operations while using a different model for others. A system that supports SQL queries but also exposes record IDs as physical pointers, or that allows navigational access through proprietary APIs, fails Rule 0.</p><blockquote><p>This rule was aimed directly at the hybrid systems of the early 1980s: products that had bolted SQL on top of existing hierarchical or network databases, offering relational queries as a layer while the underlying data management remained navigational. If you could get better performance by bypassing the SQL layer and using the underlying navigational interface directly &#8212; and many of these systems let you do exactly that &#8212; you didn&#8217;t have a relational database. You had a navigational database with a SQL interface.</p></blockquote><p><strong>Why it matters today:</strong> Some NoSQL databases offer SQL-like query languages as a thin layer over non-relational data models. Apache Cassandra&#8217;s CQL looks like SQL but doesn&#8217;t support joins or transactions in the relational sense. Using these systems&#8217; SQL syntax doesn&#8217;t give you the guarantees of the relational model. Rule 0 says: the model is what matters, not the syntax.</p><div><hr></div><h2>Rules 1 and 2: Information and guaranteed access</h2><p><strong>Rule 1</strong> &#8212; All information in a relational database is represented at the logical level in exactly one and only one way: as values in tables.</p><p><strong>Rule 2</strong> &#8212; Each and every datum (atomic value) in a relational database is guaranteed to be logically accessible by resorting to a combination of table name, primary key value, and column name.</p><p>These two rules together establish the fundamental data representation contract. All data is in tables. All data is accessible by known logical names. There are no hidden identifiers, no physical row pointers visible to the user, no secondary representations that the programmer must know about.</p><p><strong>Where this gets interesting with ORMs:</strong> Object-relational mapping libraries often expose internal identifiers &#8212; auto-increment integers, UUIDs &#8212; as primary keys that are visible to application code. This is fine and consistent with Rules 1 and 2. The problem arises when the ORM begins relying on implementation details: assuming that row IDs are sequential, or using physical offsets, or bypassing the SQL layer for &#8220;performance.&#8221; When an ORM&#8217;s behaviour depends on the physical storage layout rather than the logical schema, it&#8217;s violating the spirit of Rule 1.</p><p><strong>The ctid issue in PostgreSQL:</strong> Postgres has a system column called <code>ctid</code> &#8212; the physical location of a row on disk, expressed as <code>(page_number, tuple_offset)</code>. You can query it with <code>SELECT ctid, * FROM table</code>. This is a violation of Rule 1&#8217;s spirit &#8212; it exposes physical storage information as a queryable value. Postgres doesn&#8217;t advertise ctid as a user-facing feature for exactly this reason. It exists for internal use and should never appear in application code.</p><div><hr></div><h2>Rule 3: Systematic NULL treatment</h2><p><em>Null values (distinct from the empty character string or a string of blank characters and distinct from zero or any other number) are supported in fully relational DBMS for representing missing information and inapplicable information in a systematic way, independent of data type.</em></p><p>This rule acknowledges something genuinely uncomfortable: sometimes information is unknown or inapplicable, and the database must represent this in a consistent, type-independent way. NULL is not zero. NULL is not an empty string. NULL is the absence of a known value, and it must propagate correctly through all operations.</p><p><strong>Why NULL is so strange &#8212; and why CODD&#8217;s rule is mathematically correct:</strong> NULL introduces three-valued logic into SQL. A comparison like <code>salary &gt; 50000</code> can evaluate to TRUE, FALSE, or UNKNOWN &#8212; unknown when <code>salary</code> is NULL. This propagates through boolean expressions: <code>TRUE AND UNKNOWN = UNKNOWN</code>, <code>FALSE AND UNKNOWN = FALSE</code>, <code>TRUE OR UNKNOWN = TRUE</code>.</p><p>This is internally consistent but deeply counterintuitive. Consider:</p><p>sql</p><pre><code><code>SELECT * FROM employees WHERE salary &gt; 50000 OR salary &lt;= 50000;</code></code></pre><p>This looks like it should return all rows &#8212; every salary is either above or below 50,000. But employees with NULL salary are returned by neither condition, because both comparisons evaluate to UNKNOWN. The query misses them.</p><p>Or consider the NOT IN trap:</p><p>sql</p><pre><code><code>SELECT name FROM employees
WHERE dept_id NOT IN (SELECT dept_id FROM departments WHERE budget &gt; 1000000);</code></code></pre><p>If any row in the subquery returns NULL for <code>dept_id</code>, the entire NOT IN clause evaluates to UNKNOWN for every employee, and the query returns zero rows &#8212; silently. This is a genuine bug that has affected real production systems.</p><p><strong>Postgres handles this correctly, mostly:</strong> Postgres implements three-valued logic faithfully and provides <code>IS NULL</code>, <code>IS NOT NULL</code>, <code>IS DISTINCT FROM</code>, and <code>NULLIF</code> to handle NULL explicitly. The behaviour is correct by CODD&#8217;s specification, even when it surprises developers.</p><div><hr></div><h2>Rule 4: Active online catalog</h2><p><em>The database description is represented at the logical level in the same way as ordinary data, so that authorised users can apply the same relational language to its interrogation as they apply to the regular data.</em></p><p>The database schema &#8212; the table definitions, column types, constraints, indexes, views &#8212; is itself stored as data in tables and queryable with the same SQL used to query regular data. There&#8217;s no separate metadata API; there&#8217;s no special administrative interface required to understand the schema. The schema is just more tables.</p><p><strong>In Postgres:</strong> The <code>information_schema</code> views and <code>pg_catalog</code> system tables implement Rule 4. You can query the schema exactly as you query your data:</p><p>sql</p><pre><code><code>-- What tables exist in my database?
SELECT table_name FROM information_schema.tables
WHERE table_schema = 'public';

-- What columns does the orders table have?
SELECT column_name, data_type, is_nullable
FROM information_schema.columns
WHERE table_name = 'orders';

-- What indexes exist on a table?
SELECT indexname, indexdef
FROM pg_indexes
WHERE tablename = 'orders';</code></code></pre><p>This is elegant and powerful. It means introspection tools, schema migration frameworks, and database administration interfaces can all be built using standard SQL rather than proprietary metadata APIs. ORM frameworks use exactly this capability to understand your database schema at runtime without you having to tell them what tables exist.</p><div><hr></div><h2>Rule 5: Comprehensive data sublanguage</h2><p><em>A relational system may support several languages and various modes of terminal use. However, there must be at least one language whose statements are expressible, per some well-defined syntax, as character strings and that is comprehensive in supporting all of the following: data definition, view definition, data manipulation (interactive and by program), integrity constraints, authorization, and transaction boundaries.</em></p><p>SQL satisfies Rule 5. A single language handles CREATE TABLE, CREATE VIEW, SELECT, INSERT, UPDATE, DELETE, CHECK constraints, GRANT/REVOKE, BEGIN/COMMIT/ROLLBACK. You don&#8217;t need separate tools for schema management, data manipulation, and transaction control.</p><p><strong>Where vendors broke this in the 1980s:</strong> Early database products often had separate languages for DML (data manipulation) and DDL (data definition), or required transactions to be managed through an external library rather than within the query language itself. Separating these concerns was a Rule 5 violation &#8212; it meant you couldn&#8217;t express a complete database operation in a single language.</p><div><hr></div><h2>Rule 6: View updating</h2><p><em>All views that are theoretically updatable must be updatable by the system.</em></p><p>This is the rule most databases break most visibly, and it&#8217;s worth spending real time on.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="/__u/substackcdn.com/image/fetch/$s_!Kk8F!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F70b94f15-4c38-4030-9cf5-64fdc2f44c15_919x560.gif" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="/__u/substackcdn.com/image/fetch/$s_!Kk8F!, /__u/bytesbtrees.substack.com/w_424, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_webp, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F70b94f15-4c38-4030-9cf5-64fdc2f44c15_919x560.gif 424w, /__u/substackcdn.com/image/fetch/$s_!Kk8F!, /__u/bytesbtrees.substack.com/w_848, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_webp, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F70b94f15-4c38-4030-9cf5-64fdc2f44c15_919x560.gif 848w, /__u/substackcdn.com/image/fetch/$s_!Kk8F!, /__u/bytesbtrees.substack.com/w_1272, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_webp, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F70b94f15-4c38-4030-9cf5-64fdc2f44c15_919x560.gif 1272w, /__u/substackcdn.com/image/fetch/$s_!Kk8F!, /__u/bytesbtrees.substack.com/w_1456, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_webp, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F70b94f15-4c38-4030-9cf5-64fdc2f44c15_919x560.gif 1456w" sizes="100vw"><img src="/__u/substackcdn.com/image/fetch/$s_!Kk8F!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F70b94f15-4c38-4030-9cf5-64fdc2f44c15_919x560.gif" width="919" height="560" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/70b94f15-4c38-4030-9cf5-64fdc2f44c15_919x560.gif&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:560,&quot;width&quot;:919,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:63558,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/gif&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://bytesbtrees.substack.com/i/196645602?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F70b94f15-4c38-4030-9cf5-64fdc2f44c15_919x560.gif&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="/__u/substackcdn.com/image/fetch/$s_!Kk8F!, /__u/bytesbtrees.substack.com/w_424, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_auto, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F70b94f15-4c38-4030-9cf5-64fdc2f44c15_919x560.gif 424w, /__u/substackcdn.com/image/fetch/$s_!Kk8F!, /__u/bytesbtrees.substack.com/w_848, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_auto, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F70b94f15-4c38-4030-9cf5-64fdc2f44c15_919x560.gif 848w, /__u/substackcdn.com/image/fetch/$s_!Kk8F!, /__u/bytesbtrees.substack.com/w_1272, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_auto, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F70b94f15-4c38-4030-9cf5-64fdc2f44c15_919x560.gif 1272w, /__u/substackcdn.com/image/fetch/$s_!Kk8F!, /__u/bytesbtrees.substack.com/w_1456, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_auto, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F70b94f15-4c38-4030-9cf5-64fdc2f44c15_919x560.gif 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>A view is a saved query &#8212; a named SELECT that you can treat as a table. The question Rule 6 asks is: if a view is a simple, unambiguous projection of a base table, should you be able to <code>UPDATE</code> through it? </p><p>CODD says yes &#8212; the database must determine which views are theoretically updatable and honour that.</p><p><strong>What &#8220;theoretically updatable&#8221; means:</strong> A view is theoretically updatable if there is an unambiguous mapping between every row in the view and exactly one row in an underlying base table, and if every update to the view translates unambiguously into an update to the base table.</p><p>A view that selects a subset of rows and columns from a single table is theoretically updatable &#8212; every view row maps to exactly one base table row, and updating the view means updating that row.</p><p>sql</p><pre><code><code>-- Theoretically updatable: simple projection
CREATE VIEW eng_staff AS
SELECT emp_id, name, salary FROM employee WHERE dept = 'Engineering';

-- This SHOULD work, and in Postgres it does:
UPDATE eng_staff SET salary = 95000 WHERE emp_id = 1;</code></code></pre><p>A view that joins multiple tables, aggregates rows, or uses window functions is theoretically non-updatable &#8212; there&#8217;s no unambiguous base table row to modify.</p><p>sql</p><pre><code><code>-- Not updatable: aggregate across rows
CREATE VIEW dept_stats AS
SELECT dept, AVG(salary) as avg_sal, COUNT(*) as headcount
FROM employee GROUP BY dept;

-- What would this mean? Which rows to change?
UPDATE dept_stats SET avg_sal = 90000 WHERE dept = 'Engineering';
-- ERROR in all databases &#8212; correctly rejected</code></code></pre><p><strong>Where modern databases partially break Rule 6:</strong> Postgres supports updating simple views automatically. But it doesn&#8217;t automatically support updating views that join two tables, even when the update is theoretically unambiguous (changing a column that maps clearly to one of the joined tables). You must create explicit <code>INSTEAD OF</code> triggers to enable this. MySQL has similar limitations. CODD would say this is a partial violation: the system could determine theoretical updatability for join views in many cases, but chooses not to.</p><div><hr></div><h2>Rule 7: High-level insert, update, and delete</h2><p><em>The capability of handling a base relation or a derived relation as a single operand applies not only to the retrieval of data but also to the insertion, update, and deletion of data.</em></p><p>SQL satisfies this: <code>UPDATE orders SET status = 'shipped' WHERE order_date &lt; '2024-01-01'</code> updates an entire set of rows in one statement, not one row at a time. This is a fundamental departure from navigational databases where you position a cursor on a single record and modify it.</p><p>Set-based operations are not just syntactic convenience &#8212; they enable the query optimiser to choose the most efficient execution strategy. A set-based UPDATE on indexed data can use the index; a row-by-row cursor update must access every row individually.</p><p><strong>The RBAR anti-pattern:</strong> RBAR (Row By Agonizing Row) is the name given to the common mistake of writing SQL that processes one row at a time &#8212; typically via a cursor in a stored procedure &#8212; when a set-based operation would be more efficient. It&#8217;s a violation of Rule 7&#8217;s spirit: you&#8217;re bypassing the relational engine&#8217;s set-processing capabilities to implement navigational logic in SQL.</p><p>sql</p><pre><code><code>-- RBAR: processing one row at a time
DECLARE cur CURSOR FOR SELECT id FROM orders WHERE status = 'pending';
OPEN cur;
FETCH NEXT FROM cur INTO @id;
WHILE @@FETCH_STATUS = 0 BEGIN
    UPDATE orders SET status = 'processing' WHERE id = @id;
    FETCH NEXT FROM cur INTO @id;
END;

-- Rule 7: set-based, as CODD intended
UPDATE orders SET status = 'processing' WHERE status = 'pending';</code></code></pre><p>The second version can leverage indexes, run in parallel, and be optimised by the query planner. The first version cannot.</p><div><hr></div><h2>Rules 8 and 9: Physical and logical data independence</h2><p><strong>Rule 8</strong> &#8212; Application programs and terminal activities remain logically unimpaired whenever any changes are made to either storage representations or access methods.</p><p><strong>Rule 9</strong> &#8212; Application programs and terminal activities remain logically unimpaired when information-preserving changes of any kind that theoretically permit unimpairment are made to the base tables.</p><p>These are the two data independence rules from CODD&#8217;s 1970 paper, now formalised.</p><p>Rule 8 (physical independence) is generally well-honoured: you can add indexes, change storage parameters, partition tables, move data between tablespaces, and application SQL stays unchanged. This is the rule that lets DBAs tune performance without requiring application code changes.</p><p>Rule 9 (logical independence) is where things get interesting &#8212; and where most databases only partially comply. Adding a new column to a table (an information-preserving change) should not break existing queries. In Postgres and MySQL, it doesn&#8217;t &#8212; queries that select specific columns continue to work. But queries that use <code>SELECT *</code> will now return the new column, which may surprise applications that expected a fixed column count.</p><p><strong>The harder cases:</strong> Splitting one table into two (decomposing a wide table with mixed concerns) or merging two tables are information-preserving changes &#8212; the data hasn&#8217;t changed, just its organisation. Rule 9 says these changes shouldn&#8217;t require application modifications. In practice, they usually do &#8212; applications that named the original table must be updated to use the new table names or a compatibility view.</p><p>True Rule 9 compliance would mean the database automatically maintains compatibility views that preserve the old interface after structural reorganisation. No major database does this automatically.</p><div><hr></div><h2>Rules 10, 11, and 12: Integrity, distribution, and non-subversion</h2><p><strong>Rule 10</strong> &#8212; Integrity constraints specific to a particular relational database must be definable in the relational data sublanguage and storable in the catalog, not in the application programs.</p><p>Constraints belong in the database, not in the application. <code>CHECK (price &gt; 0)</code>, <code>FOREIGN KEY (customer_id) REFERENCES customers(id)</code>, <code>UNIQUE (email)</code> &#8212; these are Rule 10. When constraints live in application code, a different application that bypasses the ORM (a data migration script, an admin tool, a reporting query) can violate them silently.</p><p><strong>Rule 11</strong> &#8212; A relational DBMS has distribution independence.</p><p>A database should appear identical whether it is a single-server system or a distributed system. Queries written for a non-distributed database should work unchanged on a distributed one. This is the hardest rule to fully satisfy &#8212; distributed systems introduce consistency trade-offs (CAP theorem, which we&#8217;ll cover in issue #059) that make true location transparency impossible without performance costs. Every distributed database makes some compromise here.</p><p><strong>Rule 12</strong> &#8212; If a relational system has a low-level (single-record-at-a-time) language, that low-level language cannot be used to subvert or bypass the integrity rules and constraints expressed in the higher-level relational language.</p><p>Stored procedures, triggers, and embedded SQL must respect the constraints defined at the relational level. You cannot write a stored procedure that sets a foreign key to an invalid value by operating on raw storage structures. The relational constraints are non-negotiable, regardless of the interface used to access the data.</p><p><strong>Where MySQL historically broke Rule 12:</strong> For many years, MySQL&#8217;s MyISAM storage engine did not enforce foreign key constraints &#8212; even when they were defined in the schema. Application code could insert orphaned records (orders without valid customer IDs) through the normal SQL interface, and the engine would silently accept them. InnoDB enforced foreign keys correctly; MyISAM did not. This was a clear Rule 12 violation &#8212; the constraint was defined in the relational schema but not enforced.</p><div><hr></div><h2>The compliance scorecard</h2><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="/__u/substackcdn.com/image/fetch/$s_!BStP!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa9a4da9f-9464-43ea-98e9-b99192d79881_919x599.gif" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="/__u/substackcdn.com/image/fetch/$s_!BStP!, /__u/bytesbtrees.substack.com/w_424, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_webp, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa9a4da9f-9464-43ea-98e9-b99192d79881_919x599.gif 424w, /__u/substackcdn.com/image/fetch/$s_!BStP!, /__u/bytesbtrees.substack.com/w_848, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_webp, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa9a4da9f-9464-43ea-98e9-b99192d79881_919x599.gif 848w, /__u/substackcdn.com/image/fetch/$s_!BStP!, /__u/bytesbtrees.substack.com/w_1272, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_webp, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa9a4da9f-9464-43ea-98e9-b99192d79881_919x599.gif 1272w, /__u/substackcdn.com/image/fetch/$s_!BStP!, /__u/bytesbtrees.substack.com/w_1456, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_webp, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa9a4da9f-9464-43ea-98e9-b99192d79881_919x599.gif 1456w" sizes="100vw"><img src="/__u/substackcdn.com/image/fetch/$s_!BStP!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa9a4da9f-9464-43ea-98e9-b99192d79881_919x599.gif" width="919" height="599" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/a9a4da9f-9464-43ea-98e9-b99192d79881_919x599.gif&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:599,&quot;width&quot;:919,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:294289,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/gif&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://bytesbtrees.substack.com/i/196645602?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa9a4da9f-9464-43ea-98e9-b99192d79881_919x599.gif&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="/__u/substackcdn.com/image/fetch/$s_!BStP!, /__u/bytesbtrees.substack.com/w_424, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_auto, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa9a4da9f-9464-43ea-98e9-b99192d79881_919x599.gif 424w, /__u/substackcdn.com/image/fetch/$s_!BStP!, /__u/bytesbtrees.substack.com/w_848, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_auto, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa9a4da9f-9464-43ea-98e9-b99192d79881_919x599.gif 848w, /__u/substackcdn.com/image/fetch/$s_!BStP!, /__u/bytesbtrees.substack.com/w_1272, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_auto, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa9a4da9f-9464-43ea-98e9-b99192d79881_919x599.gif 1272w, /__u/substackcdn.com/image/fetch/$s_!BStP!, /__u/bytesbtrees.substack.com/w_1456, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_auto, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa9a4da9f-9464-43ea-98e9-b99192d79881_919x599.gif 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>The scorecard shows how four systems &#8212; PostgreSQL, MySQL, SQLite, and MongoDB (as a NoSQL comparison point) &#8212; score against all 13 rules. The results are instructive:</p><p><strong>PostgreSQL</strong> is the most compliant major database, passing most rules and partially complying with the genuinely difficult ones (Rule 6 view updating, Rule 9 logical independence, Rule 11 distribution independence for single-node deployments).</p><p><strong>MySQL</strong> has historically been more permissive &#8212; silent truncation of out-of-range values, optional foreign key enforcement, non-standard NULL handling in some edge cases. Modern MySQL (8.x) is significantly more correct than older versions, but its compliance history is checkered.</p><p><strong>SQLite</strong> is intentionally a single-user embedded database, not a server. It makes explicit compromises: loose typing (&#8221;type affinity&#8221; rather than strict types), no stored procedures, limited view updating. It&#8217;s not trying to be a fully relational system in the way Postgres is &#8212; it&#8217;s optimising for embeddability.</p><p><strong>MongoDB</strong> fails nearly every rule &#8212; not because it&#8217;s poorly built, but because it is deliberately not a relational database. It has a different model with different trade-offs. Measuring it against CODD&#8217;s rules is useful precisely to show what you give up when you leave the relational model.</p><div><hr></div><h2>Why these rules still matter</h2><p>CODD wrote these rules in 1985 to protect a word &#8212; &#8220;relational&#8221; &#8212; from being diluted. Forty years later, the relevant question isn&#8217;t really &#8220;does your database pass CODD&#8217;s rules?&#8221; The relevant question is: <strong>for each rule your database violates, do you know what you&#8217;re giving up?</strong></p><blockquote><p>A database that partially violates Rule 9 (logical independence) means that some schema changes will require application code changes &#8212; you should plan for that. A database that violates Rule 11 (distribution independence) means that moving from a single-server to a distributed setup will require query modifications &#8212; you should know that before committing to a distributed architecture. A database that violates Rule 12 means that bypassing the ORM with raw SQL can silently corrupt your data &#8212; you should enforce discipline around who can write migration scripts.</p></blockquote><p>The rules are not a test to pass. They&#8217;re a map of where the relational model&#8217;s guarantees end and your application&#8217;s responsibilities begin.</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://bytesbtrees.substack.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en-gb&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Thanks for reading Bytes &amp; B-Tree's! Subscribe for free to receive new posts and support my work.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div>]]></content:encoded></item><item><title><![CDATA[IMS, CODASYL & CODD's relational model — what came before SQL, why it failed, and the idea that changed everything]]></title><description><![CDATA[In 1970, a mathematician at IBM wrote an 11-page paper that made two decades of database engineering obsolete. Not because the old systems were badly built &#8212; they were engineering marvels.]]></description><link>https://bytesbtrees.substack.com/p/ims-codasyl-and-codds-relational</link><guid isPermaLink="false">https://bytesbtrees.substack.com/p/ims-codasyl-and-codds-relational</guid><dc:creator><![CDATA[System Design Roadmap]]></dc:creator><pubDate>Thu, 21 May 2026 03:30:52 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!ErmG!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbdd5f180-3324-4bc2-b78d-61bfa5047b63_905x599.gif" length="0" type="image/jpeg"/><content:encoded><![CDATA[<blockquote><p>The year is 1966. NASA is three years away from putting a man on the Moon. The Apollo programme is the largest engineering project in human history, involving 400,000 people, 20,000 contractors, and roughly 10 million individual parts. IBM wins the contract to build the information system that tracks all of it &#8212; what part goes where, what&#8217;s been ordered, what&#8217;s in stock, what&#8217;s been installed on which spacecraft.</p></blockquote><blockquote><p>The system they build is called IMS: the Information Management System. It stores data in tree structures &#8212; hierarchies of parent and child records &#8212; and processes it with transaction management that prevents the data corruption problems we covered in issue #004. IMS works. It helps put humans on the Moon. Versions of it are still running in banks and airlines today, half a century later, processing billions of transactions annually.</p></blockquote><p>IMS is also, by the measure of what came after it, a design with a fundamental flaw. Not in its engineering &#8212; the engineering is superb. The flaw is in its data model. And understanding exactly what that flaw is, and why it took until 1970 for someone to articulate the fix, is one of the most illuminating stories in the history of computing.</p><div><hr></div><h2>IMS: the hierarchical model</h2><p>IMS organises data as a forest of trees. Every record type is either a root or a child of exactly one parent type. A company has departments. A department has employees. An employee has projects. The physical storage on disk mirrors this hierarchy &#8212; children are stored near their parent, making navigation along the hierarchy fast.</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://bytesbtrees.substack.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en-gb&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Thanks for reading Bytes &amp; B-Tree's! Subscribe for free to receive new posts and support my work.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="/__u/substackcdn.com/image/fetch/$s_!ErmG!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbdd5f180-3324-4bc2-b78d-61bfa5047b63_905x599.gif" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="/__u/substackcdn.com/image/fetch/$s_!ErmG!, /__u/bytesbtrees.substack.com/w_424, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_webp, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbdd5f180-3324-4bc2-b78d-61bfa5047b63_905x599.gif 424w, /__u/substackcdn.com/image/fetch/$s_!ErmG!, /__u/bytesbtrees.substack.com/w_848, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_webp, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbdd5f180-3324-4bc2-b78d-61bfa5047b63_905x599.gif 848w, /__u/substackcdn.com/image/fetch/$s_!ErmG!, /__u/bytesbtrees.substack.com/w_1272, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_webp, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbdd5f180-3324-4bc2-b78d-61bfa5047b63_905x599.gif 1272w, /__u/substackcdn.com/image/fetch/$s_!ErmG!, /__u/bytesbtrees.substack.com/w_1456, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_webp, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbdd5f180-3324-4bc2-b78d-61bfa5047b63_905x599.gif 1456w" sizes="100vw"><img src="/__u/substackcdn.com/image/fetch/$s_!ErmG!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbdd5f180-3324-4bc2-b78d-61bfa5047b63_905x599.gif" width="905" height="599" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/bdd5f180-3324-4bc2-b78d-61bfa5047b63_905x599.gif&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:599,&quot;width&quot;:905,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:107160,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/gif&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://bytesbtrees.substack.com/i/196529723?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbdd5f180-3324-4bc2-b78d-61bfa5047b63_905x599.gif&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="/__u/substackcdn.com/image/fetch/$s_!ErmG!, /__u/bytesbtrees.substack.com/w_424, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_auto, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbdd5f180-3324-4bc2-b78d-61bfa5047b63_905x599.gif 424w, /__u/substackcdn.com/image/fetch/$s_!ErmG!, /__u/bytesbtrees.substack.com/w_848, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_auto, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbdd5f180-3324-4bc2-b78d-61bfa5047b63_905x599.gif 848w, /__u/substackcdn.com/image/fetch/$s_!ErmG!, /__u/bytesbtrees.substack.com/w_1272, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_auto, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbdd5f180-3324-4bc2-b78d-61bfa5047b63_905x599.gif 1272w, /__u/substackcdn.com/image/fetch/$s_!ErmG!, /__u/bytesbtrees.substack.com/w_1456, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_auto, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbdd5f180-3324-4bc2-b78d-61bfa5047b63_905x599.gif 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>The animated diagram builds the tree structure piece by piece: the company root, then department records branching from it, then employee records hanging from each department, then project assignment records below each employee. Each level is a different record type with its own fields. Navigation moves along the branches &#8212; to find an employee&#8217;s projects, you start at the root, navigate to the right department, navigate to the right employee, and then access their project children.</p><h3>What IMS does well</h3><p><strong>Concurrent access and recovery.</strong> IMS had proper locking, transaction management, and crash recovery from the beginning. The flat file problems from issue #004 &#8212; concurrent write corruption, atomicity failures, no crash recovery &#8212; are all handled by IMS. It is a real database engine in the most important sense.</p><p><strong>Hierarchical access is fast.</strong> If your access pattern follows the hierarchy &#8212; &#8220;give me all projects for this specific employee in this specific department&#8221; &#8212; IMS is very fast. Children are stored physically near their parents. Navigation along a known path is efficient.</p><p><strong>Consistent, structured records.</strong> IMS enforces the schema. Records have defined fields with defined types. You can&#8217;t accidentally write a string into an integer field.</p><h3>What IMS cannot do well</h3><p><strong>Cross-branch queries.</strong> The animated diagram shows this clearly. The query &#8220;find all employees currently working on the Apollo project&#8221; requires traversing every branch of the tree &#8212; every department, every employee within each department &#8212; and checking whether that employee has an Apollo project child. There is no way to go directly from a project name to the employees on it, because the hierarchy runs in one direction only: from company down to project. Going the other way means a full scan.</p><p><strong>Multiple parent relationships.</strong> In reality, an employee might belong to multiple departments (a matrix organisation). A project might be assigned to employees across different departments. IMS trees cannot represent this. Each record has exactly one parent. Relationships that aren&#8217;t hierarchical require either data duplication (store the employee record in multiple departments) or a workaround that degrades the query performance IMS was designed to provide.</p><p><strong>Schema changes.</strong> Adding a new field to the employee record type requires restructuring the physical storage of every employee record on disk. In a large IMS installation with millions of records, this is a multi-day offline operation. The application code that navigates the hierarchy is tightly coupled to the physical layout &#8212; change the layout, and you must find and update every program that touches those records.</p><p>The deeper problem, which IMS shares with all navigational databases, is this: <strong>the application code knows the physical structure of the data.</strong> Every program that reads or writes IMS records contains explicit navigation logic: go to this record type, follow this parent-child link, find this field at this byte offset. When the physical structure changes, every program that depends on it must change too.</p><div><hr></div><h2>CODASYL: the network model</h2><p>In 1969, the Conference on Data Systems Languages &#8212; CODASYL &#8212; published a specification for a more general database model. Rather than limiting relationships to parent-child trees, the CODASYL model allowed any record to participate in multiple named <strong>sets</strong> &#8212; directed relationships between record types that could form arbitrary graphs rather than trees only.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="/__u/substackcdn.com/image/fetch/$s_!h84j!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7e1fdd78-ba49-4377-b88a-6b11d84c5e29_819x579.gif" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="/__u/substackcdn.com/image/fetch/$s_!h84j!, /__u/bytesbtrees.substack.com/w_424, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_webp, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7e1fdd78-ba49-4377-b88a-6b11d84c5e29_819x579.gif 424w, /__u/substackcdn.com/image/fetch/$s_!h84j!, /__u/bytesbtrees.substack.com/w_848, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_webp, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7e1fdd78-ba49-4377-b88a-6b11d84c5e29_819x579.gif 848w, /__u/substackcdn.com/image/fetch/$s_!h84j!, /__u/bytesbtrees.substack.com/w_1272, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_webp, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7e1fdd78-ba49-4377-b88a-6b11d84c5e29_819x579.gif 1272w, /__u/substackcdn.com/image/fetch/$s_!h84j!, /__u/bytesbtrees.substack.com/w_1456, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_webp, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7e1fdd78-ba49-4377-b88a-6b11d84c5e29_819x579.gif 1456w" sizes="100vw"><img src="/__u/substackcdn.com/image/fetch/$s_!h84j!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7e1fdd78-ba49-4377-b88a-6b11d84c5e29_819x579.gif" width="819" height="579" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/7e1fdd78-ba49-4377-b88a-6b11d84c5e29_819x579.gif&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:579,&quot;width&quot;:819,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:128116,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/gif&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://bytesbtrees.substack.com/i/196529723?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7e1fdd78-ba49-4377-b88a-6b11d84c5e29_819x579.gif&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="/__u/substackcdn.com/image/fetch/$s_!h84j!, /__u/bytesbtrees.substack.com/w_424, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_auto, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7e1fdd78-ba49-4377-b88a-6b11d84c5e29_819x579.gif 424w, /__u/substackcdn.com/image/fetch/$s_!h84j!, /__u/bytesbtrees.substack.com/w_848, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_auto, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7e1fdd78-ba49-4377-b88a-6b11d84c5e29_819x579.gif 848w, /__u/substackcdn.com/image/fetch/$s_!h84j!, /__u/bytesbtrees.substack.com/w_1272, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_auto, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7e1fdd78-ba49-4377-b88a-6b11d84c5e29_819x579.gif 1272w, /__u/substackcdn.com/image/fetch/$s_!h84j!, /__u/bytesbtrees.substack.com/w_1456, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_auto, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7e1fdd78-ba49-4377-b88a-6b11d84c5e29_819x579.gif 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>The animated diagram shows a CODASYL schema with DEPARTMENT, EMPLOYEE, PROJECT, and ASSIGNMENT record types. An EMPLOYEE belongs to the EMP-IN-DEPT set (owned by DEPARTMENT) and also participates in the EMP-ASSIGNS set (owned by ASSIGNMENT). An ASSIGNMENT participates in both EMP-ASSIGNS and PROJ-ASSIGNS. The graph is more expressive than a tree &#8212; an employee can have relationships to multiple departments, multiple assignments, multiple projects &#8212; all without data duplication.</p><h3>What CODASYL improved over IMS</h3><p><strong>Many-to-many relationships.</strong> CODASYL could represent relationships that IMS required data duplication for. An employee on multiple projects, a project with employees from multiple departments &#8212; these are natural in the CODASYL model.</p><p><strong>Richer query paths.</strong> Because relationships are bidirectional and an entity can participate in multiple sets, you can navigate from any direction. To find employees on the Apollo project, you start at the Apollo PROJECT record, follow the PROJ-ASSIGNS set, and reach all ASSIGNMENT records, from which you follow EMP-ASSIGNS backwards to reach the employees. No full-tree scan required.</p><h3>What CODASYL didn&#8217;t fix</h3><p>Look at the navigation code in the animated diagram:</p><pre><code><code>FIND FIRST DEPARTMENT WHERE dept_id = 10
FIND FIRST EMPLOYEE WITHIN EMP-IN-DEPT
WHILE FOUND:
    FIND FIRST ASSIGN WITHIN EMP-ASSIGNS
    WHILE FOUND:
        GET PROJECT VIA PROJ-ASSIGNS
        FIND NEXT ASSIGN WITHIN EMP-ASSIGNS
    FIND NEXT EMPLOYEE WITHIN EMP-IN-DEPT</code></code></pre><p>Eight lines of code to answer what we would today express as a two-table JOIN. And this is the simple version &#8212; real CODASYL programs navigating complex schemas ran to hundreds of lines of cursor manipulation code.</p><p>The fundamental problem is unchanged from IMS: <strong>the application must know the physical structure of the data and navigate it explicitly.</strong> The programmer must know which set to follow, which direction to traverse, which record type sits at each position in the graph. The query specifies not what you want but how to physically retrieve it.</p><p>This creates what database historian C.J. Date called the <strong>navigation problem</strong>. When you want different data, you write different navigation code. When the schema changes, you rewrite all the navigation code that touched the changed portion. The application logic and the data structure are deeply entangled. There is no separation between the question being asked and the physical process of answering it.</p><h3>The cursor-based interface</h3><p>CODASYL uses a cursor &#8212; a pointer to a current record &#8212; as the primary interface. You move the cursor around the graph: <code>FIND FIRST</code>, <code>FIND NEXT</code>, <code>GET CURRENT</code>. The programmer explicitly manages cursor position throughout the program. This is not an abstraction &#8212; it exposes the physical navigation mechanism directly.</p><p>For a programmer who knows the schema and has written CODASYL code before, this is workable. For a business analyst who wants to ask an ad hoc question &#8212; &#8220;how many employees across all departments are assigned to Apollo?&#8221; &#8212; it is completely inaccessible. Only someone who knows the physical schema intimately can write the code to answer the question.</p><p>This was the state of the art in 1969. Then CODD wrote his paper.</p><div><hr></div><h2>CODD&#8217;s insight: separate the logical from the physical</h2><p>Edgar F. CODD was a British mathematician working at IBM&#8217;s San Jose Research Lab when he published &#8220;A Relational Model of Data for Large Shared Data Banks&#8221; in the Communications of the ACM in June 1970. He was 39 years old. The paper is 11 pages. It changed everything.</p><p>CODD&#8217;s core insight sounds simple in retrospect: <strong>the logical structure of data should be independent of its physical storage.</strong></p><p>IMS and CODASYL forced the programmer to know how data was physically laid out on disk &#8212; which record type was the parent, which set to follow, which pointer to dereference. This created an <strong>application dependency on physical structure</strong> that made schema changes expensive and ad hoc queries impossible for non-programmers.</p><p>CODD proposed that the database present data as a collection of simple, flat <strong>relations</strong> &#8212; what we now call tables. Each relation has named attributes (columns) and a set of tuples (rows). Relationships between tables are expressed by shared values (keys), not by physical pointers. The programmer expresses what data they want using operations on these relations. The database engine figures out the physical retrieval.</p><p>This separation is called <strong>data independence</strong>: the logical data model (tables and their relationships) is independent of the physical storage model (how those tables are laid out on disk, what indexes exist, how they&#8217;re partitioned). Change the physical storage &#8212; add an index, change the file layout, move data to a different disk &#8212; and the logical queries still work unchanged. The application code doesn&#8217;t know or care about the physical structure, because it never navigates it directly.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="/__u/substackcdn.com/image/fetch/$s_!Uon6!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F56f96980-913e-407c-9c50-34e53933d4c1_835x579.gif" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="/__u/substackcdn.com/image/fetch/$s_!Uon6!, /__u/bytesbtrees.substack.com/w_424, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_webp, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F56f96980-913e-407c-9c50-34e53933d4c1_835x579.gif 424w, /__u/substackcdn.com/image/fetch/$s_!Uon6!, /__u/bytesbtrees.substack.com/w_848, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_webp, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F56f96980-913e-407c-9c50-34e53933d4c1_835x579.gif 848w, /__u/substackcdn.com/image/fetch/$s_!Uon6!, /__u/bytesbtrees.substack.com/w_1272, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_webp, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F56f96980-913e-407c-9c50-34e53933d4c1_835x579.gif 1272w, /__u/substackcdn.com/image/fetch/$s_!Uon6!, /__u/bytesbtrees.substack.com/w_1456, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_webp, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F56f96980-913e-407c-9c50-34e53933d4c1_835x579.gif 1456w" sizes="100vw"><img src="/__u/substackcdn.com/image/fetch/$s_!Uon6!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F56f96980-913e-407c-9c50-34e53933d4c1_835x579.gif" width="835" height="579" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/56f96980-913e-407c-9c50-34e53933d4c1_835x579.gif&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:579,&quot;width&quot;:835,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:122504,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/gif&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://bytesbtrees.substack.com/i/196529723?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F56f96980-913e-407c-9c50-34e53933d4c1_835x579.gif&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="/__u/substackcdn.com/image/fetch/$s_!Uon6!, /__u/bytesbtrees.substack.com/w_424, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_auto, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F56f96980-913e-407c-9c50-34e53933d4c1_835x579.gif 424w, /__u/substackcdn.com/image/fetch/$s_!Uon6!, /__u/bytesbtrees.substack.com/w_848, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_auto, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F56f96980-913e-407c-9c50-34e53933d4c1_835x579.gif 848w, /__u/substackcdn.com/image/fetch/$s_!Uon6!, /__u/bytesbtrees.substack.com/w_1272, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_auto, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F56f96980-913e-407c-9c50-34e53933d4c1_835x579.gif 1272w, /__u/substackcdn.com/image/fetch/$s_!Uon6!, /__u/bytesbtrees.substack.com/w_1456, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_auto, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F56f96980-913e-407c-9c50-34e53933d4c1_835x579.gif 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>The animated diagram shows the same data from the IMS and CODASYL examples &#8212; departments, employees, projects, assignments &#8212; reorganised into flat tables. EMPLOYEE has a <code>dept_id</code> foreign key referencing DEPARTMENT. PROJECT_ASSIGN connects employees to projects by value, not by pointer. To query across the tables, you write a JOIN expressed as equality on shared key values.</p><div><hr></div><h2>Relational algebra: the mathematical foundation</h2><p>CODD didn&#8217;t just propose a data model &#8212; he grounded it in mathematics. The relational model is based on <strong>set theory</strong> and <strong>first-order predicate logic</strong>. A relation is a set of tuples, and queries are expressed as operations on sets: select (filter rows), project (choose columns), join (combine tables on a condition), union, intersection, difference.</p><p>This mathematical foundation has two important consequences.</p><p><strong>First, queries have well-defined semantics.</strong> A SQL query has a precise mathematical meaning that is independent of how the database executes it. Two queries that express the same set-theoretic operation are guaranteed to produce the same result, regardless of which execution path the database chooses. In CODASYL, different navigation paths to the same data could &#8212; under certain concurrency conditions &#8212; produce different results.</p><p><strong>Second, the query optimiser is possible.</strong> Because the logical query is separated from the physical execution, the database engine can choose different physical execution strategies for the same logical query &#8212; use an index or not, choose a hash join or a merge join, execute in parallel or sequentially &#8212; based on the actual data distribution. The programmer never has to specify the physical strategy. The engine picks it.</p><p>This is the crucial enabler of everything we&#8217;ll study in issues #030&#8211;#035: how a query planner transforms a logical query into an efficient physical execution plan. None of that is possible without data independence &#8212; the clean separation between what you ask for and how the database retrieves it.</p><div><hr></div><h2>What CODD&#8217;s paper actually said</h2><p>The paper opens with a precise diagnosis of the problem:</p><blockquote><p><em>&#8220;Future users of large data banks must be protected from having to know how the data is organized in the machine (the internal representation).&#8221;</em></p></blockquote><p>This single sentence explains IMS, CODASYL, and every navigational database. Users &#8212; including application programmers &#8212; shouldn&#8217;t need to know the physical layout. They should express queries in terms of the data&#8217;s logical meaning.</p><p>CODD then introduces the relational model: relations as mathematical sets, normal forms to eliminate redundancy, and relational algebra as the query language. The paper defines the first, second, and third normal forms &#8212; the foundations of relational schema design we&#8217;ll cover in depth in issues #050 and #051.</p><p>The paper also introduces the concept that became the PRIMARY KEY: the notion that every tuple in a relation is uniquely identified by some minimal subset of its attributes, called the primary key. Duplicate rows aren&#8217;t allowed &#8212; a relation is a set, and sets don&#8217;t have duplicates.</p><p>And it introduces foreign keys: the mechanism by which one relation references another by value. An EMPLOYEE tuple&#8217;s <code>dept_id</code> value must match the <code>dept_id</code> of some DEPARTMENT tuple. This is not a pointer &#8212; it&#8217;s a constraint on values. The constraint can be checked without knowing where either tuple is physically stored.</p><div><hr></div><h2>Why IMS and CODASYL people resisted</h2><p>CODD&#8217;s paper met substantial resistance, particularly from within IBM, where IMS was a commercial product with paying customers and an enormous installed base.</p><p>The main technical objection was performance. In 1970, computers were slow enough that the query optimisation overhead seemed prohibitive. Navigating a known path through an IMS hierarchy was faster than letting a general-purpose optimiser figure out a query plan. The IMS performance argument was legitimate &#8212; for the access patterns IMS was designed for, it was genuinely faster than early relational implementations.</p><p>The deeper resistance was organisational. Large organisations had invested heavily in IMS and CODASYL systems. The navigation code was written. The schemas were designed. The database administrators knew the systems intimately. The idea that all of it should be replaced by a new model &#8212; however elegant &#8212; was threatening.</p><p>CODD&#8217;s relational model took roughly a decade to move from paper to production. The first commercial relational databases &#8212; Oracle (then called Relational Software), IBM&#8217;s own DB2, and Ingres &#8212; arrived in the late 1970s and early 1980s. SQL, the language that implemented relational algebra in a syntax accessible to non-mathematicians, was standardised in 1986. By the early 1990s, relational databases dominated enterprise computing. IMS and CODASYL systems were legacy infrastructure.</p><p>The performance objection was resolved not by making relational systems slower &#8212; but by making hardware faster. By the mid-1980s, hardware had improved enough that a well-optimised relational query could outperform hand-written CODASYL navigation, because the optimiser could choose strategies that a human programmer wouldn&#8217;t have thought of.</p><div><hr></div><h2>What IMS and CODASYL got right that is still true</h2><p>It&#8217;s easy to read this history as &#8220;IMS and CODASYL were bad and the relational model was good.&#8221; That&#8217;s too simple.</p><p><strong>Transaction management.</strong> IMS had proper ACID transactions before the relational model was even proposed. The need for atomicity, consistency, isolation, and durability was understood and implemented in hierarchical and network databases. The relational model inherited this; it didn&#8217;t invent it.</p><p><strong>Physical data organisation still matters.</strong> The relational model achieves data independence at the logical level &#8212; your SQL query doesn&#8217;t specify the physical access path. But the physical storage still exists, and it still matters enormously for performance. When we study B-trees in issue #018, we&#8217;ll see that the physical organisation of data on disk &#8212; how related rows cluster together, how indexes are structured &#8212; determines query performance just as much as it did in IMS. The difference is that modern databases let the programmer express the logical query without specifying the physical path, while still allowing the physical structure to be tuned separately.</p><p><strong>The navigational access pattern is sometimes right.</strong> There are workloads &#8212; graph traversals, recursive hierarchies, certain time-series access patterns &#8212; where navigating explicit relationships is genuinely more efficient than joining flat tables. This is part of why graph databases exist today. The relational model is not always the right model. But it is the right default, for most workloads, most of the time.</p><blockquote><p>CODD&#8217;s insight wasn&#8217;t that navigation was wrong &#8212; it was that navigation shouldn&#8217;t be the programmer&#8217;s responsibility. The database should choose the navigation path. The programmer should express what they want. That separation of concerns is what made relational databases the foundation of modern data infrastructure.</p></blockquote><div><hr></div><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://bytesbtrees.substack.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en-gb&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Thanks for reading Bytes &amp; B-Tree's! Subscribe for free to receive new posts and support my work.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div>]]></content:encoded></item><item><title><![CDATA[Files, block devices & the OS abstraction — and the three places where it leaks]]></title><description><![CDATA[A database doesn't see platters or flash cells. It sees files. The operating system promises that files are a clean, simple interface to whatever storage hardware sits underneath.]]></description><link>https://bytesbtrees.substack.com/p/files-block-devices-and-the-os-abstraction</link><guid isPermaLink="false">https://bytesbtrees.substack.com/p/files-block-devices-and-the-os-abstraction</guid><dc:creator><![CDATA[System Design Roadmap]]></dc:creator><pubDate>Wed, 13 May 2026 03:30:56 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!jd2I!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc50611f9-1287-4527-b1a4-4ce333948531_919x599.gif" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>There&#8217;s a mental model most engineers carry about how files work: you open a file, you write bytes, you close it, those bytes are on disk. Clean. Simple. Abstracted.</p><blockquote><p>That model is accurate enough for writing a log file or saving a configuration. For a database engine managing hundreds of gigabytes under concurrent write load, it breaks down in three specific places &#8212; and each place is a source of bugs, data loss, and performance degradation that has burned real systems in production.</p></blockquote><blockquote><p>This issue is about those three leaks. Not because database engineers need to fix the OS &#8212; they don&#8217;t &#8212; but because understanding where the abstraction breaks explains every unusual thing databases do when writing and reading files. The WAL, the fsync dance, the pre-allocation of large extents, the <code>O_DIRECT</code> flag, the careful tuning of <code>checkpoint_completion_target</code> &#8212; all of it makes immediate sense once you see what the OS abstraction doesn&#8217;t actually guarantee.</p></blockquote><div><hr></div><h2>From filename to bytes: the three-layer translation</h2><p>When Postgres opens its main data file, the OS doesn&#8217;t just hand it a pointer to raw bytes on disk. It translates the filename through three distinct layers before a single byte flows.</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://bytesbtrees.substack.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en-gb&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Thanks for reading Bytes &amp; B-Tree's! Subscribe for free to receive new posts and support my work.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><blockquote><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="/__u/substackcdn.com/image/fetch/$s_!jd2I!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc50611f9-1287-4527-b1a4-4ce333948531_919x599.gif" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="/__u/substackcdn.com/image/fetch/$s_!jd2I!, /__u/bytesbtrees.substack.com/w_424, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_webp, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc50611f9-1287-4527-b1a4-4ce333948531_919x599.gif 424w, /__u/substackcdn.com/image/fetch/$s_!jd2I!, /__u/bytesbtrees.substack.com/w_848, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_webp, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc50611f9-1287-4527-b1a4-4ce333948531_919x599.gif 848w, /__u/substackcdn.com/image/fetch/$s_!jd2I!, /__u/bytesbtrees.substack.com/w_1272, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_webp, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc50611f9-1287-4527-b1a4-4ce333948531_919x599.gif 1272w, /__u/substackcdn.com/image/fetch/$s_!jd2I!, /__u/bytesbtrees.substack.com/w_1456, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_webp, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc50611f9-1287-4527-b1a4-4ce333948531_919x599.gif 1456w" sizes="100vw"><img src="/__u/substackcdn.com/image/fetch/$s_!jd2I!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc50611f9-1287-4527-b1a4-4ce333948531_919x599.gif" width="919" height="599" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/c50611f9-1287-4527-b1a4-4ce333948531_919x599.gif&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:599,&quot;width&quot;:919,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:147824,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/gif&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://bytesbtrees.substack.com/i/196527740?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc50611f9-1287-4527-b1a4-4ce333948531_919x599.gif&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="/__u/substackcdn.com/image/fetch/$s_!jd2I!, /__u/bytesbtrees.substack.com/w_424, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_auto, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc50611f9-1287-4527-b1a4-4ce333948531_919x599.gif 424w, /__u/substackcdn.com/image/fetch/$s_!jd2I!, /__u/bytesbtrees.substack.com/w_848, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_auto, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc50611f9-1287-4527-b1a4-4ce333948531_919x599.gif 848w, /__u/substackcdn.com/image/fetch/$s_!jd2I!, /__u/bytesbtrees.substack.com/w_1272, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_auto, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc50611f9-1287-4527-b1a4-4ce333948531_919x599.gif 1272w, /__u/substackcdn.com/image/fetch/$s_!jd2I!, /__u/bytesbtrees.substack.com/w_1456, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_auto, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc50611f9-1287-4527-b1a4-4ce333948531_919x599.gif 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div></blockquote><h3>Layer 1: directory entry &#8594; inode</h3><p>The filename <code>pg_data</code> is stored in a <strong>directory entry</strong> &#8212; a simple mapping from a human-readable name to an <strong>inode number</strong>. An inode is a fixed-size data structure stored at a known location on the filesystem. It contains the file&#8217;s metadata (size, permissions, timestamps, ownership) and, critically, the list of block addresses where the file&#8217;s actual data is stored.</p><p>This separation &#8212; name from inode, inode from data &#8212; is deliberate. It means a file can have multiple names (hard links) pointing to the same inode. It means renaming a file is cheap (you just update the directory entry) rather than requiring moving all the data. It means file metadata and data can be updated independently.</p><p>For databases, the inode layer creates a subtle constraint: renaming a file is atomic at the OS level, but creating a new file, writing data to it, and then renaming it to replace the old one is the standard pattern for safe file replacement &#8212; and whether the rename survives a crash depends on whether the directory entry write was fsynced. Databases doing crash-safe configuration file updates must fsync both the file and the directory containing it.</p><h3>Layer 2: inode &#8594; block addresses</h3><p>The inode contains a list of block addresses: the physical locations on disk where the file&#8217;s 4KB blocks live. In older filesystems like ext2, this was a literal array of block numbers, with indirect pointer blocks for large files. In modern filesystems like ext4, this uses an <strong>extent tree</strong>: instead of listing individual block numbers, the inode lists contiguous ranges (extents) like &#8220;blocks 4812 through 4847.&#8221; This is far more efficient for large files &#8212; a 1GB file might be described by a handful of extents instead of 262,144 individual block numbers.</p><p>The block addresses in the inode are what get handed to the block device layer for actual I/O. The filesystem translates &#8220;give me bytes 0&#8211;8191 of this file&#8221; into &#8220;read blocks 4812 and 4813 from the block device.&#8221;</p><h3>Layer 3: block device &#8594; hardware</h3><p>The block device layer presents a uniform interface: a linear array of numbered 4KB blocks. Whether the underlying hardware is an HDD, SSD, NVMe, or a RAID array, the filesystem sees the same interface. The device driver translates block numbers into hardware-specific commands (SCSI for HDDs, NVMe commands for NVMe drives).</p><p>This uniformity is what lets the same Postgres binary run on an HDD server, an NVMe-backed cloud instance, and a RAM disk with no changes. The database operates entirely in terms of file offsets; the OS translates everything below.</p><div><hr></div><h2>Abstraction leak #1: the page cache lie</h2><p>This one we covered in issue #002, but it&#8217;s worth stating precisely in the context of the full abstraction stack.</p><p>The OS page cache sits between the filesystem and the hardware. When your database calls <code>write()</code>, data lands in the page cache &#8212; kernel-managed RAM &#8212; and the OS returns success immediately. The kernel schedules the actual disk write for later. From the application&#8217;s perspective, the write completed. From the hardware&#8217;s perspective, nothing happened yet.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="/__u/substackcdn.com/image/fetch/$s_!YRin!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1328f395-70a9-4419-8caa-2d60ac8c520b_919x579.gif" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="/__u/substackcdn.com/image/fetch/$s_!YRin!, /__u/bytesbtrees.substack.com/w_424, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_webp, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1328f395-70a9-4419-8caa-2d60ac8c520b_919x579.gif 424w, /__u/substackcdn.com/image/fetch/$s_!YRin!, /__u/bytesbtrees.substack.com/w_848, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_webp, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1328f395-70a9-4419-8caa-2d60ac8c520b_919x579.gif 848w, /__u/substackcdn.com/image/fetch/$s_!YRin!, /__u/bytesbtrees.substack.com/w_1272, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_webp, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1328f395-70a9-4419-8caa-2d60ac8c520b_919x579.gif 1272w, /__u/substackcdn.com/image/fetch/$s_!YRin!, /__u/bytesbtrees.substack.com/w_1456, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_webp, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1328f395-70a9-4419-8caa-2d60ac8c520b_919x579.gif 1456w" sizes="100vw"><img src="/__u/substackcdn.com/image/fetch/$s_!YRin!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1328f395-70a9-4419-8caa-2d60ac8c520b_919x579.gif" width="919" height="579" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/1328f395-70a9-4419-8caa-2d60ac8c520b_919x579.gif&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:579,&quot;width&quot;:919,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:222530,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/gif&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://bytesbtrees.substack.com/i/196527740?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1328f395-70a9-4419-8caa-2d60ac8c520b_919x579.gif&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="/__u/substackcdn.com/image/fetch/$s_!YRin!, /__u/bytesbtrees.substack.com/w_424, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_auto, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1328f395-70a9-4419-8caa-2d60ac8c520b_919x579.gif 424w, /__u/substackcdn.com/image/fetch/$s_!YRin!, /__u/bytesbtrees.substack.com/w_848, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_auto, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1328f395-70a9-4419-8caa-2d60ac8c520b_919x579.gif 848w, /__u/substackcdn.com/image/fetch/$s_!YRin!, /__u/bytesbtrees.substack.com/w_1272, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_auto, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1328f395-70a9-4419-8caa-2d60ac8c520b_919x579.gif 1272w, /__u/substackcdn.com/image/fetch/$s_!YRin!, /__u/bytesbtrees.substack.com/w_1456, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_auto, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1328f395-70a9-4419-8caa-2d60ac8c520b_919x579.gif 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>The animated diagram shows the full lifecycle: a <code>write()</code> call fills pages in the kernel&#8217;s page cache (shown in amber as &#8220;dirty&#8221; pages), the OS returns success, the database celebrates a completed write &#8212; and then a power failure hits, and every dirty page in the cache is simply gone.</p><p><strong>Why the OS does this:</strong> Batching writes is genuinely faster. If your application writes a 100-byte record every millisecond, and the OS flushed each write to disk immediately, you&#8217;d be doing 1,000 <code>fsync()</code> operations per second, each costing 500&#181;s&#8211;30ms depending on hardware. Instead, the OS accumulates dirty pages and flushes them in larger, more efficient batches. For most applications &#8212; web servers, log writers, configuration tools &#8212; this is the right trade-off. Durability is less important than throughput.</p><p>For a database that has promised &#8220;I won&#8217;t lose your data if the power goes out,&#8221; it&#8217;s the wrong trade-off. Databases solve this by calling <code>fsync()</code> on specific files at specific times &#8212; specifically, after writing to the WAL and before acknowledging a commit. The WAL is append-only and sequential, so <code>fsync()</code> on the WAL file is as fast as fsyncing gets on a given piece of hardware.</p><p><strong>The </strong><code>O_DIRECT</code><strong> alternative:</strong> Some databases bypass the page cache entirely using the <code>O_DIRECT</code> file flag. Writes with <code>O_DIRECT</code> go directly from the database&#8217;s buffer pool to the storage hardware, skipping the OS page cache entirely. This eliminates double-buffering (the same data in both the database buffer pool and the OS page cache), gives the database precise control over what&#8217;s in memory, and removes the &#8220;dirty page accumulation and background flush&#8221; unpredictability.</p><p>The trade-off: with <code>O_DIRECT</code>, the database is solely responsible for durability. There&#8217;s no OS page cache copy to fall back on. The WAL and recovery mechanisms must be airtight. This is fine for a production-grade database engine, but it means there&#8217;s no safety net. MySQL&#8217;s InnoDB uses <code>O_DIRECT</code> by default. Postgres defaults to using the OS page cache but can be configured otherwise.</p><div><hr></div><h2>Abstraction leak #2: block alignment</h2><p>The OS block size and the database page size are not always the same number.</p><p>Linux uses 4KB blocks by default. Postgres uses 8KB pages. MySQL InnoDB uses 16KB pages. When a Postgres page is written, it occupies exactly two OS blocks. When an InnoDB page is written, it occupies four. As long as both layers agree &#8212; a database page is always written as a complete set of OS blocks &#8212; this is fine.</p><p>The problem is what happens if the write is interrupted halfway. If a system crash occurs while Postgres is writing an 8KB page that spans OS blocks 4812 and 4813, the crash might leave block 4812 with new data and block 4813 with old data. The two halves of the same database page are now from different points in time. This is called a <strong>torn write</strong>, and it is a form of corruption that the database&#8217;s recovery system must detect and handle.</p><p><strong>How ext4 and XFS handle this:</strong> Modern filesystems running in their default journaling modes provide atomicity guarantees at the OS block level &#8212; a single 4KB block write is atomic. But database pages that span multiple blocks don&#8217;t get atomicity for free. The two-block write of a Postgres 8KB page is not guaranteed to be all-or-nothing.</p><p><strong>How Postgres handles this:</strong> InnoDB addresses torn writes with the doublewrite buffer &#8212; a technique where pages are written to a known-safe region of the disk before being written to their final location, so a torn write in the final location can always be repaired from the doublewrite buffer copy. We&#8217;ll cover this in detail in issue #042.</p><p>Postgres relies on the WAL instead. Even if a torn write leaves a page half-corrupted, the WAL contains a full record of what the page should look like after recovery. On restart, Postgres replays the WAL and reconstructs any pages damaged by torn writes. The WAL record takes priority over whatever is on disk.</p><p><strong>The practical implication for schema design:</strong> This is why some PostgreSQL configurations write full-page images to the WAL after checkpoints. The setting <code>full_page_writes = on</code> (default: on) tells Postgres to write the entire 8KB page to the WAL the first time that page is modified after a checkpoint. This ensures that even if the page is torn during a crash, the full original page image is available in the WAL for recovery. It increases WAL volume by potentially a lot, but it&#8217;s the correct default for production.</p><div><hr></div><h2>Abstraction leak #3: fragmentation</h2><p>The filesystem abstraction promises that a file is a sequence of bytes. It doesn&#8217;t promise that those bytes are stored in physically contiguous locations on the storage device. They usually aren&#8217;t, and the implications for database performance are severe.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="/__u/substackcdn.com/image/fetch/$s_!PTys!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5df67507-abac-4c78-ba4d-45de053384aa_831x539.gif" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="/__u/substackcdn.com/image/fetch/$s_!PTys!, /__u/bytesbtrees.substack.com/w_424, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_webp, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5df67507-abac-4c78-ba4d-45de053384aa_831x539.gif 424w, /__u/substackcdn.com/image/fetch/$s_!PTys!, /__u/bytesbtrees.substack.com/w_848, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_webp, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5df67507-abac-4c78-ba4d-45de053384aa_831x539.gif 848w, /__u/substackcdn.com/image/fetch/$s_!PTys!, /__u/bytesbtrees.substack.com/w_1272, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_webp, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5df67507-abac-4c78-ba4d-45de053384aa_831x539.gif 1272w, /__u/substackcdn.com/image/fetch/$s_!PTys!, /__u/bytesbtrees.substack.com/w_1456, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_webp, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5df67507-abac-4c78-ba4d-45de053384aa_831x539.gif 1456w" sizes="100vw"><img src="/__u/substackcdn.com/image/fetch/$s_!PTys!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5df67507-abac-4c78-ba4d-45de053384aa_831x539.gif" width="831" height="539" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/5df67507-abac-4c78-ba4d-45de053384aa_831x539.gif&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:539,&quot;width&quot;:831,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:553085,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/gif&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://bytesbtrees.substack.com/i/196527740?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5df67507-abac-4c78-ba4d-45de053384aa_831x539.gif&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="/__u/substackcdn.com/image/fetch/$s_!PTys!, /__u/bytesbtrees.substack.com/w_424, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_auto, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5df67507-abac-4c78-ba4d-45de053384aa_831x539.gif 424w, /__u/substackcdn.com/image/fetch/$s_!PTys!, /__u/bytesbtrees.substack.com/w_848, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_auto, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5df67507-abac-4c78-ba4d-45de053384aa_831x539.gif 848w, /__u/substackcdn.com/image/fetch/$s_!PTys!, /__u/bytesbtrees.substack.com/w_1272, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_auto, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5df67507-abac-4c78-ba4d-45de053384aa_831x539.gif 1272w, /__u/substackcdn.com/image/fetch/$s_!PTys!, /__u/bytesbtrees.substack.com/w_1456, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_auto, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5df67507-abac-4c78-ba4d-45de053384aa_831x539.gif 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><h3>What fragmentation actually means</h3><p>When the OS allocates blocks for a file, it takes whatever free blocks are available &#8212; not necessarily contiguous ones. On a filesystem with lots of activity (other processes creating and deleting files, the database itself allocating and freeing space), a file being extended block by block ends up with its blocks scattered across the disk. This is fragmentation.</p><p>For SSDs, fragmentation matters much less. There&#8217;s no physical head movement &#8212; the device can access any block in ~50&#8211;150&#181;s regardless of its physical location. The difference between accessing adjacent blocks and blocks at opposite ends of the device is in the noise.</p><p>For HDDs, fragmentation is catastrophic for performance. A sequential scan of a fragmented file is not actually sequential &#8212; the disk head must seek between each fragmented extent, paying the full 5&#8211;15ms seek penalty each time. A file with 50 fragments requires 50 seeks. What should be a 1-second sequential scan becomes a 50-second seek-dominated crawl.</p><h3>What databases do about it: pre-allocation</h3><p>Rather than letting the OS allocate blocks on demand as the file grows, databases pre-allocate large contiguous extents upfront. When Postgres creates a new table segment or extends an existing one, it doesn&#8217;t ask for 8KB &#8212; it asks for 1MB or more at once, claiming a contiguous block of disk space before writing any data to it. The OS, finding a large free contiguous region, allocates it as a single extent.</p><p>This means the file&#8217;s blocks are physically adjacent on disk. When Postgres later does a sequential scan of the table, it&#8217;s a true sequential read &#8212; one seek, then continuous streaming data at full disk throughput.</p><p><strong>How Postgres pre-allocates:</strong> Postgres extends relation files in chunks defined by the <code>BLCKSZ</code> compile-time constant (8KB by default) but writes them in segments up to 1GB (<code>RELSEG_SIZE</code>). The <code>pg_prewarm</code> extension and the OS&#8217;s <code>posix_fallocate()</code> system call can pre-allocate disk space without writing data, reserving contiguous blocks immediately.</p><p><strong>WAL files: the most important case:</strong> The WAL is perhaps the most fragmentation-sensitive file in the entire database. Every commit that requires <code>fsync()</code> writes to the WAL, and WAL write latency is directly in the commit latency path. Postgres pre-allocates WAL segment files (16MB by default, configurable with <code>wal_segment_size</code>) and keeps a small number of pre-allocated segments ready at all times. This ensures that WAL writes are always sequential into pre-allocated space &#8212; never waiting for the OS to find and allocate new blocks mid-write.</p><p>The <code>wal_keep_size</code> parameter controls how many WAL segments to retain for replication standbys, and <code>max_wal_size</code> controls when checkpoints are forced. Getting these right for your workload is one of the first things to tune on a busy write-heavy database.</p><div><hr></div><h2>How the three leaks combine: a production scenario</h2><p>Consider a Postgres server under heavy write load. A bug in the application causes a large transaction to hold an open cursor for two hours. During those two hours:</p><p><strong>Leak #1 (page cache):</strong> The page cache has been accumulating dirty pages for the table being modified. Because the long transaction holds back the oldest snapshot, VACUUM can&#8217;t clean the table. Dirty pages build up. Checkpoint is delayed. When the checkpoint eventually runs, it has to flush a large backlog of dirty pages to disk all at once &#8212; a write burst that saturates the storage device and causes a spike in commit latency for all other transactions.</p><p><strong>Leak #2 (block alignment):</strong> During the checkpoint write burst, the server crashes mid-checkpoint. Some pages were written torn. On restart, Postgres replays the WAL from the last completed checkpoint. Because <code>full_page_writes</code> is on, it has the full pre-crash page images and can reconstruct everything correctly. Data loss: zero.</p><p><strong>Leak #3 (fragmentation):</strong> The autovacuum process has been unable to reclaim space on the bloated table due to the long transaction holding back the snapshot. When the transaction finally ends, autovacuum runs aggressively, reclaiming and reusing free space in the table. Some of this reclaimed space is non-contiguous &#8212; the table is now fragmented. Future sequential scans pay a fragmentation penalty until a <code>VACUUM FULL</code> or <code>CLUSTER</code> operation is run to rebuild the table with contiguous storage.</p><p>None of these is a catastrophic failure. All three are the normal, expected operation of the database and filesystem working within their known constraints. But understanding the constraints is what lets you recognise each symptom, trace it to its source, and make the correct intervention.</p><div><hr></div><h2>The three tuning knobs these leaks explain</h2><p><code>full_page_writes = on</code> &#8212; Addresses leak #2. Writes full page images to WAL after checkpoints to enable recovery from torn writes. Leave this on unless you have hardware-level write atomicity guarantees (some enterprise storage controllers provide this).</p><p><code>checkpoint_completion_target = 0.9</code> &#8212; Addresses leak #1. Spreads dirty page flushing across 90% of the checkpoint interval rather than concentrating it at the end. Prevents write bursts. Set this to 0.7&#8211;0.9 on busy write workloads.</p><p><code>wal_segment_size</code> &#8212; Addresses leak #3. Larger WAL segments mean fewer file allocations, less fragmentation, and more contiguous sequential writes. The default of 16MB is sensible; 64MB or 256MB is reasonable for write-heavy workloads on modern NVMe storage.</p><div><hr></div><h2>What&#8217;s actually behind <code>pg_test_fsync</code></h2><p>Postgres ships with a diagnostic tool, <code>pg_test_fsync</code>, that benchmarks fsync performance on your specific hardware. It tests different <code>wal_sync_method</code> options &#8212; <code>fsync</code>, <code>fdatasync</code>, <code>open_sync</code>, <code>open_datasync</code> &#8212; and reports the number of WAL writes per second achievable with each. The method that achieves the highest rate is the right choice for your hardware.</p><p>Run this before deploying a new database server. The right answer varies enormously by hardware. On some SSD configurations, <code>fdatasync</code> is 3&#8211;5&#215; faster than <code>fsync</code> because it skips fsyncing file metadata changes. On others the difference is negligible. The only way to know is to measure.</p><p>bash</p><pre><code><code># Run as the postgres user on your actual server hardware:
pg_test_fsync -s 2

# Output example (your numbers will vary by hardware):
# Compare file sync methods using one 8kB write:
# (in wal_sync_method preference order, fastest first)
# open_datasync                 3429.5/sec    291 usec/op
# fdatasync                     3152.8/sec    317 usec/op
# fsync                         2847.3/sec    351 usec/op
# fsync_writethrough               n/a
# open_sync                     2103.6/sec    476 usec/op</code></code></pre><p>The winning method becomes your <code>wal_sync_method</code> in <code>postgresql.conf</code>.</p><div><hr></div><h2>The practical takeaway</h2><p>The OS file abstraction is clean and useful &#8212; and it has three specific gaps that every serious database must work around:</p><p><strong>Gap 1</strong> &#8212; <code>write()</code> doesn&#8217;t mean &#8220;on disk.&#8221; <code>fsync()</code> does. Databases use the WAL pattern: write a compact sequential record, fsync it, then acknowledge the commit. The data pages follow asynchronously.</p><p><strong>Gap 2</strong> &#8212; Multi-block writes aren&#8217;t atomic. A crash mid-write leaves torn pages. Databases address this with either a doublewrite buffer (InnoDB) or full-page WAL writes (Postgres), so recovery always has a clean copy.</p><p><strong>Gap 3</strong> &#8212; The OS allocates blocks for throughput, not locality. Databases pre-allocate contiguous extents upfront &#8212; especially for WAL files &#8212; so that writes stay sequential and reads stay fast.</p><blockquote><p>When you see any database configuration knob related to fsync methods, WAL sizing, checkpoint spreading, or pre-allocation &#8212; it&#8217;s addressing one of these three gaps. There&#8217;s no mystery. There&#8217;s just the physics of storage and the engineering required to work within it reliably.</p></blockquote><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://bytesbtrees.substack.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en-gb&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Thanks for reading Bytes &amp; B-Tree's! Subscribe for free to receive new posts and support my work.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div>]]></content:encoded></item><item><title><![CDATA[Disk geometry & the I/O stack — why spinning disks changed software architecture, and what SSDs didn't fix as much as you'd think]]></title><description><![CDATA[The 5 to 20 milliseconds it takes a hard disk head to move to the right track shaped the entire database industry. SSDs removed the moving parts &#8212; but the asymmetry between random and sequential acces]]></description><link>https://bytesbtrees.substack.com/p/disk-geometry-and-the-io-stack-why</link><guid isPermaLink="false">https://bytesbtrees.substack.com/p/disk-geometry-and-the-io-stack-why</guid><dc:creator><![CDATA[System Design Roadmap]]></dc:creator><pubDate>Sat, 09 May 2026 01:31:23 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!n8A6!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4e1c36a1-5b58-4fcf-af11-434e56127191_919x599.gif" length="0" type="image/jpeg"/><content:encoded><![CDATA[<blockquote><p>Last week we talked about the memory hierarchy &#8212; the stack from CPU registers down to spinning metal, and the enormous latency gaps between each layer. This week we go one level deeper into the bottom two layers of that stack: hard disk drives and solid-state drives. Not because database engineers need to know how to manufacture storage hardware, but because the physical geometry of these devices creates constraints that travel all the way up through the OS, through the database engine, and into every schema and index design decision you&#8217;ll ever make.</p></blockquote><blockquote><p>There&#8217;s a number that shapes nearly every database storage decision ever made: <strong>7 to 23 milliseconds</strong>. That&#8217;s the time it takes a hard disk drive to perform a random read &#8212; the combined cost of moving a mechanical arm to the right track and waiting for the platter to rotate the right sector underneath it. During those 20 milliseconds, a 3GHz CPU executes roughly 60 million instructions. An entire sorting algorithm can run to completion. A round-trip TCP packet can cross a continent and come back. And yet, for decades, every database application had to wait this long to fetch a single row that wasn&#8217;t cached in memory.</p></blockquote><p>That constraint &#8212; mechanical movement as the bottleneck for data access &#8212; is the origin story of almost every database structure you&#8217;ll encounter in this newsletter.</p><div><hr></div><h2>Inside a hard disk drive</h2><p>A hard disk drive is, at its core, a precision optical instrument wearing the clothes of a storage device.</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://bytesbtrees.substack.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en-gb&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Thanks for reading Bytes &amp; B-Tree's! Subscribe for free to receive new posts and support my work.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><blockquote><p>The data is stored on one or more <strong>platters</strong> &#8212; circular discs coated in magnetic material, stacked on a central spindle, spinning at 5,400 to 15,000 rotations per minute. Each platter has two surfaces, and each surface has a dedicated <strong>read/write head</strong> that floats on a cushion of air just a few nanometers above the surface. The head never touches the platter &#8212; if it does, that&#8217;s a head crash, and it destroys the data beneath it permanently.</p></blockquote><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="/__u/substackcdn.com/image/fetch/$s_!n8A6!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4e1c36a1-5b58-4fcf-af11-434e56127191_919x599.gif" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="/__u/substackcdn.com/image/fetch/$s_!n8A6!, /__u/bytesbtrees.substack.com/w_424, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_webp, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4e1c36a1-5b58-4fcf-af11-434e56127191_919x599.gif 424w, /__u/substackcdn.com/image/fetch/$s_!n8A6!, /__u/bytesbtrees.substack.com/w_848, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_webp, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4e1c36a1-5b58-4fcf-af11-434e56127191_919x599.gif 848w, /__u/substackcdn.com/image/fetch/$s_!n8A6!, /__u/bytesbtrees.substack.com/w_1272, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_webp, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4e1c36a1-5b58-4fcf-af11-434e56127191_919x599.gif 1272w, /__u/substackcdn.com/image/fetch/$s_!n8A6!, /__u/bytesbtrees.substack.com/w_1456, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_webp, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4e1c36a1-5b58-4fcf-af11-434e56127191_919x599.gif 1456w" sizes="100vw"><img src="/__u/substackcdn.com/image/fetch/$s_!n8A6!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4e1c36a1-5b58-4fcf-af11-434e56127191_919x599.gif" width="919" height="599" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/4e1c36a1-5b58-4fcf-af11-434e56127191_919x599.gif&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:599,&quot;width&quot;:919,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:2716311,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/gif&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://bytesbtrees.substack.com/i/196526974?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4e1c36a1-5b58-4fcf-af11-434e56127191_919x599.gif&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="/__u/substackcdn.com/image/fetch/$s_!n8A6!, /__u/bytesbtrees.substack.com/w_424, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_auto, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4e1c36a1-5b58-4fcf-af11-434e56127191_919x599.gif 424w, /__u/substackcdn.com/image/fetch/$s_!n8A6!, /__u/bytesbtrees.substack.com/w_848, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_auto, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4e1c36a1-5b58-4fcf-af11-434e56127191_919x599.gif 848w, /__u/substackcdn.com/image/fetch/$s_!n8A6!, /__u/bytesbtrees.substack.com/w_1272, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_auto, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4e1c36a1-5b58-4fcf-af11-434e56127191_919x599.gif 1272w, /__u/substackcdn.com/image/fetch/$s_!n8A6!, /__u/bytesbtrees.substack.com/w_1456, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_auto, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4e1c36a1-5b58-4fcf-af11-434e56127191_919x599.gif 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p></p><p>The magnetic coating is divided into concentric circles called <strong>tracks</strong>. Each track is further divided into <strong>sectors</strong> &#8212; arc-shaped segments that are the smallest unit the drive can independently read or write. On modern drives, a sector is 4KB.</p><p>The heads are mounted on an <strong>actuator arm</strong> that pivots on a fixed point at the edge of the platters. To read a specific sector, the arm swings the head to the right track (seek), then waits for the platter&#8217;s rotation to bring the right sector under the head (rotational latency), and then reads continuously as the sector passes.</p><p>This is the physical geometry that every database storage decision must work around.</p><h3>Seek time: the most expensive thing a database can do</h3><p>Seek time is the time it takes for the actuator arm to swing to the correct track. This depends entirely on how far the head has to travel &#8212; from its current position to the target track. The average seek time on a modern HDD is 5 to 15 milliseconds for a random access. The minimum (adjacent tracks) is around 1ms. The maximum (full platter sweep) is around 20ms.</p><h3>Rotational latency: waiting for the platter</h3><p>Once the head is over the correct track, the platter still has to rotate until the right sector is underneath. This is rotational latency. At 7,200 RPM &#8212; a common server disk speed &#8212; the platter completes one full revolution in 8.3 milliseconds. Average rotational latency is half that: about 4ms. In the worst case (the sector just passed the head as it arrived), you wait almost a full revolution.</p><h3>Transfer time: the fast part</h3><p>Once the head is positioned correctly, data flows continuously at the drive&#8217;s full throughput &#8212; typically 100 to 200 MB/s for a modern HDD. Reading a single 8KB database page takes well under 0.1ms once the head is there. The data transfer is not the problem. The positioning is the problem.</p><h3>What this means for software</h3><p>The total random read cost &#8212; seek + rotational latency + transfer &#8212; is 7 to 23ms. But sequential reads pay the positioning cost only once, at the start. After that, data flows continuously. If you read 1,000 sequential 8KB pages, you pay one seek and then read at full throughput. If you read 1,000 random 8KB pages scattered across the disk, you pay the full 7&#8211;23ms positioning cost 1,000 times.</p><p>That&#8217;s the inequality every database storage engine is designed around:</p><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="/__u/substackcdn.com/image/fetch/$s_!GnPK!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0dd5e04e-7c3a-4ebd-9873-612dca2baa97_843x222.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="/__u/substackcdn.com/image/fetch/$s_!GnPK!, /__u/bytesbtrees.substack.com/w_424, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_webp, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0dd5e04e-7c3a-4ebd-9873-612dca2baa97_843x222.png 424w, /__u/substackcdn.com/image/fetch/$s_!GnPK!, /__u/bytesbtrees.substack.com/w_848, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_webp, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0dd5e04e-7c3a-4ebd-9873-612dca2baa97_843x222.png 848w, /__u/substackcdn.com/image/fetch/$s_!GnPK!, /__u/bytesbtrees.substack.com/w_1272, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_webp, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0dd5e04e-7c3a-4ebd-9873-612dca2baa97_843x222.png 1272w, /__u/substackcdn.com/image/fetch/$s_!GnPK!, /__u/bytesbtrees.substack.com/w_1456, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_webp, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0dd5e04e-7c3a-4ebd-9873-612dca2baa97_843x222.png 1456w" sizes="100vw"><img src="/__u/substackcdn.com/image/fetch/$s_!GnPK!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0dd5e04e-7c3a-4ebd-9873-612dca2baa97_843x222.png" width="843" height="222" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/0dd5e04e-7c3a-4ebd-9873-612dca2baa97_843x222.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:222,&quot;width&quot;:843,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:28966,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://bytesbtrees.substack.com/i/196526974?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0dd5e04e-7c3a-4ebd-9873-612dca2baa97_843x222.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="/__u/substackcdn.com/image/fetch/$s_!GnPK!, /__u/bytesbtrees.substack.com/w_424, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_auto, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0dd5e04e-7c3a-4ebd-9873-612dca2baa97_843x222.png 424w, /__u/substackcdn.com/image/fetch/$s_!GnPK!, /__u/bytesbtrees.substack.com/w_848, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_auto, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0dd5e04e-7c3a-4ebd-9873-612dca2baa97_843x222.png 848w, /__u/substackcdn.com/image/fetch/$s_!GnPK!, /__u/bytesbtrees.substack.com/w_1272, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_auto, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0dd5e04e-7c3a-4ebd-9873-612dca2baa97_843x222.png 1272w, /__u/substackcdn.com/image/fetch/$s_!GnPK!, /__u/bytesbtrees.substack.com/w_1456, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_auto, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0dd5e04e-7c3a-4ebd-9873-612dca2baa97_843x222.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a></figure></div><p>A factor of 100&#8211;300x difference. Not for a different workload. For the exact same 8MB of data, in different access patterns.</p><p>This is why database engineers obsess over access patterns. This is why B-trees cluster related keys on adjacent pages. This is why write-ahead logs append sequentially before touching scattered data pages. The entire structure of every storage engine you&#8217;ll study is a response to this single inequality.</p><div><hr></div><h2>Solid-state drives &#8212; what changed, what didn&#8217;t</h2><p>SSDs replaced mechanical components with NAND flash memory. No platters. No spindle. No actuator arm. No seek time. No rotational latency. The time to initiate a random read dropped from 7&#8211;23ms to 50&#8211;150 microseconds &#8212; a 100x improvement. Every database got meaningfully faster without any code changes just by replacing spinning disks with SSDs.</p><p>But &#8220;no moving parts&#8221; doesn&#8217;t mean &#8220;no constraints.&#8221; SSDs have their own geometry &#8212; different from HDDs, but equally consequential.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="/__u/substackcdn.com/image/fetch/$s_!6I_r!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa78802c0-e711-497c-92ff-17916f28a723_919x579.gif" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="/__u/substackcdn.com/image/fetch/$s_!6I_r!, /__u/bytesbtrees.substack.com/w_424, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_webp, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa78802c0-e711-497c-92ff-17916f28a723_919x579.gif 424w, /__u/substackcdn.com/image/fetch/$s_!6I_r!, /__u/bytesbtrees.substack.com/w_848, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_webp, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa78802c0-e711-497c-92ff-17916f28a723_919x579.gif 848w, /__u/substackcdn.com/image/fetch/$s_!6I_r!, /__u/bytesbtrees.substack.com/w_1272, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_webp, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa78802c0-e711-497c-92ff-17916f28a723_919x579.gif 1272w, /__u/substackcdn.com/image/fetch/$s_!6I_r!, /__u/bytesbtrees.substack.com/w_1456, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_webp, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa78802c0-e711-497c-92ff-17916f28a723_919x579.gif 1456w" sizes="100vw"><img src="/__u/substackcdn.com/image/fetch/$s_!6I_r!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa78802c0-e711-497c-92ff-17916f28a723_919x579.gif" width="919" height="579" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/a78802c0-e711-497c-92ff-17916f28a723_919x579.gif&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:579,&quot;width&quot;:919,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:213114,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/gif&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://bytesbtrees.substack.com/i/196526974?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa78802c0-e711-497c-92ff-17916f28a723_919x579.gif&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="/__u/substackcdn.com/image/fetch/$s_!6I_r!, /__u/bytesbtrees.substack.com/w_424, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_auto, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa78802c0-e711-497c-92ff-17916f28a723_919x579.gif 424w, /__u/substackcdn.com/image/fetch/$s_!6I_r!, /__u/bytesbtrees.substack.com/w_848, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_auto, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa78802c0-e711-497c-92ff-17916f28a723_919x579.gif 848w, /__u/substackcdn.com/image/fetch/$s_!6I_r!, /__u/bytesbtrees.substack.com/w_1272, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_auto, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa78802c0-e711-497c-92ff-17916f28a723_919x579.gif 1272w, /__u/substackcdn.com/image/fetch/$s_!6I_r!, /__u/bytesbtrees.substack.com/w_1456, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_auto, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa78802c0-e711-497c-92ff-17916f28a723_919x579.gif 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><h3>NAND flash structure: pages and blocks</h3><p>NAND flash is organised in a hierarchy. At the bottom: individual <strong>cells</strong>, each storing 1 to 4 bits depending on the type of flash (SLC, MLC, TLC, QLC). Cells are grouped into <strong>pages</strong>, typically 4KB to 16KB &#8212; the smallest unit that can be read or written. Pages are grouped into <strong>blocks</strong>, typically 128 to 512 pages &#8212; typically 256KB to 4MB of storage per block.</p><p>Here is the constraint that matters: <strong>you can only write to a page that has been erased. And you can only erase at the block level, not the page level.</strong></p><p>This means you cannot update a single page in place. To overwrite a 4KB page, the SSD must:</p><ol><li><p>Read the entire block containing that page into a buffer (~256KB)</p></li><li><p>Erase the entire block (a relatively slow operation, 1&#8211;5ms)</p></li><li><p>Modify the target page in the buffer</p></li><li><p>Write the entire block back to flash</p></li></ol><p>The SSD firmware handles this transparently &#8212; to your operating system, it looks like any other random write. But under the hood, updating 4KB of data causes the SSD to write 256KB to 512KB of data. This ratio &#8212; bytes actually written to flash divided by bytes the application intended to write &#8212; is called <strong>write amplification</strong>. On a busy database with many small random writes, write amplification factors of 10x to 30x are common.</p><h3>Wear levelling: why SSDs have a write budget</h3><p>Every erase cycle degrades the oxide layer in each flash cell. Consumer NAND flash cells are rated for roughly 3,000 erase cycles; enterprise multi-level cell (MLC) flash for around 100,000. Once a cell exceeds its cycle limit, it can no longer reliably hold charge &#8212; it starts flipping bits silently.</p><p>SSD firmware addresses this with <strong>wear levelling</strong>: it tracks the erase cycle count of every block and deliberately spreads writes across all blocks rather than hammering the same ones repeatedly. This is why an SSD that receives concentrated sequential writes in one region doesn&#8217;t wear out that region faster than the rest &#8212; the firmware is constantly moving data around in the background.</p><p>This background management means SSD write performance isn&#8217;t constant. Under heavy sustained write load, the firmware is simultaneously handling new writes, garbage-collecting blocks that are partially valid, and doing wear levelling relocations. This is what causes SSD write performance to degrade under sustained pressure &#8212; <strong>write cliff</strong> &#8212; a phenomenon that matters a lot for database write workloads.</p><h3>NVMe vs SATA: the interface matters too</h3><p>Early SSDs plugged into the SATA interface &#8212; the same connector used for spinning disks. SATA&#8217;s maximum throughput is ~550MB/s sequential, and its command queue depth is limited to 32 commands. An NVMe SSD connected directly to the PCIe bus has no such limitations: current NVMe drives deliver 3&#8211;7GB/s sequential and support command queue depths of 65,535. For a database receiving thousands of concurrent I/O requests, that queue depth difference matters enormously.</p><p>The practical takeaway: &#8220;SSD&#8221; is not one thing. A SATA SSD in a database server is meaningfully different from an NVMe SSD, which is meaningfully different from enterprise-grade NVMe with power-loss protection capacitors. The device class determines the performance envelope your database operates within.</p><div><hr></div><h2>The OS I/O stack &#8212; what happens between your code and the hardware</h2><p>When your database calls <code>write()</code> to save a WAL record, that data does not immediately go to the SSD or HDD. It goes through a stack of OS layers, each adding behaviour and each potentially delaying or buffering the actual hardware write.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="/__u/substackcdn.com/image/fetch/$s_!i4kv!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F48fd6ab2-b9b3-465b-9e78-3a4fc94520a7_919x599.gif" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="/__u/substackcdn.com/image/fetch/$s_!i4kv!, /__u/bytesbtrees.substack.com/w_424, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_webp, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F48fd6ab2-b9b3-465b-9e78-3a4fc94520a7_919x599.gif 424w, /__u/substackcdn.com/image/fetch/$s_!i4kv!, /__u/bytesbtrees.substack.com/w_848, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_webp, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F48fd6ab2-b9b3-465b-9e78-3a4fc94520a7_919x599.gif 848w, /__u/substackcdn.com/image/fetch/$s_!i4kv!, /__u/bytesbtrees.substack.com/w_1272, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_webp, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F48fd6ab2-b9b3-465b-9e78-3a4fc94520a7_919x599.gif 1272w, /__u/substackcdn.com/image/fetch/$s_!i4kv!, /__u/bytesbtrees.substack.com/w_1456, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_webp, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F48fd6ab2-b9b3-465b-9e78-3a4fc94520a7_919x599.gif 1456w" sizes="100vw"><img src="/__u/substackcdn.com/image/fetch/$s_!i4kv!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F48fd6ab2-b9b3-465b-9e78-3a4fc94520a7_919x599.gif" width="919" height="599" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/48fd6ab2-b9b3-465b-9e78-3a4fc94520a7_919x599.gif&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:599,&quot;width&quot;:919,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:146207,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/gif&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://bytesbtrees.substack.com/i/196526974?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F48fd6ab2-b9b3-465b-9e78-3a4fc94520a7_919x599.gif&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="/__u/substackcdn.com/image/fetch/$s_!i4kv!, /__u/bytesbtrees.substack.com/w_424, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_auto, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F48fd6ab2-b9b3-465b-9e78-3a4fc94520a7_919x599.gif 424w, /__u/substackcdn.com/image/fetch/$s_!i4kv!, /__u/bytesbtrees.substack.com/w_848, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_auto, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F48fd6ab2-b9b3-465b-9e78-3a4fc94520a7_919x599.gif 848w, /__u/substackcdn.com/image/fetch/$s_!i4kv!, /__u/bytesbtrees.substack.com/w_1272, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_auto, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F48fd6ab2-b9b3-465b-9e78-3a4fc94520a7_919x599.gif 1272w, /__u/substackcdn.com/image/fetch/$s_!i4kv!, /__u/bytesbtrees.substack.com/w_1456, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_auto, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F48fd6ab2-b9b3-465b-9e78-3a4fc94520a7_919x599.gif 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><h3>Layer 1: Virtual File System (VFS)</h3><p>The VFS is a kernel abstraction that presents a unified file system interface regardless of the underlying file system type. Whether Postgres is storing files on ext4, XFS, or a network file system, the database makes the same <code>read()</code>, <code>write()</code>, and <code>fsync()</code> calls. The VFS routes them to the appropriate file system driver.</p><h3>Layer 2: OS Page Cache &#8212; the silent middleman</h3><blockquote><p>This is the most important layer for database engineers to understand. The OS page cache is a region of kernel-managed RAM that transparently caches file data. When your application calls <code>write()</code>, the data lands in the page cache first. The kernel acknowledges the write as complete immediately, then schedules the actual disk write for later &#8212; in batches, at its own convenience, optimised for throughput.</p></blockquote><p>From your application&#8217;s perspective: the write succeeded. From the hardware&#8217;s perspective: nothing has happened yet.</p><p>This is not a bug. For most applications, this behaviour is desirable &#8212; it makes writes fast and amortises disk I/O. But for a database that has promised durability, it is a serious problem. If the machine loses power after a <code>write()</code> but before the kernel flushes the page cache to disk, the data is gone. The write returned success. The OS confirmed it. And the data still doesn&#8217;t exist on any non-volatile medium.</p><h3>Layer 3: fsync() &#8212; the bypass</h3><p><code>fsync()</code> is the system call that forces the OS to flush all buffered writes for a file descriptor to the storage device and wait until the device confirms. It bypasses the &#8220;defer to later&#8221; behaviour of the page cache. When Postgres calls <code>fsync()</code> after writing a WAL record, it blocks until that record is confirmed on physical storage &#8212; not just in the OS page cache.</p><p>This is expensive. On an HDD: 10&#8211;30ms. On an SSD: 500&#181;s to 5ms. On a busy system processing many commits per second, <code>fsync()</code> cost is often the single largest contributor to write latency.</p><blockquote><p>This is why the WAL exists. Instead of fsyncing every modified table page on every commit &#8212; which could be gigabytes of data &#8212; Postgres writes a compact log record (typically a few hundred bytes), fsyncs that, and returns the commit. The actual table pages are written to disk asynchronously later. The WAL is the minimum durable record needed to reconstruct whatever the async write hasn&#8217;t done yet.</p></blockquote><h3>Layer 4: O_DIRECT &#8212; going around the page cache entirely</h3><p>Some databases bypass the OS page cache entirely using the <code>O_DIRECT</code> flag when opening files. With <code>O_DIRECT</code>, all reads and writes go directly to the storage hardware, bypassing the kernel&#8217;s caching. The database manages its own in-memory page cache (Postgres&#8217;s <code>shared_buffers</code>), which it can tune precisely.</p><p><code>O_DIRECT</code> avoids double-buffering (the same data being cached both in the database&#8217;s buffer pool and the OS page cache), and it gives the database full control over what&#8217;s in memory. MySQL&#8217;s InnoDB uses <code>O_DIRECT</code> by default. Postgres does not by default, though it has an option for it.</p><p>The trade-off: with <code>O_DIRECT</code>, if the database crashes between a <code>write()</code> and an <code>fsync()</code>, the data is gone &#8212; there&#8217;s no OS page cache copy to fall back on. The database must handle this entirely with its own WAL and recovery mechanisms. This is fine for a well-designed database engine, but it means the durability guarantees live entirely in database code, not partially in the OS.</p><h3>Layer 5: I/O scheduler</h3><p>The block layer sits between the file system and the device driver. It maintains a queue of pending I/O requests and reorders them for throughput &#8212; for example, merging adjacent reads into a single larger request, or reordering requests to minimise seek distance on an HDD.</p><p>For SSDs, aggressive scheduling is counterproductive &#8212; there&#8217;s no seek cost to optimise for, and the reordering just adds latency. Modern Linux kernels use <code>mq-deadline</code> or <code>none</code> for NVMe devices, and configure the scheduler accordingly.</p><p>For HDDs on database workloads, the scheduler can meaningfully improve throughput by converting random access patterns into more sequential ones. But this is increasingly irrelevant as HDDs disappear from primary storage use cases.</p><div><hr></div><h2>What this means for database configuration</h2><p>Understanding the I/O stack makes several Postgres configuration knobs make sense immediately:</p><p><code>shared_buffers</code> &#8212; Postgres&#8217;s in-memory page cache. The larger this is, the more frequently reads are served from RAM rather than triggering OS page cache or disk reads. The general recommendation is 25% of total RAM, but the right value is &#8220;as much as you can give it while leaving room for OS and other processes.&#8221;</p><p><code>wal_sync_method</code> &#8212; controls how Postgres calls fsync. Options include <code>fsync</code> (the default, uses <code>fsync()</code>), <code>fdatasync</code> (slightly more efficient, skips fsyncing file metadata), <code>open_sync</code>, and <code>open_datasync</code> (use <code>O_SYNC</code>/<code>O_DSYNC</code> flags). The right choice depends on your OS and storage hardware.</p><p><code>synchronous_commit</code> &#8212; when set to <code>off</code>, Postgres returns commit confirmation to the application before fsyncing the WAL. This dramatically increases write throughput at the cost of potentially losing the last few transactions in a crash. Acceptable for some workloads (logging, analytics). Never acceptable for financial transactions.</p><p><code>checkpoint_completion_target</code> &#8212; controls how aggressively Postgres writes dirty (modified) pages from shared_buffers to disk between checkpoints. A lower value means more I/O concentrated at checkpoint time; a higher value spreads it out. The goal is keeping I/O smooth so the storage device doesn&#8217;t get hammered with write bursts.</p><div><hr></div><h2>The practical mental model</h2><p>Two facts to carry forward from this issue into every future discussion of database storage:</p><p><strong>1. Random I/O is fundamentally different from sequential I/O.</strong> On HDDs, random reads cost 100&#8211;300x more than sequential reads for the same data volume. On SSDs, the gap is ~10x. This inequality never goes away &#8212; it just gets smaller. Every database storage structure is an attempt to turn random access patterns into sequential ones.</p><p><strong>2. The OS page cache lies.</strong> A successful <code>write()</code> does not mean data is on disk. A successful <code>fsync()</code> does. The distinction is the foundation of database durability. When your database guarantees that a committed transaction will survive a power failure, it means there was a successful <code>fsync()</code> somewhere in the commit path.</p><p>When you see a WAL, a write-ahead log, a redo log, or a journal in any storage system, you are looking at the same idea: a compact sequential append that gets fsynced before the commit is acknowledged, so that the database can recover the full state from just the log if everything else is lost.</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://bytesbtrees.substack.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en-gb&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Thanks for reading Bytes &amp; B-Tree's! Subscribe for free to receive new posts and support my work.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div>]]></content:encoded></item><item><title><![CDATA[172 issues, completely mapped — every concept, every track, every connection]]></title><description><![CDATA[This is the syllabus.]]></description><link>https://bytesbtrees.substack.com/p/172-issues-completely-mapped-every</link><guid isPermaLink="false">https://bytesbtrees.substack.com/p/172-issues-completely-mapped-every</guid><dc:creator><![CDATA[System Design Roadmap]]></dc:creator><pubDate>Thu, 07 May 2026 18:25:49 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!ocQW!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4f5a8467-4a86-47ee-9063-160926d3fec4_1366x1232.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><em>This is the syllabus. All three years. Everything we&#8217;ll cover, in order, with enough detail that you can decide whether this newsletter is right for you before reading a single issue.</em></p><div><hr></div><p><strong>Bytes &amp; B-trees</strong> | Pre-launch #3 of 3 | Free | Bookmark recommended</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://bytesbtrees.substack.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en-gb&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Thanks for reading Bytes &amp; B-Tree's! Subscribe for free to receive new posts and support my work.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><div><hr></div><p>Most online courses and newsletters launch with a vague promise: &#8220;we&#8217;ll cover everything you need to know.&#8221; This is not that.</p><p>What follows is the exact plan for every issue of Bytes &amp; B-trees &#8212; the 10 content tracks, 172 individual issues mapped by quarter, the three Build It Yourself implementation series, the 12 annotated research papers, and the company case studies. Read this before you subscribe. If what you see here doesn&#8217;t match what you want to learn, this isn&#8217;t the newsletter for you. That&#8217;s fine &#8212; we&#8217;d rather you know now.</p><div><hr></div><h2>The 10 content tracks</h2><p>Every issue belongs to one or two of ten tracks. These describe the primary lens through which the concept is examined.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="/__u/substackcdn.com/image/fetch/$s_!ocQW!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4f5a8467-4a86-47ee-9063-160926d3fec4_1366x1232.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="/__u/substackcdn.com/image/fetch/$s_!ocQW!, /__u/bytesbtrees.substack.com/w_424, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_webp, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4f5a8467-4a86-47ee-9063-160926d3fec4_1366x1232.png 424w, /__u/substackcdn.com/image/fetch/$s_!ocQW!, /__u/bytesbtrees.substack.com/w_848, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_webp, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4f5a8467-4a86-47ee-9063-160926d3fec4_1366x1232.png 848w, /__u/substackcdn.com/image/fetch/$s_!ocQW!, /__u/bytesbtrees.substack.com/w_1272, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_webp, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4f5a8467-4a86-47ee-9063-160926d3fec4_1366x1232.png 1272w, /__u/substackcdn.com/image/fetch/$s_!ocQW!, /__u/bytesbtrees.substack.com/w_1456, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_webp, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4f5a8467-4a86-47ee-9063-160926d3fec4_1366x1232.png 1456w" sizes="100vw"><img src="/__u/substackcdn.com/image/fetch/$s_!ocQW!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4f5a8467-4a86-47ee-9063-160926d3fec4_1366x1232.png" width="596" height="537.5344070278185" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/4f5a8467-4a86-47ee-9063-160926d3fec4_1366x1232.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:1232,&quot;width&quot;:1366,&quot;resizeWidth&quot;:596,&quot;bytes&quot;:248556,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://bytesbtrees.substack.com/i/196677476?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4f5a8467-4a86-47ee-9063-160926d3fec4_1366x1232.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="/__u/substackcdn.com/image/fetch/$s_!ocQW!, /__u/bytesbtrees.substack.com/w_424, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_auto, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4f5a8467-4a86-47ee-9063-160926d3fec4_1366x1232.png 424w, /__u/substackcdn.com/image/fetch/$s_!ocQW!, /__u/bytesbtrees.substack.com/w_848, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_auto, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4f5a8467-4a86-47ee-9063-160926d3fec4_1366x1232.png 848w, /__u/substackcdn.com/image/fetch/$s_!ocQW!, /__u/bytesbtrees.substack.com/w_1272, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_auto, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4f5a8467-4a86-47ee-9063-160926d3fec4_1366x1232.png 1272w, /__u/substackcdn.com/image/fetch/$s_!ocQW!, /__u/bytesbtrees.substack.com/w_1456, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_auto, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4f5a8467-4a86-47ee-9063-160926d3fec4_1366x1232.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p><strong>Total: 172 issues &#183; 3 years &#183; ~6,000 words each &#183; 52 issues/year</strong></p><div><hr></div><h2>Year 1 &#8212; Issues #001 to #056</h2><h3>Foundations: from storage physics to the full database taxonomy</h3><div><hr></div><h3>Q1 (Issues #001&#8211;014): Why databases exist + Build It Yourself #1</h3><p><strong>#001</strong> Data, storage &amp; memory &#8212; first principles<br><strong>#002</strong> Disk geometry &amp; the I/O stack &#8212; seek time, rotational latency, and what SSDs changed<br><strong>#003</strong> Files, block devices &amp; the OS abstraction &#8212; where databases actually write<br><strong>#004</strong> Why flat files fail &#8212; the six problems that forced databases into existence<br><strong>#005</strong> IMS, CODASYL &amp; the hierarchical model &#8212; what came before relational<br><strong>#006</strong> CODD&#8217;s relational model &#8212; the idea that changed everything<br><strong>#007</strong> Atomicity at the byte level &#8212; what &#8220;all or nothing&#8221; actually means in hardware<br><strong>#008</strong> Durability &#8212; what fsync guarantees, and what it doesn&#8217;t<br><strong>#009</strong> Isolation levels visualised &#8212; dirty reads, phantom reads, serialisable anomalies<br><strong>#010</strong> SQL and relational algebra &#8212; the formal foundation behind the syntax<br><strong>#011</strong> NULLs, three-valued logic &amp; the SQL traps &#8212; why NULL is not zero or empty<br><strong>#012</strong> &#128296; <em>Build It Yourself #1 &#8212; Part 1:</em> a key-value store: crash-safe writes &amp; file pointers<br><strong>#013</strong> &#128296; <em>Build It Yourself #1 &#8212; Part 2:</em> adding a hash index<br><strong>#014</strong> &#128296; <em>Build It Yourself #1 &#8212; Part 3:</em> adding a write-ahead log &amp; crash recovery</p><blockquote><p><strong>By the end of Q1:</strong> You understand why every ACID property exists at the hardware level, and you have a working key-value store that demonstrates all of it.</p></blockquote><div><hr></div><h3>Q2 (Issues #015&#8211;029): Storage engines &amp; indexing</h3><p><strong>#015</strong> Pages, heaps &amp; row storage anatomy &#8212; how Postgres lays out rows on disk<br><strong>#016</strong> TOAST &amp; variable-length columns &#8212; how Postgres stores text and JSON that doesn&#8217;t fit<br><strong>#017</strong> Row vs columnar storage &#8212; when each wins and why the gap is enormous<br><strong>#018</strong> B-trees &#8212; the balanced tree on disk: why this structure, why this shape<br><strong>#019</strong> B+ trees &#8212; the leaf-linked difference that makes range scans fast<br><strong>#020</strong> Index-only scans, covering indexes &amp; fill factor &#8212; the performance levers<br><strong>#021</strong> Index bloat, splits &amp; the cost of maintenance &#8212; what indexes cost to keep<br><strong>#022</strong> &#10024; <em>GIN &#8212; generalised inverted indexes:</em> full-text search &amp; JSONB under the hood<br><strong>#023</strong> &#10024; <em>GiST &amp; BRIN &#8212; the specialist indexes</em> most engineers never encounter<br><strong>#024</strong> Hash indexes &amp; bloom filters &#8212; when equality beats range lookup<br><strong>#025</strong> Bitmaps, skip lists &amp; low-cardinality index structures<br><strong>#026</strong> LSM trees &#8212; why writes are cheap: the write path from memtable to SSTables<br><strong>#027</strong> SSTables, compaction &amp; write amplification &#8212; the cost the write path defers<br><strong>#028</strong> LSM vs B-tree &#8212; the fundamental trade-off that shapes engine selection<br><strong>#029</strong> RocksDB internals &#8212; the LSM tree in production at Meta, Cassandra, and TiKV</p><blockquote><p><strong>By the end of Q2:</strong> You understand every major index structure &#8212; why it exists, how it works physically, and when to choose it.</p></blockquote><div><hr></div><h3>Q3 (Issues #030&#8211;042): Query execution &amp; concurrency</h3><p><strong>#030</strong> Parse &#8594; plan &#8594; execute &#8212; the full lifecycle of a SQL query<br><strong>#031</strong> Cost-based optimisers &amp; table statistics &#8212; how Postgres decides what to do<br><strong>#032</strong> Reading EXPLAIN like an expert &#8212; every node type, every cost field<br><strong>#033</strong> Nested loop, hash join &amp; merge join &#8212; when each algorithm fires and why<br><strong>#034</strong> Join order &amp; why it matters exponentially &#8212; the combinatorial explosion<br><strong>#035</strong> Parallel query execution &#8212; when Postgres uses multiple workers, when it doesn&#8217;t<br><strong>#036</strong> Two-phase locking in depth &#8212; how row-level locking actually works<br><strong>#037</strong> MVCC &#8212; why it&#8217;s the hardest thing in databases<br><strong>#038</strong> Vacuum, dead tuples &amp; table bloat &#8212; the cost of MVCC deferred<br><strong>#039</strong> Optimistic concurrency &amp; serialisable snapshot isolation in practice<br><strong>#040</strong> &#128196; <em>Annotated paper: The ARIES algorithm</em> &#8212; the paper that defined crash recovery<br><strong>#041</strong> WAL, redo logs &amp; the crash recovery sequence &#8212; ARIES in Postgres<br><strong>#042</strong> InnoDB&#8217;s doublewrite buffer &#8212; why it exists and what it costs</p><blockquote><p><strong>By the end of Q3:</strong> You understand how a query goes from text to result, how concurrent transactions are isolated, and how databases recover from crashes.</p></blockquote><div><hr></div><h3>Q4 (Issues #043&#8211;056): Database taxonomy + Build It Yourself #2</h3><p><strong>#043</strong> NoSQL &#8212; what it genuinely gave up and what it genuinely gained<br><strong>#044</strong> Document stores &amp; the BSON choice &#8212; why MongoDB stores data the way it does<br><strong>#045</strong> Key-value stores &#8212; simplicity as a deliberate design choice<br><strong>#046</strong> Column-family stores &amp; the wide-row model &#8212; Cassandra&#8217;s data model explained<br><strong>#047</strong> Time-series databases &amp; hypertable partitioning &#8212; TimescaleDB&#8217;s approach<br><strong>#048</strong> ClickHouse&#8217;s MergeTree &#8212; columnar storage for analytical workloads<br><strong>#049</strong> Graph databases &#8212; when property graphs beat recursive SQL<br><strong>#050</strong> The database selection framework &#8212; a decision tool for any workload<br><strong>#051</strong> OLTP vs OLAP &#8212; the architectural divide and why it matters<br><strong>#052</strong> Polyglot persistence &#8212; when one database genuinely isn&#8217;t enough<br><strong>#053</strong> Year 1 synthesis &#8212; your complete mental model, connected<br><strong>#054</strong> &#128296; <em>Build It Yourself #2 &#8212; Part 1:</em> parsing SQL into an AST<br><strong>#055</strong> &#128296; <em>Build It Yourself #2 &#8212; Part 2:</em> building a logical plan<br><strong>#056</strong> &#128296; <em>Build It Yourself #2 &#8212; Part 3:</em> executing filter, projection &amp; hash join</p><blockquote><p><strong>By the end of Year 1:</strong> You understand the full database landscape from first principles. You have built a key-value store and a working query engine. You can explain any database&#8217;s core behaviour from the bottom of the storage stack.</p></blockquote><div><hr></div><h2>Year 2 &#8212; Issues #057 to #113</h2><h3>Internals &amp; Scale: real engines, distributed systems, and performance mathematics</h3><div><hr></div><h3>Q1 (Issues #057&#8211;069): Distributed foundations</h3><p><strong>#057</strong> The 8 fallacies of distributed computing &#8212; each one a class of bugs<br><strong>#058</strong> Network partitions &amp; partial failures &#8212; why distributed systems are fundamentally harder<br><strong>#059</strong> CAP theorem &#8212; what it actually claims, what it doesn&#8217;t, and how it&#8217;s misused<br><strong>#060</strong> Primary-replica replication anatomy &#8212; the standard topology dissected<br><strong>#061</strong> Statement vs row vs logical replication &#8212; the trade-offs in each format<br><strong>#062</strong> Replication lag, read-your-writes &amp; monotonic reads &#8212; consistency guarantees<br><strong>#063</strong> Multi-master &amp; leaderless topologies &#8212; when and why<br><strong>#064</strong> FLP impossibility &#8212; why consensus is provably hard in asynchronous systems<br><strong>#065</strong> &#128196; <em>Annotated paper: Paxos Made Simple</em> &#8212; Lamport&#8217;s algorithm, actually explained<br><strong>#066</strong> &#128196; <em>Annotated paper: Raft</em> &#8212; leader election with pictures<br><strong>#067</strong> Raft &#8212; log replication, safety guarantees, and membership changes<br><strong>#068</strong> etcd, CockroachDB &amp; TiKV&#8217;s Raft implementations &#8212; theory in production code<br><strong>#069</strong> Why wall-clock time is dangerous in distributed systems</p><div><hr></div><h3>Q2 (Issues #070&#8211;084): Real engine dissections</h3><p><strong>#070</strong> Lamport timestamps &amp; vector clocks &#8212; establishing order without a shared clock<br><strong>#071</strong> &#128196; <em>Annotated paper: Google TrueTime</em> &#8212; atomic clocks, GPS, and Spanner&#8217;s clock API<br><strong>#072</strong> Hybrid logical clocks in CockroachDB &#8212; the practical compromise<br><strong>#073</strong> Postgres buffer manager &amp; shared_buffers &#8212; the most important tuning knob<br><strong>#074</strong> Postgres query planner in depth &#8212; join reordering, partition pruning, statistics<br><strong>#075</strong> Postgres MVCC snapshots &amp; visibility rules &#8212; which row version does a query see?<br><strong>#076</strong> Postgres extensions architecture &#8212; how PostGIS and pgvector plug into the engine<br><strong>#077</strong> MySQL InnoDB &#8212; clustered indexes, gap locks &amp; next-key locks<br><strong>#078</strong> InnoDB binlog and the replication story<br><strong>#079</strong> &#10024; <em>Apache Arrow</em> &#8212; the universal columnar memory format<br><strong>#080</strong> &#10024; <em>DuckDB</em> &#8212; the in-process analytics revolution and why it&#8217;s architecturally novel<br><strong>#081</strong> Cassandra &#8212; consistent hashing ring, vnodes, quorum reads &amp; anti-entropy<br><strong>#082</strong> &#128196; <em>Annotated paper: Dynamo</em> &#8212; Amazon&#8217;s highly available key-value store, paragraph by paragraph<br><strong>#083</strong> Redis &#8212; eviction policies, RDB vs AOF, cluster slot-based sharding<br><strong>#084</strong> Redis Streams as a persistent event log</p><div><hr></div><h3>Q3 (Issues #085&#8211;100): Sharding, cloud databases &amp; OLAP at scale</h3><p><strong>#085</strong> Sharding strategies &#8212; range, hash &amp; directory: what each costs<br><strong>#086</strong> Hot keys, vnodes &amp; resharding without downtime<br><strong>#087</strong> Vitess &#8212; MySQL sharding at YouTube scale<br><strong>#088</strong> CockroachDB &#8212; distributed SQL from the ground up<br><strong>#089</strong> &#128196; <em>Annotated papers: Spanner &amp; F1</em> &#8212; the two papers that changed distributed databases<br><strong>#090</strong> TiDB &amp; YugabyteDB &#8212; the NewSQL generation<br><strong>#091</strong> &#10024; <em>Benchmark methodology</em> &#8212; how to run honest performance comparisons<br><strong>#092</strong> &#10024; <em>Benchmarking SurrealDB, EdgeDB &amp; Turso</em> &#8212; reproducible, methodology-first<br><strong>#093</strong> &#128196; <em>Annotated paper: Amazon Aurora</em> &#8212; why &#8220;the log is the database&#8221;<br><strong>#094</strong> Neon &#8212; copy-on-write branching for Postgres<br><strong>#095</strong> PlanetScale &#8212; online schema changes at scale<br><strong>#096</strong> Serverless databases &#8212; economics, cold starts &amp; architectural implications<br><strong>#097</strong> Snowflake &#8212; compute/storage separation dissected<br><strong>#098</strong> BigQuery&#8217;s Dremel engine &#8212; nested columnar storage<br><strong>#099</strong> ClickHouse vectorised execution internals<br><strong>#100</strong> The modern data stack &#8212; where the seams are</p><div><hr></div><h3>Q4 (Issues #101&#8211;113): Performance engineering + Build It Yourself #3</h3><p><strong>#101</strong> Query optimisation &#8212; a systematic methodology beyond &#8220;add an index&#8221;<br><strong>#102</strong> Index design methodology &#8212; choosing the right index for the right query pattern<br><strong>#103</strong> Statistics staleness &amp; its cascading effect on query plans<br><strong>#104</strong> Materialised views, lateral joins &amp; window functions<br><strong>#105</strong> Schema design for write performance &#8212; normalisation and its limits<br><strong>#106</strong> Schema design for read performance &#8212; denormalisation and its costs<br><strong>#107</strong> PgBouncer modes, ProxySQL &amp; the connection pool design space<br><strong>#108</strong> &#10024; <em>Queuing theory</em> &#8212; Little&#8217;s Law, M/M/1 queues &amp; database capacity planning mathematics<br><strong>#109</strong> The Linux page cache &amp; how databases fight it<br><strong>#110</strong> io_uring &#8212; what it means for database I/O in practice<br><strong>#111</strong> NVMe topology, NUMA effects &amp; huge pages<br><strong>#112</strong> OS &amp; kernel tuning for database workloads &#8212; the full checklist<br><strong>#113</strong> &#128296; <em>Build It Yourself #3:</em> a minimal replication log &#8212; primary, replica &amp; failover</p><blockquote><p><strong>By the end of Year 2:</strong> You can dissect real database source code, design distributed architectures, size a connection pool using queuing mathematics, and explain every major cloud database&#8217;s architectural trade-offs.</p></blockquote><div><hr></div><h2>Year 3 &#8212; Issues #114 to #172</h2><h3>Frontier, ultra-scale &amp; synthesis: what&#8217;s next and how it all connects</h3><div><hr></div><h3>Q1 (Issues #114&#8211;128): The emerging landscape</h3><p><strong>#114</strong> Approximate nearest neighbour &#8212; the ANN problem and why exact search doesn&#8217;t scale<br><strong>#115</strong> HNSW &#8212; how hierarchical navigable small worlds work<br><strong>#116</strong> IVF, FAISS &amp; product quantisation<br><strong>#117</strong> pgvector vs dedicated vector databases &#8212; an honest comparison<br><strong>#118</strong> &#10024; <em>RAG Part 1</em> &#8212; chunking strategies and their effect on recall quality<br><strong>#119</strong> &#10024; <em>RAG Part 2</em> &#8212; embedding pipeline design, staleness &amp; update latency<br><strong>#120</strong> &#10024; <em>RAG Part 3</em> &#8212; hybrid search: BM25 + vector + re-ranking models<br><strong>#121</strong> &#10024; <em>RAG Part 4</em> &#8212; the full data architecture behind a production RAG system<br><strong>#122</strong> NL-to-SQL &#8212; where it works, where it fails, and why<br><strong>#123</strong> LLM-assisted query optimisation &#8212; research vs production reality<br><strong>#124</strong> Edge databases &#8212; SQLite&#8217;s architecture, Turso &amp; libSQL<br><strong>#125</strong> CRDTs &#8212; offline-first sync without a coordination server<br><strong>#126</strong> ElectricSQL &amp; local-first architecture<br><strong>#127</strong> Materialize &amp; differential dataflow<br><strong>#128</strong> RisingWave &#8212; streaming SQL in production</p><div><hr></div><h3>Q2 (Issues #129&#8211;143): Research frontiers + company case studies</h3><p><strong>#129</strong> Exactly-once semantics across stream and store<br><strong>#130</strong> &#128196; <em>Annotated paper: The Case for Learned Index Structures</em> &#8212; Kraska et al<br><strong>#131</strong> Learned cardinality estimation &#8212; replacing histograms with models<br><strong>#132</strong> Bao &#8212; learning to optimise queries from execution feedback<br><strong>#133</strong> &#127970; <em>Company case: Netflix</em> &#8212; Cassandra, EVCache &amp; the viewing history data model<br><strong>#134</strong> &#127970; <em>Company case: Discord</em> &#8212; MongoDB &#8594; Cassandra &#8594; ScyllaDB, the full story<br><strong>#135</strong> &#127970; <em>Company case: Stripe</em> &#8212; sharding Postgres when one instance isn&#8217;t enough<br><strong>#136</strong> &#127970; <em>Company case: Uber</em> &#8212; Schemaless/Docstore for trip data at scale<br><strong>#137</strong> &#127970; <em>Company case: Cloudflare D1</em> &#8212; SQLite at the edge, globally<br><strong>#138</strong> &#127970; <em>Company case: Figma</em> &#8212; scaling Postgres from one machine to many<br><strong>#139</strong> &#128196; <em>Annotated paper: Napa</em> &#8212; powering scalable data warehousing at Google<br><strong>#140</strong> CXL memory pooling &amp; what it means for buffer management<br><strong>#141</strong> Persistent memory &#8212; what Optane taught the database industry<br><strong>#142</strong> TLA+ for distributed database protocol verification<br><strong>#143</strong> Jepsen &#8212; how Kyle Kingsbury breaks databases</p><div><hr></div><h3>Q3&#8211;Q4 (Issues #144&#8211;172): Ultra-scale design &amp; 3-year synthesis</h3><p><strong>#144</strong> Access pattern analysis for billion-row systems<br><strong>#145</strong> Partition key design &#8212; avoiding the hot key disaster<br><strong>#146</strong> Index topology for tables at extreme scale<br><strong>#147</strong> Geo-replication &#8212; designing for global latency<br><strong>#148</strong> Multi-region consistency patterns in production<br><strong>#149</strong> CRDTs in production &#8212; the failure modes nobody warns you about<br><strong>#150</strong> Event sourcing &#8212; the immutable log as a database<br><strong>#151</strong> Event sourcing pitfalls nobody warns you about<br><strong>#152</strong> CQRS &#8212; when and why to separate reads from writes<br><strong>#153</strong> Zero-downtime schema migrations at scale<br><strong>#154</strong> Database chaos engineering &#8212; breaking things safely<br><strong>#155</strong> Row-level security in depth<br><strong>#156</strong> GDPR right-to-erasure in an append-only system<br><strong>#157</strong> Audit logging without killing performance<br><strong>#158</strong> The people: Edgar F. CODD &#8212; the relational model and its reception<br><strong>#159</strong> The people: Michael Stonebraker &#8212; Ingres, Postgres, and 40 years of database design<br><strong>#160</strong> The people: Jim Gray &#8212; transactions, fault tolerance, and the 1998 Turing Award<br><strong>#161</strong> The people: Leslie Lamport &#8212; clocks, consensus, and distributed systems theory<br><strong>#162</strong> 50 years of databases &#8212; the full economic and technical arc<br><strong>#163</strong> Where storage hardware is going &#8212; NVMe, CXL, photonic interconnects<br><strong>#164</strong> Post-SQL query models &#8212; what comes after the relational interface<br><strong>#165</strong> The AI query interface endgame &#8212; where NL-to-SQL leads<br><strong>#166</strong> The 3-year concept map &#8212; Part 1: Years 1 &amp; 2 connected<br><strong>#167</strong> The 3-year concept map &#8212; Part 2: Year 3 and the full picture<br><strong>#168</strong> The complete trade-off matrix &#8212; every design decision, compiled<br><strong>#169</strong> The database selection framework &#8212; final version<br><strong>#170</strong> The performance engineering playbook &#8212; the full checklist<br><strong>#171</strong> The distributed systems decision guide<br><strong>#172</strong> Issue #172 &#8212; the final synthesis: 172 concepts, one mental model</p><blockquote><p><strong>By the end of Year 3:</strong> You can design systems for billions of records, understand and critique database research papers, explain the full 50-year history of database engineering, and synthesise every concept from all three years into a coherent, connected mental model.</p></blockquote><div><hr></div><h2>The 12 annotated research papers</h2><p>Each paper gets a full dedicated issue: we read it together, paragraph by paragraph, translate the academic language into plain engineering, explain the context, and trace what the industry actually adopted.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="/__u/substackcdn.com/image/fetch/$s_!rIDu!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F882abb31-f971-4c3e-a29b-879f9f3b9b31_1214x1178.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="/__u/substackcdn.com/image/fetch/$s_!rIDu!, /__u/bytesbtrees.substack.com/w_424, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_webp, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F882abb31-f971-4c3e-a29b-879f9f3b9b31_1214x1178.png 424w, /__u/substackcdn.com/image/fetch/$s_!rIDu!, /__u/bytesbtrees.substack.com/w_848, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_webp, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F882abb31-f971-4c3e-a29b-879f9f3b9b31_1214x1178.png 848w, /__u/substackcdn.com/image/fetch/$s_!rIDu!, /__u/bytesbtrees.substack.com/w_1272, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_webp, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F882abb31-f971-4c3e-a29b-879f9f3b9b31_1214x1178.png 1272w, /__u/substackcdn.com/image/fetch/$s_!rIDu!, /__u/bytesbtrees.substack.com/w_1456, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_webp, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F882abb31-f971-4c3e-a29b-879f9f3b9b31_1214x1178.png 1456w" sizes="100vw"><img src="/__u/substackcdn.com/image/fetch/$s_!rIDu!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F882abb31-f971-4c3e-a29b-879f9f3b9b31_1214x1178.png" width="592" height="574.4448105436574" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/882abb31-f971-4c3e-a29b-879f9f3b9b31_1214x1178.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:1178,&quot;width&quot;:1214,&quot;resizeWidth&quot;:592,&quot;bytes&quot;:189758,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://bytesbtrees.substack.com/i/196677476?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F882abb31-f971-4c3e-a29b-879f9f3b9b31_1214x1178.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="/__u/substackcdn.com/image/fetch/$s_!rIDu!, /__u/bytesbtrees.substack.com/w_424, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_auto, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F882abb31-f971-4c3e-a29b-879f9f3b9b31_1214x1178.png 424w, /__u/substackcdn.com/image/fetch/$s_!rIDu!, /__u/bytesbtrees.substack.com/w_848, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_auto, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F882abb31-f971-4c3e-a29b-879f9f3b9b31_1214x1178.png 848w, /__u/substackcdn.com/image/fetch/$s_!rIDu!, /__u/bytesbtrees.substack.com/w_1272, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_auto, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F882abb31-f971-4c3e-a29b-879f9f3b9b31_1214x1178.png 1272w, /__u/substackcdn.com/image/fetch/$s_!rIDu!, /__u/bytesbtrees.substack.com/w_1456, /__u/bytesbtrees.substack.com/c_limit, /__u/bytesbtrees.substack.com/f_auto, /__u/bytesbtrees.substack.com/q_auto:good, /__u/bytesbtrees.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F882abb31-f971-4c3e-a29b-879f9f3b9b31_1214x1178.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><div><hr></div><h2>How to use this guide</h2><p><strong>Bookmark this page.</strong> Each time a new issue arrives, return here to see how it connects to what came before and what comes next. The issue references in each week&#8217;s diagram panels all point to entries in this map.</p><p>The curriculum is designed so that every concept you encounter in Year 3 was seeded in Year 1 &#8212; this is the document that shows you where. The B-tree you learn in issue #019 reappears in issue #073 (Postgres buffer manager), and again in issue #146 (index topology at scale). Each time with more context, more depth, and more connection to the surrounding concepts.</p><div><hr></div><h2>One last thing</h2><p>Databases are one of those rare engineering topics where genuine understanding compounds dramatically. The engineer who knows why a B-tree organises data the way it does doesn&#8217;t just understand B-trees &#8212; they immediately have better intuition about LSM trees, write amplification, columnar storage, and why certain Cassandra data models are catastrophically slow while others are fast.</p><p>Understanding is recursive in this domain. The more you have, the faster new concepts click into place.</p><p>That compounding is what the three-year structure is designed to produce. Not 172 isolated facts. One interconnected mental model, built one piece at a time, week by week, until databases stop being black boxes and start being something you genuinely understand.</p><p><strong>Issue #001 ships this Sunday. See you then.</strong></p><div><hr></div><blockquote><p><em>Subscribe &#8212; free tier always available. Issue #001: &#8220;Data, storage &amp; memory &#8212; first principles.&#8221; Free for all subscribers.</em></p></blockquote><div><hr></div><p><em>&#169; Bytes &amp; B-trees &#183; Substack &#183; Weekly &#183; <a href="/__u/bytesbtrees.substack.com/">bytesbtrees.substack.com</a></em><br><em>Publish order: &#9312; Promotional &#8594; &#9313; Course Introduction &#8594; &#9314; This curriculum guide &#8594; Issue #001 (Sunday)</em></p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://bytesbtrees.substack.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en-gb&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Thanks for reading Bytes &amp; B-Tree's! Subscribe for free to receive new posts and support my work.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div>]]></content:encoded></item><item><title><![CDATA[How to learn databases properly — and how this newsletter will take you there]]></title><description><![CDATA[Most engineers learn databases backwards &#8212; from the interface down.]]></description><link>https://bytesbtrees.substack.com/p/how-to-learn-databases-properly-and</link><guid isPermaLink="false">https://bytesbtrees.substack.com/p/how-to-learn-databases-properly-and</guid><dc:creator><![CDATA[System Design Roadmap]]></dc:creator><pubDate>Wed, 06 May 2026 16:21:59 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!nL3X!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0693b673-c070-4b18-911a-1c08a170def3_608x608.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><em>Most engineers learn databases backwards &#8212; from the interface down. We&#8217;re going the other direction. Here&#8217;s the map, the philosophy, and exactly what you&#8217;ll be able to do by the end.</em></p><div><hr></div><p><strong>Bytes &amp; B-trees</strong> | Pre-launch #2 of 3 | Free to read</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://bytesbtrees.substack.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en-gb&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Thanks for reading Bytes &amp; B-Tree's! Subscribe for free to receive new posts and support my work.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><div><hr></div><p>There&#8217;s a particular kind of database problem that trips up even experienced engineers. Not the easy ones &#8212; slow queries, missing indexes, bad schema decisions. The harder kind: a transaction that committed but whose data was partially lost after a failover. A replication lag that spiked mysteriously during a schema migration. A Cassandra cluster that returned stale reads even though you configured quorum. A Postgres table that autovacuum couldn&#8217;t keep up with despite running constantly.</p><p>These problems share a pattern. They&#8217;re invisible to engineers who learned databases from the interface down &#8212; from SQL syntax, to ORM usage, to &#8220;add an index when slow.&#8221; They&#8217;re immediately understandable to engineers who know what&#8217;s happening inside the engine. The difference between the two isn&#8217;t years of experience. It&#8217;s a specific kind of knowledge: the internals.</p><p>That knowledge is what this newsletter builds, systematically, over three years.</p><div><hr></div><h2>The learning philosophy</h2><p>Every concept in this newsletter follows the same sequence. We start with the <strong>problem</strong> &#8212; the specific constraint or failure mode that motivated someone to invent the mechanism in the first place. We ask: what would happen without it? What breaks? What becomes impossible?</p><p>Only after that question is fully answered do we introduce the solution. This order matters. The mechanism only makes sense in light of the problem it solves. Presented the other way &#8212; mechanism first, motivation second &#8212; it&#8217;s just memorisation.</p><p>Then we go into the <strong>implementation</strong>: how the mechanism actually works, with real code, real configurations, and real data structures. Not pseudocode. Not simplifications that obscure how it actually behaves. The real thing &#8212; or close enough that the difference doesn&#8217;t matter until you&#8217;re reading source code.</p><p>Then we <strong>anchor it in a real system</strong> &#8212; Postgres, RocksDB, Cassandra, DynamoDB, whichever shows the concept most clearly in production code.</p><p>Then we examine the <strong>trade-offs</strong>: what did this design gain, and what did it give up? What workloads does it help? What workloads does it hurt? When would you choose differently?</p><blockquote><p>The goal is not that you can recite how MVCC works. The goal is that when your Postgres tables start bloating, you know immediately that it&#8217;s a vacuum problem, and you know exactly why &#8212; because you understand what MVCC is doing under the hood and what vacuum&#8217;s job is. That&#8217;s the difference between knowledge and understanding.</p></blockquote><div><hr></div><h2>The three-year arc</h2><h3>Year 1 &#8212; Foundations (Issues #001&#8211;056)</h3><p><strong>What you&#8217;ll be able to do by the end:</strong></p><p>By the end of Year 1, you understand why databases exist, how data is physically stored and retrieved, what ACID actually guarantees at the hardware level, how indexes are structured internally, how queries are parsed and executed, and how crash recovery works. You&#8217;ll have built a working key-value store and a minimal query engine from scratch. You&#8217;ll be able to look at any database&#8217;s core behaviour and understand the first-principles reason it works the way it does.</p><p><strong>Topics covered:</strong></p><ul><li><p>Storage &amp; I/O physics &#8212; bits, bytes, pages, disk geometry</p></li><li><p>B-trees &amp; LSM trees &#8212; the two index structures that run the world</p></li><li><p>ACID from first principles &#8212; atomicity, durability, isolation at the byte level</p></li><li><p>MVCC &amp; WAL internals &#8212; how Postgres actually manages concurrent transactions</p></li><li><p>SQL execution engine &#8212; parse, plan, execute, optimise</p></li><li><p>Database taxonomy &#8212; relational, document, key-value, columnar, graph, time-series</p></li><li><p>Build It Yourself #1: a key-value store (crash-safe writes &#8594; hash index &#8594; WAL)</p></li><li><p>Build It Yourself #2: a minimal SQL query engine (parser &#8594; logical plan &#8594; executor)</p></li></ul><div><hr></div><h3>Year 2 &#8212; Internals &amp; Scale (Issues #057&#8211;113)</h3><p><strong>What you&#8217;ll be able to do by the end:</strong></p><p>You can dissect real database source code, design distributed architectures, choose between Raft and Paxos for a given use case, configure connection pools using queuing mathematics, and explain exactly why CockroachDB and Spanner make the trade-offs they do.</p><p><strong>Topics covered:</strong></p><ul><li><p>Postgres buffer manager, query planner, and MVCC snapshot mechanism &#8212; at the code level</p></li><li><p>Distributed consensus &#8212; Raft, Paxos, FLP impossibility, the real implementations</p></li><li><p>Replication topologies &#8212; primary-replica, multi-master, leaderless, and when each breaks</p></li><li><p>Sharding strategies &#8212; range, hash, directory, hot keys, and resharding without downtime</p></li><li><p>NewSQL &#8212; CockroachDB, Spanner, TiDB &#8212; the ACID + horizontal scale problem</p></li><li><p>DuckDB &amp; Apache Arrow &#8212; the unbundled analytics revolution</p></li><li><p>Cloud databases &#8212; Aurora, Neon, PlanetScale, Snowflake, ClickHouse</p></li><li><p>Queuing theory &#8212; Little&#8217;s Law, M/M/1 queues, and database capacity planning mathematics</p></li><li><p>Build It Yourself #3: a minimal replication log with primary and replica</p></li></ul><div><hr></div><h3>Year 3 &#8212; Frontier &amp; Ultra-scale (Issues #114&#8211;172)</h3><p><strong>What you&#8217;ll be able to do by the end:</strong></p><p>You can design systems for billions of records, explain how vector search works at the index level, architect a production RAG pipeline correctly, read and critique database research papers independently, and synthesise three years of concepts into a coherent mental model you&#8217;ll use for the rest of your career.</p><p><strong>Topics covered:</strong></p><ul><li><p>Vector search &#8212; HNSW, IVF, FAISS, approximate nearest neighbour</p></li><li><p>RAG as a database architecture problem &#8212; chunking, embeddings, hybrid search, re-ranking</p></li><li><p>AI-native query interfaces &#8212; NL-to-SQL, LLM query optimisation</p></li><li><p>Edge databases &amp; local-first &#8212; SQLite, Turso, CRDTs, ElectricSQL</p></li><li><p>Streaming databases &#8212; Materialize, RisingWave, exactly-once semantics</p></li><li><p>Research frontiers &#8212; learned indexes, learned cardinality estimation, Bao</p></li><li><p>Company case studies &#8212; Netflix, Discord, Stripe, Uber, Cloudflare, Figma</p></li><li><p>Ultra-scale system design &#8212; 10B-record architectures, multi-region consistency</p></li><li><p>Event sourcing, CQRS, zero-downtime migrations, chaos engineering</p></li><li><p>The 3-year synthesis &#8212; every concept map and decision framework, compiled</p></li></ul><div><hr></div><h2>How each week works</h2><p>Every Sunday morning, a new issue arrives. The main piece &#8212; free for everyone &#8212; is 4,000&#8211;5,000 words covering the week&#8217;s concept from problem to mechanism to trade-off. The paid section includes: hands-on experiments you can run on your own machine, the Query Lab puzzle and its solution from the previous week, and (when applicable) the Build It Yourself implementation code.</p><h3>The weekly production cycle</h3><p>Day Activity Monday Research &#8212; primary sources, paper re-reads, code exploration, angle confirmed Tuesday Outline &#8212; section structure, diagrams listed, code examples scoped Wednesday&#8211;Thursday Write &#8212; full draft written, all code verified and run, diagrams built Friday Edit &#8212; technical accuracy check, clarity pass, Query Lab puzzle set Saturday Format &#8212; Substack layout, social thread drafted, GitHub repo pushed Sunday Ship &#8212; issue at 8am, thread at 9am, next issue outlined</p><div><hr></div><h2>The three quality tests every issue passes</h2><p>Before any issue ships, it passes three internal tests:</p><p><strong>1. The code test</strong> &#8212; every code sample must run without modification on a standard installation of the relevant system. No &#8220;you might need to adjust this.&#8221; It runs, or it doesn&#8217;t ship.</p><p><strong>2. The newcomer test</strong> &#8212; could someone encounter this concept for the very first time in this issue and come away with a complete, correct mental model? Not a simplified one &#8212; a correct one.</p><p><strong>3. The expert test</strong> &#8212; does an engineer who already knows this topic learn at least one non-obvious thing? If the answer to any of these three is no, the issue gets rewritten.</p><div><hr></div><h2>What the Query Lab is</h2><p>Every issue ends with a Query Lab puzzle &#8212; an extreme SQL, Cypher, or reasoning challenge that forces you to apply the week&#8217;s concept directly. Not &#8220;write a SELECT statement.&#8221; More like: given this table size, page size, and row count, calculate exactly how many 8KB pages Postgres read before and after an index was added &#8212; and explain the I/O difference in terms of what the storage engine actually did.</p><p>Solutions &#8212; with multiple valid approaches and annotations on each &#8212; are published in the following issue&#8217;s paid section.</p><div><hr></div><h2>What to do before issue #001</h2><p>Read the curriculum guide &#8212; Pre-launch #3, the next article in this series. It maps every one of the 172 issues with enough detail that you&#8217;ll know exactly what you&#8217;re signing up for before you subscribe. Then subscribe &#8212; free is fine to start.</p><p>If issue #001 isn&#8217;t what we described, unsubscribe immediately. We&#8217;d rather have readers who are genuinely here than subscribers who are being polite.</p><div><hr></div><blockquote><p><strong>Issue #001 ships this Sunday.</strong><br>&#8220;Data, storage &amp; memory &#8212; first principles.&#8221; Free for all subscribers. Paid section includes four hands-on experiments and the first Query Lab puzzle.</p></blockquote><div><hr></div><p><em>Next: Pre-launch #3 &#8212; the complete curriculum guide. Every issue mapped, every track explained, every connection drawn. Long by design. Bookmark it.</em></p><div><hr></div><p><em>&#169; Bytes &amp; B-trees &#183; Substack &#183; Weekly &#183;<a href="/__u/bytesbtrees.substack.com/"> </a> <a href="/__u/bytesbtrees.substack.com/">bytesbtrees.substack.com</a></em></p><p>Thanks for reading Bytes &amp; B-Tree's! Subscribe for free to receive new posts and support my work.</p>]]></content:encoded></item></channel></rss>