| name | rails-database-indexes |
| description | Design and implement database indexes for optimal query performance. Use when creating tables, optimizing slow queries, or improving database performance. |
Rails Database Indexes Specialist
Specialized in designing effective database indexes for ActiveRecord models.
When to Use This Skill
- Creating new database tables
- Optimizing slow queries
- Adding foreign key indexes
- Implementing composite indexes
- Ensuring unique constraints
Core Principles
- Index Foreign Keys: Always index foreign key columns
- Index Frequently Queried Columns: Columns in WHERE, ORDER BY
- Composite Indexes: Multiple columns used together
- Unique Indexes: Enforce uniqueness at database level
- Selective Indexing: Index columns with high cardinality
Implementation Guidelines
Migration Generation Workflow
ALWAYS generate migrations using Rails generator commands first:
# Generate a new migration
bundle exec rails generate migration AddIndexesToUsers
# For simple index additions, you can specify columns
bundle exec rails generate migration AddIndexToUsers email:index
# For complex indexes, use a descriptive name
bundle exec rails generate migration AddCompositeIndexesToOrders
This creates a timestamped migration file like db/migrate/20250120XXXXXX_add_indexes_to_users.rb.
Basic Index Migration
After generating the migration file, edit it to add index definitions:
class AddIndexesToUsers < ActiveRecord::Migration[7.0]
def change
# Single column index
add_index :users, :email
# Unique index
add_index :users, :email, unique: true
# Foreign key index
add_index :orders, :user_id
# Composite index
add_index :orders, [:user_id, :status]
# Named index
add_index :posts, :published_at, name: 'idx_posts_published'
end
end
Foreign Key Indexes
# Generate table creation migration
bundle exec rails generate migration CreateOrders
Edit the generated migration:
class CreateOrders < ActiveRecord::Migration[7.0]
def change
create_table :orders do |t|
t.references :user, foreign_key: true, index: true
t.string :status
t.decimal :total_amount
t.timestamps
end
# Additional indexes
add_index :orders, :status
add_index :orders, :created_at
end
end
Composite Indexes
# Generate migration for composite indexes
bundle exec rails generate migration AddCompositeIndexesToOrders
Edit the generated migration:
class AddCompositeIndexes < ActiveRecord::Migration[7.0]
def change
# WHY: Queries often filter by user_id AND status together
add_index :orders, [:user_id, :status]
# WHY: Queries filter by category and sort by created_at
add_index :posts, [:category_id, :created_at]
# Order matters in composite indexes!
# This index helps: WHERE user_id = X AND status = Y
# This index also helps: WHERE user_id = X
# This index does NOT help: WHERE status = Y
end
end
Unique Indexes
# Generate migration for unique indexes
bundle exec rails generate migration AddUniqueIndexesToUsers
Edit the generated migration:
class AddUniqueIndexes < ActiveRecord::Migration[7.0]
def change
# Single column unique
add_index :users, :email, unique: true
# Composite unique (user can have one profile per type)
add_index :user_profiles, [:user_id, :profile_type], unique: true
# Case-insensitive unique (PostgreSQL)
execute <<-SQL
CREATE UNIQUE INDEX index_users_on_lower_email
ON users (LOWER(email));
SQL
end
end
Partial Indexes (PostgreSQL)
# Generate migration for partial indexes
bundle exec rails generate migration AddPartialIndexes
Edit the generated migration:
class AddPartialIndexes < ActiveRecord::Migration[7.0]
def change
# WHY: Index only active users for performance
add_index :users, :email, where: 'active = true', name: 'index_active_users_on_email'
# WHY: Index only published posts
add_index :posts, :published_at, where: 'published = true'
end
end
Index Types
# Generate migration for specialized indexes
bundle exec rails generate migration AddSpecializedIndexes
Edit the generated migration:
class AddSpecializedIndexes < ActiveRecord::Migration[7.0]
def change
# B-tree (default, good for equality and range queries)
add_index :users, :created_at
# GiST for full-text search (PostgreSQL)
execute <<-SQL
CREATE INDEX index_posts_on_content_search
ON posts USING GiST (to_tsvector('english', content));
SQL
# GIN for JSONB columns (PostgreSQL)
add_index :events, :metadata, using: :gin
end
end
Removing Indexes
# Generate migration to remove indexes
bundle exec rails generate migration RemoveUnusedIndexes
Edit the generated migration:
class RemoveUnusedIndexes < ActiveRecord::Migration[7.0]
def change
# Remove unused index
remove_index :users, :username
# Remove by name
remove_index :posts, name: 'idx_posts_published'
end
end
Index Strategy Guidelines
When to Add Indexes
- Foreign key columns (always)
- Columns in WHERE clauses (frequently)
- Columns in ORDER BY clauses
- Columns in JOIN conditions
- Columns with unique constraints
When NOT to Add Indexes
- Tables with few rows (< 1000)
- Columns with low cardinality (few distinct values)
- Columns that are rarely queried
- Small tables that fit in memory
Composite Index Order
# Query: WHERE user_id = X AND status = Y ORDER BY created_at
# Best index order:
add_index :orders, [:user_id, :status, :created_at]
# This index can serve:
# 1. WHERE user_id = X
# 2. WHERE user_id = X AND status = Y
# 3. WHERE user_id = X AND status = Y ORDER BY created_at
Analyzing Indexes
# Check query execution plan
User.where(email: 'test@example.com').explain
# PostgreSQL: Check index usage
ActiveRecord::Base.connection.execute(<<-SQL)
SELECT
schemaname,
tablename,
indexname,
idx_scan,
idx_tup_read,
idx_tup_fetch
FROM pg_stat_user_indexes
ORDER BY idx_scan;
SQL
Tools to Use
Correct tool order for creating indexes:
Bash: Generate migration withrails generate migrationcommandEdit: Modify the generated migration file to add index definitionsBash: Runrails db:migrateto apply changesRead: Review existing indexes and schema
Bash Commands
# Generate migration
bundle exec rails generate migration AddIndexToUsers email:index
# Run migration
bundle exec rails db:migrate
# Check schema
bundle exec rails db:schema:dump
# PostgreSQL: List indexes
bundle exec rails dbconsole
\di
# Check slow queries (PostgreSQL)
SELECT * FROM pg_stat_statements ORDER BY total_time DESC LIMIT 10;
Workflow
- Identify Slow Queries: Use monitoring or logs
- Analyze Query Plan: Use EXPLAIN
- Generate Migration: Use
rails generate migrationcommand - Edit Migration: Add index definitions to generated file
- Run Migration: Apply to database with
rails db:migrate - Verify Performance: Test query speed
- Monitor: Check index usage over time
Related Skills
rails-query-optimization: Understanding query patternsrails-model-design: Understanding associations
Coding Standards
Key Reminders
- ALWAYS use
rails generate migrationto create migration files - Never create migration files directly with
Writetool - Use
Edittool to modify generated migration files - Always index foreign keys
- Index columns used in WHERE and ORDER BY
- Composite index order matters (most selective first)
- Unique indexes enforce data integrity
- Monitor index usage and remove unused indexes
- Indexes speed up reads but slow down writes
- Use partial indexes for conditional queries (PostgreSQL)
- Test index impact with EXPLAIN