Skip to main content

Command Palette

Search for a command to run...

PostgreSQL Concurrent Indexes in Rails: Avoiding Downtime in Production

When working with large production databases, adding an index may seem like a simple migration. However, on tables with millions of records, a regular index creation can lock writes and impact your application's availability.

Updated
4 min read
Y
Senior Ruby on Rails Engineer sharing practical insights on Rails, React, system design, performance optimization,. My goal is to simplify complex concepts through real-world examples and hands-on experiences.

In this article, we'll explore what concurrent indexes are, why they matter, and how to safely create them in Ruby on Rails.

The Problem with Normal Index Creation

Suppose we have a large users table with millions of records.

A typical Rails migration might look like:

add_index :users, :email

Behind the scenes, PostgreSQL executes:

CREATE INDEX index_users_on_email ON users(email);

While PostgreSQL builds the index, it acquires locks on the table.

On large tables this can lead to:

  • Slower API responses

  • Blocked INSERT operations

  • Blocked UPDATE operations

  • Blocked DELETE operations

  • Potential downtime during deployment

For example, imagine:

users table = 50 million rows

Creating an index on such a table can take several minutes, and during that time write operations may be affected.

The Solution: Concurrent Indexes

PostgreSQL provides a safer alternative:

CREATE INDEX CONCURRENTLY

This allows PostgreSQL to build the index while minimizing disruption to ongoing database operations.

In Rails, this can be achieved using:

class AddIndexToUsersEmail < ActiveRecord::Migration[7.1] disable_ddl_transaction!

def change

add_index :users, :email, algorithm: :concurrently

end end

How Concurrent Indexes Work
Normal Index Creation
Lock table

Scan rows

Build index

Unlock table

Concurrent Index Creation
Scan table

Build index in background

Track ongoing changes

Finalize index

The application continues to serve requests while the index is being created.

Why disable_ddl_transaction! Is Required

A common mistake is forgetting:

disable_ddl_transaction!

PostgreSQL does not allow:

CREATE INDEX CONCURRENTLY

inside a transaction.

Rails wraps migrations in a transaction by default, so we must disable it before creating concurrent indexes.

Without it, Rails raises an error during migration.

Complete Rails Example

class AddIndexToBookingsUserId < ActiveRecord::Migration[7.1] disable_ddl_transaction!

def change

add_index :bookings, :user_id, algorithm: :concurrently

end

end

When Should You Use Concurrent Indexes?

Concurrent indexes are recommended when:

  • The table contains millions of rows

  • The application is live

  • Downtime is unacceptable

  • The database receives frequent writes

Typical examples include:

  • Users table

  • Orders table

  • Bookings table

  • Transactions table

When Is a Regular Index Fine?

For smaller tables and development environments:

add_index :users, :email

is usually sufficient.

Since concurrent index creation performs additional work, it is generally slower than normal index creation.

Trade-offs

Normal Index Concurrent Index
Faster creation Slower creation
Blocks writes Minimal locking
Simpler Requires special handling
Risky on large production tables Safer for production

Real Production Scenario

Imagine a booking platform with:

bookings table = 100 million rows

A slow query is identified:

Booking.where(hotel_id: params[:hotel_id])

The obvious fix is:

add_index :bookings, :hotel_id

Running a normal index creation during business hours could affect booking APIs.

Instead, deploying:

add_index :bookings, :hotel_id, algorithm: :concurrently

allows the application to remain available while the index is built.

Checking Existing Indexes

To view indexes in PostgreSQL:

SELECT * FROM pg_indexes WHERE tablename = 'users';

You can also use:

\d users

from the PostgreSQL console.

Interview Question:

Why do we use concurrent indexes in Rails?

A good answer is:

Normal index creation can lock writes on large tables and impact application availability. PostgreSQL provides CREATE INDEX CONCURRENTLY to build indexes with minimal locking. In Rails, this is achieved using algorithm: :concurrently along with disable_ddl_transaction!. Although concurrent indexes take longer to build, they are much safer for production systems with large datasets and high traffic.

Conclusion

Creating indexes is one of the easiest ways to improve query performance, but doing it incorrectly in production can introduce downtime.

Whenever you're adding indexes to large, high-traffic tables, consider using concurrent indexes. The migration may take longer to complete, but your users won't experience disruptions while it runs.

A few extra minutes during deployment are often worth avoiding minutes of downtime in production.