<?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: devTalk</title>
    <description>The latest articles on DEV Community by devTalk (@devtalk94).</description>
    <link>https://dev.to/devtalk94</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%2F3228626%2F5495a7f0-c356-4253-865c-b83286ec1fe4.jpg</url>
      <title>DEV Community: devTalk</title>
      <link>https://dev.to/devtalk94</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/devtalk94"/>
    <language>en</language>
    <item>
      <title>Laravel Migrations: Add Foreign Keys Without Locking</title>
      <dc:creator>devTalk</dc:creator>
      <pubDate>Fri, 02 Oct 2026 14:44:59 +0000</pubDate>
      <link>https://dev.to/devtalk94/laravel-migrations-add-foreign-keys-without-locking-3mp6</link>
      <guid>https://dev.to/devtalk94/laravel-migrations-add-foreign-keys-without-locking-3mp6</guid>
      <description>&lt;p&gt;&lt;a href="https://dev-talk.com/post/laravel-add-indexes-large-tables-without-locking" rel="noopener noreferrer"&gt;Part 1&lt;/a&gt; covered indexes and &lt;a href="https://dev-talk.com/post/laravel-migrations-add-change-columns-without-downtime" rel="noopener noreferrer"&gt;Part 2&lt;/a&gt; covered columns. The next Laravel migrations most teams get wrong are the ones that tie two tables together: foreign keys and check constraints.&lt;/p&gt;

&lt;p&gt;A foreign key looks like metadata — a rule the database enforces, not data it stores. But adding one to an existing table means scanning every row in the child table to prove the rule already holds, and that scan happens under a lock. On a busy &lt;code&gt;orders&lt;/code&gt; table referencing a busy &lt;code&gt;customers&lt;/code&gt; table, that's two tables at risk from one migration.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nc"&gt;Schema&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;table&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'orders'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Blueprint&lt;/span&gt; &lt;span class="nv"&gt;$table&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$table&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;foreignId&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'customer_id'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;constrained&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This article covers what that migration actually does on MySQL and PostgreSQL, the two-step pattern that avoids blocking writes, and the cascading-delete mistake that has nothing to do with locking at all.&lt;/p&gt;

&lt;h3&gt;
  
  
  PostgreSQL: a lighter lock than you'd expect, still a full scan
&lt;/h3&gt;

&lt;p&gt;Adding a foreign key constraint in Postgres doesn't take the heaviest lock available. Most forms of &lt;code&gt;ADD CONSTRAINT&lt;/code&gt; take an &lt;code&gt;ACCESS EXCLUSIVE&lt;/code&gt; lock, but &lt;code&gt;ADD FOREIGN KEY&lt;/code&gt; specifically only requires &lt;code&gt;SHARE ROW EXCLUSIVE&lt;/code&gt; — and it takes that same lock on &lt;em&gt;both&lt;/em&gt; tables, the one gaining the constraint and the one it references.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;SHARE ROW EXCLUSIVE&lt;/code&gt; still blocks every &lt;code&gt;INSERT&lt;/code&gt;, &lt;code&gt;UPDATE&lt;/code&gt;, and &lt;code&gt;DELETE&lt;/code&gt; on both tables for the duration of the scan, even though it's lighter than &lt;code&gt;ACCESS EXCLUSIVE&lt;/code&gt;. On a large &lt;code&gt;orders&lt;/code&gt; table, that scan is the whole problem: Postgres has to walk every existing row to confirm it satisfies the new constraint before it can finish adding it.&lt;/p&gt;

&lt;p&gt;The fix is the same shape as the &lt;code&gt;NOT NULL&lt;/code&gt; pattern from Part 2: split the constraint from its validation.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;?php&lt;/span&gt;

&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Illuminate\Database\Migrations\Migration&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="no"&gt;Illuminate\Support\Facades\DB&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;Migration&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;up&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// Step 1: add the constraint without checking existing rows.&lt;/span&gt;
        &lt;span class="c1"&gt;// Still takes SHARE ROW EXCLUSIVE, but only briefly — no scan.&lt;/span&gt;
        &lt;span class="no"&gt;DB&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;statement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'
            ALTER TABLE orders
            ADD CONSTRAINT orders_customer_id_foreign
            FOREIGN KEY (customer_id) REFERENCES customers (id)
            NOT VALID
        '&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="c1"&gt;// Step 2: validate separately. This does the actual scan,&lt;/span&gt;
        &lt;span class="c1"&gt;// but only takes SHARE UPDATE EXCLUSIVE — it does not block writes.&lt;/span&gt;
        &lt;span class="no"&gt;DB&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;statement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'ALTER TABLE orders VALIDATE CONSTRAINT orders_customer_id_foreign'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;down&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="no"&gt;DB&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;statement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'ALTER TABLE orders DROP CONSTRAINT orders_customer_id_foreign'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run step 1 as part of your normal deploy — it's fast. Run step 2 separately, during a quiet period if the table is large enough that the scan takes real time, since it's safe to run any time after step 1 without blocking the app.&lt;/p&gt;

&lt;p&gt;Laravel's schema builder doesn't currently expose a fluent &lt;code&gt;NOT VALID&lt;/code&gt; modifier for foreign keys the way it does &lt;code&gt;online()&lt;/code&gt; for indexes, so this one stays as raw &lt;code&gt;DB::statement()&lt;/code&gt; calls regardless of Laravel version.&lt;/p&gt;

&lt;h3&gt;
  
  
  MySQL: online by default, with one setting you'll likely need
&lt;/h3&gt;

&lt;p&gt;MySQL's story here is better news. Adding a foreign key on InnoDB can use the same &lt;code&gt;ALGORITHM=INPLACE, LOCK=NONE&lt;/code&gt; online DDL from Part 1:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;?php&lt;/span&gt;

