Skip to content

Squashing Migrations — Complete Guide

DodaTech Updated 2026-06-28 9 min read

In this tutorial, you will learn about Squashing Migrations. We cover key concepts, practical examples, and best practices to help you master this topic.

Learn squashing migrations: combine hundreds of old Migration files into a single baseline migration for faster fresh database setups, cleaner history, and improved CI/CD pipeline performance in long-lived projects.

What You Learn

You will learn what migration squashing is, why it is necessary for long-lived projects, how to squash migrations in Alembic, Django, Flyway, and Prisma, how to handle dependencies between squashed and unsquashed migrations, and best practices for squashing in team environments.

Why It Matters

After months or years of development, projects accumulate hundreds of migration files. Creating a fresh database requires applying all of them, which takes minutes. CI/CD pipelines slow down. New developers wait for migrations to complete. Squashing combines old migrations into a single baseline, reducing fresh setup time from minutes to seconds.

Real-World Use

DodaTech's backend had 347 migration files accumulated over 3 years. Fresh database setup took 4 minutes and 23 seconds. After squashing the first 300 migrations into a single baseline, fresh setup took 8 seconds. CI pipeline time dropped from 12 minutes to 8 minutes.

When to Squash

const squashCriteria = {
    migrationCount: 347,  // Over 100 migrations = consider squashing
    freshSetupTime: '4m 23s',  // Over 30 seconds = consider squashing
    oldestMigration: '2023-01-15',  // Over 6 months old = consider squashing
    CI_pipelineTime: '12m',  // Migration portion contributes significantly
};

function shouldSquash(metrics) {
    return (
        metrics.migrationCount > 100 ||
        metrics.freshSetupTime > 30 ||
        metrics.CI_pipelineTime > 5
    );
}

Expected output: Squashing is recommended when migration count exceeds 100, fresh setup takes over 30 seconds, or CI pipeline time is dominated by migrations. Older migrations (over 6 months) are unlikely to be rolled back individually.

Alembic Squashing

# Check current migration count
alembic history | wc -l
# Output: 347

# Squash migrations up to a specific revision
alembic merge -m "squash" $(alembic heads | awk '{print $1}')

# Alternative: Create a squash migration manually
alembic revision --autogenerate -m "squash_001_to_300"
# migrations/squash_001_to_300.py
"""Squash migrations 001 through 300.

Combine all schema changes from the first 300 migrations
into a single baseline migration.
"""
from alembic import op
import sqlalchemy as sa

revision = 'squash_revision_001'
down_revision = None  # This becomes the new base

def upgrade():
    # Combined schema from all 300 migrations
    op.create_table(
        'users',
        sa.Column('id', sa.Integer(), primary_key=True),
        sa.Column('email', sa.String(255), nullable=False),
        sa.Column('name', sa.String(200)),
        sa.Column('created_at', sa.DateTime(), server_default=sa.func.now()),
    )
    op.create_index('idx_users_email', 'users', ['email'], unique=True)

    op.create_table(
        'orders',
        sa.Column('id', sa.Integer(), primary_key=True),
        sa.Column('user_id', sa.Integer(), nullable=False),
        sa.Column('total', sa.Float(), nullable=False),
        sa.Column('status', sa.String(20), server_default='pending'),
    )
    op.create_foreign_key('fk_orders_users', 'orders', 'users', ['user_id'], ['id'])
    # ... all other schema elements from squashed migrations

def downgrade():
    # Reverse everything
    op.drop_table('orders')
    op.drop_table('users')

Expected output: Squash migration combines all schema changes from 300 migrations into a single file. It becomes the new base (down_revision = None). Fresh databases start with the squash migration, skipping the 300 individual files.

Django Squashing

# Django provides built-in squash support
python manage.py squashmigrations users 0001 0300

# Output:
# Creating squash migration for users...
# Will squash 300 migrations into 1
# Created new migration: users/migrations/0301_squash_0001_0300.py
# users/migrations/0301_squash_0001_0300.py
# Auto-generated Django squash migration
from django.db import migrations, models

