Expert: design, speed and engines · Indexes, query plans and views · topic 47 of 62 · 30 min
How indexes work
Explanation
The problem: reading everything
Ask SHOP for the orders of customer 1 and the database has a choice. If nothing helps it, it reads every row of orders and keeps those with customer_id = 1. That is a full table scan. With 8 rows nobody notices. With 5 crore orders on a sale day, each scan reads gigabytes, and a page that ran this query on every visit would grind to a halt.
What an index is
An index is a separate structure that stores the values of one or more columns in sorted order, each with a pointer to its row. In SQLite the pointer is the rowid; in PostgreSQL it is the row's physical location; in MySQL's InnoDB, secondary indexes store the primary key. Because the values are sorted, the database can find a value without looking at every entry.
B-trees in plain words
Almost every database uses a B-tree (balanced tree) for ordinary indexes. Picture a tree of pages. The top page holds a few hundred sorted keys that split the whole range into parts ("A to F go left, G to M next..."). Each part points to a page lower down that splits again, until the bottom pages (leaves) hold the actual values and row pointers.
- Sorted: values are in order, so a range like order_date between two dates is one walk along the leaves.
- Balanced: every leaf is the same distance from the top, so every lookup takes the same few steps.
- Wide: each page holds hundreds of keys, so even crores of rows need only about three or four levels.
- Fast: the number of steps grows with the logarithm of the row count, written O(log n). Doubling the table adds roughly one comparison, not double the work.
Indexes you already have
Primary keys and UNIQUE constraints are indexed automatically, because the database must check them quickly on every insert. In SQLite, an INTEGER PRIMARY KEY is the rowid itself, so the table is stored sorted by it. A composite key such as order_items (order_id, product_id) gets an automatic index named sqlite_autoindex_order_items_1.
Creating, listing and dropping indexes
CREATE INDEX idx_orders_customer ON orders(customer_id);
SELECT name, tbl_name FROM sqlite_master WHERE type = 'index';
-- sqlite_autoindex_order_items_1 | order_items
-- idx_orders_customer | orders
DROP INDEX idx_orders_customer;The query itself does not change. You still write WHERE customer_id = 1; the query planner, the part of the database that decides how to run a query, notices the index and uses it. Other engines list indexes differently: \di in PostgreSQL's psql or the pg_indexes view, and SHOW INDEX FROM orders in MySQL.
The cost of an index
- Writes: every INSERT, every DELETE, and every UPDATE of an indexed column must also update each index. A table with ten indexes does eleven writes per new row.
- Storage: an index is a copy of its columns plus pointers. Indexes on a big table can be gigabytes.
- Memory: indexes compete with data for the cache. Unused indexes waste it.
Selectivity: which columns deserve an index?
Selectivity is how few rows a value picks out. An email or an order id picks one row out of crores: very selective, a perfect index. A status column with four values, where Delivered covers 90% of rows, picks almost everything; reading the index and then nearly every row is slower than a plain scan, so the planner ignores such an index. Good candidates are columns in WHERE, JOIN ... ON and ORDER BY of frequent queries, with many distinct values.
Common mistakes
- Indexing every column "just in case". Fix: index for real, frequent queries; drop indexes nothing uses.
- Forgetting the foreign key columns. Fix: index orders.customer_id, order_items.product_id and similar join columns.
- Indexing a low-selectivity column such as a yes/no flag. Fix: skip it, or use a partial index on the rare value (next topic).
- Creating the same index twice under different names. Fix: list existing indexes before adding one.
- Judging speed on tiny test tables. Fix: test with realistic row counts; on 8 rows a scan is always fine.
Need help?
Stuck on a word or an idea? Search this course, or every course, and jump straight to the explanation or a worked question.
Try:
Watch the lesson
How indexes work · 1/7
Without help, finding rows means reading the whole table. That is a full table scan, and on big tables it is slow.
Prefer a video by a teacher? Find free videos on “how database indexes work b-tree explained full table scan” on YouTube ↗
In real life: Checking a PNR in an instant
A railway booking system holds crores of bookings. When you type a 10-digit PNR, the app runs a query like SELECT ... WHERE pnr = ?. Because pnr is the key (or has a unique index), the database walks a B-tree a few levels deep and returns your booking in milliseconds, whether the table holds one lakh rows or fifty crore. Without that index, every PNR check would read the whole table.
CREATE UNIQUE INDEX idx_bookings_pnr ON bookings(pnr);
SELECT train_no, journey_date, status FROM bookings WHERE pnr = '4512367890';Practice (0/4 done)
Create an index named idx_orders_customer on orders(customer_id). Then list every index in the database: return name and tbl_name from sqlite_master where type = 'index', sorted by name.
Shoppers filter by category all day. Create an index named idx_products_category on products(category), then write the query it helps: return name and price of products in the 'Kitchen' category.
Create idx_orders_status on orders(status) and idx_orders_date on orders(order_date). status has only four values, so the status index is not selective: drop it. Finish by returning the name of every index on the orders table (from sqlite_master, type = 'index' and tbl_name = 'orders').
An app's users table has 2 crore rows. Which single-column index will help the most?
Quick quiz
Read more (free)
Related help
Before this, learn
This topic builds on these. If something here feels new, open them first.
Related topics
- Composite, covering, unique and partial indexesIndexes, query plans and views · same module
- Reading query plansIndexes, query plans and views · same module
- Writing fast queriesIndexes, query plans and views · same module
- Indexes: making queries fastBackend Development · SQL in depth
- Joins: combining tablesBackend Development · SQL in depth
- INSERT, UPDATE and DELETEBackend Development · Databases and SQL basics
Questions people ask about this
Open a question to see the answer and why. The first ones are this topic's own quiz, so try the quiz above before you peek.
What is a full table scan?
Answer: Reading every row of a table to find the matches
Without a usable index, the database must look at every row.
Why can a B-tree find a value among crores of rows in a few steps?
Answer: It is sorted, balanced and wide, so each level narrows the search a lot
Each page splits the range into hundreds of parts, so a handful of levels is enough.
Which column gets an index automatically in SQLite and PostgreSQL?
Answer: A column with a UNIQUE constraint
UNIQUE and PRIMARY KEY are enforced with an index. Foreign keys are not indexed automatically.
A table gets 5,000 inserts a second and has 12 indexes. What is the likely problem?
Answer: Every insert must also update 12 indexes, slowing writes
Each index is extra work on every write. Drop the ones no query uses.
After CREATE INDEX idx_orders_customer ON orders(customer_id), how must you change WHERE customer_id = 1?
Answer: Nothing; the planner uses the index on its own
SQL is declarative; the planner decides to use the index.
An index exists on (customer_id, order_date). Which WHERE clause can NOT use it well?Related
Answer: order_date > '2026-01-01'
A composite index is sorted by its first column first. Without customer_id, the database cannot jump into it.
Which SQLite plan line means the index alone answered the query?Related
Answer: SEARCH orders USING COVERING INDEX idx_x (customer_id=?)
COVERING means every needed column was in the index.
From Reading query plans
A unique index on email exists. How many rows with a NULL email can SQLite or PostgreSQL store?Related
Answer: As many as you like
NULLs are not equal to each other, so they do not clash. SQL Server differs and allows one.
In SQLite, you write INSERT INTO tickets (customer_id, subject, status) VALUES (3, 'Hi', DEFAULT). What happens?Related
Answer: A syntax error: SQLite does not accept DEFAULT in VALUES
Leave the column out instead. PostgreSQL, MySQL and SQL Server do accept DEFAULT there.
From INSERT
Pass the quiz (60% or more) or mark this topic as done when you are ready.
Your progress is saved on this device only.
Next steps
- 4 more topics in “Indexes, query plans and views” before its exam: Composite, covering, unique and partial indexes, Reading query plans, Writing fast queries, Views and materialized views.
- Module 10 exam: Indexes, query plans and views · 13 questions · 25 min, without help. 5 topics of this module still to finish first.
- Then the next module, Procedures, triggers, SQL engines and NoSQL. 62 topics left in the course before the final exam.