&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Illuminate\Database\Migrations\Migration&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="no"&gt;Illuminate\Support\Facades\DB&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Illuminate\Support\Facades\Schema&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;Migration&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;up&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Schema&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;hasColumn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'orders'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'customer_id'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;hasForeignKey&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="no"&gt;DB&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;statement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'SET SESSION lock_wait_timeout = 5'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="c1"&gt;// Adding a foreign key needs foreign_key_checks off for this&lt;/span&gt;
        &lt;span class="c1"&gt;// session, or ALGORITHM=INPLACE is rejected outright.&lt;/span&gt;
        &lt;span class="no"&gt;DB&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;statement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'SET SESSION foreign_key_checks = 0'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="no"&gt;DB&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;statement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'
                ALTER TABLE orders
                ADD CONSTRAINT orders_customer_id_foreign
                FOREIGN KEY (customer_id) REFERENCES customers (id),
                ALGORITHM=INPLACE, LOCK=NONE
            '&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;finally&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="no"&gt;DB&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;statement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'SET SESSION foreign_key_checks = 1'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="no"&gt;DB&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;statement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'SET SESSION lock_wait_timeout = DEFAULT'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;down&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="no"&gt;DB&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;statement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'
            ALTER TABLE orders
            DROP FOREIGN KEY orders_customer_id_foreign,
            ALGORITHM=INPLACE, LOCK=NONE
        '&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;hasForeignKey&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;DB&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;table&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'information_schema.table_constraints'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'table_schema'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;DB&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;getDatabaseName&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
            &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'table_name'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'orders'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'constraint_name'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'orders_customer_id_foreign'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;exists&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;foreign_key_checks = 0&lt;/code&gt; line is the part most examples skip. Without it, MySQL rejects &lt;code&gt;ALGORITHM=INPLACE&lt;/code&gt; on a foreign key addition outright with an error, because validating referential integrity against existing rows is exactly the check that algorithm normally skips doing inline. Turning checks off for the session lets the in-place algorithm proceed; MySQL still builds the constraint correctly, it just doesn't re-verify every existing row against it in the same blocking way a &lt;code&gt;COPY&lt;/code&gt;-algorithm rebuild would.&lt;/p&gt;

&lt;p&gt;Setting &lt;code&gt;foreign_key_checks = 0&lt;/code&gt; is a session-level setting in the code above, not a global one — it only affects the connection running this migration, not concurrent queries from your application.&lt;/p&gt;

&lt;p&gt;The metadata lock trap from Part 1 applies here too, and arguably matters more: a foreign key's &lt;code&gt;ALTER&lt;/code&gt; has to coordinate with two tables, so a long-running transaction on either &lt;code&gt;orders&lt;/code&gt; or &lt;code&gt;customers&lt;/code&gt; can stall it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Check constraints: the same two-step pattern
&lt;/h3&gt;

&lt;p&gt;Check constraints follow Postgres's general rule rather than the foreign-key exception: adding one with &lt;code&gt;ADD CONSTRAINT ... CHECK (...)&lt;/code&gt; takes a full &lt;code&gt;ACCESS EXCLUSIVE&lt;/code&gt; lock if you validate it immediately, because by default Postgres scans the table to confirm every row passes. &lt;code&gt;NOT VALID&lt;/code&gt; works here too, and the same two-step split applies:&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="c1"&gt;-- Step 1: instant, no scan.&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;orders&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;orders_total_positive&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;total&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&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;span class="c1"&gt;-- Step 2: does the scan, takes SHARE UPDATE EXCLUSIVE only.&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;orders&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;orders_total_positive&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 same mechanism Part 2 used to add &lt;code&gt;NOT NULL&lt;/code&gt; to an existing column without a blocking scan — a &lt;code&gt;CHECK (col IS NOT NULL)&lt;/code&gt; constraint, validated separately, then promoted. Foreign keys, check constraints, and &lt;code&gt;NOT NULL&lt;/code&gt; all converge on the same underlying trick in Postgres: separate "define the rule" from "prove existing data follows it."&lt;/p&gt;

&lt;h3&gt;
  
  
  The gotcha that isn't about locking: cascading deletes on hot tables
&lt;/h3&gt;

&lt;p&gt;Once the foreign key exists, &lt;code&gt;onDelete('cascade')&lt;/code&gt; changes what a single &lt;code&gt;DELETE&lt;/code&gt; does at runtime, every time, for the life of the constraint:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nc"&gt;Schema&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;table&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'orders'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Blueprint&lt;/span&gt; &lt;span class="nv"&gt;$table&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$table&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;foreignId&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'customer_id'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;constrained&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;onDelete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'cascade'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Deleting one customer with 50,000 historical orders now deletes 50,001 rows in a single transaction, each one individually checked and logged. On MySQL that's one long-held lock on the &lt;code&gt;orders&lt;/code&gt; table for the whole cascade; on Postgres it's one long transaction that can block autovacuum and hold back replication while it runs. Neither database warns you about this at migration time — the constraint itself is cheap to add, and the cost only shows up the first time someone deletes a parent row with a lot of children.&lt;/p&gt;

&lt;p&gt;For high-volume child tables, &lt;code&gt;onDelete('restrict')&lt;/code&gt; (the default) paired with an explicit, chunked application-level delete is usually safer than &lt;code&gt;cascade&lt;/code&gt;, precisely because it puts the pacing under your control instead of the database's.&lt;/p&gt;

&lt;h3&gt;
  
  
  Quick reference
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Operation&lt;/th&gt;
&lt;th&gt;Lock taken&lt;/th&gt;
&lt;th&gt;Safe pattern&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Add FK (PostgreSQL)&lt;/td&gt;
&lt;td&gt;SHARE ROW EXCLUSIVE on both tables, held for the scan&lt;/td&gt;
&lt;td&gt;NOT VALID, then VALIDATE CONSTRAINT separately&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Add CHECK constraint (PostgreSQL)&lt;/td&gt;
&lt;td&gt;ACCESS EXCLUSIVE if validated immediately&lt;/td&gt;
&lt;td&gt;NOT VALID, then VALIDATE CONSTRAINT separately&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Add FK (MySQL/InnoDB)&lt;/td&gt;
&lt;td&gt;None, with ALGORITHM=INPLACE, LOCK=NONE&lt;/td&gt;
&lt;td&gt;Requires foreign_key_checks=0 for the session&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;onDelete('cascade') on a hot table&lt;/td&gt;
&lt;td&gt;Long-held lock/transaction at delete time, not add time&lt;/td&gt;
&lt;td&gt;Prefer restrict() + chunked application deletes&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Before you run it in production
&lt;/h3&gt;

