9.12. Indexes
Table of Contents
Why Indexes Matter
Indexes are one of the most important tools to make database queries fast.
Without indexes, the database usually has to scan every row in a table to answer many queries. This is called a full table scan and becomes very slow when you have thousands or millions of rows.
With an index, the database can quickly jump to the rows you need, similar to how you use the index at the back of a book to find a word without reading every page.
Key idea: Indexes speed up read queries, but they cost extra memory and slow down writes.
You should create indexes on purpose, not randomly.
In this chapter you will learn what indexes are, how they work conceptually, when to create them, and common patterns and pitfalls, with concrete SQL examples.
What Is an Index?
An index is an additional data structure that a database maintains to help find rows quickly.
Imagine a table users:
CREATE TABLE users (
id SERIAL PRIMARY KEY,
email TEXT NOT NULL,
username TEXT NOT NULL,
created_at TIMESTAMP NOT NULL
);If you run:
SELECT * FROM users WHERE email = 'alice@example.com';
Without an index on email, the database:
- Starts at the first row.
- Checks if
email = 'alice@example.com'. - Moves to the next row.
- Repeats until the end.
With an index on email:
CREATE INDEX idx_users_email ON users (email);The database uses the index to directly locate the row(s) with that email, usually in logarithmic time instead of linear time.
Informally:
- Without index: time β proportional to number of rows, $O(n)$.
- With index: time β proportional to $\log n$, $O(\log n)$, often much faster in practice.
The index does not replace the table. It is an extra structure stored on disk (and partly cached in memory) that references table rows.
How Indexes Work Conceptually
Different databases and index types use different internal structures. The most common for general indexing is the B-tree (balanced tree).
You do not need to know tree algorithms in detail, but the idea is:
- The index stores sorted values of the indexed column(s).
- Each value points to the location of the row in the table.
- Because data is sorted, the database can use efficient search algorithms to find a value quickly.
For example, if you index email:
- The index might internally hold entries like:
| row pointer | |
|---|---|
| alice@example.com | row #17 |
| bob@example.org | row #328 |
| charlie@example.net | row #215 |
To find bob@example.org, the database searches inside the tree structure, not the whole table.
You cannot see or change the internal structure of standard indexes.
You interact with indexes only by defining them and letting the database choose how to use them.
Basic Index Syntax
Most SQL databases use similar syntax for creating and dropping indexes.
Creating a simple index
CREATE INDEX index_name ON table_name (column_name);Example:
CREATE INDEX idx_users_email ON users (email);
Now queries that filter by email can use this index:
SELECT * FROM users WHERE email = 'alice@example.com';The database query planner decides when to use the index. You do not call the index directly.
Dropping an index
If an index is not useful or is hurting performance (for writes), you can drop it:
DROP INDEX idx_users_email;The table and its data remain. Only the index is removed.
Be careful when dropping indexes in production. Some queries might become very slow if they depended on that index.
Unique indexes
A unique index enforces that the indexed column combination has no duplicates.
Syntax often looks like:
CREATE UNIQUE INDEX idx_users_email_unique ON users (email);or by using a constraint when creating the table:
CREATE TABLE users (
id SERIAL PRIMARY KEY,
email TEXT NOT NULL UNIQUE
);Unique indexes are especially useful for things like:
- User emails
- Usernames
- API keys
They both speed up lookups and enforce data correctness.
When Indexes Help
Indexes help mainly when your query:
- Filters rows with a
WHEREcondition. - Joins tables with
JOIN ... ON .... - Orders results with
ORDER BY. - Enforces uniqueness.
Speeding up WHERE conditions
If you frequently search by a column, an index can help.
Example table:
CREATE TABLE orders (
id SERIAL PRIMARY KEY,
user_id INT NOT NULL,
status TEXT NOT NULL,
created_at TIMESTAMP NOT NULL
);Common query:
SELECT * FROM orders WHERE user_id = 42;Index:
CREATE INDEX idx_orders_user_id ON orders (user_id);Now, looking up all orders for a user can be very fast.
Another example, filtering by status:
SELECT * FROM orders WHERE status = 'PAID';If this query is common, you might create:
CREATE INDEX idx_orders_status ON orders (status);Speeding up joins
Joins usually compare foreign keys to primary keys or unique columns. Indexes on those columns are critical.
Example:
CREATE TABLE users (
id SERIAL PRIMARY KEY,
...
);
CREATE TABLE orders (
id SERIAL PRIMARY KEY,
user_id INT NOT NULL REFERENCES users(id),
...
);Query:
SELECT *
FROM users u
JOIN orders o ON o.user_id = u.id
WHERE u.id = 42;Indexes that help:
users.idis already indexed because it is a primary key.- Add an index on
orders.user_id:
CREATE INDEX idx_orders_user_id ON orders (user_id);Without this index, the database may have to scan many orders for each user during joins.
Speeding up ORDER BY
If you sort by a column, an index on that column can let the database read rows already in sorted order.
Example:
SELECT * FROM orders ORDER BY created_at DESC LIMIT 20;Helpful index:
CREATE INDEX idx_orders_created_at ON orders (created_at);Now the database can quickly find the latest 20 orders by traversing the index in reverse order, instead of sorting all rows.
Speeding up combined filtering and sorting
Indexes are especially powerful when the same index supports both filtering and sorting.
Example:
SELECT * FROM orders
WHERE user_id = 42
ORDER BY created_at DESC
LIMIT 10;Index:
CREATE INDEX idx_orders_user_created_at
ON orders (user_id, created_at);
This is a multi-column index. It is sorted first by user_id, then by created_at within each user_id. The database can:
- Jump to the section where
user_id = 42. - Read rows already ordered by
created_at.
This can avoid both a full scan and a sort step.
Multi-Column Indexes
A multi-column index covers more than one column.
CREATE INDEX idx_orders_user_status
ON orders (user_id, status);This index is particularly useful for queries that filter on user_id first, then status, such as:
SELECT * FROM orders
WHERE user_id = 42 AND status = 'PAID';Index column order matters
The order of columns in a multi-column index is critical.
For the index:
CREATE INDEX idx_orders_user_status
ON orders (user_id, status);
Most database planners can use it efficiently when your WHERE clauses use:
WHERE user_id = ?WHERE user_id = ? AND status = ?
But often not when you do:
WHERE status = ?only.
In many systems, the index is organized by the first column first. If your query does not filter by the leading column, the index is less useful.
Consider two indexes:
CREATE INDEX idx_orders_user_status ON orders (user_id, status);
CREATE INDEX idx_orders_status_user ON orders (status, user_id);Which one is better depends on your most common queries.
- If most queries are
WHERE user_id = ?orWHERE user_id = ? AND status = ?, use(user_id, status). - If most queries are
WHERE status = ?orWHERE status = ? AND user_id = ?, use(status, user_id).
You rarely need both, because each index has a cost.
Covering queries
A covering index is an index that contains all the columns a query needs, so the database does not need to read the main table rows at all.
Example:
CREATE INDEX idx_orders_user_created_status
ON orders (user_id, created_at, status);Query:
SELECT user_id, created_at, status
FROM orders
WHERE user_id = 42
ORDER BY created_at DESC
LIMIT 10;All three columns are in the index. The database can execute the query just using the index. This can be very fast.
However, adding many columns to indexes increases index size and maintenance cost, so do it only for important queries.
Indexes and Primary / Foreign Keys
Primary keys and many unique constraints create indexes automatically.
Primary keys
When you declare a primary key:
CREATE TABLE users (
id SERIAL PRIMARY KEY,
...
);
The database typically creates a unique index on id behind the scenes. You do not need to create another one on the same column.
You will almost always see:
PRIMARY KEYcolumns are already indexed.
Foreign keys
A foreign key constraint does not always automatically create an index. Many databases recommend you manually index foreign key columns.
Example:
CREATE TABLE orders (
id SERIAL PRIMARY KEY,
user_id INT NOT NULL REFERENCES users(id),
...
);Add an index:
CREATE INDEX idx_orders_user_id ON orders (user_id);This helps for:
- Queries that filter by
user_id. - Joins with
users. - Deleting or updating users, since the database must check related
ordersrows.
Rule of thumb:
Every foreign key column should usually have an index to avoid slow joins and slow deletes/updates.
Costs and Trade-offs of Indexes
Indexes are not free. You must understand their costs.
Extra storage
Each index is stored on disk. If your table has millions of rows and many indexes, index files can become large.
Rough idea:
- If a table is 1 GB and you create 3 indexes, your database storage might become more than 2 or 3 GB, depending on the columns and index types.
Slower writes
Whenever you:
INSERTa rowUPDATEan indexed columnDELETEa row
the database must also update all relevant indexes.
If a table has many indexes, inserts and updates become slower, because every index must be adjusted.
Example:
INSERT INTO users (email, username, created_at)
VALUES ('new@example.com', 'newuser', NOW());
If you have indexes on email, username, and created_at, the database must:
- Insert the row into the table.
- Insert the new
emailinto the email index. - Insert the new
usernameinto the username index. - Insert the new
created_atinto the created_at index.
This is still fast but slower than without indexes. On write-heavy systems, too many indexes can hurt overall performance.
Maintenance complexity
As your application grows, you may need to:
- Add indexes for new queries.
- Remove indexes that are no longer used.
- Tune index choices when performance problems appear.
You should keep track of why each index exists. Many teams document important indexes next to their creation migrations.
Choosing What to Index
You should not index every column. Instead, decide based on how the application queries the data.
Good candidates for indexes
Typical good candidates:
- Primary keys (already indexed).
- Foreign keys (you usually add indexes).
- Columns used often in
WHEREconditions. - Columns used in
JOINconditions. - Columns used frequently in
ORDER BY. - Columns used frequently in
GROUP BY.
Example decisions:
- If you often query:
SELECT * FROM users WHERE email = ?;
Index email.
- If you often query:
SELECT * FROM orders WHERE user_id = ? ORDER BY created_at DESC LIMIT 10;
Index (user_id, created_at) as a multi-column index.
Columns that usually should not be indexed
Some columns are often bad index candidates:
- Columns with very few distinct values, like booleans:
is_deleted,is_active. - Columns that change very frequently, like counters or frequently updated timestamps.
- Large text fields where searches are rare or unstructured, like
descriptionorcontent(unless you use full-text search indexes, which are specialized).
Example:
CREATE TABLE products (
id SERIAL PRIMARY KEY,
name TEXT NOT NULL,
is_active BOOLEAN NOT NULL,
description TEXT
);
Indexing is_active alone is usually not very helpful:
CREATE INDEX idx_products_is_active ON products (is_active);If almost all products are active, the database still has to look at many rows, so the index will not improve much. It may even be slower because it has to read the index and then many table rows.
However, is_active might be useful as part of a multi-column index, for example:
CREATE INDEX idx_products_is_active_name
ON products (is_active, name);if you frequently query:
SELECT * FROM products
WHERE is_active = TRUE
ORDER BY name
LIMIT 50;Using query patterns
Look at your most common queries and design indexes for them. For example, imagine these frequent queries:
-- 1. Find user by email
SELECT * FROM users WHERE email = ?;
-- 2. Recent orders of a user
SELECT * FROM orders
WHERE user_id = ?
ORDER BY created_at DESC
LIMIT 20;
-- 3. Admin list of paid orders, newest first
SELECT * FROM orders
WHERE status = 'PAID'
ORDER BY created_at DESC
LIMIT 100;Good index plan:
CREATE UNIQUE INDEX idx_users_email ON users (email);
CREATE INDEX idx_orders_user_created
ON orders (user_id, created_at DESC);
CREATE INDEX idx_orders_status_created
ON orders (status, created_at DESC);Using EXPLAIN to See Index Usage
Most databases provide an EXPLAIN command that shows how a query will be executed. This is very useful to check if your indexes are being used.
Example (syntax varies slightly by database, here is a generic style):
EXPLAIN
SELECT * FROM users
WHERE email = 'alice@example.com';A simplified result might say something like:
Index Scan using idx_users_email on users- or
Seq Scan on users(sequential scan, means no index used)
If you see sequential scan on a big table for an important query, you likely need a better index.
You do not need to memorize EXPLAIN output, but you should know that:
- It shows whether the query uses an index.
- It estimates how many rows will be read.
- It is a main tool for debugging slow queries.
Indexing Patterns and Examples
Here are some common scenarios and example indexes you can use.
Pattern 1: Login by username or email
Table:
CREATE TABLE users (
id SERIAL PRIMARY KEY,
email TEXT NOT NULL UNIQUE,
username TEXT NOT NULL UNIQUE,
password TEXT NOT NULL,
created_at TIMESTAMP NOT NULL
);Queries:
SELECT * FROM users WHERE email = ?;
SELECT * FROM users WHERE username = ?;Indexes:
- Unique index on
email(via constraint). - Unique index on
username(via constraint).
Both queries become fast.
Pattern 2: Activity feed for a user
Table:
CREATE TABLE posts (
id SERIAL PRIMARY KEY,
user_id INT NOT NULL,
body TEXT NOT NULL,
created_at TIMESTAMP NOT NULL
);Query:
SELECT * FROM posts
WHERE user_id = ?
ORDER BY created_at DESC
LIMIT 20;Index:
CREATE INDEX idx_posts_user_created
ON posts (user_id, created_at DESC);
This index covers exactly the WHERE and ORDER BY, and the database can usually avoid extra sorting.
Pattern 3: Search by prefix
Simple B-tree indexes can help with prefix matches like WHERE name LIKE 'Ali%'.
Table:
CREATE TABLE customers (
id SERIAL PRIMARY KEY,
name TEXT NOT NULL
);Query:
SELECT * FROM customers
WHERE name LIKE 'Ali%';Index:
CREATE INDEX idx_customers_name ON customers (name);
Because the prefix is fixed at the start of the string, the index can be used.
But for a pattern like '%ali%', a normal index is not very helpful, you may need full-text or special indexes, which belong to more advanced topics.
Pattern 4: Soft deletes
Many applications have a column deleted_at or is_deleted to implement soft deletes.
Table:
CREATE TABLE users (
id SERIAL PRIMARY KEY,
email TEXT NOT NULL UNIQUE,
deleted_at TIMESTAMP NULL
);Common queries:
SELECT * FROM users
WHERE email = ? AND deleted_at IS NULL;Index:
CREATE INDEX idx_users_email_not_deleted
ON users (email)
WHERE deleted_at IS NULL; -- partial index (not available in every DB)
A partial index only includes rows that match the condition. It can be smaller and faster and is especially useful when most rows are deleted or inactive.
Partial indexes are database-specific features, so details may differ, but the idea is the same.
Common Mistakes With Indexes
Beginners often make these mistakes:
1. Indexing everything
- Creating an index on every column, "just in case".
- Result: large database size, slow inserts and updates, complicated maintenance.
Better approach:
- Start with primary keys and foreign keys.
- Add indexes only when a query needs them.
2. Ignoring multi-column indexes
- Creating separate indexes:
CREATE INDEX idx_orders_user_id ON orders (user_id);
CREATE INDEX idx_orders_created_at ON orders (created_at);- But the main query is:
SELECT * FROM orders
WHERE user_id = ?
ORDER BY created_at DESC;In many databases, the separate indexes will not help as much as a combined one:
CREATE INDEX idx_orders_user_created
ON orders (user_id, created_at DESC);3. Wrong column order
Using an index (status, user_id) while most queries filter WHERE user_id = ? and rarely filter or sort by status.
Correct order:
CREATE INDEX idx_orders_user_status
ON orders (user_id, status);4. Not indexing foreign keys
Forgetting to index user_id in orders:
CREATE TABLE orders (
id SERIAL PRIMARY KEY,
user_id INT NOT NULL REFERENCES users(id),
...
);
Without the index, queries and deletes involving user_id can be slow.
5. Expecting indexes to fix every performance problem
Indexes only help for certain patterns. They cannot fix:
- Very large results (if you always read millions of rows).
- Very slow application logic after the query.
- Poor schema design or missing constraints.
Indexes are one tool, not magic.
Summary
Indexes are essential for fast database queries in backend development. You learned that:
- An index is an extra data structure that helps find rows quickly.
- Indexes speed up reads, but cost storage and slow down writes.
- Primary keys and unique constraints usually create indexes.
- You should index:
- Keys used in joins.
- Columns used often in
WHERE,ORDER BY, andGROUP BY. - Multi-column indexes are powerful, but column order is crucial.
- You can use tools like
EXPLAINto see if a query is using an index. - Avoid indexing everything, and always design indexes based on real query patterns.
With this understanding, you can start making intentional indexing decisions in your backend applications and be better prepared for deeper database tuning topics.
Views: 7
KAHIBARO