<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>sudohulk · notes</title><description>Short, unfinished things, republished whenever they are tended.</description><link>https://sudohulk.com/</link><language>en</language><image><url>https://sudohulk.com/icons/icon-144.png</url><title>sudohulk · notes</title><link>https://sudohulk.com/</link><width>144</width><height>144</height></image><item><title>Full-page writes and torn pages</title><link>https://sudohulk.com/writing/full-page-writes-and-torn-pages/</link><guid isPermaLink="false">https://sudohulk.com/notes/full-page-writes-and-torn-pages/</guid><description>Why the first change to a page after a checkpoint carries the whole 8 KB page in WAL.</description><pubDate>Tue, 22 Sep 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p id=&quot;p-22pkhs&quot;&gt;A &lt;strong&gt;torn page&lt;/strong&gt; is what you get when the operating system writes only part of an 8 KB PostgreSQL page before power is lost: the disk sector size is smaller than the page, so half the page can be new and half old. Replaying a normal WAL record against such a page would corrupt it further, because the record assumes the page looked exactly like it did before the change.&lt;/p&gt;
&lt;p id=&quot;p-yhre84&quot;&gt;PostgreSQL’s answer is the &lt;strong&gt;full-page write&lt;/strong&gt; (also called a full-page image, FPI): the first time a page is modified after a checkpoint, the WAL record carries the entire page, not just the delta. On recovery, that copy simply overwrites whatever is on disk, torn or not. Later changes to the same page before the next checkpoint carry only deltas, because the full copy in the log has already made the page’s starting state safe.&lt;/p&gt;
&lt;aside class=&quot;callout callout-gotcha&quot; data-callout=&quot;gotcha&quot; data-label=&quot;Gotcha&quot;&gt;&lt;p&gt;&lt;strong&gt;Gotcha&lt;/strong&gt;&lt;/p&gt;&lt;p id=&quot;p-2bf7q4&quot;&gt;This is why WAL volume spikes right after a checkpoint, and why spreading checkpoints out (&lt;code&gt;checkpoint_completion_target&lt;/code&gt;) or checkpointing less often reduces WAL, at the cost of longer recovery.&lt;/p&gt;&lt;/aside&gt;
&lt;p id=&quot;p-equzfm&quot;&gt;&lt;code&gt;full_page_writes&lt;/code&gt; can be turned off on storage that guarantees atomic page writes. Most people should not.&lt;/p&gt;
&lt;p id=&quot;p-l60mzs&quot;&gt;Related: &lt;a href=&quot;https://sudohulk.com/writing/pd-lsn-vs-record-lsn&quot; class=&quot;wikilink&quot; data-wikilink=&quot;pd-lsn-vs-record-lsn&quot;&gt;pd_lsn vs record LSN&lt;/a&gt;.&lt;/p&gt;
</content:encoded><category>postgresql</category><category>databases</category></item><item><title>Lazy transaction id assignment</title><link>https://sudohulk.com/writing/lazy-xid-assignment/</link><guid isPermaLink="false">https://sudohulk.com/notes/lazy-xid-assignment/</guid><description>A transaction does not get an xid at BEGIN; it gets one the first time it writes.</description><pubDate>Tue, 22 Sep 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p id=&quot;p-o9cdys&quot;&gt;&lt;code&gt;BEGIN&lt;/code&gt; does not hand out a &lt;strong&gt;transaction id&lt;/strong&gt; (xid). A transaction that only reads never gets one; it runs under a lightweight &lt;strong&gt;virtual transaction id&lt;/strong&gt; made of the backend id and a per-backend counter. The real 32-bit xid is assigned lazily, on the first operation that needs to mark a tuple: an &lt;code&gt;INSERT&lt;/code&gt;, &lt;code&gt;UPDATE&lt;/code&gt;, &lt;code&gt;DELETE&lt;/code&gt;, or a lock that has to be recorded.&lt;/p&gt;
&lt;p id=&quot;p-ydyt4k&quot;&gt;Xids are scarce, and they wrap around. Every assigned xid must eventually be frozen by vacuum to avoid wraparound, so handing them to read-only transactions would burn through the space and create work for no reason.&lt;/p&gt;
&lt;aside class=&quot;callout callout-gotcha&quot; data-callout=&quot;gotcha&quot; data-label=&quot;Gotcha&quot;&gt;&lt;p&gt;&lt;strong&gt;Gotcha&lt;/strong&gt;&lt;/p&gt;&lt;p id=&quot;p-2hra14&quot;&gt;&lt;code&gt;txid_current()&lt;/code&gt; forces an xid to be assigned. &lt;code&gt;pg_current_xact_id_if_assigned()&lt;/code&gt; (PG 13+) tells you whether one exists without creating it.&lt;/p&gt;&lt;/aside&gt;
&lt;p id=&quot;p-sy8v2c&quot;&gt;Related: &lt;a href=&quot;https://sudohulk.com/writing/pd-lsn-vs-record-lsn&quot; class=&quot;wikilink&quot; data-wikilink=&quot;pd-lsn-vs-record-lsn&quot;&gt;pd_lsn vs record LSN&lt;/a&gt;, and a post on what happens between BEGIN and COMMIT once it is written.&lt;/p&gt;
</content:encoded><category>postgresql</category></item><item><title>pd_lsn vs record LSN</title><link>https://sudohulk.com/writing/pd-lsn-vs-record-lsn/</link><guid isPermaLink="false">https://sudohulk.com/notes/pd-lsn-vs-record-lsn/</guid><description>Two LSNs that are easy to mix up, and the rule recovery uses to compare them.</description><pubDate>Mon, 21 Sep 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p id=&quot;p-fhgxjp&quot;&gt;Every WAL record has an address in the log stream: its &lt;strong&gt;record LSN&lt;/strong&gt;. Every 8 KB data page also carries an LSN in its header, &lt;code&gt;pd_lsn&lt;/code&gt;: the &lt;strong&gt;page LSN&lt;/strong&gt;, which is the address just past the most recent WAL record already applied to that page.&lt;/p&gt;
&lt;p id=&quot;p-sq1ept&quot;&gt;The rule that makes crash recovery idempotent: when replaying a record for a page, PostgreSQL applies it only if &lt;code&gt;pd_lsn&lt;/code&gt; is older than the record’s LSN. If the page already shows a newer LSN, that change reached disk before the crash and is skipped.&lt;/p&gt;
&lt;aside class=&quot;callout callout-gotcha&quot; data-callout=&quot;gotcha&quot; data-label=&quot;Gotcha&quot;&gt;&lt;p&gt;&lt;strong&gt;Gotcha&lt;/strong&gt;&lt;/p&gt;&lt;p id=&quot;p-t18o0o&quot;&gt;An LSN is meaningful only inside one server. Two nodes’ &lt;code&gt;1/2D3E&lt;/code&gt; have nothing to do with each other. Never compare LSNs across machines.&lt;/p&gt;&lt;/aside&gt;
&lt;p id=&quot;p-hwzjco&quot;&gt;Related: &lt;a href=&quot;https://sudohulk.com/writing/full-page-writes-and-torn-pages&quot; class=&quot;wikilink&quot; data-wikilink=&quot;full-page-writes-and-torn-pages&quot;&gt;full-page-writes-and-torn-pages&lt;/a&gt; (once written).&lt;/p&gt;
</content:encoded><category>postgresql</category></item></channel></rss>