Skip to content
Navigation

Type at least two characters. Search covers page titles, headings, tags and database names.

↑ ↓ to navigateEnter to openEsc to close0 pages
Operationsintermediate

Schema Migration Tools

What a migration tool must provide, how the common ones differ, and the online schema change tools for large tables.

3 min readIntermediateUpdated Edit this page

Schema changes must be versioned, reviewed and applied the same way in every environment. That is what a migration tool provides; the specific tool matters less than using one consistently.

What a tool must provide

  • Ordered, versioned migrations stored in the repository.
  • A record of what has been applied, in the database itself.
  • A lock so two application instances starting simultaneously do not apply the same migration twice.
  • Checksums, so an already-applied migration cannot be edited silently.
  • A dry run or plan output for review.

Common tools

ToolStyleNotes
FlywaySQL files, versionedSimple and explicit; wide engine support
LiquibaseXML/YAML/SQL changesetsAbstraction across engines; more machinery
AlembicPython, autogeneratedIntegrates with SQLAlchemy; review generated output
golang-migrateSQL files, up and downMinimal, CLI-first
Rails / Django / LaravelFramework-nativeConvenient; still needs the practices below
Atlas / SkeemaDeclarativeDesired-state schema; generates the diff
SqitchDependency-orderedExplicit dependencies rather than sequential numbering

Practices independent of the tool

  • One logical change per migration, so a failure is easy to understand and resume.
  • Set a lock timeout in the migration session so DDL fails fast instead of blocking the table behind a long query.
  • Separate schema changes from data backfills. Backfills are batched, resumable jobs — not part of the deploy's critical path.
  • Never edit an applied migration. Write a new one.
  • Run migrations as a role that owns the objects, distinct from the application role.

Online schema change for large tables

When a change requires a table rewrite in MySQL or MariaDB and the table is too large to lock:

# gh-ost: reads the binlog, no triggers on the original table.
gh-ost \
  --host=db.internal --database=shop --table=orders \
  --alter="ADD COLUMN dispatch_note TEXT" \
  --allow-on-master --initially-drop-ghost-table \
  --max-load=Threads_running=50 \
  --critical-load=Threads_running=200 \
  --cut-over=atomic --execute
 
# pt-online-schema-change: trigger-based; older and widely deployed.
pt-online-schema-change \
  --alter "ADD COLUMN dispatch_note TEXT" \
  D=shop,t=orders --execute

Both build a shadow table, copy rows in chunks, keep it current, and swap at the end. gh-ost throttles against real server load, which is why it is generally preferred where the binlog is available.

PostgreSQL needs these less often — most common changes are metadata-only — but pg_repack fills the equivalent role for rewriting a bloated table without a long exclusive lock.