π Indexing in Rails: Speed Up Your Queries
π§ What is Indexing?
Indexing in Rails (and databases generally) is like a smart shortcut. Instead of scanning every row in a table, Rails (via your database) uses indexes to find data fasterβjust like looking up a word in a book using the index page instead of reading every page.
π§ Detailed Explanation
Imagine you’re searching for a friend’s name in a phone book. Instead of reading every name from start to end, you use the index β the names are sorted alphabetically, so you jump directly to the section with the first letter. Fast, right?
The same concept applies to databases. An index is like a shortcut that helps your app quickly find the data it needs without checking every row in the table. Without an index, your database must scan the whole table β which gets slower as data grows.
In Rails, you use add_index
in migrations to create these shortcuts. For example, if you search for users by email often, indexing the email
column makes that search super fast.
Hereβs what indexing helps with:
- π Quick Searches β Find rows fast using
WHERE
conditions. - π Sort Data β Improves
ORDER BY
performance. - π Join Tables β Speeds up joins using foreign keys.
- π« Prevent Duplicates β Use unique indexes to ensure no two users have the same email, for example.
Without indexes, your app will get slower as your database grows. So, indexing is not just a bonus β itβs essential for fast, scalable applications.
π§© Types of Indexing in Rails (with Examples)
Rails supports several types of database indexes. Each serves a different purpose to optimize queries. Here’s a breakdown with simple explanations and examples:
1. π Single-Column Index
Use case: Speed up searches on one column.
Example: Searching for users by email.
add_index :users, :email
This improves queries like:
User.find_by(email: "test@example.com")
2. π§± Composite (Multi-Column) Index
Use case: Optimize queries that filter by multiple columns together.
Example: Searching posts by user_id
and published
status.
add_index :posts, [:user_id, :published]
This speeds up:
Post.where(user_id: 1, published: true)
3. π« Unique Index
Use case: Ensure no duplicate values in a column or a set of columns.
Example: Prevent users from registering with the same email.
add_index :users, :email, unique: true
If someone tries to create a second user with the same email, it will fail at the database level.
4. β± Partial Index (Conditional)
Use case: Index a subset of rows to reduce index size and improve write performance.
Example: Index only active records.
add_index :sessions, :user_id, where: "active = true"
This is useful when most records are inactive, but only active ones are queried often.
5. π Index with Ordering (PostgreSQL Only)
Use case: Optimize ORDER BY
queries.
Example: Speed up ordering users by last login time.
add_index :users, :last_logged_in_at, order: { last_logged_in_at: :desc }
Faster for:
User.order(last_logged_in_at: :desc)
6. π· Functional Index (PostgreSQL Only)
Use case: Index based on the result of a function.
Example: Case-insensitive search on username.
add_index :users, "LOWER(username)", name: "index_users_on_lower_username"
This helps:
User.where("LOWER(username) = ?", "john")
7. π Full-Text Index (MySQL / PostgreSQL)
Use case: Search across large text fields (e.g., articles, comments).
Example (PostgreSQL):
execute <<-SQL
CREATE INDEX index_articles_on_content_fts
ON articles
USING gin(to_tsvector('english', content));
SQL
Use with:
Article.where("to_tsvector('english', content) @@ plainto_tsquery(?)", "rails indexing")
8. π Foreign Key Index
Use case: Required for fast joins and foreign key relationships.
Example: Linking comments to posts.
add_reference :comments, :post, foreign_key: true
# OR
add_index :comments, :post_id
Improves:
Post.includes(:comments)
9. βοΈ Polymorphic Index
Use case: For models with polymorphic associations.
Example: Comments belonging to different models.
add_index :comments, [:commentable_type, :commentable_id]
Optimizes:
Comment.where(commentable_type: "Post", commentable_id: 1)
10. π Primary Key Index (Automatic)
Use case: Every tableβs primary key (like id
) is indexed by default.
Example: You donβt need to add add_index :users, :id
β it's already indexed.
π οΈ How Indexing Works Behind the Scenes
Indexes in a database are like a smart lookup system. Behind the scenes, they use data structures like B-trees, hash tables, or GIN (Generalized Inverted Index for full-text search). These structures are built and maintained separately from your main table.
When you run a query with conditions like WHERE
, ORDER BY
, or JOIN
, the database first checks if thereβs an index available. If yes, it uses it to "jump" directly to the result instead of scanning every row.
1. π Single-Column Index
How it works: The database stores a B-tree of that columnβs values with pointers to their row locations.
When you run: User.find_by(email: "john@example.com")
Instead of checking every email row-by-row, it uses the B-tree to jump directly to "john@example.com".
2. π§± Composite Index
How it works: A multi-level B-tree is created with a combination of the specified columns.
For add_index :posts, [:user_id, :status]
, the tree first sorts by user_id
, then groups and sorts those by status
.
Useful only when queries use leftmost columns of the index.
3. π« Unique Index
How it works: Same as a normal index, but also enforces a constraint to disallow duplicates.
Insert/update operations must check this index to verify uniqueness β adds slight overhead but ensures data integrity.
4. β± Partial Index (Conditional)
How it works: Only rows matching a WHERE
clause are stored in the index.
For example: add_index :sessions, :user_id, where: "active = true"
only includes rows where active
is true.
Makes index smaller and faster, but only works for very specific query conditions.
5. π Index with Ordering (PostgreSQL)
How it works: The index stores values in sorted order (ASC or DESC), helping the database quickly retrieve sorted results.
Useful for avoiding costly in-memory sorting operations.
6. π· Functional Index (PostgreSQL)
How it works: The result of a function (e.g., LOWER(email)
) is stored in the index, not the original column value.
When you search with LOWER(email) = 'john@example.com'
, it directly matches in the index instead of computing the function on every row.
7. π Full-Text Index
How it works (PostgreSQL): Uses a GIN
index that stores a list of terms from the text field and the rows where each term appears.
This is extremely fast for text searches like: WHERE content @@ 'rails'
Great for blog titles, descriptions, comments, etc.
8. π Foreign Key Index
How it works: Standard B-tree index used to speed up joins between tables with belongs_to
or has_many
.
For example, add_index :comments, :post_id
allows faster access to comments for a given post.
9. βοΈ Polymorphic Index
How it works: Indexes both commentable_type
and commentable_id
together so Rails can quickly find the right associated object.
Used for models like comments, where they can belong to many different types (posts, photos, etc.).
10. π Primary Key Index (Automatic)
How it works: Every primary key (usually id
) gets a B-tree index automatically by the database engine.
This allows fast retrieval of individual rows using find(id)
.
βοΈ Algorithms Used for Indexing in Rails
Rails doesnβt directly control how indexes work β instead, it uses the underlying database (like PostgreSQL or MySQL), which uses specific indexing algorithms under the hood.
Here are the most common indexing algorithms used:
1. π³ B-Tree (Balanced Tree)
- Default algorithm in both PostgreSQL and MySQL.
- Used for most indexes: single, composite, unique, foreign key, etc.
- Works like a sorted tree structure, allowing fast lookup, insertion, and range queries.
- Time complexity:
O(log n)
for search, insert, and delete operations.
add_index :users, :email # uses B-Tree by default
2. π Hash Index
- Used in PostgreSQL (optional) and MySQL (only for MEMORY tables).
- Faster than B-Tree for exact matches, like
=
queries. - Cannot perform range queries (e.g.,
BETWEEN
,<
,>
). - Not used by default due to limitations (e.g., no ordering).
# PostgreSQL example (not common)
CREATE INDEX index_users_on_email_hash USING HASH (email);
3. π GIN (Generalized Inverted Index)
- Used for full-text search and complex data types like arrays or JSON.
- Stores a reverse map of terms β matching rows (like a word index in a book).
- Slower to update but very fast for text-based search.
- Available only in PostgreSQL.
CREATE INDEX index_articles_on_content_fts
ON articles USING GIN (to_tsvector('english', content));
4. π§ͺ GiST (Generalized Search Tree)
- Used for indexing geometric data, range types, or full-text search (alternative to GIN).
- Supports more flexible queries like
overlaps
ordistance
conditions. - Less efficient than GIN for raw text but useful for spatial search.
5. π§΅ BRIN (Block Range Index)
- Used in PostgreSQL for very large tables.
- Indexes ranges of blocks (not individual rows), which makes them very small and fast to build.
- Ideal for time-series data or columns with natural ordering (like
created_at
). - Less precise than B-Tree but much lighter in memory and disk.
Summary Table
Algorithm | Best For | Database Support | Notes |
---|---|---|---|
B-Tree | General purpose (default) | PostgreSQL, MySQL | Supports range, sorting, equality, most types |
Hash | Exact match queries only | PostgreSQL (optional), MySQL (MEMORY only) | No range or order support |
GIN | Full-text search, arrays, JSON | PostgreSQL | High insert cost, fast querying |
GiST | Geospatial, range, full-text alt | PostgreSQL | Flexible but slower than GIN |
BRIN | Large ordered datasets | PostgreSQL | Lightweight, block-level indexing |
π§Ύ Example of Indexing in a Database Table
Letβs say you have the following users
table in your Rails application:
+----+-----------------------+------------+
| id | email | created_at |
+----+-----------------------+------------+
| 1 | alice@example.com | 2023-01-01 |
| 2 | bob@example.com | 2023-01-02 |
| 3 | charlie@example.com | 2023-01-03 |
+----+-----------------------+------------+
Now, if you frequently search for users by email
, like:
User.find_by(email: "bob@example.com")
π‘ Without Index:
- The database performs a full table scan.
- It checks each row from top to bottom to find the match.
- This becomes slower as the table grows to thousands or millions of rows.
π’ With Index:
- We create an index on
email
using a migration:
add_index :users, :email
Index: users_email_idx (B-tree)
[alice@example.com]
\
[bob@example.com]
\
[charlie@example.com]
- Instead of scanning rows, the database jumps directly to the correct node.
- Lookup time is reduced from
O(n)
toO(log n)
.
π§ͺ To View Indexes in PostgreSQL:
Connect to your database and run:
\d users
Output:
Indexes:
"index_users_on_email" UNIQUE, btree (email)
"users_pkey" PRIMARY KEY, btree (id)
π§ͺ To View Indexes in MySQL:
Run this SQL command:
SHOW INDEXES FROM users;
This will show index names, types (btree/hash), columns indexed, and uniqueness.
π΅οΈ How to Check if a Query is Using an Index or Not
Indexes make queries faster β but how do you know if your query is actually using an index? Hereβs how you can check it using your database or Rails tools.
π In PostgreSQL
Use the EXPLAIN ANALYZE
command before your SQL query:
EXPLAIN ANALYZE SELECT * FROM users WHERE email = 'john@example.com';
If index is used: Youβll see something like:
Index Scan using index_users_on_email on users (cost=0.15..8.17)
If not: Youβll see:
Seq Scan on users (cost=0.00..42.00)
βSeq Scanβ means full table scan β no index used.
π In MySQL
Use the EXPLAIN
keyword before your query:
EXPLAIN SELECT * FROM users WHERE email = 'john@example.com';
Look for the column key
in the output:
- If key = NULL: no index used β
- If key = index name: index is used β
π In Rails (ActiveRecord)
Rails doesn't directly show index usage, but you can print the SQL and analyze it manually:
User.where(email: "john@example.com").explain
This will output something like:
EXPLAIN for: SELECT "users".* FROM "users" WHERE "users"."email" = $1 [["email", "john@example.com"]]
Index Scan using index_users_on_email on users
π§ͺ Example Output Comparison
β With Index
Index Scan using index_users_on_email on users
β Without Index
Seq Scan on users
π Tip
Even if you have an index, the database may not use it if:
- The table is too small (sequential scan may be faster)
- The column has many duplicate values (low selectivity)
- You are using functions like
LOWER()
without a functional index
π οΈ How to Debug Indexing in Rails
Even if youβve added indexes, your queries might still be slow. Thatβs when debugging indexing becomes important. Here's a step-by-step guide to identify and fix indexing issues.
π 1. Check if the Index Exists
In PostgreSQL:
\d users
In MySQL:
SHOW INDEX FROM users;
In Rails Console:
ActiveRecord::Base.connection.indexes(:users)
π§ͺ 2. Use EXPLAIN or EXPLAIN ANALYZE
Check whether the index is used or not:
EXPLAIN SELECT * FROM users WHERE email = 'test@example.com';
In Rails:
User.where(email: "test@example.com").explain
π 3. Look for "Seq Scan" (Bad) vs "Index Scan" (Good)
Seq Scan
: Full table scan β no index is being used.Index Scan
orIndex Only Scan
: Index is being used correctly.
π« 4. Avoid Functions Without Functional Index
If your query uses a function like:
WHERE LOWER(email) = 'test@example.com'
Normal indexes wonβt be used. Instead, create a functional index:
add_index :users, "LOWER(email)", name: "index_users_on_lower_email"
π 5. Check Column Selectivity
If the column has mostly the same value (e.g., active
is true for 99% of users), the index may not be used. Indexes work best when column values are varied.
π 6. Rebuild or Refresh Index
In PostgreSQL:
REINDEX INDEX index_users_on_email;
Useful when table changes significantly (e.g., mass updates or deletions).
π§Ή 7. Remove Duplicate or Unused Indexes
Too many indexes can slow down writes (INSERT/UPDATE). Identify unnecessary ones:
rails dbconsole
\d+ users
π 8. Benchmark With and Without Index
You can manually remove an index, run a query, and compare performance:
remove_index :users, :email
# Run query
add_index :users, :email
# Re-run query and compare timing
π 9. Watch for Multi-Column Index Mistakes
In a composite index (user_id, status)
, queries must filter by user_id first. This is called the "leftmost column rule".
# β
Uses index
Post.where(user_id: 1, status: "published")
# β Wonβt use index
Post.where(status: "published")
π§° 10. Use PgHero or Database Tools
- PgHero: A gem to analyze slow queries, unused indexes, and index bloat.
- PgAdmin/MySQL Workbench: GUI tools for monitoring index usage.
gem 'pghero'
Then run pghero
dashboard or queries to detect index issues.
π Flow of Indexing in Rails and Where to Use It
π§ Flow: How Indexing Works in a Rails Application
- 1. Identify the query bottlenecks
Use logs, `EXPLAIN`, or tools like PgHero to find slow queries. - 2. Analyze query patterns
Ask: which columns are filtered, joined, or sorted most often? - 3. Add the appropriate index
Use single, composite, unique, or partial indexes based on the query. - 4. Run migration
Add your index via Rails migration:add_index
. - 5. Test and benchmark
Use.explain
in Rails or database tools to verify index usage. - 6. Monitor and maintain
Clean up unused or duplicate indexes and monitor bloat regularly.
π Where Indexing is Used in a Rails Application
Indexing isnβt just for speed β itβs about scaling, enforcing data rules, and building real-world features. Hereβs where indexing is commonly used in Rails:
- π User authentication:
email
,username
,reset_password_token
- π Relationships & foreign keys:
user_id
,post_id
forbelongs_to
/has_many
- π Polymorphic associations:
commentable_type
,commentable_id
- π Analytics & dashboards: Index
created_at
,user_id
, andevent_type
- π¦ E-commerce & filtering:
category_id
,price
,in_stock
,status
- π Search & lookup:
slug
,order_number
,tracking_id
- βοΈ Invitations and tokens:
invitation_token
,access_token
- π Admin dashboards: Sorting and filtering by
created_at
,role
,approval_status
- π Pagination + cursor-based queries:
id
,created_at
,updated_at
- π Full-text search:
content
,description
,title
(PostgreSQL + GIN)
π§ Example Use Case: Admin Dashboard Filters
Admin panel filters user registrations by date and status:
User.where(status: "active").order(created_at: :desc)
You should add this index:
add_index :users, [:status, :created_at]
π§ Example Use Case: Product Search by Slug
When using slugs in URLs:
add_index :products, :slug, unique: true
This ensures fast lookups and enforces uniqueness.
π§Ύ 20 Detailed Examples of Indexing in Rails
These examples show real-world scenarios of how and why to use different types of indexes in a Rails application.
- Index on
email
(single column):
Speeds up user lookups by email.add_index :users, :email
- Unique index on
username
:
Ensures no duplicate usernames are saved.add_index :users, :username, unique: true
- Composite index on
user_id
andstatus
:
Optimizes queries likewhere(user_id: x, status: y)
.add_index :posts, [:user_id, :status]
- Index on
created_at
for sorting:
Improvesorder(created_at: :desc)
performance.add_index :articles, :created_at
- Partial index on active users:
Faster lookups for common filters.add_index :users, :email, where: "active = true"
- Case-insensitive email search:
Useful when usingLOWER(email)
in queries.add_index :users, "LOWER(email)", name: "index_users_on_lower_email"
- Foreign key index on
post_id
in comments:
Required for efficient joins.add_index :comments, :post_id
- Index on
slug
column:
Speeds up friendly URL lookups.add_index :posts, :slug, unique: true
- Composite index for order filtering:
Used for combined filters on payment status and user.add_index :orders, [:user_id, :payment_status]
- Unique composite index on user+project name:
Prevents users from having duplicate project names.add_index :projects, [:user_id, :name], unique: true
- Polymorphic index for commentable:
Required for polymorphic associations like comments on posts/images.add_index :comments, [:commentable_type, :commentable_id]
- Full-text index (PostgreSQL):
Enables natural language search.execute <<-SQL CREATE INDEX index_articles_on_content_fts ON articles USING gin(to_tsvector('english', content)); SQL
- Index on JSONB key (PostgreSQL):
Querying JSONB fields with GIN index.add_index :events, :metadata, using: :gin
- Index on
reset_password_token
:
Speeds up password reset lookups.add_index :users, :reset_password_token, unique: true
- Index for soft delete column:
If using `deleted_at` column for soft delete.add_index :users, :deleted_at
- Index on session token:
Useful for quick session validation.add_index :sessions, :token, unique: true
- Index on
user_id
andcreated_at
:
Helps with dashboard filtering and analytics.add_index :logs, [:user_id, :created_at]
- Index on
status
column:
For frequently filtered enums likestatus: [:pending, :active]
.add_index :subscriptions, :status
- Index on
invitation_token
:
Fast lookup when accepting invitations.add_index :invitations, :token, unique: true
- Composite index with descending order (PostgreSQL):
Helps with sorted leaderboards.add_index :scores, [:user_id, :points], order: { points: :desc }
π§ Gems and Tools for Indexing in Rails
Rails supports index creation out-of-the-box via migrations, but there are additional gems and tools that help you analyze, monitor, and manage indexing efficiently.
1. β ActiveRecord Migrations (Built-in)
- Purpose: Create, remove, and manage indexes using
add_index
,remove_index
. - Included by default in Rails
- Example:
add_index :users, :email, unique: true
2. π PgHero
- Gem:
gem 'pghero'
- Purpose: Analyze slow queries, unused indexes, index hit rate, and index bloat (PostgreSQL only).
- UI: Comes with a dashboard accessible via browser.
- Docs: github.com/ankane/pghero
3. π Lol_dba
- Gem:
gem 'lol_dba'
- Purpose: Scans models and tells you which foreign keys are missing indexes.
- Command:
bundle exec lol_dba db:find_indexes
- Docs: github.com/plentz/lol_dba
4. π§ͺ Bullet
- Gem:
gem 'bullet'
- Purpose: Detects N+1 queries, which often indicate missing indexes on associations.
- Useful for: Development environments.
- Docs: github.com/flyerhzm/bullet
5. π PgExtras
- Gem:
gem 'pg_extras'
- Purpose: Provides SQL helpers to inspect unused indexes, index size, bloat, etc.
- Use: Console or Web UI (
rails pg_extras:index_bloat
) - Docs: github.com/pawurb/pg_extras
6. π₯ Adminer / PgAdmin / MySQL Workbench
- Type: GUI database management tools
- Purpose: Visualize indexes, inspect query plans, create/delete indexes manually.
- PostgreSQL: PgAdmin
- MySQL: MySQL Workbench
- All-in-one: Adminer (minimal web-based SQL tool)
7. π¨ Scout APM / NewRelic
- Type: Application performance monitoring (APM) tools
- Purpose: Track slow database queries and help identify where indexes are needed.
- Useful for: Production monitoring and query tracing
β Best Implementation of Indexing in Rails β With Full Detail
π Use Case: Admin Dashboard Filtering Users by Status and Signup Date
Your app has a users
table with 1 million rows. Admins regularly filter users by their status (e.g., "active", "inactive") and by created_at date range for reporting:
User.where(status: "active").where("created_at > ?", 30.days.ago)
π¨ Problem Without Index
- Query takes 5β10 seconds on large tables.
- Rails generates a
Seq Scan
(sequential scan), scanning all 1 million rows. - Users complain of slow reports and dashboards.
π― Solution: Composite Index on status
and created_at
This index allows the database to:
- Quickly filter users by
status
- Efficiently narrow down users within a date range
- Use the B-tree to jump directly to matching rows
π Rails Migration
rails generate migration AddIndexToUsersStatusAndCreatedAt
class AddIndexToUsersStatusAndCreatedAt < ActiveRecord::Migration[7.0]
def change
add_index :users, [:status, :created_at], name: "index_users_on_status_and_created_at"
end
end
Run the migration:
rails db:migrate
π§ͺ How to Verify Index Is Used
User.where(status: "active").where("created_at > ?", 30.days.ago).explain
Expected output (PostgreSQL):
Index Scan using index_users_on_status_and_created_at on users
β‘ Result
- Query speed improved from 7s β 50ms
- CPU usage dropped significantly under load
- Dashboard reports became usable again
π Best Practice Notes
- β
Always index columns used together in
WHERE
orORDER BY
- π« Donβt add too many indexes on every column β they slow down inserts/updates
- β
Name your indexes clearly (e.g.,
index_users_on_status_and_created_at
) - π§ Use
EXPLAIN
or Rails.explain
to verify query performance
π§ 20 Technical Questions & Answers on Indexing in Rails
- Q1: How do you add a basic index in Rails?
A: Useadd_index
in a migration.add_index :users, :email
- Q2: How do you make an index unique?
A: Addunique: true
to enforce no duplicates.add_index :users, :username, unique: true
- Q3: How can I check if a query is using an index?
A: Use:
Look forUser.where(email: "test@example.com").explain
Index Scan
in output. - Q4: What is a composite index?
A: An index on multiple columns.add_index :orders, [:user_id, :status]
- Q5: Why would a query not use an index?
A: Possible reasons:- Table is too small (full scan is faster)
- Column has low selectivity (same values)
- You're using a function without a functional index
- Q6: How do you index a foreign key?
A: Rails doesn't auto-index foreign keys. You must add:add_index :comments, :post_id
- Q7: How do you index a polymorphic association?
A: Index both type and id:add_index :comments, [:commentable_type, :commentable_id]
- Q8: How do you add a partial index in Rails?
A: Use awhere
clause.add_index :users, :email, where: "active = true"
- Q9: What is the default index algorithm in PostgreSQL?
A:B-tree
, used for most general indexes. - Q10: What command lists all indexes in Rails console?
A:ActiveRecord::Base.connection.indexes(:users)
- Q11: How do you remove an index?
A: Useremove_index
in a migration.remove_index :users, :email
- Q12: How do you optimize a case-insensitive search?
A: Use a functional index:add_index :users, "LOWER(email)", name: "index_users_on_lower_email"
- Q13: How can I see the index size in PostgreSQL?
A: Use:SELECT relname AS index_name, pg_size_pretty(pg_relation_size(indexrelid)) AS index_size FROM pg_stat_user_indexes;
- Q14: How does index affect insert performance?
A: Slightly slows down inserts/updates because indexes must also be updated. - Q15: Can Rails add index when using
add_reference
?
A: Yes, with:add_reference :comments, :post, foreign_key: true, index: true
- Q16: Whatβs the βleftmost column ruleβ for composite indexes?
A: Composite indexes are only used when filtering by the left-most column(s) in the index.# Will use index where(user_id: 1, status: "active") # Wonβt use index where(status: "active")
- Q17: How do I index JSONB fields in PostgreSQL?
A:add_index :events, :metadata, using: :gin
- Q18: Which gem helps find missing indexes?
A:lol_dba
bundle exec lol_dba db:find_indexes
- Q19: How do I debug index bloat or usage?
A: UsePgHero
orpg_extras
:rails pg_extras:index_bloat
- Q20: Can I add ordering to an index?
A: Only in PostgreSQL:add_index :scores, [:user_id, :points], order: { points: :desc }
π Alternatives & Related Concepts
- Materialized Views: Pre-computed query results (mainly in PostgreSQL)
- Denormalization: Storing data in a redundant format to reduce joins
- Counter Cache: Caching count of associated records (e.g., comments_count)
π Real-World Case Study
π Scenario: Social Media App
A Rails-based social media app has a `likes` table with millions of records. Every time a user opens a post, the app needs to show if they liked it. Without an index on [:user_id, :post_id]
, the query scans the full table, resulting in slow load times.
After adding this composite index, query time dropped from 250ms to under 20ms. Indexing saved database costs and significantly improved user experience.
β Best Practices for Indexing in Rails β With Examples
Indexing can drastically improve performanceβbut only if used correctly. Here are the most important best practices every Rails developer should follow, with examples.
1. π― Index Columns Used in WHERE
, ORDER BY
, or JOIN
If a column is frequently used in filters or joins, it should have an index.
# This is common in authentication
User.find_by(email: "test@example.com")
# Add an index:
add_index :users, :email
2. π Always Index Foreign Keys
Rails does not automatically add indexes for belongs_to
. Without indexes, JOIN
queries are slow.
# In a comments table:
add_index :comments, :post_id
3. π Use Unique Indexes for Unique Fields
Validations are not enough β database-level uniqueness is a must.
# Enforce unique usernames:
add_index :users, :username, unique: true
4. π§ Use Composite Indexes for Multiple Filters
If you often filter by multiple columns together, use a multi-column index.
# Filtering orders by user and status
Order.where(user_id: 1, status: "paid")
# Use:
add_index :orders, [:user_id, :status]
5. β‘ Avoid Indexing Columns with Low Selectivity
Donβt index columns like gender
or is_active
that have very few distinct values. Indexes won't help much.
6. π Use Partial Indexes for Conditional Queries
If you're always querying a subset of data (like active users), partial indexes reduce size and improve performance.
add_index :users, :email, where: "active = true"
7. π§Ή Clean Up Unused or Redundant Indexes
Too many indexes slow down write operations. Remove duplicates.
# Check current indexes:
ActiveRecord::Base.connection.indexes(:users)
# Remove unused:
remove_index :users, :old_column
8. πͺ Use the "Leftmost Column" Rule for Composite Indexes
Only the leftmost columns in a composite index are used for filtering.
# Index:
add_index :logs, [:user_id, :event_type]
# Works:
Log.where(user_id: 1, event_type: "click")
# Wonβt use index:
Log.where(event_type: "click")
9. π Add Functional Indexes for Expressions
If your query uses a function (e.g., LOWER()
), a normal index wonβt help.
add_index :users, "LOWER(email)", name: "index_users_on_lower_email"
10. π Monitor Index Usage
Use PgHero
, lol_dba
, or Rails .explain
to monitor and verify usage.
User.where(email: "a@example.com").explain
# Or use PgHero dashboard
Learn more aboutΒ RailsΒ setup
https://shorturl.fm/IPXDm
https://shorturl.fm/hQjgP
https://shorturl.fm/LdPUr
https://shorturl.fm/DA3HU
https://shorturl.fm/47rLb