10.14 Indexes
Table of Contents
Why Indexes Matter in SQL
Indexes are one of the most important tools for database performance. They can make queries hundreds or thousands of times faster, but they can also slow down writes if used incorrectly.
In this chapter you will learn what indexes are, how they work conceptually, and how to use them effectively in SQL, with a focus on relational databases like PostgreSQL and MySQL.
Key idea: An index speeds up reading data, but adds overhead to writing data.
Use indexes to speed up queries, not just because you can.
What Is an Index?
Think of an index in a book. If you want to find the page that mentions "binary trees", you do not read every page. You go to the index at the back of the book, find "binary trees", and it tells you the pages.
A database index plays the same role for tables:
- A table is like the full book, containing all rows.
- An index is like the book index, a smaller structure that helps the database jump directly to relevant rows.
Without an index, the database often needs to scan the whole table to find matching rows. This is called a sequential scan or full table scan.
With an index, the database can often jump directly to the needed rows, using a faster search algorithm, usually something like a balanced tree.
How Indexes Work Conceptually
You do not need to know the exact internal algorithms to use indexes well, but you should understand the basic idea.
Most relational databases use B-tree indexes by default.
Table vs Index
Imagine a simple users table:
CREATE TABLE users (
id SERIAL PRIMARY KEY,
email VARCHAR(255) NOT NULL,
username VARCHAR(50) NOT NULL,
created_at TIMESTAMP NOT NULL
);The data lives in a table like this:
| id | username | created_at | |
|---|---|---|---|
| 1 | a@example.com | alice | 2024-01-01 10:00:00 |
| 2 | b@example.org | bob | 2024-01-02 11:00:00 |
| 3 | charlie@example.io | charlie | 2024-01-03 12:00:00 |
| … | … | … | … |
If you run:
SELECT * FROM users WHERE email = 'b@example.org';- Without an index on
email: the database checks each row, one by one, until it finds the match. - With an index on
email: the database looks upb@example.orgin the index structure, finds the row locations quickly, and jumps to them.
The index is a separate structure, stored by the database engine.
You can imagine an index on email like this:
| row pointer | |
|---|---|
| a@example.com | 1 |
| b@example.org | 2 |
| charlie@example.io | 3 |
| … | … |
The row pointer tells the database where to find the actual row in the table.
Creating and Dropping Indexes
You usually create indexes explicitly, except for primary keys and some unique constraints that create them automatically.
Creating a Simple Index
Syntax:
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 = 'bob@example.org';The database can choose to use the index if it thinks it is faster.
Dropping an Index
If an index is unused or harmful to performance, you can remove it:
DROP INDEX idx_users_email;In some databases, like MySQL, the index name is scoped to the table, and you often use:
ALTER TABLE users DROP INDEX idx_users_email;Always check your specific database syntax, but the concept is the same.
Primary Keys, Unique Constraints, and Indexes
Some constraints automatically create indexes.
Primary Key
When you define a primary key:
CREATE TABLE users (
id SERIAL PRIMARY KEY,
...
);
The database automatically creates an index on id. This index:
- Ensures
idvalues are unique. - Makes lookups by
idvery fast.
You rarely need to create an extra index on a primary key column, because it already has one.
Unique Constraint
If you define a unique constraint:
ALTER TABLE users ADD CONSTRAINT users_email_key UNIQUE (email);The database creates a unique index under the hood.
A unique index:
- Ensures no two rows can have the same value in the indexed column.
- Speeds up lookups by that column.
Rule: A PRIMARY KEY or UNIQUE constraint already has an index. Do not create a second index on the same column set.
When Indexes Help
Indexes are useful when queries frequently:
- Filter rows with
WHEREon a column or columns. - Join tables on specific columns.
- Order results with
ORDER BY. - Enforce uniqueness of a column or combination of columns.
Simple Filter Example
Table:
CREATE TABLE orders (
id SERIAL PRIMARY KEY,
user_id INT NOT NULL,
status VARCHAR(20) NOT NULL,
created_at TIMESTAMP NOT NULL
);Query:
SELECT *
FROM orders
WHERE user_id = 42;
If you frequently query by user_id, create an index:
CREATE INDEX idx_orders_user_id ON orders (user_id);Now the database can quickly find all orders for one user.
Multiple Filters Example
Query:
SELECT *
FROM orders
WHERE user_id = 42
AND status = 'PAID';You might add an index on both columns:
CREATE INDEX idx_orders_user_status ON orders (user_id, status);This can be much faster than scanning the entire table.
When Indexes Do Not Help Much
Indexes are not magic. They do not help in every scenario.
Common cases where indexes are less useful:
- Very small tables (for example 50 rows). A full table scan is often cheap.
- A column with very few distinct values, such as a boolean (
is_deleted), especially if most rows share the same value. - Queries that do not filter or sort by the indexed columns.
- Operations that need almost all rows anyway.
Example: Low Selectivity
Suppose you have:
CREATE TABLE logs (
id SERIAL PRIMARY KEY,
level VARCHAR(10) NOT NULL, -- 'INFO', 'WARN', 'ERROR'
message TEXT NOT NULL,
created_at TIMESTAMP NOT NULL
);
If 95 percent of rows have level = 'INFO', the query
SELECT * FROM logs WHERE level = 'INFO';
might not benefit from an index on level. The database engine might decide it is faster to scan the table rather than use the index, because almost all rows match.
The concept here is selectivity. A highly selective column has many distinct values relative to the number of rows, so each value matches only a few rows. High selectivity is good for indexing.
How Indexes Affect Writes
Indexes speed up reads but slow down writes.
Whenever you:
INSERTa row,UPDATEan indexed column,DELETEa row,
the database must also update every index that involves those columns.
Example
If orders has these indexes:
CREATE INDEX idx_orders_user_id ON orders (user_id);
CREATE INDEX idx_orders_status ON orders (status);
CREATE INDEX idx_orders_user_status ON orders (user_id, status);And you run:
INSERT INTO orders (user_id, status, created_at)
VALUES (42, 'PAID', NOW());The database will:
- Write the row to the table.
- Add entries to all three indexes.
More indexes mean more work per write, which means slower inserts and updates.
Rule: Every index has a cost on INSERT, UPDATE, and DELETE.
Create indexes only when they solve a real query performance problem.
Types of Indexes (Conceptual Overview)
Different databases support different index types. As a beginner you mainly need to recognize these concepts.
Single Column Index
Indexes a single column.
CREATE INDEX idx_users_username ON users (username);Useful for queries like:
SELECT * FROM users WHERE username = 'alice';Composite (Multi-column) Index
Indexes multiple columns in a specific order.
CREATE INDEX idx_orders_user_status
ON orders (user_id, status);This index can help with:
SELECT * FROM orders
WHERE user_id = 42 AND status = 'PAID';and often with:
SELECT * FROM orders
WHERE user_id = 42;but not with:
SELECT * FROM orders
WHERE status = 'PAID';This is because of how most B-tree composite indexes work: they are most useful if the query filters from left to right in the index definition.
A simple analogy:
- Index on
(user_id, status)is ordered like a phonebook by user, then within each user by status. - You can quickly find "all statuses for user 42" (filtering by
user_id) or "all orders for user 42 with status PAID". - But you cannot as easily find "all users with status PAID" without scanning many entries.
Unique Index
Ensures no duplicate values for the indexed column or column combination.
Example, unique username:
CREATE UNIQUE INDEX idx_users_username_unique
ON users (username);Or a compound uniqueness, such as one email per user and provider:
CREATE UNIQUE INDEX idx_social_accounts_user_provider
ON social_accounts (user_id, provider);
Now the pair (user_id, provider) must be unique.
Indexes for Sorting
Indexes can help with ORDER BY queries.
Example:
CREATE INDEX idx_orders_created_at
ON orders (created_at);This can make:
SELECT * FROM orders
ORDER BY created_at DESC
LIMIT 20;much faster, especially on large tables.
Many databases can use a single index for both filtering and ordering, for example:
CREATE INDEX idx_orders_user_created
ON orders (user_id, created_at);can help queries like:
SELECT *
FROM orders
WHERE user_id = 42
ORDER BY created_at DESC
LIMIT 10;Choosing Good Indexes for Queries
You should create indexes based on the queries your application actually runs.
Typical Workflow
- Write your queries.
- Run them on realistic data.
- Find slow queries using database tools.
- Inspect query plans (for example
EXPLAIN). - Add or adjust indexes for those queries.
- Test the performance again.
Example: Finding Orders for One User
Query:
SELECT *
FROM orders
WHERE user_id = 42
ORDER BY created_at DESC
LIMIT 20;Good index:
CREATE INDEX idx_orders_user_created
ON orders (user_id, created_at DESC);Why:
user_idis in theWHERE.created_atis in theORDER BY.- The index order matches the query needs.
Example: Enforcing Unique Emails and Fast Lookups
User table:
CREATE TABLE users (
id SERIAL PRIMARY KEY,
email VARCHAR(255) NOT NULL,
...
);Requirement:
- Email must be unique.
- Most lookups by email.
Use a unique index, typically via a constraint:
ALTER TABLE users
ADD CONSTRAINT users_email_unique UNIQUE (email);This gives you both uniqueness and fast lookups.
Examples of Helpful vs Useless Indexes
Helpful Index Example
Query:
SELECT *
FROM posts
WHERE published = TRUE
AND author_id = 10
ORDER BY created_at DESC
LIMIT 20;Helpful index:
CREATE INDEX idx_posts_author_published_created
ON posts (author_id, published, created_at DESC);How it helps:
- Filters by
author_idandpublished. - Orders by
created_at. - Index matches the sequence used in the query.
Useless or Redundant Index Example
Table:
CREATE TABLE users (
id SERIAL PRIMARY KEY,
email VARCHAR(255) UNIQUE,
...
);Bad idea:
CREATE INDEX idx_users_email ON users (email); -- Redundant
The UNIQUE constraint already created an index on email. The new index just doubles the write cost without benefit.
Another bad pattern:
CREATE INDEX idx_users_email ON users (email);
CREATE INDEX idx_users_email_id ON users (email, id);The second index might be useful for a more specific query, but you should check carefully whether you actually need both.
Practical SQL Examples
Assume you have this schema:
CREATE TABLE products (
id SERIAL PRIMARY KEY,
name VARCHAR(255) NOT NULL,
category_id INT NOT NULL,
price NUMERIC(10, 2) NOT NULL,
available BOOLEAN NOT NULL,
created_at TIMESTAMP NOT NULL
);Scenario 1: Filter by Category and Availability
Frequent query:
SELECT *
FROM products
WHERE category_id = 5
AND available = TRUE
ORDER BY created_at DESC
LIMIT 50;Good index:
CREATE INDEX idx_products_category_available_created
ON products (category_id, available, created_at DESC);Reason:
category_idthenavailablein theWHERE.created_atin theORDER BY.- The database can use the index to filter and sort at the same time.
Scenario 2: Search by Name Prefix
Query:
SELECT *
FROM products
WHERE name LIKE 'iphone%';
For basic LIKE 'prefix%' searches, a normal index on name can help:
CREATE INDEX idx_products_name ON products (name);But note:
- For
LIKE '%phone%'(wildcard at the start), a simple B-tree index is usually not used effectively. - Full text search or special index types might be better, but those are more advanced topics.
Scenario 3: Join by Foreign Key
Schema:
CREATE TABLE categories (
id SERIAL PRIMARY KEY,
name VARCHAR(100) NOT NULL
);
CREATE TABLE products (
id SERIAL PRIMARY KEY,
category_id INT NOT NULL REFERENCES categories(id),
...
);Query:
SELECT p.*, c.name AS category_name
FROM products p
JOIN categories c ON p.category_id = c.id
WHERE c.name = 'Electronics';Indexes that help:
categories.idalready has an index because it is a primary key.- To speed up
WHERE c.name = 'Electronics', add:
CREATE INDEX idx_categories_name ON categories (name);- To speed up joins from categories to products (for example, find all products in a category), an index on
products.category_idis also useful:
CREATE INDEX idx_products_category_id ON products (category_id);Measuring and Understanding Index Effects
You should verify that an index is actually used and helpful.
Although details vary by database, the usual steps are:
- Use a tool like
EXPLAINorEXPLAIN ANALYZEto see the query plan. - Look for words like
Index ScanorIndex Seekin the plan, instead ofSeq ScanorTable Scan. - Compare execution times before and after creating the index.
Example in PostgreSQL:
EXPLAIN ANALYZE
SELECT *
FROM orders
WHERE user_id = 42
ORDER BY created_at DESC
LIMIT 20;- Before index: you might see
Seq Scan on ordersand a high cost. - After index: you might see
Index Scan using idx_orders_user_createdand a much lower cost.
You do not need to fully understand query plans yet, but you should know that they exist and are the main way to check whether indexes are doing their job.
Summary and Rules of Thumb
Indexes:
- Make queries faster by avoiding full table scans.
- Cost extra work on
INSERT,UPDATE, andDELETE. - Are automatically created for primary keys and unique constraints.
- Are most useful on columns used in
WHERE,JOIN, andORDER BY.
Common rules of thumb:
- Index columns that you filter on frequently.
- Index columns used for joins between tables.
- Use composite indexes for common combinations of filters and sorting.
- Do not create duplicate or unnecessary indexes.
- Test performance changes before and after adding an index.
As you build more queries, you will come back to indexes often. They are one of the main levers for improving SQL performance, and understanding them early will make you a much better backend developer.
Views: 8
KAHIBARO