Skip to content

Intermediate · SQL in depth · topic 24 of 54 · 18 min

Indexes: making queries fast

Explanation

An index is a sorted copy of one or more columns with pointers back to the rows, so the database can jump to matching rows instead of reading the whole table. Indexes make reads fast but make every insert and update a little slower. Index the columns you search, join and sort on, and check with EXPLAIN that the index is really used.

Why queries get slow

Without help, WHERE phone = '9810012345' makes the database read every row and test it. That is called a full table scan. With 500 rows nobody notices. With 50 lakh rows, every login or search reads 50 lakh rows, and the app crawls.

Think of it like this: Your phone contacts are sorted by name, so you find "Ravi" in a second by jumping to R. Finding "the person whose number ends in 4321" means scrolling through everyone. The index at the back of a textbook works the same way: words in sorted order with page numbers, so you never read the whole book to find "photosynthesis".

A database index is that sorted list. Most databases store it as a B-tree, a structure that stays sorted as you add rows, so finding one value among crores of rows takes only a handful of steps. Each entry points back to the full row.

sql
CREATE INDEX idx_orders_customer ON orders(customer_id);

-- now this can jump straight to customer 2's orders
SELECT id, amount FROM orders WHERE customer_id = 2;
Name indexes idx_<table>_<columns> so everyone knows what they are for.

UNIQUE indexes

A UNIQUE index does two jobs: it speeds up lookups and it refuses duplicates. It is how you guarantee one account per email or phone, even when two signup requests arrive at the same moment, which a check in your JavaScript code cannot promise. A PRIMARY KEY or a UNIQUE column constraint creates one for you automatically.

sql
CREATE UNIQUE INDEX idx_customers_phone ON customers(phone);

INSERT INTO customers (name, phone) VALUES ('Aarav', '9810012345');
INSERT INTO customers (name, phone) VALUES ('Someone', '9810012345');
-- Error: UNIQUE constraint failed: customers.phone
The second insert is rejected by the database itself.

Composite indexes and column order

A composite index covers several columns, sorted by the first column, then by the second inside it, like a phone book sorted by surname and then first name. An index on (customer_id, order_date) helps queries that filter on customer_id, or on customer_id and order_date. It cannot help a query that filters only on order_date, just as a surname-first phone book cannot find everyone named "Priya".

Query on ordersCan use index (customer_id, order_date)?
WHERE customer_id = 2Yes
WHERE customer_id = 2 AND order_date >= '2026-03-01'Yes, best case
WHERE customer_id = 2 ORDER BY order_dateYes, and no extra sort needed
WHERE order_date >= '2026-03-01'No, first column is missing

Checking the plan

Ask the database how it will run a query. In SQLite write EXPLAIN QUERY PLAN before the query: SCAN means it reads the whole table, SEARCH ... USING INDEX means it jumps. In PostgreSQL use EXPLAIN (the plan) or EXPLAIN ANALYZE (actually runs it and shows real times); look for Seq Scan versus Index Scan.

output
sqlite> EXPLAIN QUERY PLAN SELECT * FROM orders WHERE customer_id = 2;
-- before the index:
SCAN orders
-- after CREATE INDEX idx_orders_customer:
SEARCH orders USING INDEX idx_orders_customer (customer_id=?)
SCAN reads every row; SEARCH uses the index.

The cost, and when not to index

  • Every INSERT, UPDATE and DELETE must also update every index on the table, so too many indexes slow writes.
  • Indexes take disk space and memory.
  • Tiny tables (a few hundred rows) are fast to scan anyway.
  • Columns with very few different values (status with 3 values, a true/false flag) rarely help alone.
  • A function on the column, like WHERE LOWER(email) = ..., cannot use a plain index on email.
Tip: A good starting set: the primary key, every foreign key column (orders.customer_id), columns that must be unique (email, phone), and the columns of your slowest frequent query, found with EXPLAIN. Add more only when a real query needs them.
Careful: PostgreSQL does not index foreign key columns automatically. Joins and deletes on a big table with an unindexed foreign key are a very common cause of slow production apps.

Watch the lesson

Indexes: making queries fast · 1/7

Indexes: making queries fast

Queries that are fast with a hundred rows can be painfully slow with a crore rows. Indexes fix that.

Prefer a video by a teacher? Find free videos on “database indexes explained b-tree explain query plan” on YouTube ↗

In real life: OTP login by phone number

A grocery app with 80 lakh users logs people in by phone number. Without an index, every OTP request scans the whole users table, and at the evening rush the database CPU hits 100 percent. The fix is one line: CREATE UNIQUE INDEX on users(phone). Lookups drop from seconds to under a millisecond, and as a bonus the database now refuses a second account with the same phone, which also closes a bug where double-tapping "Sign up" created two users.

sql
CREATE UNIQUE INDEX idx_users_phone ON users(phone);
One index: fast logins and no duplicate accounts.

Practice (0/3 done)

Task 1 · Code · SQL

Table: customers(id, name, phone), 4 rows with different phones. Step 1: create a UNIQUE index named idx_customers_phone on customers(phone). The starter then tries INSERT OR IGNORE of a customer whose phone already exists (OR IGNORE means: if a unique rule is broken, skip the row quietly instead of failing) and counts the customers. With your index the duplicate is skipped and the count is 4.

Task 2 · Code · SQL

Table: orders(id, customer_id, order_date, amount, status). The app's slowest query is WHERE customer_id = ? AND order_date >= ?. Create a composite index named idx_orders_customer_date with customer_id first and order_date second. The query in the starter lists the index's columns in order, to check your work.

Task 3 · Question

A users table has 50 lakh rows. The app runs these often: login by email, list a user's orders (orders.user_id), and an admin report filtered by is_active (true or false). Which indexes are the best first choice?

Quick quiz

Question 1

1.What does an index mainly do?

Question 2

2.An index exists on (customer_id, order_date). Which WHERE clause can NOT use it well?

Question 3

3.What is the main cost of adding many indexes to a table?

Question 4

4.In SQLite, EXPLAIN QUERY PLAN shows "SCAN users". What does that mean?

Question 5

5.Why use a UNIQUE index on email instead of only checking for duplicates in your Node.js code?

Read more (free)

Pass the quiz (60% or more) or mark this topic as done when you are ready.

Your progress is saved on this device only.

← Previous: Subqueries, CTEs and window functions