By WebNest Studio Engineering Team
Database and SQL Tutorial
Database Indexes
An index is a separate data structure that a database maintains alongside a table so it can find rows without scanning every single one. Without an index, looking up a row by a value means the database engine reads the entire table from start to end — a "full table scan" — checking every row against your condition. On a table with a few hundred rows this is instant; on a table with fifty million rows it can take minutes.
Most relational databases implement indexes using a B-tree (balanced tree) structure. A B-tree keeps indexed values in sorted order across a tree of pages, so the engine can jump directly to the relevant branch instead of reading everything, in roughly logarithmic time rather than linear time. Some databases also offer hash indexes, full-text indexes, or specialized indexes (like GIN/GiST in PostgreSQL for JSON or geometric data), but the B-tree is the default and most common case you will encounter.
Why it matters
A query that takes ten seconds on a large table can take ten milliseconds with the right index. An index is the first thing to check when a query is slow, and the most common fix. It is not free, though: every index slows down writes, so knowing where to add one, and where not to, is a core skill.
How a B-Tree Index Speeds Up Reads
Think of a B-tree index like the index at the back of a textbook: instead of reading every page to find "transactions", you look up "T" in the index and jump straight to the right pages. A database B-tree stores indexed column values in sorted order, with each node pointing to a range of child nodes. Searching for a value means comparing against a handful of nodes as you descend the tree, rather than comparing against every row. This is why an indexed lookup on a huge table can return in milliseconds while an unindexed lookup on the same table might take seconds or longer.
The Write-Side Cost
Indexes are not free. Every time you INSERT, UPDATE, or DELETE a row, the database must also update every index that covers the changed columns, to keep the index's sorted structure consistent with the table's actual data. A table with five indexes means every write potentially touches six data structures instead of one. This is why indexing is a trade-off, not something to apply blindly to every column.
- Reads become faster — the engine can locate matching rows without scanning the whole table.
- Writes become slower — every INSERT/UPDATE/DELETE must also maintain each affected index.
- Storage grows — an index is extra data stored on disk, separate from the table itself.
When to Add an Index
Add an index on columns that are frequently used in WHERE clauses, JOIN conditions, ORDER BY clauses, or as foreign keys — especially on large, read-heavy tables. Avoid indexing columns that are rarely queried, columns on small tables (a full scan of 200 rows is already fast), or columns on tables with very heavy write traffic where the index-maintenance cost would outweigh the read benefit. Primary keys are indexed automatically by virtually every database; the real decision is about secondary indexes on other columns.
Examples
Creating an index on a frequently filtered column
-- A users table with 5 million rows, frequently filtered by email during login
CREATE INDEX idx_users_email ON users (email);
-- This query now uses the index instead of scanning all 5 million rows
SELECT id, name, email
FROM users
WHERE email = 'asha@example.com';
id: 348221 | name: Asha Verma | email: asha@example.com
(1 row returned in ~2ms using idx_users_email, versus ~800ms with a full table scan)
A composite index supporting a common two-column filter
-- Orders are frequently queried by customer_id and then filtered by status
CREATE INDEX idx_orders_customer_status ON orders (customer_id, status);
SELECT id, total_amount
FROM orders
WHERE customer_id = 1029 AND status = 'PENDING';
id: 88213 | total_amount: 149.99
id: 88450 | total_amount: 39.50
(2 rows returned quickly via idx_orders_customer_status)
Common Mistakes
- Adding an index to every column "just in case," which slows down every write and wastes storage without a matching read benefit.
- Expecting an index to help a query that filters on a function of the column (e.g. WHERE YEAR(created_at) = 2026) — a plain index cannot be used unless it is built to match the expression.
- Forgetting that column order matters in a composite (multi-column) index — an index on (customer_id, status) helps queries filtering by customer_id alone or by both columns, but does not help a query that filters only by status.
- Not indexing foreign key columns, which makes JOINs between related tables slow as both tables grow.
Exercise
Orders are looked up by customer constantly. Create an index named idx_orders_customer on customer_id, and a composite index named idx_orders_customer_status on customer_id and status. Then run the query the indexes are meant to serve: the ids of customer 1's paid orders, in order.
Expected output: id, 1, 3 (one per line)
CREATE TABLE orders (id INTEGER PRIMARY KEY, customer_id INTEGER, status TEXT);
INSERT INTO orders (id, customer_id, status) VALUES
(1, 1, 'paid'),
(2, 2, 'paid'),
(3, 1, 'paid'),
(4, 1, 'pending');
-- TODO: index on customer_id
-- TODO: composite index on customer_id and status
-- TODO: ids of customer 1's paid orders, ordered by id
Show hints
- The syntax is
CREATE INDEX index_name ON table_name (column). - A composite index lists its columns in order, separated by commas.
Show solution
CREATE TABLE orders (id INTEGER PRIMARY KEY, customer_id INTEGER, status TEXT);
INSERT INTO orders (id, customer_id, status) VALUES
(1, 1, 'paid'),
(2, 2, 'paid'),
(3, 1, 'paid'),
(4, 1, 'pending');
CREATE INDEX idx_orders_customer ON orders (customer_id);
CREATE INDEX idx_orders_customer_status ON orders (customer_id, status);
SELECT id
FROM orders
WHERE customer_id = 1 AND status = 'paid'
ORDER BY id;
Quick quiz
1. What is the cost of adding an index?
- A. Reads become slower
- B. The table can no longer be updated
- C. Queries must be rewritten
- D. Inserts, updates and deletes become slower, and the index takes disk space
Show answer
D. Every write has to update each index on the table as well.
2. A composite index is on (last_name, first_name). Which filter can use it efficiently?
- A. WHERE first_name = 'Asha'
- B. WHERE last_name = 'Rao'
- C. WHERE age = 30
- D. None of them
Show answer
B. A composite index is usable from its leftmost column onwards.
3. Which data structure do most database indexes use?
- A. A B-tree
- B. A linked list
- C. A stack
- D. A plain array
Show answer
A. It keeps the keys sorted and finds a value in a small number of steps, even in a huge table.
Interview questions
What is an index and how does it speed up a query?
Show answer
An index is a separate, sorted structure, usually a B-tree, that maps the values of one or more columns to the locations of the matching rows. Without it, the database reads every row of the table to find matches, which is a full table scan. With it, the database walks the tree to the value in a number of steps proportional to the logarithm of the table size and reads only the matching rows.
Which columns should be indexed, and which should not?
Show answer
Index columns that appear often in WHERE conditions, join conditions and ORDER BY, and foreign key columns. Columns with many distinct values benefit most. Avoid indexing columns with very few distinct values, such as a boolean, small tables, and columns that are rarely used for searching. Tables that are written far more than they are read should have few indexes, since each one slows every write.
Key Points to Remember
- An index is a separate sorted structure (typically a B-tree) that lets the database find rows without scanning the whole table.
- Indexes speed up reads but slow down writes and use extra storage — every index must be maintained on INSERT, UPDATE, and DELETE.
- Add indexes to columns used in WHERE, JOIN, and ORDER BY on large, read-heavy tables; avoid over-indexing small or write-heavy tables.
- Composite index column order matters — it supports queries that filter on a matching left-to-right prefix of the indexed columns.
Practice in the playground
Write and run your own code for this topic, or start from the exercise or an example. Runs your SQL on SQLite in your browser. Standard SQL works; a few features that exist only in PostgreSQL or MySQL do not. Your code for each topic is kept in this browser.
Run the examples
Change an input, predict the result, then compare it with the output. Explain why the result changes.
These examples need a project environment on your computer, so they can be copied but not run here. Codelab runs Python, FastAPI backends with SQLite, plain Java 17 and HTML/CSS/JavaScript.
Written by
WebNest Studio Engineering Team
Practical lessons from developers working with Java, Python, React, databases and production systems.
Led by Vansh Duggal, Founder, WebNest Studio · Full-stack & software engineering