When updating a database schema in a Ruby on Rails application, zero-downtime migrations ensure that database changes do not cause application downtime or errors. Here are key techniques to achieve zero-downtime migrations:
1. Add New Columns Without Removing Old Ones
- Why? If a column is removed, existing code referencing it will break.
- How? First, add the new column. Then, update the application to use the new column before removing the old one.
class AddNewColumnToUsers < ActiveRecord::Migration[6.1]
def change
add_column :users, :new_column, :string
end
end
2. Use add_column With Defaults Carefully
- Adding a column with a default and NOT NULL constraint locks the table in some databases (e.g., Postgres).
- Instead, add the column without constraints first and then update records in batches.
class AddNewFieldToUsers < ActiveRecord::Migration[6.1]
def change
add_column :users, :new_field, :string, null: true
end
end
Then, backfill the data asynchronously:
User.update_all(new_field: 'default_value')
Finally, apply constraints safely:
class AddNotNullConstraintToUsers < ActiveRecord::Migration[6.1]
def change
change_column_null :users, :new_field, false
end
end
3. Indexing Without Downtime
- Adding an index locks the table, making inserts and updates slow.
- Use the algorithm: :concurrently option (PostgreSQL only).
class AddIndexToUsers < ActiveRecord::Migration[6.1]
disable_ddl_transaction!
def change
add_index :users, :email, unique: true, algorithm: :concurrently
end
end
4. Renaming Columns or Tables Safely
- Rails’ rename_column locks the table. Instead:
- Add a new column.
- Copy data to the new column.
- Update application code to use the new column.
- Drop the old column later.
5. Removing Columns Without Breaking Queries
- Old application instances may still reference a column.
- Instead, follow a three-step process:
- Stop using the column in the application.
- Deploy the change.
- Remove the column in a separate migration.
6. Dropping Tables Safely
- Ensure no application code references the table before dropping it.
class DropOldTable < ActiveRecord::Migration[6.1]
def up
drop_table :old_table
end
def down
# Optional rollback (recreate table)
end
end
7. Avoid Locking Large Tables
- For large tables, break changes into smaller, incremental migrations.
- Avoid change_column on large tables, as it locks the entire table.
Best Practices
- Use disable_ddl_transaction! for index migrations.
- Deploy migrations separately from application code changes.
- Backfill data in batches to avoid database overload.
Test migrations on a staging environment before production.
Would you like help structuring your migrations for a specific use case? 🚀
Discover the power of Ruby on Rails
for your projects!
Explore our Ruby on Rails expertise
Start a project
Share On: