<?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"><channel><title><![CDATA[Reliability Notes]]></title><description><![CDATA[Reliability Notes]]></description><link>https://reliability-notes.hashnode.dev</link><image><url>https://cdn.hashnode.com/uploads/logos/6ae15a05f006f9358bc99b31/9d71aa74-38ad-4ee9-a80a-a07ceec8cba8.png</url><title>Reliability Notes</title><link>https://reliability-notes.hashnode.dev</link></image><generator>RSS for Node</generator><lastBuildDate>Sun, 01 Nov 2026 01:30:44 GMT</lastBuildDate><atom:link href="https://reliability-notes.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Debugging a Slow Postgres Migration: What the Query Plan Told Us]]></title><description><![CDATA[We had a migration that should have taken four minutes. It took forty. The first mistake was looking at CPU. The database was idle. The second mistake was…]]></description><link>https://reliability-notes.hashnode.dev/debugging-a-slow-postgres-migration-what-the-query-plan-told-us</link><guid isPermaLink="true">https://reliability-notes.hashnode.dev/debugging-a-slow-postgres-migration-what-the-query-plan-told-us</guid><category><![CDATA[backend]]></category><category><![CDATA[Databases]]></category><category><![CDATA[postgres]]></category><dc:creator><![CDATA[Noah Bennett]]></dc:creator><pubDate>Wed, 28 Oct 2026 05:05:49 GMT</pubDate><content:encoded><![CDATA[<p>We had a migration that should have taken four minutes. It took forty.</p>
<p>The first mistake was looking at CPU. The database was idle. The second mistake was looking at locks - plenty of them, but short. What actually mattered was in the query plan: a sequential scan on a table that had grown past the point where our index helped.</p>
<h2>What we found</h2>
<p>The migration added a column with a default and backfilled it in one statement. On a table with 40M rows that meant a full rewrite while holding a lock.</p>
<pre><code class="language-sql">ALTER TABLE events ADD COLUMN source text DEFAULT 'legacy';
</code></pre>
<p>Postgres 11+ makes adding a column with a constant default cheap, but the backfill was ours to write, and we wrote it as a single UPDATE.</p>
<h2>What worked</h2>
<p>Three changes, in order of impact:</p>
<ol>
<li>Add the column as nullable, with no default.</li>
<li>Backfill in batches of 10k with a short sleep between.</li>
<li>Set the default afterwards, in a separate transaction.</li>
</ol>
<p>The migration went from 40 minutes and a lock queue to 6 minutes with no visible stall.</p>
<h2>The part I keep coming back to</h2>
<p>The query plan gave us the answer in about ten seconds. We spent two hours on dashboards before we looked at it. Observability is not a substitute for reading the plan.</p>
]]></content:encoded></item></channel></rss>