Skip to content

Rails Migrations Deep — Schema Evolution and Data Migration

DodaTech Updated 2026-06-28 4 min read

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

Rails migrations manage database schema changes over time with reversible migrations, data migrations, indexes, foreign keys, and zero-downtime strategies for production deployments.

What You'll Learn

By the end of this tutorial, you'll write reversible migrations, create data migrations, add indexes and foreign keys, use change method helpers, and deploy migrations in production.

Why Migrations Matter

Migrations version the database schema alongside code. Every schema change is tracked, reversible, and consistently applied across all environments.

Real-World Use

A team adds a product ratings feature. They create a Migration for the ratings table, add foreign keys to products and users, backfill rating data, and deploy without downtime.

Migrations Path

flowchart LR
  A[ActiveRecord] --> B[Migrations Deep]
  B --> C[Reversible]
  B --> D[Data Migrations]
  B --> E[Indexes]
  B --> F[Foreign Keys]
  B --> G{You Are Here}
  style G fill:#f90,color:#fff

Reversible Migrations

Use change method for automatic reversibility.

class CreateProducts < ActiveRecord::Migration[7.1]
  def change
    create_table :products do |t|
      t.string :name, null: false
      t.text :description
      t.decimal :price, precision: 10, scale: 2, null: false
      t.references :category, foreign_key: true
      t.boolean :published, default: false
      t.datetime :published_at
      t.timestamps
    end

    add_index :products, :name
    add_index :products, [:category_id, :published]
  end
end

Irreversible Migrations

Handle non-reversible changes with up/down.

class RemoveUnusedColumns < ActiveRecord::Migration[7.1]
  def up
    remove_column :products, :legacy_field
    remove_column :products, :deprecated_flag
  end

  def down
    add_column :products, :legacy_field, :string
    add_column :products, :deprecated_flag, :boolean, default: false
  end
end

Data Migrations

Migrate data alongside schema changes.

class BackfillProductSlugs < ActiveRecord::Migration[7.1]
  def up
    Product.where(slug: nil).find_each do |product|
      product.update!(slug: product.name.parameterize)
    end
  end

  def down
    # Data migration cannot be reversed
    raise ActiveRecord::IrreversibleMigration
  end
end

# Better approach for large tables - batch processing
class BackfillProductSlugs < ActiveRecord::Migration[7.1]
  def up
    Product.where(slug: nil).in_batches(of: 1000) do |batch|
      batch.each do |product|
        product.update_column(:slug, product.name.parameterize)
      end
    end
  end
end

Indexes and Performance

Add indexes for query performance.

class AddIndexesForPerformance < ActiveRecord::Migration[7.1]
  def change
    # Composite index for common query
    add_index :orders, [:user_id, :status, :created_at], name: "idx_orders_user_status_date"

    # Partial index for filtered queries
    add_index :products, :price, where: "published = true", name: "idx_products_published_price"

    # Index with order
    add_index :reviews, [:product_id, :created_at], order: { created_at: :desc }

    # Concurrent index in production
    disable_ddl_transaction!
    add_index :users, :email, algorithm: :concurrently
  end
end

Common Mistakes

1. Running Data Migrations in Schema Migration

Data migrations should be separate from schema migrations. Use a distinct migration.

2. Not Testing Rollbacks

Always test the down method. Irreversible migrations should raise explicitly.

3. Missing Indexes for Foreign Keys

Rails adds foreign key indexes automatically with references foreign_key: true.

4. Blocking Writes in Production

Adding a column with a default locks the table. Use add_column then backfill.

5. Not Using find_each for Large Tables

Loading all records at once causes memory issues. Use in_batches or find_each.

Practice Questions

1. What is the change method?

A migration method that Rails can reverse automatically for most operations.

2. How do you add a foreign key in a migration?

t.references :category, foreign_key: true or add_foreign_key :products, :categories.

3. What is a partial index?

An index that only includes rows matching a WHERE condition.

4. How do you safely run migrations in production?

Use algorithm: :concurrently for indexes, add columns without defaults, backfill separately.

5. Challenge: Write a migration that adds a column with a default and backfills.

class AddSlugToProducts < ActiveRecord::Migration[7.1]
  def up
    add_column :products, :slug, :string
    add_index :products, :slug, unique: true
    Product.where(slug: nil).find_each do |product|
      product.update_column(:slug, product.name.parameterize)
    end
    change_column_null :products, :slug, false
  end

  def down
    remove_column :products, :slug
  end
end

FAQ

Can I revert a migration after deployment?

Run rails db:migrate:down VERSION=version to revert. For production, create a new migration.

What is the difference between change_column and change_column_null?

change_column modifies type/options. change_column_null sets NOT NULL constraint.

How do I create a migration for a join table?

create_join_table :products, :categories creates a products_categories table.

What is disable_ddl_transaction?

Allows DDL operations that cannot run inside a transaction, like concurrent index creation.

{{< faq "Can I use SQL in migrations?" "Yes. Use execute("SQL") for raw SQL operations." >}}

Mini Project: Schema Evolution for a Blog

Create migrations for a blog's schema evolution.

class CreateBlogSchema < ActiveRecord::Migration[7.1]
  def change
    create_table :posts do |t|
      t.string :title, null: false
      t.string :slug, null: false
      t.text :body
      t.references :author, null: false, foreign_key: { to_table: :users }
      t.string :status, default: "draft"
      t.timestamps
    end
    add_index :posts, :slug, unique: true
    add_index :posts, [:author_id, :status, :created_at], name: "idx_posts_author_status_date"
    add_index :posts, :created_at, order: :desc
  end
end

What's Next

Rails Validations Deep Rails Associations Deep Rails Forms Deep

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro