---
topic: system-design
author: Crashtech Editorial
date: Sep 1, 2026 · read: 6 min
---

Database Indexing Explained: Which Index Type Actually Fits Your Query

B-tree, hash, composite, and covering indexes each solve a different query shape. Pick the wrong one and you pay index overhead without the speedup.

When a query slows down, the instinct is to look at application code first — the ORM call, the ID lookup, the loop that fetches records one at a time. But just as often, the real bottleneck sits at the storage layer: the database is scanning millions of rows to answer a query that should only ever touch a handful of them. The fix is usually an index — but “add an index” is not a single move. Different index types solve genuinely different query shapes, and picking the wrong one means paying the storage and write cost of indexing without getting the read speedup you were after.

What an index actually does

Without an index, a database answering WHERE email = '[email protected]' has exactly one option: read every row, check the email column, keep going until it’s checked them all. This is a table scan, and its cost scales linearly with table size — fine at a thousand rows, unusable at a hundred million.

An index is a separate, precomputed data structure — physically sorted or hashed by the indexed column — that lets the database jump close to the matching rows directly. The database maintains this structure automatically on every write, trading write cost for read speed. That trade-off, not “indexes are free performance,” is the entire mental model worth keeping.

B-tree: the default for a reason

Most relational databases default to a B-tree (or one of its variants, like B+ trees) as the general-purpose index structure. A B-tree keeps keys sorted in a balanced tree, so both exact lookups and range queries traverse the tree in logarithmic time — O(log n) instead of O(n).

What B-trees are good at default choice
Exact matches (WHERE id = 5), range queries (WHERE age BETWEEN 20 AND 40), sorted output (ORDER BY without a separate sort step), and prefix matches on strings (WHERE name LIKE ‘Sm%’).
What they cost the trade-off
Every insert, update, or delete touching the indexed column must keep the tree balanced — write overhead that scales with how many indexes exist on that table.

Because a B-tree handles both exact matches and ranges reasonably well, it’s the correct default whenever you’re unsure which query pattern will dominate. Reach for something more specialized only when you know the access pattern well enough to exploit it.

Hash indexes: faster, but narrower

A hash index computes a hash of the key and jumps directly to the matching bucket — no tree traversal at all. For pure exact-match lookups, this beats a B-tree.

The catch: hashing destroys ordering. A hash index cannot serve a range query (WHERE age > 30) or provide sorted output, because rows with adjacent values don’t land in adjacent buckets. Use a hash index only when you’re certain the access pattern is exclusively exact-match — a session-token lookup table is a good candidate; a timestamp column you’ll also want to range-query is not.

The gut-check for choosing between them

Ask one question: will this column ever be queried with a range, a sort, or a prefix match? If yes, use a B-tree. If the answer is genuinely always “no” — pure key lookups only — a hash index is the faster, narrower tool.

Advertisement

Composite indexes: column order is the whole design decision

A composite (multi-column) index covers several columns together, e.g. (last_name, first_name). It’s physically sorted by the first column, then the second within each group of matching first-column values — like a phone book sorted by last name, then first name within each last name.

This has a direct consequence: the index can serve a query filtering on last_name alone, or on last_name and first_name together, but cannot efficiently serve a query filtering on first_name alone — the index isn’t sorted that way, so the database would have to scan the whole structure anyway.

Do

Order composite index columns to match your actual query patterns, leading with the column most queries filter on regardless of whether other columns are present.

Don't

Assume a composite index on (A, B) helps a query that filters only on B. It won’t — build a separate index on B if that access pattern is common too.

Covering indexes: skip the table entirely

Normally, an index gets the database close to the matching rows, but then it still has to fetch the full row from the table to read any column not in the index. A covering index eliminates that extra step by including every column the query needs directly in the index itself — the database answers the entire query from the index alone, never touching the table.

This removes a real cost (one extra disk/page lookup per matching row) but isn’t free to adopt everywhere: a covering index is larger than a narrow one, since it’s carrying extra columns, and it pays a larger write-update cost on every insert or update. It’s a targeted optimization for specific hot queries, not a default to apply to every index in a schema.

The cost side nobody skips past fast enough

Every index type shares the same tax: it must be updated on every write that touches its columns. A table with ten indexes pays that update cost ten separate times on every insert. This is why “just add an index” is not a universally safe move on a write-heavy table — the right question is always which specific query patterns are slow enough to justify the write-side cost of speeding them up, not “which columns could theoretically benefit.”

Takeaway

Indexing isn’t a single lever — it’s a small decision tree matched to query shape. B-tree for the general case, especially anything involving ranges or sorting. Hash indexes when the access pattern is provably exact-match-only. Composite indexes when queries filter on multiple columns together, ordered to match the most common filter pattern. Covering indexes when a specific hot query justifies skipping the table lookup entirely. Every one of them costs write throughput to gain read throughput — the job is matching the type, and the decision to add one at all, to queries that are actually slow, not sprinkling indexes on every column that might someday matter.

Advertisement

Frequently asked questions

Why does a database need an index at all — why can't it just scan the table?

A table scan reads every row to find matches, so cost grows linearly with table size — fine for a thousand rows, unusable for a billion. An index is a precomputed structure that lets the database jump close to the matching rows instead of inspecting every one, the same way a book's index lets you skip straight to a page instead of reading cover to cover.

When is a hash index better than a B-tree index?

A hash index is faster for exact-match lookups (WHERE id = 5) because it computes a hash and jumps directly to the bucket — no tree traversal. But it can't serve range queries (WHERE age > 30) or sorted output at all, since hashing destroys ordering. B-trees are slightly slower for exact matches but handle both exact matches and ranges, which is why they're the default in most relational databases.

What is a composite index and why does column order matter?

A composite index covers multiple columns together, like (last_name, first_name). It can serve queries filtering on last_name alone, or on both columns together — but not on first_name alone, because the index is physically sorted by the first column first. Column order should match your most common filter patterns, leading with the column most queries filter on.

What's a covering index and why is it faster than a normal index?

A covering index includes every column a query needs, directly in the index itself. The database can answer the query by reading only the index and never touching the actual table rows — skipping an extra disk lookup per matching row. The trade-off is a larger index that costs more to store and update on every write.

What's the real cost of adding an index, beyond disk space?

Every index must be updated on every INSERT, UPDATE, or DELETE that touches its columns, so more indexes mean slower writes. A table with ten indexes pays that update cost ten times per write. Indexes are a read/write trade-off, not a free performance upgrade — index the columns your queries actually filter or sort on, not every column that might someday be useful.

/* Comments */