Database Migration Tools: Alembic, Flyway, Liquibase, Versioning

Database Migration Tools: Alembic, Flyway, Liquibase, Versioning

Database migrations are the practice of version-controlling schema changes. A good migration tool applies changes in order, handles conflicts, and supports rollback. This article compares the three leading tools and covers migration strategies.

Why Migrations?

Without migrations, schema changes are applied manually or through ad-hoc scripts:

  • "Did we run the ALTER TABLE on staging?"

  • "Which servers have the new index?"

  • "The deploy failed because the migration hadn't run yet."

Migrations solve these problems by making schema changes repeatable, versioned, and automated.

Alembic

Alembic is the migration tool for SQLAlchemy (Python). It generates migration scripts from model definitions and supports auto-generation.

Setup

alembic.ini

[alembic]

script_location = alembic

sqlalchemy.url = postgresql://user:pass@localhost/mydb

alembic init alembic

Creating a Migration

alembic/versions/0001_create_users.py

"""create users table

Revision ID: 0001

Revises: None

Create Date: 2026-05-12

"""

from alembic import op

import sqlalchemy as sa

def upgrade():

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(100)),

sa.Column('created_at', sa.DateTime(), server_default=sa.func.now()),

)

op.create_index('idx_users_email', 'users', ['email'], unique=True)

def downgrade():

op.drop_index('idx_users_email')

op.drop_table('users')

Auto-Generation

Generate migration from model changes

alembic revision --autogenerate -m "add avatar column"

This compares the current database state against SQLAlchemy models and generates upgrade() and downgrade() functions.

Running Migrations

alembic upgrade head # Apply all pending migrations

alembic upgrade +2 # Apply next 2 migrations

alembic downgrade -1 # Rollback 1 migration

alembic history # View migration history

alembic current # Show current revision

Flyway

Flyway is a Java-based migration tool that works with SQL scripts. It is simple, opinionated, and supports multiple databases.

Directory Structure

sql/

V1__create_users.sql

V2__add_avatar_column.sql

V3__create_orders_table.sql

Migration Script

\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\-- V1__create_users.sql

CREATE TABLE users (

id BIGSERIAL PRIMARY KEY,

email VARCHAR(255) NOT NULL UNIQUE,

name VARCHAR(100),

created_at TIMESTAMPTZ DEFAULT NOW()

);

\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\-- V2__add_avatar_column.sql

ALTER TABLE users ADD COLUMN avatar_url VARCHAR(500);

Configuration

flyway.conf

flyway.url = jdbc:postgresql://localhost:5432/mydb

flyway.user = app_user

flyway.password = secure_password

flyway.locations = filesystem:sql

flyway.baselineOnMigrate = true

Running

flyway migrate

flyway undo # Flyway Teams only

flyway info # Show migration status

flyway validate # Check for changes to applied migrations

Checksum Validation

Flyway stores a checksum of each migration script. If a script is modified after being applied, flyway validate detects the change and warns:

If a deployed migration is edited, Flyway catches it and refuses to apply further migrations until the discrepancy is resolved.

Liquibase

Liquibase supports multiple change formats: SQL, XML, YAML, and JSON. It is the most flexible but most verbose option.

Changeset (XML)

xmlns="http://www.liquibase.org/xml/ns/dbchangelog"

xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog

http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-latest.xsd">

Changeset (YAML)

databaseChangeLog:

\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\- changeSet:

id: 2

author: bob

changes:

\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\- addColumn:

tableName: users

columns:

\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\- column:

name: avatar_url

type: varchar(500)

Rolling Back

liquibase rollbackCount 1 # Rollback 1 changeset

liquibase rollbackToDate 2026-05-10T12:00:00 # Rollback to specific date

liquibase rollback --tag v1.0 # Rollback to tagged release

Tool Comparison

| Feature | Alembic | Flyway | Liquibase | |---------|---------|--------|-----------| | Language | Python | Java/SQL | Java | | Auto-generation | Yes (from models) | No | No | | Rollback | Code-defined | Undo migration | Rollback changeset | | CI/CD integration | Pipeline script | Maven/Gradle/CLI | Maven/Gradle/CLI | | Complex logic | Python code | SQL only | SQL + XML/YAML | | Learning curve | Medium | Low | Medium | | Database support | SQLAlchemy-supported | 20+ databases | 15+ databases |

Migration Strategy Patterns

Linear Migrations

The simplest pattern: each migration depends on the previous one.

V1 → V2 → V3 → V4

Branching with Merges

For larger teams, branches create divergent migration histories:

V1 → V2 → V3 → V4 (main branch)

└→ V2a → V2b (feature branch)

When the feature branch merges, use flyway merge or adjust Alembic revision dependencies.

Repeatable Migrations

Views, functions, and stored procedures benefit from repeatable migrations that reapply on every change:

\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\-- Flyway: R__update_user_view.sql

CREATE OR REPLACE VIEW active_users AS

SELECT * FROM users WHERE deleted_at IS NULL;

Alembic: revision_identifiers.py with %(revision)s

Or mark as "revision_identifiers = False" for repeatable patterns

Best Practices

  • One change per migration : Adding a column and creating a table in the same migration makes rollback harder.

2\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\. Test both directions : Always test upgrade() and downgrade() before merging. 3\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\. Use CI validation : Run flyway validate or alembic check in CI to catch mistakes. 4\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\. Avoid long-running locks : Use CREATE INDEX CONCURRENTLY instead of CREATE INDEX in migrations. 5\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\. Never modify applied migrations : Create a new migration to fix issues. 6\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\. Database-as-code : Store migrations in version control alongside application code.

Database migrations bring schema changes into the software development lifecycle. Choose the tool that fits your language ecosystem, establish clear conventions, and make schema changes as routine as code changes.