How to Add an Index to Speed Up a Slow Postgres Query

When a Postgres query is slow and the table is not huge, a missing index is almost always the cause. EXPLAIN ANALYZE shows you the seq scan in seconds, and a single CREATE INDEX statement fixes it - no schema change, no downtime.

DevOps Engineerpostgresindexingsql

Why a 10,000-row table can be slow

A table with 10,000 rows is tiny by database standards, but Postgres still reads every single row when there is no index on the column you are filtering by. That is a sequential scan - the database cannot skip to the matching rows, so it reads them all before discarding the ones that don't match.

A search query like this will trigger a seq scan if name has no index:

SELECT * FROM products WHERE LOWER(name) LIKE 'widget%';

At 10,000 rows that costs milliseconds. At 1 million rows it costs seconds. Index it now, before the table grows.

Step 1: confirm the seq scan with EXPLAIN ANALYZE

Connect to Postgres and prefix the slow query with EXPLAIN ANALYZE:

EXPLAIN ANALYZE
SELECT * FROM products WHERE LOWER(name) LIKE 'widget%';

Look for Seq Scan on products in the plan output. The rows= and actual time= values show how many rows were read and how long it took. A seq scan with a high row count is the signal to add an index.

Step 2: add the right index

For a LOWER(name) LIKE 'prefix%' pattern on Postgres, a functional btree index with text_pattern_ops is the correct choice - it covers prefix matching after case folding:

CREATE INDEX idx_products_name_lower
    ON products (LOWER(name) text_pattern_ops);

ANALYZE products;   -- update planner statistics immediately

text_pattern_ops tells Postgres that the index should support LIKE/ILIKE prefix patterns, not just equality and range comparisons. Without it, the planner may not choose the index for LIKE 'widget%'.

Step 3: verify the plan changed

Run EXPLAIN ANALYZE again on the same query:

EXPLAIN ANALYZE
SELECT * FROM products WHERE LOWER(name) LIKE 'widget%';

You should now see Index Scan using idx_products_name_lower instead of Seq Scan. Query time drops from seconds to single-digit milliseconds.

When to use a trigram index instead

If you need substring matching (LIKE '%widget%' - the pattern does not anchor at the start), btree cannot help. Use a GIN trigram index instead:

CREATE EXTENSION IF NOT EXISTS pg_trgm;
CREATE INDEX idx_products_name_trgm
    ON products USING GIN (name gin_trgm_ops);

Trigram indexes are larger and slower to build, but they handle arbitrary substrings. For prefix-only search, the functional btree approach above is faster and smaller.

Production notes

Want to try it hands-on? HeyDevJob gives you this exact setup in a live cloud workspace in your browser - edit it, run it, and see it work. Free, nothing to install.

Try it in a workspace →

What you'll practice

FAQ

How do I find out if Postgres is doing a sequential scan?

Run EXPLAIN ANALYZE in front of your query. Look for 'Seq Scan' in the output - it means Postgres is reading every row. 'Index Scan' or 'Bitmap Index Scan' means an index is being used.

Why does my Postgres LIKE query ignore the index?

A standard btree index on a text column does not support LIKE patterns unless you use text_pattern_ops (for prefix patterns) or pg_trgm + a GIN index (for substring patterns). Create the index with the right operator class.

How do I add an index without locking my production table?

Use CREATE INDEX CONCURRENTLY - Postgres builds the index in the background without taking a write lock, so inserts and updates continue. It takes longer to build but is safe on live tables.

Keep learning

Fix the Nginx 502 Bad GatewayDevOps projectRecover a Crashed Linux ServiceDevOps projectCorrect a Postgres Port MismatchDevOps projectDevOps roadmapStep by step to hiredDevOps interview questionsSTAR answersAll DevOps projectsProjects hub

Learn it by doing. Open this in a live cloud workspace, make the change yourself, and keep a record of the work you can share.

Open the workspace →