How an Index Works (B-tree, full scan vs index scan)
How an Index Works
This is the first page in the Databases section. We'll use one running example on every page: a food delivery app (like Swiggy or Zomato) with an orders table that keeps growing.
The Problem: Finding One User's Orders
Our app stores every order in one table. After a year, it has 10 million rows. A row is one record, here one order.
A user opens "My Orders". The app runs this query (a question we ask the database):
SELECT * FROM orders WHERE user_id = 42;
With no extra help, the database has only one option: read every row and check whether user_id is 42. This is called a full scan.
1orders table (10,000,000 rows)2 3Row 1 user_id = 7 ✗4Row 2 user_id = 42 ✓5Row 3 user_id = 913 ✗6...7Row 10,000,000 user_id = 55 ✗8 9→ checked 10,000,000 rows to find 12 ordersIt works with 100 rows. With 10 million rows and thousands of users opening the page every second, the database gets very slow.
So we need a way to jump straight to user 42's orders without reading everything.
The Idea: An Index Is Like a Book's Index
Think of a 1,000-page textbook. To find "B-tree", you don't read every page. You go to the index at the back. It lists topics in alphabetical order with page numbers, so you find the word quickly and jump to the right page.
A database index works the same way:
- It is a separate structure, stored next to the table.
- It keeps the values of one column (like
user_id) sorted. - Each entry stores the value plus a pointer, meaning the address of the full row in the table.
The index is a lookup list. Next, let's see how the database searches that list so fast.
How a B-tree Works
Both PostgreSQL and MongoDB use a B-tree as their default index. A B-tree is a sorted, balanced tree:
- Sorted: values are kept in order, like the book index.
- Balanced: every path from the top to the bottom has the same length, so every search takes about the same number of steps.
The top box is the root. The bottom boxes are leaves, which hold the actual values and pointers. Each box in between says "smaller values go left, bigger values go right".
To find user_id = 42, the database starts at the root and goes down one level at a time, choosing a direction at each step. It never looks at the other branches.
How many steps for 10 million rows?
In a real database, each box (called a page) holds hundreds of values, not two. Say about 300 per page:
1Level 1 (root): 300 values2Level 2: 300 × 300 = 90,0003Level 3: 300 × 300 × 300 = 27,000,0004 527 million > 10 million6→ only about 3 page reads to find any user_idThat is the meaning of O(log n): as the table grows, the number of steps grows very slowly. Ten times more rows adds roughly one extra step, not ten times more work.
Because the values are sorted, a B-tree also helps with more than exact matches:
| Query type | Example | Why the B-tree helps |
|---|---|---|
| Equality | user_id = 42 | Walk down to one spot |
| Range | created_at > '2026-10-01' | Find the start, then read forward in order |
| Sorting | ORDER BY created_at | Data is already in order, no extra sort needed |
Now let's compare the two ways the database can run our query.
Full Scan vs Index Scan
| Full Scan | Index Scan | |
|---|---|---|
| What it does | Reads every row | Walks the B-tree, then reads only matching rows |
| PostgreSQL name | Seq Scan | Index Scan |
| MongoDB name | COLLSCAN | IXSCAN |
| Rows checked (our example) | 10,000,000 | about 12 |
| Speed on big tables | Slow | Fast |
| Best when | Table is tiny, or you need most rows | You need a few rows from a big table |
1Full scan: Query → read row 1 → row 2 → ... → row 10,000,000 → 12 matches2Index scan: Query → B-tree root → middle → leaf → 12 pointers → 12 rowsA full scan is not always bad. If a query needs 80% of the table, reading everything in order can be faster than jumping around through pointers. The database's query planner (the part that decides how to run a query) makes this choice for you.
Next, let's create the index and prove the database actually uses it.
Creating and Checking an Index
Create an index on user_id:
CREATE INDEX idx_orders_user_id ON orders (user_id);
Then ask the database how it runs the query. EXPLAIN ANALYZE (PostgreSQL) and explain("executionStats") (MongoDB) show the plan and real timing:
EXPLAIN ANALYZE SELECT * FROM orders WHERE user_id = 42;
Example output before and after (numbers are illustrative):
1BEFORE (no index)2 PostgreSQL: Seq Scan on orders3 Rows Removed by Filter: 99999884 Execution Time: ~800 ms5 MongoDB: stage: COLLSCAN6 totalDocsExamined: 10000000, nReturned: 127 8AFTER (with index)9 PostgreSQL: Index Scan using idx_orders_user_id on orders10 Execution Time: ~0.1 ms11 MongoDB: stage: IXSCAN12 totalDocsExamined: 12, nReturned: 12What to look for: in MongoDB, compare documents examined with documents returned. If the database examines 10 million to return 12, you are missing an index.
Indexes make reads fast, but they are not free. Let's see what they cost.
The Cost of an Index
An index is a second copy of some data, kept in sorted order. That has two costs:
- Storage: the index takes extra disk space and memory (RAM).
- Slower writes: every
INSERT,UPDATE, orDELETEmust also update every index on that table, so the sorted order stays correct.
1New order placed:2 write row to orders table3 + update idx_orders_user_id4 + update idx_orders_restaurant_id5 + update idx_orders_created_at6→ 1 write becomes 4 writes| Few indexes | Many indexes | |
|---|---|---|
| Reads | Slower for unindexed queries | Fast for many query types |
| Writes | Fast | Slower |
| Storage | Small | Large |
| Choose when | Write-heavy (logs, events) | Read-heavy (user-facing pages) |
Rule of thumb: index the columns your important queries filter or sort on, and nothing more.
Now let's put this into words you can say in an interview.
Interview Angle
How to Explain it in an Interview
Common mistakes:
- Saying "indexes make everything faster". They make reads faster and writes slower.
- Forgetting that the planner may still choose a full scan, for example on a tiny table or when most rows match.
- Not mentioning how to verify with
EXPLAIN ANALYZEorexplain().
Summary
1No index → full scan (Seq Scan / COLLSCAN) → reads every row → slow2Index → sorted B-tree + pointers to rows3B-tree lookup → O(log n) → ~3 page reads for 10M rows4Also helps → ranges (>, <, BETWEEN) and ORDER BY5Cost → extra storage + every write updates every index6Verify → EXPLAIN ANALYZE / explain("executionStats")Our orders table is fast to search by user now, but "My Orders" also needs to filter by status and sort by date. Next, we'll look at composite indexes and why the order of columns matters.