&lt;p&gt;Run &lt;code&gt;php artisan migrate --pretend&lt;/code&gt; and confirm &lt;code&gt;NOT VALID&lt;/code&gt; or &lt;code&gt;ALGORITHM=INPLACE, LOCK=NONE&lt;/code&gt; appears in the SQL. On Postgres, run the &lt;code&gt;VALIDATE CONSTRAINT&lt;/code&gt; step as its own migration so you control when the table scan happens. On MySQL, remember &lt;code&gt;foreign_key_checks = 0&lt;/code&gt; is required for the in-place algorithm to be accepted at all. Before adding &lt;code&gt;onDelete('cascade')&lt;/code&gt; to a high-volume relationship, ask what a single parent delete will actually do to the child table at 2am, not just whether the migration runs cleanly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Related reading:&lt;/strong&gt; this builds on &lt;a href="https://dev-talk.com/post/laravel-add-indexes-large-tables-without-locking" rel="noopener noreferrer"&gt;Part 1: Adding Indexes to Large Tables in Laravel Without Locking Them&lt;/a&gt; and &lt;a href="https://dev-talk.com/post/laravel-migrations-add-change-columns-without-downtime" rel="noopener noreferrer"&gt;Part 2: Add &amp;amp; Change Columns Without Downtime&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://dev-talk.com/post/laravel-migrations-add-foreign-keys-without-locking" rel="noopener noreferrer"&gt;DEV Talk&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>laravel</category>
      <category>laravelmigrations</category>
      <category>foreignkeys</category>
      <category>mysql</category>
    </item>
    <item>
      <title>Laravel Migrations: Add &amp; Change Columns Without Downtime</title>
      <dc:creator>devTalk</dc:creator>
      <pubDate>Thu, 01 Oct 2026 18:10:27 +0000</pubDate>
      <link>https://dev.to/devtalk94/laravel-migrations-add-change-columns-without-downtime-373n</link>
      <guid>https://dev.to/devtalk94/laravel-migrations-add-change-columns-without-downtime-373n</guid>
      <description>&lt;p&gt;&lt;a href="https://dev-talk.com/post/adding-indexes-to-large-tables-in-laravel-without-locking-them" rel="noopener noreferrer"&gt;Part 1&lt;/a&gt; covered indexes. The next place teams get surprised in production is the &lt;code&gt;up()&lt;/code&gt; method sitting right above the index migration: adding a column, backfilling it, and eventually making it required. Each of those three steps can lock a table for a different reason, and the fix for one can be the wrong fix for another.&lt;/p&gt;

&lt;p&gt;This part covers what's actually safe to run directly, what needs to be split into stages, and why &lt;code&gt;-&amp;gt;change()&lt;/code&gt; deserves more suspicion than &lt;code&gt;-&amp;gt;index()&lt;/code&gt; ever did.&lt;/p&gt;

&lt;h2&gt;
  
  
  Adding a column: usually cheap, except when it isn't
&lt;/h2&gt;

&lt;p&gt;A plain nullable column with no default is the safe case everywhere. Postgres treats it as a metadata-only change; recent MySQL and MariaDB can add it "instantly" without touching existing rows.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nc"&gt;Schema&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;table&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'orders'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Blueprint&lt;/span&gt; &lt;span class="nv"&gt;$table&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
   &lt;span class="nv"&gt;$table&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'tracking_number'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;nullable&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two things turn this cheap operation into a table rewrite:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A&lt;/strong&gt;&lt;code&gt;NOT NULL&lt;/code&gt;&lt;strong&gt;column with no default, on old MySQL/MariaDB.&lt;/strong&gt; MySQL 8.0.12+ and MariaDB 10.3.2+ support &lt;code&gt;ALGORITHM=INSTANT&lt;/code&gt; for this case, so it's fast. Anything older rewrites the whole table under a lock while every row gets a value, which blocks reads and writes for minutes on a large table.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A volatile default on PostgreSQL.&lt;/strong&gt; &lt;code&gt;ADD COLUMN approved_at TIMESTAMPTZ DEFAULT now()&lt;/code&gt; looks as trivial as &lt;code&gt;DEFAULT false&lt;/code&gt;, but it isn't. A constant default (&lt;code&gt;false&lt;/code&gt;, &lt;code&gt;0&lt;/code&gt;, a literal string) is metadata-only on Postgres 11+: the value is returned lazily until the table is rewritten anyway. A volatile default — &lt;code&gt;now()&lt;/code&gt;, &lt;code&gt;random()&lt;/code&gt;, a subquery — returns a different value per row, so Postgres has no choice but to rewrite every row immediately, under an &lt;code&gt;ACCESS EXCLUSIVE&lt;/code&gt; lock, on every version.&lt;/p&gt;

&lt;p&gt;The safe pattern for both cases is the same: split the default out of the column addition.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;?php&lt;/span&gt;

&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Illuminate\Database\Migrations\Migration&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Illuminate\Database\Schema\Blueprint&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="no"&gt;Illuminate\Support\Facades\DB&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Illuminate\Support\Facades\Schema&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;Migration&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
   &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;up&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt;
   &lt;span class="p"&gt;{&lt;/span&gt;
       &lt;span class="c1"&gt;// Step 1: add the column with no default, no NOT NULL.&lt;/span&gt;
       &lt;span class="c1"&gt;// Metadata-only on Postgres and modern MySQL/MariaDB.&lt;/span&gt;
       &lt;span class="nc"&gt;Schema&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;table&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'orders'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Blueprint&lt;/span&gt; &lt;span class="nv"&gt;$table&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
           &lt;span class="nv"&gt;$table&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;timestampTz&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'approved_at'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;nullable&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
       &lt;span class="p"&gt;});&lt;/span&gt;

       &lt;span class="c1"&gt;// Step 2: set the default for *future* inserts only.&lt;/span&gt;
       &lt;span class="c1"&gt;// Existing rows stay NULL — no rewrite, no backfill here.&lt;/span&gt;
       &lt;span class="no"&gt;DB&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;statement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'ALTER TABLE orders ALTER COLUMN approved_at SET DEFAULT NULL'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
   &lt;span class="p"&gt;}&lt;/span&gt;

   &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;down&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt;
   &lt;span class="p"&gt;{&lt;/span&gt;
       &lt;span class="nc"&gt;Schema&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;table&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'orders'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Blueprint&lt;/span&gt; &lt;span class="nv"&gt;$table&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
           &lt;span class="nv"&gt;$table&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;dropColumn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'approved_at'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
       &lt;span class="p"&gt;});&lt;/span&gt;
   &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Backfilling the existing rows is a separate concern — covered in Part 5 of this series — because looping an &lt;code&gt;UPDATE&lt;/code&gt; over millions of rows in a migration's &lt;code&gt;up()&lt;/code&gt; has its own locking and transaction-length problems.&lt;/p&gt;

&lt;h2&gt;
  
  
  Instant column operations (Laravel 12/13, MySQL)
&lt;/h2&gt;

&lt;p&gt;Laravel's schema builder can now ask MySQL explicitly for its instant algorithm:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nc"&gt;Schema&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;table&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'orders'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Blueprint&lt;/span&gt; &lt;span class="nv"&gt;$table&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
   &lt;span class="nv"&gt;$table&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'tracking_number'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;nullable&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;instant&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Chaining &lt;code&gt;instant()&lt;/code&gt; onto a column requests MySQL's INSTANT algorithm, which performs certain schema changes without a full table rebuild regardless of table size. It only works for columns appended to the end of the table, so it can't be combined with &lt;code&gt;after()&lt;/code&gt; or &lt;code&gt;first()&lt;/code&gt;, and not every column type or operation supports it — if the operation is incompatible, MySQL raises an error rather than silently falling back to a slower algorithm.&lt;/p&gt;

&lt;p&gt;It can be combined with &lt;code&gt;lock()&lt;/code&gt; the same way index operations can:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nv"&gt;$table&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'tracking_number'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;nullable&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;instant&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;lock&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'none'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This doesn't change anything about PostgreSQL, which has no equivalent concept — on Postgres, "fast" simply means metadata-only, as described above.&lt;/p&gt;