class Migration(migrations.Migration):
    replaces = [
        ('users', '0001_initial'),
        ('users', '0002_add_email_verified'),
        ('users', '0003_add_phone'),
        # ... all replaced migrations listed
        ('users', '0300_add_last_login'),
    ]

    initial = True

    dependencies = []

    operations = [
        migrations.CreateModel(
            name='User',
            fields=[
                ('id', models.AutoField(primary_key=True)),
                ('email', models.EmailField(max_length=255, unique=True)),
                ('name', models.CharField(max_length=200)),
                ('email_verified', models.BooleanField(default=False)),
                ('phone', models.CharField(max_length=20, blank=True)),
                ('last_login', models.DateTimeField(null=True)),
            ],
        ),
    ]

Expected output: Django squash migration lists all replaced migrations in the replaces attribute. The migration combines all operations into one. Django knows the squash replaces the original migrations and skips them.

Flyway Squashing

# Flyway approach: Create a new baseline
# 1. Extract the current schema
pg_dump --schema-only --no-owner mydb > schema.sql

# 2. Clean the schema dump to create a baseline migration
# Remove: search_path, SET statements, comments
# Keep: CREATE TABLE, CREATE INDEX, CREATE VIEW statements

# 3. Create the baseline migration
mv schema.sql sql/V9999__baseline.sql

# 4. Update Flyway baseline configuration
flyway baseline --baselineVersion=9999
-- sql/V9999__baseline.sql
-- Squash of all migrations before this point

CREATE TABLE IF NOT EXISTS users (
    id SERIAL PRIMARY KEY,
    email VARCHAR(255) NOT NULL UNIQUE,
    name VARCHAR(200),
    email_verified BOOLEAN DEFAULT FALSE,
    phone VARCHAR(20),
    created_at TIMESTAMP DEFAULT NOW()
);

CREATE TABLE IF NOT EXISTS orders (
    id SERIAL PRIMARY KEY,
    user_id INTEGER NOT NULL REFERENCES users(id),
    total NUMERIC(10,2) NOT NULL,
    status VARCHAR(20) DEFAULT 'pending',
    created_at TIMESTAMP DEFAULT NOW()
);

CREATE INDEX idx_users_email ON users(email);
-- All tables, indexes, and constraints from squashed migrations

Expected output: Flyway baseline migration contains the complete schema as one SQL file. The baseline version (9999) is after all previous migrations. Flyway marks all previous migrations as applied and starts fresh databases from the baseline.

Prisma Squashing

# Prisma approach: Recreate the migration history
# 1. Ensure current schema.prisma reflects desired state

