This guide shows you how to pick the right database index for a slow Rails query, then verify it actually helps. You’ll learn to read the query plan, choose a composite index, and measure the improvement without guessing.
What You’ll Need
- A Rails app (Rails 5 or newer recommended) with PostgreSQL, MySQL, or SQLite (PostgreSQL used in examples).
- Access to the Rails console (
rails console) and the database console (psqlfor Postgres,mysqlfor MySQL). - A query that is noticeably slow in development or production logs.
- Basic familiarity with ActiveRecord and SQL.
Step-by-Step
- Identify the slow query. Look in the Rails log (
log/development.logorlog/production.log) for lines that saySELECTwith a high duration, or use theActiveRecord::Base.connection.executeto run a query and time it. For example, if you have aPostmodel and you often callPost.where(author_id: 1).order(created_at: :desc), that’s your candidate. - Get the query plan before indexing. In your database console, run
EXPLAIN ANALYZEwith the exact query. For Postgres:EXPLAIN ANALYZE SELECT * FROM posts WHERE author_id = 1 ORDER BY created_at DESC;
Note the
Seq Scan(sequential scan) and theExecution Time. This is your baseline. - Decide what to index. Look at the
WHEREandORDER BYclauses. For equality conditions (author_id = ?), an index on that column helps. For sorting, the index should include the sort column after the equality column. So a composite index on(author_id, created_at)is ideal because it lets the database find rows by author and return them already sorted. - Add the index via a migration. In Rails, generate a migration:
rails generate migration AddIndexToPostsAuthorCreatedAt
Edit the migration to add the composite index:
add_index :posts, [:author_id, :created_at]
Run
rails db:migrate. - Re-run the query plan. In the database console, run the same
EXPLAIN ANALYZEagain. You should seeIndex ScanorIndex Only Scaninstead ofSeq Scan. Note the new execution time. - Measure the real-world improvement. In Rails console, time the query before and after (if you can revert, or compare with a similar query). Use
Benchmark.measure:Benchmark.measure { Post.where(author_id: 1).order(created_at: :desc).to_a }Run it a few times to get a stable average. Compare to the baseline.
- Check for over-indexing. Each index slows down writes (INSERT, UPDATE, DELETE) and takes disk space. If the query is already fast enough (under ~10ms), consider not adding an index. Also, avoid duplicate indexes—e.g., if you already have an index on
author_idalone, the composite index might make it redundant, but it’s not always harmful.
Common Mistakes
- Indexing the wrong column. If you index only
created_atbut the query filters byauthor_idfirst, the database may still do a sequential scan. - Forgetting the sort order. For
ORDER BY created_at DESC, a composite index on(author_id, created_at)works, but if you hadORDER BY created_at ASCin some queries and DESC in others, the index can be scanned backward, so it’s fine. - Using a function in the WHERE clause. For example,
WHERE lower(email) = 'x'won’t use a regular index onemail; you need an expression index. - Not analyzing the table. After adding an index, run
ANALYZE(Postgres) orOPTIMIZE TABLE(MySQL) to update statistics so the planner uses the index.
Tips
- Use a covering index if the query only selects a few columns. For example, if you only need
idandtitle, an index on(author_id, created_at, title)could allow an index-only scan. - Test with realistic data. A query that is fast on 100 rows may be slow on 1 million. Use production-like data volumes.
- Use
EXPLAIN(without ANALYZE) for a quick check—it doesn’t execute the query, so it’s safe on production. - Consider partial indexes for queries that always have a condition like
WHERE deleted_at IS NULL.
Troubleshooting
Why is the index not being used? Check that the query’s WHERE clause matches the index’s leftmost column. If you query WHERE created_at > ? but the index is on (author_id, created_at), the database can’t use it because author_id is not constrained. Also, ensure the column types match—e.g., if author_id is a string and you pass an integer, the index may be ignored.
The query is still slow after adding the index. Run EXPLAIN ANALYZE again and look for Bitmap Heap Scan or Sort steps. If you see a Sort, the index isn’t covering the ORDER BY. Try adding the sort column to the index. Also, check if the table has many updates that cause index bloat—run VACUUM (Postgres) or OPTIMIZE TABLE (MySQL).
Migration fails because index name is too long. Rails auto-generates a name like index_posts_on_author_id_and_created_at which is fine, but if you have a long table name, you can specify a shorter name with name: option.
I added an index but the query plan still shows Seq Scan on a small table. For very small tables (e.g., a few hundred rows), the database may prefer a sequential scan because it’s faster than using an index. That’s normal—don’t force it.
Frequently Asked Questions
Q: How do I know which queries need an index? A: Look for queries that appear frequently in logs with high duration. Use EXPLAIN to see if they do sequential scans on large tables.
Q: Can I have too many indexes? A: Yes, each index adds overhead on writes and uses disk space. Aim for indexes that serve multiple queries, and remove unused ones (check with pg_stat_user_indexes in Postgres).
Q: What’s the difference between a single-column and a composite index? A: A composite index on (a, b) can speed up queries that filter by a alone, by a and b, or by a and sort by b. A single-column index on a only helps queries filtering by a.
Q: Should I index boolean columns? A: Usually not, because the selectivity is low—the database may still scan most rows. Only index if the boolean is true for a tiny fraction of rows and you filter on it often.
Q: How do I test an index without affecting production? A: Create the index in a staging environment with production-like data, run the query, and compare plans. Or use EXPLAIN on production (without ANALYZE) to see if the index would be used—but be careful not to run heavy queries.
Q: My query uses a JOIN, how do I index that? A: Index the foreign key columns used in the JOIN condition. For example, if you join posts to authors on author_id, ensure author_id is indexed in both tables.
Related
Method: the standard formula for this calculation, rounded as each result states. See how the calculators work or report an error.
More cards in this drawer
- Server Repair Fails: Fix Restart Loops
- Plane Crazy Summer Event Schedule & Dates3 tier
- How to Choose a Secure IMAP Email Client
- Tower Defense RNG Codes: Active List & How to Redeem
- RCS Chats On or Off in Google Messages
- How to Choose a Phone That Supports VoLTE on Your Carrier