<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Sayonil Mitra</title>
    <description>The latest articles on DEV Community by Sayonil Mitra (@sayonilmitra).</description>
    <link>https://dev.to/sayonilmitra</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3960044%2Fb5cd9573-e7bf-4b3f-9208-0ee11220b88a.png</url>
      <title>DEV Community: Sayonil Mitra</title>
      <link>https://dev.to/sayonilmitra</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/sayonilmitra"/>
    <language>en</language>
    <item>
      <title>The Safe Way to Add a NOT NULL Column to a Large Postgres Table</title>
      <dc:creator>Sayonil Mitra</dc:creator>
      <pubDate>Fri, 11 Sep 2026 09:23:41 +0000</pubDate>
      <link>https://dev.to/sayonilmitra/the-safe-way-to-add-a-not-null-column-to-a-large-postgres-table-4lin</link>
      <guid>https://dev.to/sayonilmitra/the-safe-way-to-add-a-not-null-column-to-a-large-postgres-table-4lin</guid>
      <description>&lt;p&gt;If you're adding a brand-new column and can give it a constant default, this is already fast and safe in modern Postgres:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt; &lt;span class="k"&gt;ADD&lt;/span&gt; &lt;span class="k"&gt;COLUMN&lt;/span&gt; &lt;span class="n"&gt;plan&lt;/span&gt; &lt;span class="nb"&gt;text&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="s1"&gt;'free'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Since Postgres 11, this is a single statement — no table rewrite, and no scan to verify existing rows either. Postgres knows every existing row will read back as the default value, and a non-null default can't violate NOT NULL, so there's nothing to check. This works at any table size.&lt;/p&gt;

&lt;h2&gt;
  
  
  The actual risk: NOT NULL on a column that already has data
&lt;/h2&gt;

&lt;p&gt;The dangerous case is different: a column that already exists in a populated table, where you now want to enforce NOT NULL retroactively — a field that's been optional since launch and now needs to be required, for example.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt; &lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;COLUMN&lt;/span&gt; &lt;span class="n"&gt;plan&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run this directly and Postgres has to scan every existing row to prove none of them are null, holding an ACCESS EXCLUSIVE lock for the entire scan. On a small table that's instant. On a table with tens of millions of rows, that scan can run for minutes — and every read and write against the table queues up behind it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The pattern: separate "add the constraint" from "prove it holds"
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Backfill any remaining NULLs, in batches:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;plan&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'free'&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;plan&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="k"&gt;BETWEEN&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="mi"&gt;50000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="c1"&gt;-- repeat for the next id range&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A single unbatched UPDATE over millions of rows is its own problem — one long transaction holding row locks and generating a pile of WAL/vacuum work all at once.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Add the constraint as NOT VALID first:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt;
  &lt;span class="k"&gt;ADD&lt;/span&gt; &lt;span class="k"&gt;CONSTRAINT&lt;/span&gt; &lt;span class="n"&gt;users_plan_not_null&lt;/span&gt; &lt;span class="k"&gt;CHECK&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;plan&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;VALID&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With NOT VALID, Postgres adds the constraint without scanning existing rows — it's the usual ACCESS EXCLUSIVE lock, but only briefly, since there's no data to check.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Validate it separately:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt; &lt;span class="n"&gt;VALIDATE&lt;/span&gt; &lt;span class="k"&gt;CONSTRAINT&lt;/span&gt; &lt;span class="n"&gt;users_plan_not_null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the step that actually scans the table — but per Postgres's own documentation it takes only a SHARE UPDATE EXCLUSIVE lock, which doesn't block normal reads or writes. Traffic keeps flowing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Set NOT NULL for real:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt; &lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;COLUMN&lt;/span&gt; &lt;span class="n"&gt;plan&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt; &lt;span class="k"&gt;DROP&lt;/span&gt; &lt;span class="k"&gt;CONSTRAINT&lt;/span&gt; &lt;span class="n"&gt;users_plan_not_null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Since Postgres 12, if a validated CHECK constraint already guarantees the column can't be null, this step is metadata-only — Postgres trusts the existing constraint instead of re-scanning.&lt;/p&gt;

&lt;h2&gt;
  
  
  The same distinction shows up with DROP COLUMN
&lt;/h2&gt;

&lt;p&gt;The database-level operation is nearly free in Postgres — metadata-only, the column is marked dropped but not physically removed, and disk space isn't reclaimed by routine autovacuum on its own, only by later row rewrites or a forced VACUUM FULL/CLUSTER. MySQL/InnoDB used to require a full table rebuild for DROP COLUMN, same as any other lock-holding ALTER — but MySQL 8.0.29 extended instant DDL to cover it too, under similar conditions to instant ADD COLUMN.&lt;/p&gt;