# 2. Delete old migration files (keep a backup)
rm -rf prisma/migrations/*

# 3. Create a fresh baseline migration
npx prisma migrate dev --name baseline

# Output:
# Your database is now in sync with your schema.
# Created migration: prisma/migrations/20260628000000_baseline
// prisma/schema.prisma (final state after all migrations)
model User {
  id          Int      @id @default(autoincrement())
  email       String   @unique
  name        String?
  phone       String?
  emailVerified Boolean @default(false)
  lastLogin   DateTime?
  orders      Order[]
  createdAt   DateTime @default(now())
}

model Order {
  id        Int      @id @default(autoincrement())
  userId    Int
  user      User     @relation(fields: [userId], references: [id])
  total     Float
  status    String   @default("pending")
  createdAt DateTime @default(now())
}

Expected output: Prisma squashing deletes old migrations and creates a single baseline migration. The schema.prisma file reflects the final schema state. New databases start with the baseline migration.

Verifying the Squash

# Verify squash correctness with a fresh database
#!/bin/bash
set -euo pipefail

echo "=== Squash Verification ==="

# Create a fresh database from old migrations
echo "Setting up database with old migrations..."
createdb test_squash_old
export DATABASE_URL=postgresql://localhost/test_squash_old
alembic upgrade head
pg_dump --schema-only test_squash_old > schema_old.sql

# Create a fresh database from squash
echo "Setting up database with squash..."
createdb test_squash_new
export DATABASE_URL=postgresql://localhost/test_squash_new
# Apply only the squash migration
alembic upgrade squash_revision_001
pg_dump --schema-only test_squash_new > schema_new.sql

# Compare schemas
echo "Comparing schemas..."
diff schema_old.sql schema_new.sql
if [ $? -eq 0 ]; then
    echo "PASS: Squash produces identical schema"
else
    echo "FAIL: Schema mismatch between old and squash"
    exit 1
fi

# Clean up
dropdb test_squash_old
dropdb test_squash_new
echo "=== Verification Complete ==="

Expected output: Verification creates databases from both old migrations and the squash, dumps schemas, and compares them. Identical schemas confirm the squash is correct. This test should run in CI after squashing.

Updating Dependencies

# Before squash: migrations in unsquashed branch depend on old migration
# migrations/0301_add_new_feature.py
"""Add new feature after squash."""
revision = '0301'
down_revision = '0300'  # Depends on old migration 0300

# After squash: update dependency to squash revision
# migrations/0301_add_new_feature.py (updated)
"""Add new feature after squash."""
revision = '0301'
down_revision = 'squash_revision_001'  # Now depends on squash

Expected output: After squashing, all unsquashed migrations that depended on old migrations must be updated to depend on the squash revision. This ensures the migration chain remains intact.

Common Mistakes

1. Squashing Too Early

Squashing migrations that are still being rolled back in different environments causes problems. Only squash migrations that have been applied everywhere and are unlikely to need individual rollback.

2. Not Verifying the Squash

A squash that produces a different schema than the original migrations causes data loss or application errors. Always verify the squash produces an identical schema. Test against a production backup copy.

3. Forgetting to Update Downstream Dependencies

Unsquashed migrations that depend on old migrations break after squashing. Update all downstream migration dependencies. Run the full migration chain from scratch to verify.

4. Squashing Without Team Coordination

Squashing changes the migration history. Team members with feature branches that include old migrations will have conflicts. Coordinate the squash timing. Communicate the new baseline to all developers.

5. Deleting Squashed Migrations

After squashing, keep old migration files during a transition period. Some environments may still need them. Remove them only after verifying all environments have applied the squash.

6. Not Updating CI/CD Pipelines

After squashing, CI pipelines should use the new baseline. Update CI configuration to start from the squash. Verify pipeline time improvement after squashing.

Practice Questions

1. What is migration squashing?

Combining many old migration files into a single baseline migration that represents the cumulative schema. Fresh databases start from the baseline, skipping the individual old migrations.

2. When should you squash migrations?

When migration count exceeds 100, fresh database setup takes over 30 seconds, CI pipeline time is dominated by migrations, or the oldest migrations are over 6 months old and unlikely to need individual rollback.

3. How do you verify a squash is correct?

Create fresh databases from both old migrations and the squash. Dump both schemas. Compare them. If they are identical, the squash is correct. Use pg_dump or equivalent for your database.

4. What happens to unsquashed migrations after squashing?

Unsquashed migrations must update their dependencies to point to the squash revision. The squash becomes the new base. All subsequent migrations chain from the squash.

Challenge

Squash a projects migration directory with 300+ migrations: analyze migration count and fresh setup time, create a squash migration combining all migrations into one, verify the squash produces identical schema, update all downstream migration dependencies, update CI/CD to use the baseline, and create a rollback plan if the squash causes issues.

FAQ

Can I squash migrations that have already been applied to production?

Yes, squashing is safe for applied migrations. The squash represents the cumulative schema. Production databases already at that schema do not re-run squashed migrations. Fresh databases use the squash.

Should I keep old migration files after squashing?

Keep them during a transition period (2-4 weeks) in case rollback is needed. After verifying all environments have applied the squash, archive or delete old migration files.

How often should I squash?

Every 6-12 months or when migration count exceeds 100. Squashing more frequently reduces the benefit. Squashing less frequently allows migration count to grow too large.

Does squashing affect the ability to rollback?

After squashing, you cannot rollback individual old migrations. You can only rollback the entire squash or migrations after it. Consider this before squashing recent migrations.

How do I handle squashing in a team environment?

Coordinate the squash timing. Notify all developers. Ask them to merge their branches before squashing. After squashing, update all open branches to depend on the new baseline.

Can I automate squashing in CI/CD?

Yes, but manual review is recommended. Automate the verification step (compare old vs new schema). Require manual approval for the squash merge. CI can run verification automatically.

Mini Project: Squash Automation

Build a squash automation tool that: counts migrations and measures setup time, recommends when to squash, creates a squash migration for Alembic or Django, verifies the squash produces identical schema, updates downstream migration dependencies, runs the full migration chain from scratch, and generates a report of time saved.

What's Next

Now that you understand squashing migrations, learn about Testing Migrations for ensuring migration correctness before production deployment.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro