paulserban.eu

Writing Edition

Paul Serban

Writing, snippets & book notes

Cheat sheet - Post

Safe SQL Schema Migrations with the Expand-Contract Pattern

← Back to Database Migrations in SQL: Fundamentals, Practices, and the Domain Language Every Team Needs

Database migrations are versioned, scripted schema changes, and the hard part isn't the SQL syntax but safely applying it to live, stateful data that concurrent old and new application code both depend on.

Overview

Key Concepts

Migration Domain Vocabulary

Migration Tooling Architecture

Locking Behavior

The Expand-Contract Pattern

Trade-offs / Caveats

Example in Practice

Renaming email to email_address on a live users table, expand-contract style:

-- Expand
ALTER TABLE users ADD COLUMN email_address TEXT;
# Migrate: batched backfill, not a single UPDATE
BATCH_SIZE = 5_000
def backfill_email_address(engine):
    with engine.connect() as conn:
        while True:
            result = conn.execute(text("""
                UPDATE users SET email_address = email
                WHERE id IN (
                    SELECT id FROM users WHERE email_address IS NULL LIMIT :batch_size
                )"""), {"batch_size": BATCH_SIZE})
            conn.commit()
            if result.rowcount == 0:
                break
            time.sleep(0.1)

Between expand and contract, the app is deployed to dual-write both columns, then switched to read only email_address once backfill and monitoring confirm consistency. Only after an observation period does contract run:

-- Contract
ALTER TABLE users DROP COLUMN email;

Related topics