&lt;h2&gt;
  
  
  -&amp;gt;change(): the one that looks safe and usually isn't
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;change()&lt;/code&gt; is the method most likely to cause an incident, because the code reads like a one-line fix:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nc"&gt;Schema&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;table&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'users'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Blueprint&lt;/span&gt; &lt;span class="nv"&gt;$table&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
   &lt;span class="nv"&gt;$table&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'bio'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;change&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// was VARCHAR(255)&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three separate problems hide behind that one line.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. It silently drops every modifier you don't repeat
&lt;/h3&gt;

&lt;p&gt;As of Laravel 11, changing a column requires you to include every modifier you want to keep — any attribute not explicitly restated is dropped, even if a previous migration set it. This is a behavior change from Laravel 10, where unlisted modifiers were retained.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// If `votes` was unsigned, had a default of 0, and a comment —&lt;/span&gt;
&lt;span class="c1"&gt;// and you only write this — all three are gone after the migration runs.&lt;/span&gt;
&lt;span class="nc"&gt;Schema&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;table&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'users'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Blueprint&lt;/span&gt; &lt;span class="nv"&gt;$table&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
   &lt;span class="nv"&gt;$table&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;integer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'votes'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;nullable&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;change&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// Correct: restate everything you want to keep.&lt;/span&gt;
&lt;span class="nc"&gt;Schema&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;table&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'users'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Blueprint&lt;/span&gt; &lt;span class="nv"&gt;$table&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
   &lt;span class="nv"&gt;$table&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;integer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'votes'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;unsigned&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;comment&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'my comment'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;nullable&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;change&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This isn't a locking issue, it's a data-definition footgun, but it belongs in this article because it's the thing most likely to bite you the first time you touch &lt;code&gt;change()&lt;/code&gt; in a Laravel 11+ app. Laravel 11 also removed the &lt;code&gt;doctrine/dbal&lt;/code&gt; dependency that older versions needed for &lt;code&gt;change()&lt;/code&gt; to work at all — if you're upgrading from Laravel 10 or earlier, you can drop that package from &lt;code&gt;composer.json&lt;/code&gt; once you're on 11+.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. It's usually a full table rewrite
&lt;/h3&gt;

&lt;p&gt;Changing a column's type — &lt;code&gt;string&lt;/code&gt; to &lt;code&gt;text&lt;/code&gt;, &lt;code&gt;integer&lt;/code&gt; to &lt;code&gt;bigInteger&lt;/code&gt;, resizing a &lt;code&gt;varchar&lt;/code&gt; — forces most databases to rewrite every row to verify the new type fits, under an exclusive lock for the duration. There's no &lt;code&gt;CONCURRENTLY&lt;/code&gt; or &lt;code&gt;LOCK=NONE&lt;/code&gt; equivalent for a type change in either MySQL or PostgreSQL; this is one of the few operations covered in this series that doesn't have an online-DDL escape hatch within plain SQL.&lt;/p&gt;

&lt;p&gt;The practical workaround is to not change the column's type in place at all: add a new column with the correct type, backfill it, cut the application over to it, then drop the old column in a later migration. That's slower to ship but it never blocks the table.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. PostgreSQL needs an explicit cast for some type changes
&lt;/h3&gt;

&lt;p&gt;When the new type can't be implicitly cast from the old one, Postgres needs to be told how to convert each existing value, using the &lt;code&gt;using&lt;/code&gt; modifier:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nc"&gt;Schema&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;table&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'users'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Blueprint&lt;/span&gt; &lt;span class="nv"&gt;$table&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
   &lt;span class="nv"&gt;$table&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'birthday'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;using&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'birthday::date'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;change&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Leaving this off when the implicit cast doesn't exist fails the migration outright — which is actually the safer failure mode, since you find out in CI rather than mid-table-rewrite in production.&lt;/p&gt;

&lt;h2&gt;
  
  
  Adding NOT NULL to an existing column
&lt;/h2&gt;

&lt;p&gt;This is a different operation from adding a new &lt;code&gt;NOT NULL&lt;/code&gt; column, and it's the one most commonly underestimated because the column already exists and already has data.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;MySQL:&lt;/strong&gt; &lt;code&gt;ALTER TABLE ... MODIFY COLUMN ... NOT NULL&lt;/code&gt; scans the table to confirm no existing row is null, and on InnoDB this is an online operation in modern versions — but it still has to scan, so it takes time proportional to table size and can still be blocked by the metadata lock issue from Part 1.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;PostgreSQL:&lt;/strong&gt; &lt;code&gt;SET NOT NULL&lt;/code&gt; scans the entire table to verify no nulls exist, holding the lock for the whole scan. On Postgres 12+, there's a two-step way around this: add a &lt;code&gt;NOT VALID&lt;/code&gt; check constraint first (which doesn't scan existing rows), validate it separately (which scans but only takes a lighter lock that doesn't block writes), then &lt;code&gt;SET NOT NULL&lt;/code&gt; — which, once a validated check constraint already proves the column has no nulls, skips its own table scan entirely.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;?php&lt;/span&gt;

