Concept lesson · Foundations
Database indexes: B-trees, composite keys and query access
Start here
Definition
A database index is a maintained data structure that maps searchable keys to records or contains the data needed by a query. It can reduce the records inspected for a read, at the cost of extra space and maintenance on writes.
Why it matters: Without a suitable index, finding a few rows can require scanning a large table. The right index organizes keys for the specific filter, order and limit that the application asks for.
The ordered index on (author,title,id) places one author’s books together in title order. Additional table reads depend on which output fields are covered.
Read the diagram step by step
- Sorted entries are Butler/Kindred/12, Butler/Parable/14, Le Guin/A Wizard/11, and Le Guin/The Dispossessed/13.
- A query for Le Guin ordered by title seeks to the first Le Guin entry, then scans the two adjacent keys.
- IDs 11 and 13 identify the base rows. Missing output fields require row fetches; a covering index can still need heap visibility checks, depending on the database and visibility state.
- Filtering title alone cannot generally use the same narrow author-first range. Index writes and bytes are the cost.
Worked example
An author/title index places Le Guin’s books next to each other. A query seeks to Le Guin, reads the entries for IDs 11 and 13 in title order, and fetches base rows when needed for missing fields or visibility checks.
Key takeaways
- Start with the query’s filter, order and limit.
- Composite key order determines which ranges are easy to search.
- Each maintained index adds work to relevant writes.
You will learn to
- Explain how an index narrows a lookup.
- Order a composite index for a concrete filter and sort.
- Show the write, memory, and storage cost of maintaining indexes.
Practice in this chapter
8 interview questions with model answers and follow-ups.
Go to interview practiceUseful foundations: Databases, data models, and ACID transactions
Workload and timing examples are interview assumptions.
Dotted concept links open the relevant explanation in a new tab.
01Database index: definition and tradeoff
A database index is a maintained data structure that maps searchable keys to records or contains data needed by a query. A key is the field or ordered combination of fields used for lookup, such as author and title. Think of a library catalog: to find books by Ursula Le Guin, you consult the author catalog rather than walking past every shelf. The catalog points to books; it is not a second copy of every page inside them.
Suppose our database has Book(id, author, title, publishedYear). Without a useful index, a query for one author may inspect every book row. With an author index, the engine can locate the relevant author entries and then fetch their rows. An index trades extra stored structure and write work for less work on selected reads.
02Worked example: author and title lookup
Consider a Book table containing the following four rows. We want to find Le Guin’s books and return them in title order:
| ID | Author | Title |
|---|---|---|
| 11 | Le Guin | A Wizard of Earthsea |
| 12 | Butler | Kindred |
| 13 | Le Guin | The Dispossessed |
| 14 | Butler | Parable of the Sower |
The query is:
SELECT id, author, title
FROM Book
WHERE author = 'Le Guin'
ORDER BY title, id;
It returns IDs 11 and 13, in that order. Adding id makes the order deterministic if two books have the same title. For this example, assume an ordinary alphabetical collation; the database's configured collation determines the actual text ordering.
A simplified ordered index on (author, title, id) contains (Butler, Kindred, 12), (Butler, Parable..., 14), (Le Guin, A Wizard..., 11), and (Le Guin, The Dispossessed, 13).
- The query asks for
author = 'Le Guin'ordered by title and ID. - The database seeks to the first index entry with that author.
- It scans the adjacent Le Guin entries in title order.
- It reads rows 11 and 13 if the requested output needs fields unavailable from the index.
- It stops when the author changes or the requested limit is met.
A seek navigates directly to a relevant key range; a scan then walks entries. Here the index has converted a whole-table search into a narrow seek and scan. If the table is tiny, a scan may still be cheaper; the query optimizer estimates these costs rather than treating any existing index as mandatory.
Selectivity describes how narrowly a predicate filters records. State the matched fraction to avoid terminology ambiguity: 100 matching rows out of one million is 0.01%, while 900,000 matches is 90%. The first query may avoid much table work with an index; the second may be cheaper as a sequential scan. Physical row placement, cached pages and which columns are returned still affect the decision. The mere existence of an index cannot establish the faster plan.
- 1 → 2seek author rangeQuery: author = Le Guin → Ordered author/title index
- 2 → 3first matching titleOrdered author/title index → Entry: A Wizard of Earthsea → row 11
- 2 → 4next matching titleOrdered author/title index → Entry: The Dispossessed → row 13
- 3 → 5fetch row 11 if neededEntry: A Wizard of Earthsea → row 11 → Book rows
- 4 → 5fetch row 13 if neededEntry: The Dispossessed → row 13 → Book rows
03B-tree, hash and inverted indexes
The book example needs both an author lookup and title ordering. Index structures organize searchable keys differently, so a structure that narrows an exact lookup may not support an ordered range or a word search. Compare the structures by how they reach the candidate records.
A B-tree index keeps search keys in sorted order inside a balanced tree of storage pages. It lets a database find a key without checking every row, and it can scan a consecutive range of keys.
To read the tree below, start with three terms:
- A page is a block of data that the storage engine manages as a unit. One page can contain many keys.
- The root is the entry page. Keys in internal pages act as signposts to the next page. For example, a separator at 40 sends a search for 50 to the side containing keys 40 and above.
- A leaf is a page at the bottom. Balanced means every root-to-leaf path has the same number of levels. The example is a B+ tree, a common B-tree variant: its searchable record entries are in the leaves, which are linked for scans.
A small B+ tree illustrates the B-tree family. Separator keys route searches; record entries are in leaves here. General B-tree variants may also store records in internal nodes.
Remember: Root chooses a range; internal pages narrow it; a leaf finds the entry.
Read the diagram
- The root separator 40 chooses one child page.
- At the internal page containing 60, key 50 selects the child below 60.
- The leaf containing 40 and 50 holds the matching key and record reference.
- All leaves have equal depth. Linked leaves support ranges in this B+ tree example.
An equality query asks for one exact value, such as id = 42. A range query asks for values between bounds, such as years 2000 through 2010. A B-tree can answer both: descend to the first matching key, then follow the ordered entries if more matches are needed.
A hash index applies a hash function to a search key to choose a bucket, a group of candidate entries. Different keys can share a bucket, so the engine still checks which entry actually matches. Hash buckets group by hash value, not by the original key’s order; they do not naturally support scanning consecutive years.
An inverted index maps a term to the documents containing it. Its postings list contains document IDs and may also include counts or positions. For example, green → [D1, D3] means documents D1 and D3 contain “green.” It is called inverted because it goes from term to documents, reversing the document-to-terms view.
Follow each query to the entries it matches. The bucket assignment is illustrative; a real hash function determines it.
Remember: A year range needs order. An exact key needs a match. A search term needs document IDs.
Read the diagram
- Trace three concrete queries to their matching entries.
- B-tree: seek year 2000, then scan ordered entries 2000, 2005 and 2010. The nearby tree diagram shows page routing.
- Hash: key 42 hashes to bucket 2, with candidates 42 and 86. Compare actual keys to select 42.
- Inverted: term green points to postings D1 and D3, whose documents contain green.
Try from memoryWhich of these supports scanning the next ten years in order?
The B-tree. It preserves year order, so it can seek to the first year and scan onward. A hash bucket does not preserve that order; a term postings list answers a different question.
| Query you need to answer | Useful structure | What the engine follows |
|---|---|---|
Find id = 42 |
B-tree or a suitable hash index | Ordered search path, or bucket candidates |
| Find years 2000 through 2010 in order | B-tree | First qualifying key, then ordered entries |
| Find documents containing “green” | Inverted index | The term’s postings list |
| Find one customer’s newest orders | Composite B-tree | Customer group, then timestamp/ID ordering |
Interview tip: start with the query, then justify the index. Equality does not automatically make a hash index better than a B-tree; consider the database’s supported operations, measurements and other query needs.
An index does not necessarily sort the underlying table the same way. Some engines cluster table records around a primary key; others keep index entries separate from table pages. An index-only scan also depends on coverage and visibility rules in the chosen database.
Composite means the key contains several fields; it is not a competing tree algorithm. Covering means the index contains the fields needed by a query; it is not a separate universal storage structure.
04Composite indexes, key order and covering queries
The author/title example used several fields to group related records and order them within a group. Apply the same idea to customer order history: first isolate one customer, then return only that customer’s newest orders. Consider this query:
SELECT orderId, createdAt, total
FROM Orders
WHERE customerId = 'C27'
ORDER BY createdAt DESC, orderId DESC
LIMIT 20;
The leftmost prefix rule says that a composite B-tree index most directly supports lookups using its first column, or its first several columns together. It is a useful starting point for B-tree reasoning, not a universal claim that an engine can never use a later column. Optimizers may use skip scans or combine indexes depending on data distribution and implementation. In an interview, explain why your selected leading columns narrow the work directly, then inspect a plan in a real system.
A covering index includes fields needed by the query, such as total, to reduce row fetches where the engine allows it. The cost is a larger index and more updates when those fields change.
For PostgreSQL, the concrete candidate is:
CREATE INDEX orders_customer_newest
ON Orders (customerId, createdAt DESC, orderId DESC)
INCLUDE (total);
Read down the rows. Customer comes first, timestamp second, and unique order ID breaks timestamp ties.
Remember: Group by the first field; sort inside that group by the next.
Read the diagram
- Follow the contiguous C27 rows and their timestamp/ID ordering.
- C26 precedes C27; C28 follows C27, even if its timestamp is newer.
- Within C27, 10:03 precedes 10:00; at 10:00, O400 precedes O399.
Try from memoryWhy is C28’s 10:09 order below C27’s older orders?
Customer is the first sort field. Timestamps order entries only within each customer group.
The query expression must match the access path too. An ordinary index on email does not provide the same ordered keys as lower(email). PostgreSQL supports an expression index on lower(email) when case-normalized lookup is the intended rule. That normalization has to match the product’s equality semantics; adding an index does not decide which spellings should count as the same address. See expression indexes.
05Index maintenance and write amplification
An index must stay consistent with changes to the records it describes. Write amplification is the additional physical write work created by one logical application change. Index maintenance contributes to that cost because changing one row can require updating several stored structures.
Insert book 15: (Le Guin, The Left Hand of Darkness, 1969). The database writes the row and adds entries to each maintained index: the primary-key index, author/title index, and perhaps a publication-year index. Updates of indexed fields remove or supersede old entries and install new ones; deletes must maintain the indexes too.
The engine also writes recovery logs. Index pages may split, use more cache memory and add disk writes. Ten indexes do not make every read ten times faster; a write affecting all ten must maintain ten extra structures.
| Choice | Read benefit | Cost |
|---|---|---|
| Author index | Find one author's books | Extra entry per book |
| Author/title index | Filter author and return ordered titles | Larger composite key |
| Covering order index | Potentially fewer table fetches | Copies more fields into index |
| Unused index | No observed query benefit | Still consumes writes, space, maintenance |
Measure actual query use before removing an index: a rare month-end report or constraint may still depend on it. An index used to enforce uniqueness is part of correctness as well as read performance.
06Keyset pagination versus OFFSET
Pagination returns a bounded portion of a result instead of every matching row at once. After C27’s newest orders have been returned, the next request needs a continuation rule. An offset skips a count of earlier results; keyset pagination continues after the last ordering key that the client received.
For C27's next page, a cursor can encode the last seen (createdAt, orderId). The next query continues below that tuple in the same ordering. A cursor is a position in a chosen ordering, not necessarily a database transaction kept open between requests.
The first and next pages share one descending ordering. The dashed line is the exclusive cursor boundary, not a snapshot of the database.
Remember: Continue after a tuple, not after a count.
Read the diagram
- First page returns O402 and O400.
- The cursor contains the final timestamp and O400.
- A strict tuple comparison returns O399 and O398, including the timestamp tie.
- Concurrent changes are not frozen unless the design adds snapshot semantics.
In a sharded database, first find the shard holding C27’s orders. A local index finds rows within that shard; it does not tell the client which shard to contact. Global searches need a distributed index or queries to several shards. Explain the API query, shard key and local index together.
For a compact example, use two rows per page instead of twenty. Assume createdAt is non-null and never changes, orderId is unique, and all four orders belong to C27:
| Order ID | Creation time (UTC) | Page |
|---|---|---|
| O402 | 2026-09-22 10:03:00 | First |
| O400 | 2026-09-22 10:00:00 | First; cursor boundary |
| O399 | 2026-09-22 10:00:00 | Second |
| O398 | 2026-09-22 09:58:00 | Second |
After returning O402 and O400, the next PostgreSQL query is:
SELECT orderId, createdAt, total
FROM Orders
WHERE customerId = 'C27'
AND (createdAt, orderId) <
(TIMESTAMPTZ '2026-09-22 10:00:00+00', 'O400')
ORDER BY createdAt DESC, orderId DESC
LIMIT 2;
It returns O399 and O398. The strict tuple comparison handles the timestamp tie without repeating O400 or skipping O399. This assumes createdAt has type timestamptz and the ID comparison orders O399 before O400; use matching types and ordering in the real schema.
An order inserted with a newer timestamp belongs before this boundary and appears when the user refreshes the first page. A backdated insert may appear on a later page. That is why a stable cursor prevents shifts from newer inserts but does not freeze the dataset. See PostgreSQL's LIMIT and OFFSET documentation.
07Interview example: index customer order history
Interviewer: “How would you make customer order history fast?”
Candidate: “The request filters one customer and returns the newest twenty orders. I would use a composite index with customer first, then descending creation time and an order-ID tie breaker. The database seeks into that customer's range and reads a small ordered slice. A cursor carries the last timestamp and ID for the next page. I accept extra index writes and space, and verify the plan and latency using realistic customer sizes.”
This is more useful than saying “add a B-tree”: it explains which keys the tree contains and which work the query avoids.
A query plan describes the operations the database intends to use, such as an index seek, a table scan or a sort. Inspecting that plan tests whether the engine actually uses the access path the design relies on.
To verify the candidate in PostgreSQL, begin with EXPLAIN on the exact SELECT, including its filter, sort and limit. On a representative test workload, EXPLAIN (ANALYZE, BUFFERS) executes that query and reports actual work. Compare estimated and actual row counts, rows discarded by filters, sort work, buffers touched and table fetches. If estimates are poor, inspect statistics and skew before assuming another index is the answer. Repeat for a large customer and for cold versus warm cache conditions, then measure write cost. These observations test why the index helps; an “Index Scan” label alone is not a success criterion. See using EXPLAIN.
Practise the interview questions
Say your answer aloud before opening the model answer. Then answer the follow-up and compare the reasoning.
What is an index, in plain language?
Reveal a model answer
“It is a maintained search structure that helps locate records without checking every row. An author catalog points to books by an author. In a database, the index stores searchable keys and enough information to find or return matching data.”
Interviewer follow-up
Why not create one for every field?
Reveal the follow-up answer
Every maintained index consumes space and adds work to relevant inserts, updates, and deletes. I choose indexes from actual queries and constraints.
What the answer must demonstrate: Explain the read/write tradeoff.
How does an index on (author, title, id) answer author = Le Guin ordered by title?
Reveal a model answer
“It seeks to the first Le Guin entry and scans that contiguous author range in title order. It fetches the matching book rows only if required fields or visibility checks need them, then stops at the range end or limit. The benefit is avoiding unrelated authors, not assuming every query can be served entirely from the index.”
Interviewer follow-up
Would it help a query on title alone equally well?
Reveal the follow-up answer
Not necessarily: author is the leading ordering. The engine may use another access method, but a title-leading index directly matches that different query.
What the answer must demonstrate: Walk the keys rather than naming the structure.
Which index fits customer history sorted newest first?
Reveal a model answer
“I start with customerId, then createdAt descending, then orderId descending for ties. Equality on customer narrows the range and the remaining order supports the requested slice. I would include returned columns only if reducing row lookups justifies a larger index.”
Interviewer follow-up
Why include orderId when timestamps exist?
Reveal the follow-up answer
Two orders can share a timestamp. A unique tie breaker creates a deterministic order and a complete pagination cursor.
What the answer must demonstrate: Explain equality, ordering, and tie breaking.
When inserting a new book row with ID 15, what additional work do maintained indexes require?
Reveal a model answer
“The table gets a row and each maintained index gets a corresponding entry. The storage engine also performs its logging and any page maintenance required. Extra indexes therefore increase write amplification, memory pressure, and storage even if this insert is only one business operation.”
Interviewer follow-up
What about an update to an indexed title?
Reveal the follow-up answer
The author/title index must reflect the new key. The exact update mechanism depends on the engine, but later queries must find the correct title for the row version they are allowed to read.
What the answer must demonstrate: Account for all maintained structures.
What is a covering index?
Reveal a model answer
“It contains the fields needed to answer a query, potentially avoiding separate row fetches. For order history I might include total with the ordering keys. Whether an index-only scan is actually possible also depends on the engine’s visibility rules and query plan.”
Interviewer follow-up
What is the cost of including total?
Reveal the follow-up answer
More index bytes and maintenance when total changes. I measure whether saved reads justify that cost.
What the answer must demonstrate: Do not promise every covered query avoids all table access.
Why can a large OFFSET be expensive?
Reveal a model answer
“The database may still walk past the earlier matching entries before returning the requested page. A keyset cursor lets the next query seek after the last seen ordering tuple. I use a stable tie breaker and define how concurrent inserts affect the browsing session.”
Interviewer follow-up
Does a cursor guarantee an unchanged snapshot?
Reveal the follow-up answer
No. It identifies a position. A snapshot across pages requires an additional consistency/version mechanism if the product needs it.
What the answer must demonstrate: Separate ordering and snapshot consistency.
Why might the optimizer ignore an index?
Reveal a model answer
“A query matching 90% of a table may do more work through index-to-row lookups than through a sequential scan; a query matching 100 rows in a million has a different cost. I inspect estimated versus actual rows, buffers, filtering and sort work for the exact query. Small tables, stale statistics and data skew can change the plan.”
Interviewer follow-up
Would a low-cardinality boolean index always be useless?
Reveal the follow-up answer
No. It can help selective partial queries or specific engine strategies. The useful question is how much work it avoids for this query and distribution.
What the answer must demonstrate: Avoid absolute rules disconnected from data.
Does an index solve finding data across shards?
Reveal a model answer
“A local index searches within its storage owner. The request still needs to identify the right shard, or query a distributed index or multiple owners. For customer history, customer-based routing and a customer/time local index work together.”
Interviewer follow-up
What if the query does not include the shard key?
Reveal the follow-up answer
I need a separate location/index path or bounded fanout, with its latency and consistency costs.
What the answer must demonstrate: Explain routing before local lookup.
Blank-page exercise · 15 minutes
Build the answer yourself
Design an index and cursor for customer order history. Trace one read and one insert.
- Write the query with filter, ordering, and limit.
- Show at least four actual ordered keys.
- Explain the write cost of each index.
- Describe ties and concurrent changes between pages.
Check that each component and design decision follows from your requirements and workload.
Recall the key ideas
Answer from memory before opening each card. Explain why the choice works and what it costs. Revisit missed cards tomorrow.
Database indexes: B-trees, composite keys and query accessHow do you justify the cost of an index?Recall first, then reveal
Database indexes: B-trees, composite keys and query accessFor one customer’s newest orders, what should a composite index put first?Recall first, then reveal
Customer ID narrows the search, followed by the ordering fields and a tie-breaker. Their order must match the query.
Find the customer → order their rows → take the page.
Return to lessonDatabase indexes: B-trees, composite keys and query accessWrite overheadRecall first, then reveal
One row write can update several indexes plus recovery logs.
Every index must be maintained.
Return to lessonFinal revision
Summary and interview notes
An index exchanges extra storage and write maintenance for less work on specific queries. Choose its keys from the filter, requested ordering and limit, then verify the plan on representative data rather than assuming an index is always faster.
Remember these points
- B-tree key order supports equality, ranges and compatible ordering; composite and covering describe properties, not separate tree algorithms.
- Equality on customer plus ordered timestamp and unique ID supports a deterministic history page.
- A covering index may reduce row fetches, but engine visibility rules can still require them.
- Finding a few rows and scanning most of a table have different costs; measure rows examined and actual work.
- A keyset cursor gives an ordering boundary, not an unchanged snapshot across requests.
Interview tips
- Write the actual query before proposing an index, then trace seek, scan and any row fetch.
- Explain the write and storage cost of every added key or included field.
- Use a plan to compare estimated and actual work; do not treat an Index Scan label as sufficient evidence.
Important qualifications
- Leftmost-prefix reasoning is a useful starting point, but current PostgreSQL can use skip scans in suitable distributions.
- Normalization, collation and expressions must match the intended query semantics.
- The PostgreSQL DDL is an implementation example; other engines have different clustering, coverage and visibility rules.
Technical references
- PostgreSQL multicolumn indexesB-tree column-order behavior and implementation-specific optimizations.
- PostgreSQL index documentationIndex types, ordered access, covering indexes, and maintenance considerations.
- PostgreSQL index-only scans and covering indexesINCLUDE payload columns and transaction visibility constraints.
- PostgreSQL using EXPLAINEstimated versus actual query work, row counts and buffer observations.
- PostgreSQL indexes on expressionsIndexing an expression such as lower(email) to match a query.
Practice marks stay in this browser.