← Home ← Codex ← DEBT ← Engine
Browse by Category
+ added · updated 7d
← Back to glossary

Safe Database Migration Patterns

Database PHP 5.0+ Intermediate
debt(d7/e7/b7/t7)
d7 Detectability Operational debt — how invisible misuse is to your safety net

Closest to 'only careful code review or runtime testing' (d7). While tools like gh-ost and pt-online-schema-change exist for online schema changes, they don't automatically detect unsafe migration patterns in your migration files. Laravel migrations or similar frameworks don't warn you about adding NOT NULL columns without defaults to large tables. Detection typically requires careful code review of migration files against table sizes, or discovering the problem during deployment when table locks occur.

e7 Effort Remediation debt — work required to fix once spotted

Closest to 'cross-cutting refactor across the codebase' (e7). Fixing an unsafe migration pattern often requires the expand-contract approach: add nullable column, deploy code changes, backfill data, add constraints, deploy again, then remove old column. This spans multiple deployments, requires coordinating code changes with schema changes, and may involve multiple migration files plus application code changes. The quick_fix describes a multi-step process across separate deploys.

b7 Burden Structural debt — long-term weight of choosing wrong

Closest to 'strong gravitational pull' (b7). Migration practices apply broadly to all database contexts (web, cli) and every future schema change must follow these patterns to avoid downtime. Once a codebase has large tables, every migration becomes a potential incident. The choice to use expand-contract patterns affects deployment pipelines, release processes, and how every developer writes migrations going forward. It's not quite architectural (b9) but significantly shapes development workflow.

t7 Trap Cognitive debt — how counter-intuitive correct behaviour is

Closest to 'serious trap' (t7). The misconception explicitly states that developers believe 'running migrations as part of deployment is always safe.' This contradicts the expected behavior because what works fine on a dev database with 100 rows becomes catastrophic on production with millions of rows. The intuitive approach (add NOT NULL column, deploy) is exactly wrong for large tables. Developers coming from small-scale applications will consistently guess wrong about migration safety.

About DEBT scoring →

Also Known As

migration best practices zero-downtime migration schema migration patterns

TL;DR

Expand-contract, backward-compatible migrations run alongside a zero-downtime deploy — never rename or drop columns in the same deploy as the code change.

Explanation

Database migrations must be safe to run against the old code (before deploy) and backward-compatible with the new code after deploy. The expand-contract pattern: Step 1 (Expand) — add the new column as nullable, keep old column; Step 2 — deploy new code that writes to both columns; Step 3 — backfill old data into new column; Step 4 (Contract) — remove old column once fully migrated. Never: rename a column in one deploy (code and DB diverge), drop a column without a deploy that stops using it first, or add a NOT NULL column without a default. Large table changes: use pt-online-schema-change (MySQL) or pg_repack (PostgreSQL) to avoid long table locks. Tools: Phinx, Doctrine Migrations, Laravel migrations.

Diagram

flowchart LR
    subgraph Unsafe - Breaking
        M1[Rename column - breaks old app]
        M2[Drop column - breaks old app]
        M3[Change type - breaks old app]
    end
    subgraph Safe - Expand Contract
        S1[Add new column nullable]
        S2[Deploy app reading both columns]
        S3[Backfill data]
        S4[Deploy app using new column only]
        S5[Drop old column]
        S1 --> S2 --> S3 --> S4 --> S5
    end
    style M1 fill:#f85149,color:#fff
    style M2 fill:#f85149,color:#fff
    style M3 fill:#f85149,color:#fff
    style S1 fill:#238636,color:#fff
    style S5 fill:#238636,color:#fff

Common Misconception

Running migrations as part of deployment is always safe. Migrations that add NOT NULL columns without defaults, rename columns, or change types lock tables on large datasets — potentially causing downtime. Zero-downtime migrations use expand-contract: add new column, backfill, deploy app, remove old column in a separate release.

Why It Matters

Migration best practices ensure schema changes are safe, reversible, and compatible with rolling deploys — a poorly written migration can lock a table for minutes, causing downtime.

Common Mistakes

  • Adding a NOT NULL column without a default to a large table — locks the table while backfilling.
  • Dropping a column that application code still references — deploy order matters.
  • Irreversible migrations with no down() — prevents rollback when a deploy fails.
  • Running migrations inside the deploy step rather than as a separate, pre-deploy step.

Code Examples

✗ Vulnerable
// Dangerous migration — locks large table:
public function up(): void {
    Schema::table('orders', function($t) {
        $t->string('tracking_code')->notNull(); // Locks table while adding NOT NULL to 10M rows
    });
}

// Safe — add nullable first, backfill, then add constraint:
// Step 1: $t->string('tracking_code')->nullable();
// Step 2: backfill existing rows
// Step 3: $t->string('tracking_code')->notNull()->change();
✓ Fixed
-- Safe: add nullable column (backward compatible with old code)
ALTER TABLE users ADD COLUMN display_name VARCHAR(100) NULL;

-- Safe: rename via expand-contract (3 deploys)
-- Deploy 1: add new column
ALTER TABLE users ADD COLUMN full_name VARCHAR(100);
-- Deploy 2: write both columns, read from old one
-- Deploy 3 (after backfill): read from new column, then drop old
ALTER TABLE users DROP COLUMN name;

-- UNSAFE: rename in one migration (breaks running old code)
-- ALTER TABLE users RENAME COLUMN name TO full_name;

-- Large table: use pt-online-schema-change (MySQL) — no table lock
$ pt-online-schema-change --alter 'ADD INDEX (email)' D=myapp,t=users --execute

Added 15 Mar 2026
Edited 22 Mar 2026
Views 83
Rate this term
No ratings yet
🤖 AI Guestbook educational data only
| |
Last 30 days
0 pings S 0 pings M 0 pings T 0 pings W 0 pings T 2 pings F 1 ping S 3 pings S 0 pings M 0 pings T 1 ping W 1 ping T 0 pings F 2 pings S 0 pings S 1 ping M 0 pings T 0 pings W 2 pings T 1 ping F 1 ping S 0 pings S 0 pings M 0 pings T 0 pings W 1 ping T 0 pings F 0 pings S 2 pings S 0 pings M
No pings yet today
PetalBot 1 Google 1
Amazonbot 9 ChatGPT 9 Ahrefs 6 Google 5 PetalBot 5 Perplexity 3 SEMrush 3 Scrapy 3 Twitter/X 2 Unknown AI 1 Bing 1 Brave Search 1 Applebot 1
crawler 47 crawler_json 2
🧱 FUNDAMENTALS — new to this? Start with the ground floor.
Database database A database is an organized collection of data stored electronically, designed so programs can efficiently retrieve, add, update, and delete information.

Nearly every application needs to remember information between sessions. Databases provide the reliable, fast, and organized storage that makes persistent data possible at any scale.

💡 Always use prepared statements with placeholders—never concatenate user input directly into database queries.

Ask Codex about Database →
DEV INTEL Tools & Severity
🟠 High ⚙ Fix effort: High
⚡ Quick Fix
Never lock tables in a migration during business hours — add columns as nullable first, backfill, add constraints, then make NOT NULL in a later deploy after code no longer uses the old schema
📦 Applies To
PHP 5.0+ web cli laravel doctrine
🔗 Prerequisites
🔍 Detection Hints
Adding NOT NULL column without default on large table; renaming column in migration while old code still references old name
Auto-detectable: ✗ No gh-ost pt-online-schema-change laravel-migrations
⚠ Related Problems
🤖 AI Agent
Confidence: Medium False Positives: Low ✗ Manual fix Fix: Medium Context: File Tests: Update


✓ schema.org compliant