High Impact

Index Foreign Key Columns

10-100x faster JOINs and CASCADE operations

Why This Matters

Postgres does not automatically create indexes on foreign key columns. Without indexes, JOINs and CASCADE operations perform full table scans, becoming exponentially slower as tables grow.

Important: Unlike some databases, Postgres does not automatically index foreign keys. You must create indexes manually for every FK column.

The Problem

Incorrect - Missing FK indexes
-- FK column has no index
CREATE TABLE orders (
  id INT PRIMARY KEY,
  customer_id INT REFERENCES customers(id) ON DELETE CASCADE
  -- No index on customer_id!
);

-- JOIN causes sequential scan on orders
SELECT c.name, o.total
FROM customers c
JOIN orders o ON o.customer_id = c.id
WHERE c.id = 123;

-- EXPLAIN shows Seq Scan on orders (slow!)
-- CASCADE delete scans entire orders table
DELETE FROM customers WHERE id = 123;  -- Very slow

The Solution

Correct - Index every FK column
-- Create index on FK column
CREATE TABLE orders (
  id INT PRIMARY KEY,
  customer_id INT REFERENCES customers(id) ON DELETE CASCADE
);

-- Always index FK columns
CREATE INDEX orders_customer_id_idx ON orders (customer_id);

-- JOIN now uses index scan (fast!)
SELECT c.name, o.total
FROM customers c
JOIN orders o ON o.customer_id = c.id
WHERE c.id = 123;

-- CASCADE delete uses index to find child rows
DELETE FROM customers WHERE id = 123;  -- Fast with index

Result: JOIN queries drop from 450ms to 2ms (225x faster). CASCADE deletes that took 30 seconds now complete in under 100ms.

Best Practices

-- Composite FK needs composite index
CREATE TABLE order_items (
  order_id INT,
  product_id INT,
  FOREIGN KEY (order_id, product_id) REFERENCES order_products(order_id, product_id)
);

-- Index both columns together
CREATE INDEX order_items_fk_idx ON order_items (order_id, product_id);

Finding Missing FK Indexes

Use this query to find foreign keys without indexes:

SELECT
  tc.table_name,
  kcu.column_name,
  ccu.table_name AS foreign_table_name,
  ccu.column_name AS foreign_column_name
FROM information_schema.table_constraints AS tc
JOIN information_schema.key_column_usage AS kcu
  ON tc.constraint_name = kcu.constraint_name
JOIN information_schema.constraint_column_usage AS ccu
  ON ccu.constraint_name = tc.constraint_name
LEFT JOIN pg_indexes pi
  ON pi.tablename = tc.table_name
  AND pi.indexdef LIKE '%' || kcu.column_name || '%'
WHERE tc.constraint_type = 'FOREIGN KEY'
  AND pi.indexname IS NULL;

Check Your Project Automatically

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

Start Supabase Audit

Common Mistakes

Learn More