&lt;p&gt;The real risk with DROP COLUMN usually isn't the database — it's the application layer: old instances mid-rollout, SELECT * queries, ORM models that still list the field, and queued background jobs can all still be reaching for that column the instant it disappears. The fix there is sequencing — remove every application reference first, confirm via logs/APM that nothing's touching it, and only then run the drop.&lt;/p&gt;

&lt;h2&gt;
  
  
  Correction
&lt;/h2&gt;

&lt;p&gt;I got two things wrong in the original version of this post and I'd rather flag it than quietly edit it away: I'd claimed ADD COLUMN ... NOT NULL DEFAULT in one statement still needed a table scan under lock — it doesn't, since Postgres 11's fast-default optimization covers the NOT NULL check too, not just the table rewrite. I also overstated MySQL's DROP COLUMN cost as still requiring a rebuild — 8.0.29 fixed that. Thanks to a rigorous fact-check pass for catching both. The reasoning above reflects the correction.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why I'm writing this
&lt;/h2&gt;

&lt;p&gt;I got tired of re-deriving this pattern from memory (or from old Slack threads) every time a migration like this came up, so I built &lt;a href="https://sqltoolkit.dev/migration-checker" rel="noopener noreferrer"&gt;Migration Checker&lt;/a&gt; — paste an ALTER TABLE statement and it flags dangerous patterns like NOT NULL without a default, DROP COLUMN, DROP PRIMARY KEY, and a handful of others, before you run anything for real. It correctly tells apart ADD COLUMN ... NOT NULL DEFAULT (safe) from SET NOT NULL on an existing column (dangerous). It's part of &lt;a href="https://sqltoolkit.dev" rel="noopener noreferrer"&gt;SQL Toolkit&lt;/a&gt;, a set of free, browser-only SQL tools (formatter, JSON/CSV/Excel → SQL, schema docs) — nothing leaves your browser, no signup.&lt;/p&gt;

&lt;p&gt;If you want the deeper walkthroughs, I wrote them up as standalone guides:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://sqltoolkit.dev/guides/add-not-null-column-postgres" rel="noopener noreferrer"&gt;Add a NOT NULL column without locking your table&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://sqltoolkit.dev/guides/drop-column-production-risks" rel="noopener noreferrer"&gt;What DROP COLUMN actually breaks in production&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Curious what migration foot-guns you've hit — always looking for more patterns to add.&lt;/p&gt;

</description>
      <category>database</category>
      <category>sql</category>
      <category>webdev</category>
      <category>postgres</category>
    </item>
    <item>
      <title>Simple SQL Tool</title>
      <dc:creator>Sayonil Mitra</dc:creator>
      <pubDate>Sat, 30 May 2026 14:05:41 +0000</pubDate>
      <link>https://dev.to/sayonilmitra/simple-sql-tool-f9b</link>
      <guid>https://dev.to/sayonilmitra/simple-sql-tool-f9b</guid>
      <description>&lt;p&gt;I am a full stack dev but randomly got interested in SQL. Something about combining queries felt like an interesting challenge, like piecing together a puzzle. Then I thought, why not make a tool which uses SQL somehow. At that time I did not know what tool to make or what my tool should do. Later I discovered, a converter can be handy. It is not life changing, ground breaking tool that will blow someone's mind. I am a fan of little utility tools which make small mundane tasks easy on a daily basis. So I built one as such. Is it useful for a lot of people in a large scale? Only time will tell I guess...&lt;br&gt;
(I will keep adding more complex features, just to see how far this project can go)&lt;/p&gt;

&lt;p&gt;If you have any feedback on these tools or want to check them out:&lt;br&gt;
&lt;a href="https://sqltoolkit.dev/json-to-sql" rel="noopener noreferrer"&gt;https://sqltoolkit.dev/json-to-sql&lt;/a&gt;&lt;br&gt;
May sound very simple but looked really cool to me that json can be converted to an sql insertion query.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://sqltoolkit.dev/csv-to-sql" rel="noopener noreferrer"&gt;https://sqltoolkit.dev/csv-to-sql&lt;/a&gt;&lt;br&gt;
This idea I got from my seeing my project managers. They have a massive CSV file and want to import that into some database (I know every major platform has easy import for CSVs, but still, wanted to build it)&lt;/p&gt;

&lt;p&gt;Update: this little side project grew way more than I expected when I wrote this. It's now a full SQL Toolkit (&lt;a href="https://sqltoolkit.dev" rel="noopener noreferrer"&gt;https://sqltoolkit.dev&lt;/a&gt;) with a bunch more free tools - the original SQL formatter/beautifier, plus SQL to JSON, SQL to CSV, Excel to SQL, a schema doc generator, and a migration risk analyzer. Still no signup, still runs entirely in your browser. Would love feedback if you try it out.&lt;/p&gt;

</description>
      <category>sql</category>
      <category>webdev</category>
      <category>javascript</category>
      <category>showdev</category>
    </item>
  </channel>
</rss>