&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Illuminate\Database\Migrations\Migration&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="no"&gt;Illuminate\Support\Facades\DB&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;Migration&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
   &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="nv"&gt;$withinTransaction&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

   &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;up&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt;
   &lt;span class="p"&gt;{&lt;/span&gt;
       &lt;span class="c1"&gt;// Step 1: add the constraint without checking existing rows.&lt;/span&gt;
       &lt;span class="no"&gt;DB&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;statement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"
           ALTER TABLE orders
           ADD CONSTRAINT orders_status_not_null
           CHECK (status IS NOT NULL) NOT VALID
       "&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

       &lt;span class="c1"&gt;// Step 2: validate separately — scans the table, but only&lt;/span&gt;
       &lt;span class="c1"&gt;// takes a SHARE UPDATE EXCLUSIVE lock, not an exclusive one.&lt;/span&gt;
       &lt;span class="no"&gt;DB&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;statement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'ALTER TABLE orders VALIDATE CONSTRAINT orders_status_not_null'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

       &lt;span class="c1"&gt;// Step 3: now cheap, because the validated constraint already&lt;/span&gt;
       &lt;span class="c1"&gt;// proves no row is null.&lt;/span&gt;
       &lt;span class="no"&gt;DB&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;statement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'ALTER TABLE orders ALTER COLUMN status SET NOT NULL'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

       &lt;span class="c1"&gt;// Step 4: the CHECK constraint is now redundant.&lt;/span&gt;
       &lt;span class="no"&gt;DB&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;statement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'ALTER TABLE orders DROP CONSTRAINT orders_status_not_null'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
   &lt;span class="p"&gt;}&lt;/span&gt;

   &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;down&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt;
   &lt;span class="p"&gt;{&lt;/span&gt;
       &lt;span class="no"&gt;DB&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;statement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'ALTER TABLE orders ALTER COLUMN status DROP NOT NULL'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
   &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run steps 1–2 in one migration and steps 3–4 in a later one if the table is large enough that you want the validation to happen during a quiet period, separately from the final constraint change.&lt;/p&gt;

&lt;h2&gt;
  
  
  MySQL's lock modifier applies here too
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;lock()&lt;/code&gt; modifier from Part 1 isn't index-specific — it applies to column and foreign key definitions as well, with the same four modes: &lt;code&gt;none&lt;/code&gt; (concurrent reads and writes), &lt;code&gt;shared&lt;/code&gt; (reads only), &lt;code&gt;exclusive&lt;/code&gt; (nothing), and &lt;code&gt;default&lt;/code&gt; (MySQL picks). If the requested lock mode is incompatible with the specific operation, MySQL raises an error rather than silently using a stricter lock.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nv"&gt;$table&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'tracking_number'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;nullable&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;lock&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'none'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;As with &lt;code&gt;inplace()&lt;/code&gt; on indexes, this is explicit opt-in: MySQL won't downgrade an operation to fit your requested lock level, it'll just fail, which is what you want in a migration you're about to run against production.&lt;/p&gt;

&lt;h2&gt;
  
  
  Renaming and dropping columns: the deploy-ordering problem
&lt;/h2&gt;

&lt;p&gt;This isn't a database locking problem — it's a rolling-deploy problem, and it catches teams who've already solved the locking issues above.&lt;/p&gt;

&lt;p&gt;Dropping a column the application still queries doesn't break the database. It breaks whichever of your app's processes are still running old code that references the now-gone column, during the window between when the migration runs and when every server has picked up the new deploy.&lt;/p&gt;

&lt;p&gt;The sequence that avoids this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Deploy code that stops reading or writing the old column.&lt;/li&gt;
&lt;li&gt;Wait until every old process has fully drained — confirm via your deploy tooling, not a guess.&lt;/li&gt;
&lt;li&gt;Only then ship the migration that drops the column.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Renaming a column is the same problem twice: it's effectively a drop of the old name and an add of the new one from the point of view of any code still running the old version.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Don't do this in one step on a live table:&lt;/span&gt;
&lt;span class="nc"&gt;Schema&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;table&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'users'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Blueprint&lt;/span&gt; &lt;span class="nv"&gt;$table&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
   &lt;span class="nv"&gt;$table&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;renameColumn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'full_name'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'display_name'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The safer sequence: add &lt;code&gt;display_name&lt;/code&gt; as a new column, write to both columns from the application for one deploy cycle, backfill &lt;code&gt;display_name&lt;/code&gt; from &lt;code&gt;full_name&lt;/code&gt;, switch all reads to &lt;code&gt;display_name&lt;/code&gt;, then drop &lt;code&gt;full_name&lt;/code&gt; in a later migration once old code has drained.&lt;/p&gt;

&lt;h2&gt;
  
  
  Quick reference
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Operation&lt;/th&gt;
&lt;th&gt;Risk&lt;/th&gt;
&lt;th&gt;Safe pattern&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Add nullable column, no default&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;Run directly&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Add column with constant default&lt;/td&gt;
&lt;td&gt;None on Postgres 11+/modern MySQL&lt;/td&gt;
&lt;td&gt;Run directly&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Add column with volatile default (now())&lt;/td&gt;
&lt;td&gt;Full table rewrite&lt;/td&gt;
&lt;td&gt;Add column first, set default separately&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Add NOT NULL column, no default&lt;/td&gt;
&lt;td&gt;Full rewrite on MySQL &amp;lt; 8.0.12&lt;/td&gt;
&lt;td&gt;Add nullable, backfill, then constrain&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;-&amp;gt;change() type change&lt;/td&gt;
&lt;td&gt;Full table rewrite, dropped modifiers&lt;/td&gt;
&lt;td&gt;New column → backfill → cutover → drop old&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SET NOT NULL on existing column&lt;/td&gt;
&lt;td&gt;Full table scan under exclusive lock (Postgres)&lt;/td&gt;
&lt;td&gt;NOT VALID check → VALIDATE → SET NOT NULL&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Drop/rename column&lt;/td&gt;
&lt;td&gt;Breaks old app processes mid-deploy&lt;/td&gt;
&lt;td&gt;Stop referencing → drain → drop, in that order&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Before you run it in production
&lt;/h2&gt;

&lt;p&gt;Run &lt;code&gt;php artisan migrate --pretend&lt;/code&gt; and read the generated SQL for every modifier you expect to still be there after a &lt;code&gt;change()&lt;/code&gt;. Split any volatile default into "add column" then "set default" as two statements. For &lt;code&gt;NOT NULL&lt;/code&gt; on an existing Postgres column, use the &lt;code&gt;NOT VALID&lt;/code&gt; constraint path rather than a bare &lt;code&gt;SET NOT NULL&lt;/code&gt;. Never drop or rename a column in the same deploy that stops using it — give old processes time to drain first.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Related reading:&lt;/strong&gt; if you haven't locked down index creation yet, start with &lt;a href="https://dev-talk.com/post/adding-indexes-to-large-tables-in-laravel-without-locking-them" rel="noopener noreferrer"&gt;Part 1: Adding Indexes to Large Tables in Laravel Without Locking Them&lt;/a&gt;, which covers &lt;code&gt;CREATE INDEX CONCURRENTLY&lt;/code&gt; and MySQL's &lt;code&gt;ALGORITHM=INPLACE, LOCK=NONE&lt;/code&gt; in detail.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://dev-talk.com/post/laravel-migrations-add-change-columns-without-downtime" rel="noopener noreferrer"&gt;DEV Talk&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>laravel</category>
      <category>laravelmigrations</category>
      <category>mysql</category>
      <category>postgres</category>
    </item>
    <item>
      <title>🚨 Laravel developers: adding an index to a large production table isn't always harmless.</title>
      <dc:creator>devTalk</dc:creator>
      <pubDate>Sun, 27 Sep 2026 16:49:52 +0000</pubDate>
      <link>https://dev.to/devtalk94/laravel-developers-adding-an-index-to-a-large-production-table-isnt-always-harmless-1jm0</link>
      <guid>https://dev.to/devtalk94/laravel-developers-adding-an-index-to-a-large-production-table-isnt-always-harmless-1jm0</guid>
      <description>&lt;p&gt;A simple:&lt;/p&gt;

&lt;p&gt;$table-&amp;gt;index(...)&lt;/p&gt;

&lt;p&gt;can lead to locking, slow queries, and application impact when the table has millions of rows.&lt;/p&gt;

&lt;p&gt;I wrote about safer approaches to adding indexes without unnecessarily blocking your Laravel app.&lt;/p&gt;

&lt;p&gt;👇 Read the guide:&lt;br&gt;
&lt;/p&gt;
&lt;div class="crayons-card c-embed text-styles text-styles--secondary"&gt;
    &lt;div class="c-embed__content"&gt;
        &lt;div class="c-embed__cover"&gt;
          &lt;a href="https://dev-talk.com/post/adding-indexes-to-large-tables-in-laravel-without-locking-them" class="c-link align-middle" rel="noopener noreferrer"&gt;
            &lt;img alt="" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-talk.com%2Fstorage%2Fimages%2Fposts%2F1790527375_database-index.webp" height="433" class="m-0" width="799"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="c-embed__body"&gt;
        &lt;h2 class="fs-xl lh-tight"&gt;
          &lt;a href="https://dev-talk.com/post/adding-indexes-to-large-tables-in-laravel-without-locking-them" rel="noopener noreferrer" class="c-link"&gt;
            Adding Indexes to Large Tables in Laravel Without Locking Them
          &lt;/a&gt;
        &lt;/h2&gt;
          &lt;p class="truncate-at-3"&gt;
            Learn how to add indexes to large MySQL and PostgreSQL tables in Laravel without locking production — with working migration code and gotchas.
          &lt;/p&gt;
        &lt;div class="color-secondary fs-s flex items-center"&gt;
            &lt;img alt="favicon" class="c-embed__favicon m-0 mr-2 radius-0" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-talk.com%2Fstorage%2Fimages%2Fsite%2Ffavicon68adda75def64.png" width="114" height="114"&gt;
          dev-talk.com
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;


</description>
      <category>database</category>
      <category>laravel</category>
      <category>devops</category>
      <category>backenddevelopment</category>
    </item>
    <item>
      <title>Stop repeating your where() clauses in Laravel 🔁</title>
      <dc:creator>devTalk</dc:creator>
      <pubDate>Thu, 13 Aug 2026 13:06:14 +0000</pubDate>
      <link>https://dev.to/devtalk94/stop-repeating-your-where-clauses-in-laravel-5169</link>
      <guid>https://dev.to/devtalk94/stop-repeating-your-where-clauses-in-laravel-5169</guid>
      <description>&lt;p&gt;If you've ever needed multiple aggregate queries (count, sum, avg) on the same filtered data, you've probably copy-pasted the same where() chain over and over.&lt;/p&gt;

&lt;p&gt;The problem? Laravel's query builder is mutable. Once you run one query, chaining more methods onto it doesn't give you a "fresh" copy of your filters.&lt;/p&gt;

&lt;p&gt;The fix: PHP's clone keyword.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nv"&gt;$query&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Event&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'status'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'active'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'created_at'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'&amp;gt;='&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;subDays&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

&lt;span class="nv"&gt;$totalCount&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;clone&lt;/span&gt; &lt;span class="nv"&gt;$query&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nb"&gt;count&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="nv"&gt;$groupedCount&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;clone&lt;/span&gt; &lt;span class="nv"&gt;$query&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;selectRaw&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'count(*) as count'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;groupBy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'event'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same filters, zero repetition, no leftover state messing up your results.&lt;/p&gt;

&lt;p&gt;Small trick, but it keeps analytics/reporting code a lot cleaner. &lt;/p&gt;

&lt;p&gt;Full write-up with more examples 👇&lt;br&gt;
&lt;a href="https://dev-talk.com/post/stop-repeating-your-where-clauses-use-clone-for-multiple-aggregates-in-laravel" rel="noopener noreferrer"&gt;Read Article&lt;/a&gt;&lt;/p&gt;

</description>
      <category>laravel</category>
      <category>php</category>
      <category>webdev</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>🚀 Laravel 13.24 Added a Small Rule That Solves a Really Common API Headache</title>
      <dc:creator>devTalk</dc:creator>
      <pubDate>Thu, 13 Aug 2026 02:50:24 +0000</pubDate>
      <link>https://dev.to/devtalk94/laravel-1324-added-a-small-rule-that-solves-a-really-common-api-headache-52e3</link>
      <guid>https://dev.to/devtalk94/laravel-1324-added-a-small-rule-that-solves-a-really-common-api-headache-52e3</guid>
      <description>&lt;p&gt;Ever had a client send extra, unexpected keys inside a filter or settings array? A typo, stale frontend field, or someone poking at your validation?&lt;/p&gt;

&lt;p&gt;Laravel finally has a clean, built-in way to lock this down: Rule::arrayKeys().&lt;/p&gt;

&lt;p&gt;'filter' =&amp;gt; Rule::arrayKeys([&lt;br&gt;
    'status',&lt;br&gt;
    'author',&lt;br&gt;
    'tag',&lt;br&gt;
]),&lt;/p&gt;

&lt;p&gt;Send anything outside status, author, or tag, and validation fails — with a message that actually tells you what went wrong (not just a vague "must be an array").&lt;/p&gt;

&lt;p&gt;In the article I cover:&lt;br&gt;
🔹 How array_keys differs from the old array:key1,key2 syntax (and why the old error messages were misleading)&lt;br&gt;
🔹 Pairing it with required_array_keys() when some keys must be present&lt;br&gt;
🔹 Using the :unexpected placeholder for genuinely helpful error responses&lt;br&gt;
🔹 A full real-world example validating an API filter payload&lt;/p&gt;

&lt;p&gt;No more writing custom Rule classes just to check array shapes. &lt;/p&gt;

&lt;p&gt;Full writeup + code here: &lt;a href="https://dev-talk.com/post/laravel-array-keys-validation-rule-stop-accepting-unexpected-array-data" rel="noopener noreferrer"&gt;Link&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;What's your go-to approach for validating nested array payloads in Laravel? Curious if anyone's been doing this a different way.&lt;/p&gt;

</description>
      <category>laravel</category>
      <category>api</category>
      <category>webdev</category>
      <category>php</category>
    </item>
    <item>
      <title>Argon2 vs Bcrypt: The Modern Standard for Secure Passwords</title>
      <dc:creator>devTalk</dc:creator>
      <pubDate>Tue, 11 Aug 2026 14:24:45 +0000</pubDate>
      <link>https://dev.to/devtalk94/argon2-vs-bcrypt-the-modern-standard-for-secure-passwords-13l5</link>
      <guid>https://dev.to/devtalk94/argon2-vs-bcrypt-the-modern-standard-for-secure-passwords-13l5</guid>
      <description>&lt;p&gt;Password hashing is one of those "everyone knows it matters, nobody wants to research it at midnight" topics. This post breaks down Bcrypt vs Argon2 in plain English — no crypto degree required — and gives you a clear answer on which one to use in 2026, plus a zero-downtime migration path if you're still on Bcrypt.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://dev-talk.com/post/argon2-vs-bcrypt-the-modern-standard-for-secure-passwords" rel="noopener noreferrer"&gt;Read full article&lt;/a&gt;&lt;/p&gt;

</description>
      <category>security</category>
      <category>webdev</category>
      <category>programming</category>
      <category>beginners</category>
    </item>
    <item>
      <title>Preventing Password Reuse in Laravel 🔐</title>
      <dc:creator>devTalk</dc:creator>
      <pubDate>Thu, 30 Jul 2026 16:16:08 +0000</pubDate>
      <link>https://dev.to/devtalk94/preventing-password-reuse-in-laravel-513i</link>
      <guid>https://dev.to/devtalk94/preventing-password-reuse-in-laravel-513i</guid>
      <description>&lt;p&gt;Wrote up how I built &lt;a href="https://github.com/DasunMuthuruwan/laravel-password-history" rel="noopener noreferrer"&gt;&lt;code&gt;laravel-password-history&lt;/code&gt;&lt;/a&gt; — a Composer package that stops users from recycling old passwords, using a polymorphic history table so it works with any Eloquent model, not just &lt;code&gt;User&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Covers the validation rule, the auto-pruning logic, a scheduled cleanup command, and a service-provider bug I ran into (translations silently failing outside the console context).&lt;/p&gt;

&lt;p&gt;📖 Full post: &lt;a href="https://dev-talk.com/post/preventing-password-reuse-in-laravel-building-a-password-history-package" rel="noopener noreferrer"&gt;Article&lt;/a&gt;&lt;br&gt;
📦 &lt;code&gt;composer require devdasun/password-history&lt;/code&gt;&lt;br&gt;
🔗 &lt;a href="https://github.com/DasunMuthuruwan/laravel-password-history" rel="noopener noreferrer"&gt;Repo&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Feedback and PRs welcome, especially from anyone using non-standard auth setups.&lt;/p&gt;

</description>
      <category>laravel</category>
      <category>php</category>
      <category>security</category>
      <category>backenddevelopment</category>
    </item>
    <item>
      <title>MySQL WITH Clause &amp; CTEs - A Complete Guide with Examples</title>
      <dc:creator>devTalk</dc:creator>
      <pubDate>Sat, 06 Jun 2026 11:08:30 +0000</pubDate>
      <link>https://dev.to/devtalk94/mysql-with-clause-ctes-a-complete-guide-with-examples-50ba</link>
      <guid>https://dev.to/devtalk94/mysql-with-clause-ctes-a-complete-guide-with-examples-50ba</guid>
      <description>&lt;h1&gt;
  
  
  Stop Nesting Subqueries — Use CTEs Instead 🧠
&lt;/h1&gt;

&lt;p&gt;Ever written a SQL query so deeply nested you couldn't&lt;br&gt;
remember what the outer SELECT was even doing?&lt;/p&gt;

&lt;p&gt;Yeah. We've all been there.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Common Table Expressions (CTEs)&lt;/strong&gt; — introduced in MySQL 8.0&lt;br&gt;
via the &lt;code&gt;WITH&lt;/code&gt; keyword — are the clean, readable alternative&lt;br&gt;
you've been looking for.&lt;/p&gt;

&lt;h2&gt;
  
  
  What you'll learn in this guide
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;🔹 What a CTE actually is (no fluff)&lt;/li&gt;
&lt;li&gt;🔹 Basic &lt;code&gt;WITH&lt;/code&gt; clause syntax&lt;/li&gt;
&lt;li&gt;🔹 CTEs vs Subqueries — side by side&lt;/li&gt;
&lt;li&gt;🔹 Chaining multiple CTEs in one query&lt;/li&gt;
&lt;li&gt;🔹 &lt;code&gt;WITH RECURSIVE&lt;/code&gt; for hierarchies &amp;amp; org charts&lt;/li&gt;
&lt;li&gt;🔹 Real-world examples: running totals, deduplication, DELETE&lt;/li&gt;
&lt;li&gt;🔹 Pitfalls to avoid (infinite loops, indexing, MySQL version)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The core idea in 10 seconds
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="n"&gt;big_orders&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;order_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;customer_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;total_amount&lt;/span&gt;
    &lt;span class="k"&gt;FROM&lt;/span&gt;   &lt;span class="n"&gt;orders&lt;/span&gt;
    &lt;span class="k"&gt;WHERE&lt;/span&gt;  &lt;span class="n"&gt;total_amount&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;500&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;customer_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;order_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;total_amount&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;   &lt;span class="n"&gt;big_orders&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;
&lt;span class="k"&gt;JOIN&lt;/span&gt;   &lt;span class="n"&gt;customers&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;customer_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;customer_id&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. You named a query block &lt;code&gt;big_orders&lt;/code&gt; and used it&lt;br&gt;
like a table. No nested mess. No repeated logic.&lt;/p&gt;

&lt;h2&gt;
  
  
  Ready to level up your SQL?
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://dev-talk.com/post/mysql-with-clause-common-table-expressions-ctes-a-complete-guide" rel="noopener noreferrer"&gt;👉 Read the full guide&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;─────────────────────────────&lt;br&gt;
💬 Do you already use CTEs in your projects?&lt;br&gt;
What's the most complex CTE you've written?&lt;br&gt;
Share it below — let's learn from each other! 👇&lt;br&gt;
─────────────────────────────&lt;/p&gt;

</description>
      <category>mysql</category>
      <category>database</category>
      <category>webdev</category>
      <category>development</category>
    </item>
    <item>
      <title>Redis Is Not Just a Cache — 8 Use Cases With Real Code</title>
      <dc:creator>devTalk</dc:creator>
      <pubDate>Sun, 24 May 2026 08:56:59 +0000</pubDate>
      <link>https://dev.to/devtalk94/redis-is-not-just-a-cache-8-use-cases-with-real-code-4d9a</link>
      <guid>https://dev.to/devtalk94/redis-is-not-just-a-cache-8-use-cases-with-real-code-4d9a</guid>
      <description>&lt;p&gt;Everyone learns Redis as a cache on day one.&lt;br&gt;
Most developers never go further.&lt;/p&gt;

&lt;p&gt;But here's the truth — the teams building at scale &lt;br&gt;
use Redis for session storage, real-time leaderboards, &lt;br&gt;
distributed locks, AI vector search, and a lot more.&lt;/p&gt;

&lt;p&gt;In this post I break down 8 real Redis use cases &lt;br&gt;
with actual commands and code so you can start &lt;br&gt;
applying them today.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What's inside:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;⚡ 8 production-grade Redis use cases explained clearly&lt;/li&gt;
&lt;li&gt;💻 Real Redis commands for every use case&lt;/li&gt;
&lt;li&gt;🤖 How Redis powers modern AI systems (Redis Iris)&lt;/li&gt;
&lt;li&gt;📊 Quick reference table at the end&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Whether you're a beginner just getting started or &lt;br&gt;
a backend dev looking to level up — this one's for you.&lt;/p&gt;

&lt;p&gt;Drop a ❤️ if you found it useful and comment &lt;br&gt;
which use case surprised you the most 👇&lt;br&gt;
&lt;a href="https://dev-talk.com/post/top-8-redis-use-cases-every-developer-should-know" rel="noopener noreferrer"&gt;READ ARTICLE&lt;/a&gt;&lt;/p&gt;

</description>
      <category>redis</category>
      <category>webdev</category>
      <category>programming</category>
      <category>ai</category>
    </item>
    <item>
      <title>🔍 Struggling with slow search in your app?</title>
      <dc:creator>devTalk</dc:creator>
      <pubDate>Sun, 17 May 2026 10:44:59 +0000</pubDate>
      <link>https://dev.to/devtalk94/struggling-with-slow-search-in-your-app-3mm8</link>
      <guid>https://dev.to/devtalk94/struggling-with-slow-search-in-your-app-3mm8</guid>
      <description>&lt;p&gt;I just published a beginner-friendly guide on Elasticsearch — &lt;br&gt;
the search engine powering GitHub, Netflix, and Uber.&lt;/p&gt;

&lt;p&gt;Here's what you'll learn in 3 minutes:&lt;/p&gt;

&lt;p&gt;✅ What Elasticsearch is (and when NOT to use it)&lt;br&gt;
✅ Core concepts — Index, Document, Shard, Replica&lt;br&gt;
✅ How to install it locally with Docker in 1 command&lt;br&gt;
✅ Writing your first search query (with fuzzy/typo support!)&lt;br&gt;
✅ Real-world use cases — e-commerce, logs, site search&lt;/p&gt;

&lt;p&gt;💡 The biggest mistake beginners make?&lt;br&gt;
Using Elasticsearch as their primary database. It's not. &lt;br&gt;
Use it alongside PostgreSQL/MySQL — and watch your search fly.&lt;/p&gt;

&lt;p&gt;👇 &lt;a href="https://dev-talk.com/post/elasticsearch-for-beginners-a-simple-guide-to-getting-started" rel="noopener noreferrer"&gt;Full beginner guide here&lt;/a&gt;&lt;/p&gt;

&lt;h1&gt;
  
  
  Elasticsearch #BackendDevelopment #WebDevelopment
&lt;/h1&gt;

&lt;h1&gt;
  
  
  OpenSource #ELKStack #SearchEngine #Programming #DevCommunity
&lt;/h1&gt;

</description>
      <category>webdev</category>
      <category>programming</category>
      <category>softwaredevelopment</category>
      <category>elasticsearch</category>
    </item>
    <item>
      <title>AWK vs MAWK: Differences, Performance &amp; Real-World Examples</title>
      <dc:creator>devTalk</dc:creator>
      <pubDate>Thu, 14 May 2026 11:35:10 +0000</pubDate>
      <link>https://dev.to/devtalk94/awk-vs-mawk-differences-performance-real-world-examples-4k79</link>
      <guid>https://dev.to/devtalk94/awk-vs-mawk-differences-performance-real-world-examples-4k79</guid>
      <description>&lt;p&gt;TL;DR — There are two popular AWK implementations &lt;br&gt;
on Linux and most developers only know one of them.&lt;/p&gt;

&lt;p&gt;MAWK processes a 500MB file in 3.1 seconds.&lt;br&gt;
GAWK takes 8.4 seconds for the same task.&lt;/p&gt;

&lt;p&gt;Same syntax. No script changes needed.&lt;br&gt;
Just a faster engine under the hood.&lt;/p&gt;

&lt;p&gt;Quick decision rule I now follow:&lt;/p&gt;

&lt;p&gt;→ Start with mawk for speed&lt;br&gt;
→ Switch to gawk only if you need extensions&lt;br&gt;
→ Your scripts stay portable either way&lt;/p&gt;

&lt;p&gt;&lt;a href="https://dev-talk.com/post/awk-vs-mawk-differences-performance-real-world-examples" rel="noopener noreferrer"&gt;Read article&lt;/a&gt;&lt;/p&gt;

</description>
      <category>linux</category>
      <category>bash</category>
      <category>devops</category>
      <category>programming</category>
    </item>
    <item>
      <title>🔥 Laravel devs — stop using DB::enableQueryLog()</title>
      <dc:creator>devTalk</dc:creator>
      <pubDate>Thu, 14 May 2026 11:31:25 +0000</pubDate>
      <link>https://dev.to/devtalk94/laravel-devs-stop-using-dbenablequerylog-c57</link>
      <guid>https://dev.to/devtalk94/laravel-devs-stop-using-dbenablequerylog-c57</guid>
      <description>&lt;p&gt;I just published a quick but powerful tip that most &lt;br&gt;
Laravel developers overlook completely.&lt;/p&gt;

&lt;p&gt;Did you know Laravel has 6 built-in Eloquent query &lt;br&gt;
debugging methods?&lt;/p&gt;

&lt;p&gt;Most of us write this the hard way:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="no"&gt;DB&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;enableQueryLog&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="nv"&gt;$users&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;User&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'active'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="nf"&gt;dd&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;DB&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;getQueryLog&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When you can just do this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nc"&gt;User&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'active'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;orderBy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'created_at'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'desc'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;ddRawSql&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One line. Full raw SQL. No setup. No package needed.&lt;/p&gt;

&lt;p&gt;Here's the full toolkit:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Method&lt;/th&gt;
&lt;th&gt;What it does&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;-&amp;gt;dd()&lt;/td&gt;
&lt;td&gt;Dump results and die&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;-&amp;gt;dump()&lt;/td&gt;
&lt;td&gt;Dump results, continue execution&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;-&amp;gt;ddRawSql()&lt;/td&gt;
&lt;td&gt;Full raw SQL with bindings, then die&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;-&amp;gt;dumpRawSql()&lt;/td&gt;
&lt;td&gt;Full raw SQL, continue execution&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;-&amp;gt;toSql()&lt;/td&gt;
&lt;td&gt;SQL string with ? placeholders&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;-&amp;gt;getBindings()&lt;/td&gt;
&lt;td&gt;Array of all binding values&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;I covered all 6 methods with real Artisan Tinker &lt;br&gt;
examples in my latest article.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://dev-talk.com/post/laravel-sql-query-debugging-dd-and-ddrawsql-tips" rel="noopener noreferrer"&gt;👉 Read it here&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Drop a 🔥 if you didn't know about -&amp;gt;ddRawSql()!&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>development</category>
      <category>softwaredevelopment</category>
      <category>laravel</category>
    </item>
  </channel>
</rss>
