CMD Guide
HomeSystem DesignDatabases

Types of Indexes

An index works by keeping a copy of the indexed column(s) in a separate structure that is kept pre-sorted (a B-tree) or pre-hashed (a hash table), with the leaves pointing back at the table rows — so the engine can binary-search or hash straight to the matches instead of reading every page.

The "seven types" you will be asked about are not seven different data structures. Under the hood there are only three: the B-tree (ordered), the hash table (unordered, equality-only), and the inverted index (term → rows, for text). The other names describe two orthogonal properties layered on a B-tree — what role it plays (primary / unique = a B-tree plus a constraint) and whether it dictates the table's physical order (clustered vs non-clustered). "Composite" just means the B-tree key has more than one column. Hold that map and the catalog below stops being a list to memorise. How the B-tree and hash structures are actually built and traversed on disk is the subject of the next lesson, Storage Engines — How B-tree & LSM-tree Work; here we care about which kind of index to reach for and why.

The seven types at a glance

TypeUnderlying structureOrdered?Per tableBest for
PrimaryB-tree + PK constraintYesOneExact lookup by identity; integrity
UniqueB-tree + uniqueness constraintYesManyEnforcing distinct values (email, SKU)
ClusteredB-tree that is the tableYesOneRange scans & sorting on the key
Non-clustered (secondary)B-tree pointing at rowsYesManySelective filters & joins
CompositeB-tree with a multi-column keyYes (by prefix)ManyMulti-column filters / sorts
Full-textInverted index (term → rows)By termManyKeyword / phrase search in text
HashHash tableNoEngine-dependentEquality-only lookups

Notice that rows 1, 2, 4 and 5 all sit on the same B-tree machinery — only their role and key shape differ. Rows 3, 6 and 7 are where the physical structure actually changes.

Trace: one lookup through a secondary index

The single most important behaviour to internalise is what happens when you filter on a non-clustered index of a clustered table. Take a concrete InnoDB (MySQL) setup and follow one query byte for byte.

Now run SELECT * FROM Customers WHERE Email = 'alice@example.com';

  1. Read 1 — secondary root. Compare 'alice@example.com' against the separator keys, pick a child.
  2. Read 2 — secondary internal. Narrow again, descend to the right leaf.
  3. Read 3 — secondary leaf. Find the entry ('alice@example.com' → CustomerID = 8842). Crucial detail: in InnoDB a secondary-index leaf stores the primary key value, not a physical row pointer. We now know the row's identity but still have none of its columns.
  4. Reads 4–6 — clustered index. Traverse the PK B-tree for CustomerID = 8842: root → internal → leaf. The clustered leaf holds the full row, so we finally read Name and Address.

That is 6 logical page reads to return one row — the classic "double lookup" (also called a bookmark lookup). Compare it to the alternative: a full table scan touches all ~25,000 leaf pages. Six versus twenty-five thousand is the entire reason indexes exist.

diagram
diagram

The second half of that trace (reads 4–6) is pure overhead you can sometimes delete. If the index already contains every column the query needs, the engine returns straight from the secondary leaf and never touches the clustered index — a covering index. Add Name to the key:

CREATE INDEX idx_email_name ON Customers (Email, Name);

Now SELECT Name FROM Customers WHERE Email = 'alice@example.com' costs 3 reads, not 6. In MySQL's EXPLAIN you will see Using index; in Postgres, an Index Only Scan. The catch: a covering index is wider, so it costs more disk and more work on every write — you are trading write cost for read cost on one hot query.

One more thing the trace exposes: the engine can only seek when the predicate is sargable — the indexed column appears bare on one side of the comparison. WHERE YEAR(created_at) = 2024 or WHERE email = 12345 (type mismatch) wrap or coerce the column and force a scan; rewrite as WHERE created_at >= '2024-01-01' AND created_at < '2025-01-01'.

Primary and Unique — B-trees with a constraint

A primary index is the B-tree the database builds automatically for the PRIMARY KEY. It is unique, not-null, and — in InnoDB and SQL Server by default — it is also the clustered index, so it defines physical row order. One per table, because a table has one primary key.

A unique index is the same B-tree with a distinctness rule but no not-null/clustering implications; you can have many. Beyond speeding reads, it enforces integrity: every insert or key-update checks the tree for a duplicate before committing — a small, worth-it write cost.

CREATE TABLE Customers (
  CustomerID INT PRIMARY KEY,        -- primary (usually clustered) index
  Email      VARCHAR(255) NOT NULL,
  Name       VARCHAR(100)
);

CREATE UNIQUE INDEX idx_customers_email ON Customers (Email);

After this, a duplicate email is rejected and WHERE Email = ? resolves through the trace above.

Clustered vs Non-clustered — who owns the row

The distinction the trace hinges on. A clustered index stores the rows themselves in its leaves, sorted by the key — the index is the table. Only one per table (data can be physically sorted one way), and it makes range scans and ORDER BY on the key nearly free, because matching rows sit contiguously on disk.

A non-clustered (secondary) index is a lighter B-tree whose leaves hold the key plus a reference back to the row (the PK value in InnoDB, a physical row-id in a heap table like SQL Server without a clustered index or in Oracle). Many per table. Great for selective filters; poor when the predicate matches a large fraction of rows, where the optimizer will (correctly) prefer a full scan over thousands of random row fetches.

-- SQL Server: physically order the table by (LastName, FirstName)
CREATE CLUSTERED INDEX idx_cust_name ON Customers (LastName, FirstName);

