Follow these guidelines to write maintainable, safe, and effective database migrations.
Version Numbering Strategies
Strategy 1: Timestamp-Based (Recommended)
20250120143000__add_user_email.sql
20250121091500__create_orders_table.sql
Format: YYYYMMDDHHmmss
Advantages:
- ✅ No merge conflicts with parallel development
- ✅ Chronological ordering
- ✅ Supports distributed teams
Disadvantages:
- ⚠️ Less human-readable
- ⚠️ Doesn’t convey significance
Best for: Teams with frequent parallel development
Strategy 2: Semantic Versioning
v1.0.0__initial_release.sql
v1.1.0__add_user_profiles.sql
v1.1.1__fix_profile_constraint.sql
v2.0.0__redesign_authentication.sql
Format: v<major>.<minor>.<patch>
Advantages:
- ✅ Conveys change significance
- ✅ Aligns with application versioning
- ✅ Clear breaking change indication
Disadvantages:
- ⚠️ Requires coordination
- ⚠️ Merge conflicts possible
Best for: Teams with coordinated releases
Strategy 3: Sequential with Milestones
001__initial_schema.sql
002__add_users.sql
...
100__v1_0_release.sql
101__add_analytics.sql
...
200__v2_0_release.sql
Advantages:
- ✅ Simple and clear
- ✅ Milestones mark releases
Disadvantages:
- ⚠️ Merge conflicts in parallel work
- ⚠️ Gaps can be confusing
Best for: Small teams, sequential development
Migration File Best Practices
Keep Migrations Small
✅ Good - Focused
❌ Bad - Too Large
-- 005__add_user_email.sql
ALTER TABLE users ADD COLUMN email TEXT;
- Single, clear purpose
- Fast execution
- Easy to review
- Simple to rollback
Guideline: One logical change per file
Include Rollback Planning
Document rollback approach in comments:
-- 010__add_payment_methods_table.sql
-- Rollback: DROP TABLE payment_methods;
-- Impact: Requires app deployment v2.5.0+
CREATE TABLE payment_methods (
id SERIAL PRIMARY KEY,
user_id INTEGER REFERENCES users(id),
card_last_four TEXT NOT NULL,
created_at TIMESTAMP DEFAULT NOW()
);
Transaction Handling
Bytebase Automatic Transactions: Bytebase automatically wraps all SQL statements in a migration file within a single transaction. You typically don’t need to add explicit BEGIN/COMMIT statements.
When Bytebase’s automatic transactions are sufficient:
- Single-file migrations with multiple statements
- Standard DDL operations (CREATE, ALTER, DROP)
- Simple DML operations (INSERT, UPDATE, DELETE)
When you might need explicit transaction control:
- Batched operations requiring commits between chunks
- Long-running data migrations that need progress checkpoints
- Database-specific requirements like PostgreSQL’s
CREATE INDEX CONCURRENTLY (which cannot run in a transaction)
Example of batched migration (when needed):
-- 015__batch_archive_logs_dml.sql
-- Note: This uses explicit batching for large data operation
DO $$
DECLARE
batch_size INT := 10000;
deleted_count INT;
BEGIN
LOOP
DELETE FROM logs
WHERE id IN (
SELECT id FROM logs
WHERE created_at < '2024-01-01'
LIMIT batch_size
);
GET DIAGNOSTICS deleted_count = ROW_COUNT;
EXIT WHEN deleted_count < batch_size;
COMMIT; -- Commit each batch
PERFORM pg_sleep(0.1); -- Throttle
END LOOP;
END $$;
Database-specific transaction support:
- ✅ PostgreSQL: DDL in transactions (except
CONCURRENTLY operations)
- ❌ MySQL: DDL commits immediately (implicit commit)
- ✅ SQL Server: DDL in transactions
-- 020__migrate_legacy_permissions_dml.sql
-- Migrates old role system to new permission model
-- Old: roles.name -> New: permissions.scope + permissions.action
-- Mapping:
-- 'admin' -> 'database:*'
-- 'editor' -> 'database:write'
-- 'viewer' -> 'database:read'
UPDATE permissions
SET
scope = CASE
WHEN roles.name = 'admin' THEN 'database'
WHEN roles.name = 'editor' THEN 'database'
WHEN roles.name = 'viewer' THEN 'database'
END,
action = CASE
WHEN roles.name = 'admin' THEN '*'
WHEN roles.name = 'editor' THEN 'write'
WHEN roles.name = 'viewer' THEN 'read'
END
FROM roles
WHERE permissions.role_id = roles.id;
Documentation
Maintain Migration Changelog
Document significant schema changes:
# Database Changelog
## v2.0.0 (2025-01-20)
- Added `user_preferences` table for customization
- Migrated legacy role system to new permissions model
- **Breaking**: Removed deprecated `user_settings` table
## v1.5.0 (2025-01-15)
- Added email column to users table
- Created indexes on frequently queried columns
Document Schema Dependencies
Track dependencies between application and schema:
# Schema Dependencies
## users.email Column
- Added: v1.5.0 (migration 010)
- Required by: Auth Service v2.0+
- Can remove after: All instances upgraded to v2.1+
This helps coordinate schema cleanup with application deployments.
Common Anti-Patterns to Avoid
❌ Modifying Applied Migrations
Don’t:
-- 005__add_email.sql (already deployed)
ALTER TABLE users ADD COLUMN email TEXT; -- Changed to VARCHAR
Do:
-- 006__fix_email_type.sql
ALTER TABLE users ALTER COLUMN email TYPE VARCHAR(255);
❌ Skipping Version Numbers
Don’t:
001__init.sql
002__add_users.sql
010__add_products.sql ← Why skip 003-009?
Do:
001__init.sql
002__add_users.sql
003__add_products.sql
❌ Mixing DDL and DML Without Suffix
Don’t:
-- 015__add_orders.sql
CREATE TABLE orders (...);
INSERT INTO orders VALUES (...); -- DML without _dml suffix
Do:
-- 015__add_orders.sql (DDL only)
CREATE TABLE orders (...);
-- 016__seed_orders_dml.sql (DML separate)
INSERT INTO orders VALUES (...);
❌ Long-Running Migrations in Production
Don’t:
-- 020__huge_migration.sql
UPDATE users SET legacy_field = NULL; -- Locks 10M rows for 10 minutes
Do:
-- 020__migrate_users_batch_dml.sql
-- Batch processing with throttling (see Performance section)
Next Steps