Database Indexing Strategies That Actually Work
Back to Blog
July 11, 2025
9 min read

Database Indexing Strategies That Actually Work

My queries were taking 8 seconds. I added one index and got them down to 80ms. Here's what I learned about database indexing the hard way.

DatabasePostgreSQLPerformanceBackend

Database Indexing Strategies That Actually Work

My app was slow. Like, "users are complaining" slow.

I checked the logs. One query was taking 8 seconds. Eight seconds! For a simple user lookup.

I added one index. Query time dropped to 80ms.

Here's what I learned about database indexing (and the mistakes I made along the way).

The Problem: Full Table Scans

My query looked innocent:

SELECT * FROM users WHERE email = 'user@example.com';

Postgres was scanning all 50,000 rows to find one user. Every. Single. Time.

I checked with EXPLAIN:

EXPLAIN SELECT * FROM users WHERE email = 'user@example.com'; -- Seq Scan on users (cost=0.00..1234.00 rows=1)

"Seq Scan" = full table scan = slow.

The Fix: Add an Index

CREATE INDEX idx_users_email ON users(email);

Now the same query:

EXPLAIN SELECT * FROM users WHERE email = 'user@example.com'; -- Index Scan using idx_users_email (cost=0.29..8.31 rows=1)

Query time: 8 seconds → 80ms.

One line of SQL. Huge difference.

When to Add Indexes

Add indexes on columns you use in WHERE clauses:

-- This query needs an index on status SELECT * FROM orders WHERE status = 'pending'; CREATE INDEX idx_orders_status ON orders(status);

Add indexes on foreign keys:

-- This query needs an index on user_id SELECT * FROM orders WHERE user_id = 123; CREATE INDEX idx_orders_user_id ON orders(user_id);

Add indexes on columns used in JOINs:

-- This needs indexes on both join columns SELECT * FROM orders o JOIN order_items oi ON o.id = oi.order_id; CREATE INDEX idx_order_items_order_id ON order_items(order_id);

Composite Indexes: Order Matters

I had a query that filtered by status AND date:

SELECT * FROM orders WHERE status = 'pending' AND created_at > '2025-01-01';

I added two indexes:

CREATE INDEX idx_orders_status ON orders(status); CREATE INDEX idx_orders_created_at ON orders(created_at);

Still slow. Postgres could only use one index.

Fixed it with a composite index:

CREATE INDEX idx_orders_status_created ON orders(status, created_at);

Now it's fast. But order matters!

-- This uses the index WHERE status = 'pending' AND created_at > '2025-01-01' -- This uses the index WHERE status = 'pending' -- This does NOT use the index efficiently WHERE created_at > '2025-01-01'

Put the most selective column first (the one that filters out the most rows).

The Index I Shouldn't Have Added

I got excited and added indexes everywhere:

CREATE INDEX idx_users_name ON users(name); CREATE INDEX idx_users_created_at ON users(created_at); CREATE INDEX idx_users_updated_at ON users(updated_at); CREATE INDEX idx_users_last_login ON users(last_login);

My writes got slower. Way slower.

Every INSERT or UPDATE now had to update 4 indexes. My database was spending more time maintaining indexes than serving queries.

I checked which indexes were actually being used:

SELECT schemaname, tablename, indexname, idx_scan FROM pg_stat_user_indexes WHERE schemaname = 'public' ORDER BY idx_scan;

Three of those indexes had idx_scan = 0. Never used.

I dropped them:

DROP INDEX idx_users_created_at; DROP INDEX idx_users_updated_at; DROP INDEX idx_users_last_login;

Writes got fast again.

Partial Indexes: Index Only What You Need

I had a query that only looked at active users:

SELECT * FROM users WHERE status = 'active' AND email = 'user@example.com';

I could index the whole table, but 95% of my users are active. That's a lot of wasted index space.

Used a partial index instead:

CREATE INDEX idx_active_users_email ON users(email) WHERE status = 'active';

Smaller index, faster queries, less disk space.

JSONB Indexes: For JSON Columns

I had a JSONB column with user preferences:

SELECT * FROM users WHERE preferences->>'theme' = 'dark';

Slow. Added a GIN index:

CREATE INDEX idx_users_preferences ON users USING GIN (preferences);

Now JSONB queries are fast.

What Didn't Work

Indexing everything: Made writes slow and wasted disk space.

Using LIKE with leading wildcard: Indexes don't help with LIKE '%search%'. They only help with LIKE 'search%'.

Indexing low-cardinality columns: Don't index boolean columns or columns with only a few distinct values. Not worth it.

Monitoring Index Usage

I check index usage monthly:

-- Find unused indexes SELECT schemaname, tablename, indexname, idx_scan FROM pg_stat_user_indexes WHERE idx_scan = 0 AND indexname NOT LIKE 'pg_%' ORDER BY pg_relation_size(indexrelid) DESC;

If an index hasn't been used in a month, I drop it.

The Results

Before:

  • Slow queries (8 seconds)
  • Users complaining
  • Database CPU at 80%

After:

  • Fast queries (80ms)
  • Happy users
  • Database CPU at 20%

Cost: $0. Just better indexes.

Should You Add Indexes?

Yes, but not everywhere.

Add indexes on:

  • Columns in WHERE clauses
  • Foreign keys
  • Columns used in JOINs

Don't index:

  • Every column "just in case"
  • Low-cardinality columns
  • Columns you never query

And always check if your indexes are actually being used.


Fighting slow queries? I'd love to hear what worked for you. Hit me up on LinkedIn.