Critical Impact

Add Indexes on WHERE and JOIN Columns

100-1000x faster queries on large tables

Why This Matters

Queries filtering or joining on unindexed columns cause full table scans, which become exponentially slower as tables grow. What takes 10ms on 1,000 rows can take 10 seconds on 1 million rows.

This is the single most common performance issue in Postgres databases. In production, you'll see this when:

Warning: Sequential scans on tables with more than 10,000 rows are almost always a sign of a missing index. Use EXPLAIN ANALYZE to confirm.

The Problem

Without an index, Postgres must scan every row in the table to find matches:

Incorrect - Sequential scan on large table
-- No index on customer_id causes full table scan
SELECT * FROM orders WHERE customer_id = 123;

-- EXPLAIN shows:
-- Seq Scan on orders (cost=0.00..25000.00 rows=100 width=85)
--   Filter: (customer_id = 123)
--   Rows Removed by Filter: 999900

The Seq Scan indicates a full table scan. With 1 million rows, Postgres reads all of them just to find 100 matching orders.

The Solution

Create an index on columns used in WHERE clauses:

Correct - Index scan
-- Create index on frequently filtered column
CREATE INDEX orders_customer_id_idx ON orders (customer_id);

SELECT * FROM orders WHERE customer_id = 123;

-- EXPLAIN shows:
-- Index Scan using orders_customer_id_idx (cost=0.42..8.44 rows=100 width=85)
--   Index Cond: (customer_id = 123)

For JOIN columns, always index the foreign key side:

-- Index the referencing column (the FK side)
CREATE INDEX orders_customer_id_idx ON orders (customer_id);

SELECT c.name, o.total
FROM customers c
JOIN orders o ON o.customer_id = c.id;

Result: Query time drops from 450ms to 2ms - a 225x improvement. The difference grows even larger as the table size increases.

Check Your Project Automatically

Connect your Supabase project to detect missing indexes and other performance issues.

Start Supabase Audit

Common Mistakes

How to Find Missing Indexes

Use this query to find tables being sequentially scanned:

SELECT 
  relname AS table_name,
  seq_scan,
  seq_tup_read,
  idx_scan,
  seq_tup_read / seq_scan AS avg_rows_per_scan
FROM pg_stat_user_tables
WHERE seq_scan > 0
ORDER BY seq_tup_read DESC
LIMIT 10;

High seq_tup_read with low idx_scan indicates tables that need indexes.

Learn More