-- A secondary index to accelerate a different access path
CREATE INDEX idx_orders_customer ON Orders (CustomerID);

Composite — the leftmost-prefix rule

A composite index keys on several columns treated as one concatenated sort key. The order of the columns is load-bearing: the engine can seek using a leading prefix only.

CREATE INDEX idx_orders_cust_date ON Orders (CustomerID, OrderDate);

Against (CustomerID, OrderDate):

So order the columns to match how you actually filter: equality columns first, then the range column. A common mistake is then adding a redundant single-column index on CustomerID — the composite already covers it.

Full-text — an inverted index for words

A B-tree keys on the whole column value, so LIKE '%database%' (a leading wildcard) cannot use it and falls back to a scan. A full-text index instead tokenises the text into words and builds an inverted index — each term maps to the rows containing it — enabling multi-word, phrase, and relevance-ranked search. Building and updating it is heavier (parsing, stop-word removal, stemming), often applied asynchronously, and it uses extra storage; queries need dedicated syntax.

-- MySQL / InnoDB
CREATE FULLTEXT INDEX idx_articles_content ON Articles (Content);
SELECT * FROM Articles WHERE MATCH(Content) AGAINST('database indexing');

-- PostgreSQL uses a GIN index over tsvector
CREATE INDEX idx_articles_fts ON Articles USING GIN (to_tsvector('english', Content));

Hash — O(1) equality, nothing else

A hash index runs the key through a hash function and jumps to the bucket holding the pointer — average O(1) for equality, versus a B-tree's O(log N). The price is total: it stores no order, so it is useless for <, >, BETWEEN, ORDER BY, or even prefix matches like LIKE 'John%'.

-- PostgreSQL: an explicit hash index (equality only)
CREATE INDEX idx_customer_code_hash ON Customers USING HASH (CustomerCode);

Engine reality checks that interviewers probe:

Two specializations worth naming: partial and bitmap

Beyond the seven, two variants come up the moment a workload is skewed.

Pitfalls

When to use which — and what it costs

B-tree vs hash. Default to a B-tree: it serves equality and ranges, sorting, and prefixes. Choose a hash index only when the workload is 100% equality, ideally in memory, and you have measured the B-tree descent as the bottleneck. Choose hash when every query is col = value on a large, uniformly-distributed key; prefer B-tree when any range, sort, or prefix query exists — which is almost always.

Clustered vs heap / secondary-only. Cluster on the column you scan in ranges — a timestamp on an append-only events table is the textbook win. Cost: only one per table, mid-table inserts cause page splits, and every secondary index inflates to carry the clustering key. Choose clustering when range/sort on one key dominates reads and inserts are roughly sequential; prefer a synthetic sequential PK (or a heap) when writes dominate and access is by point lookup.

Composite vs several single-column indexes. One composite (A, B) beats two separate indexes when queries always constrain A (optionally B): a single seek, and possibly covering. Cost: usable left-to-right only, and larger entries. Prefer separate single-column indexes (letting the optimizer index-merge) when A and B are genuinely queried independently.

Full-text / external engine vs LIKE. Use full-text for word, phrase, and relevance search over large text. Cost: async rebuilds, storage, special syntax, stop-word/stemming surprises. Prefer a plain B-tree for exact match or LIKE 'prefix%'; prefer a dedicated engine (Elasticsearch/OpenSearch) when search relevance is the product, not a feature.

Takeaways


Re-authored and deepened for this guide, drawing on Markus Winand's Use The Index, Luke! (sargability, leftmost prefix, clustered vs secondary lookups); the MySQL 8.0 Reference Manual (InnoDB clustered/secondary index layout, adaptive hash index, MEMORY-engine hash defaults, full-text search); the PostgreSQL documentation (B-tree, hash, and GIN index types; hash indexes WAL-logged since PostgreSQL 10); Alex Petrov's Database Internals; and Martin Kleppmann's Designing Data-Intensive Applications. B-tree and LSM-tree internals are covered in the companion lesson “Storage Engines — How B-tree & LSM-tree Work.”

🤖 Don't fully get this? Learn it with Claude

Stuck on Types of Indexes? Open Claude, copy a block below, and it'll teach you this exact concept — visually and interactively.

🎨 Explain it visually

Build the mental picture, not memorization.

I just read a lesson on **Types of Indexes** (System Design) and want to truly understand it. Explain Types of Indexes from first principles using ONE vivid real-world analogy and a visual mental model — draw it as ASCII art or a clear step-by-step diagram — with a concrete example using real numbers. Then ask me one question to check I got the mental picture, and wait for my reply. If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.
🤔 Walk me through it (interactive)

Socratic — adapts to where you're stuck.

Teach me **Types of Indexes** interactively. Ask me ONE guiding question at a time, wait for my answer, and adapt to my confusion — build the idea with me step by step instead of explaining it all at once. If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.
🧪 Quiz me & fix my gaps

Active recall exposes what you missed.

Quiz me on **Types of Indexes** with 5 questions, easy to tricky, ONE at a time. Tell me if each answer is right; at the end, explain clearly what I got wrong and why. If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.
🧠 Make it stick

Intuition + hook + flashcards for long-term memory.

Help me remember **Types of Indexes** for the long term: give the one-sentence intuition, a memorable hook/mnemonic, a tiny worked example, and 3 active-recall flashcards (Q -> A). If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.

📝 My notes