How does a hash index locate a row? Walk through what happens from the lookup key to the row pointer, and describe how the index is laid out on disk.
answer
- h(key) → bucket number, one page read
- hash narrows, key comparison decides
- collisions → overflow chain
- no order in a bucket number
- O(1) probe vs O(log n) descent
basics
~20 sA hash function turns the key into a bucket number. The engine reads that one bucket, compares the stored keys against the search key (different keys can collide), and follows the matching entry's row pointer. Roughly constant cost, equality lookups only.
solid answer
~50 sA hash index stores entries in a fixed set of **buckets**. On insert the engine computes `h(key)`, reduces it to a bucket number (classically `h(key) mod N`), and writes `(key or key-hash, row pointer)` there. On lookup it computes the same hash, jumps straight to that bucket with no tree descent and no comparisons on the way down, reads it, and compares candidate keys with the search key, because two different keys can land in the same bucket. Matching entries give row identifiers, which the engine uses to fetch the rows themselves. Cost is essentially constant: one hash computation, one bucket read, plus a short overflow walk if that bucket has spilled. It does not grow with table size, unlike a B+tree's O(log n) page descents. The price is that a bucket number carries no ordering information, so the structure can only answer *is this exact key present*.
code
text · 9 linesh("[email protected]") % 8 = 3
h("[email protected]") % 8 = 3 <- same bucket, different key
bucket 0: [ ... ]
bucket 1: [ ]
bucket 2: [ ("[email protected]", row#912) ]
bucket 3: [ ("[email protected]", row#17), ("[email protected]", row#204) ] -> overflow page -> [ ... ]
...
bucket 7: [ ("[email protected]", row#55) ]go deeper
Be able to state the three steps — hash the key, read one bucket, compare keys and follow pointers — and that it only serves equality.
Add why the final key comparison is mandatory, what an overflow chain is, and the O(1) versus O(log n) comparison with its cache caveat.
Discuss load factor and dynamic bucket splitting, entry size when only the hash is stored, and the operational reality that hash indexes are rarely the default choice.
Frame it as an access-method selection question: what fraction of the workload is single-key equality, what the buffer-cache behaviour really is, and whether one extra specialised index earns its write and maintenance cost.
## What a hash index is An index is a side structure mapping a key value to the location of rows holding it. A **hash index** does that mapping with a hash function rather than a sorted tree. Conceptually the index is an array of *buckets* (slots, usually one disk page each). A hash function `h` takes the indexed value and produces a large integer; the engine reduces that to a bucket number, classically `h(key) mod N` for `N` buckets. ## The write path Inserting a row computes `h(key)`, finds the bucket, and appends an entry. The entry holds a row identifier (a physical address in heap-organised engines, or the primary key in clustered-index engines) and either the full key or just the stored hash value. Storing only the hash keeps entries tiny and fixed-width regardless of how long the key is — an attractive property for indexing long URLs or hashes — but then a final key comparison must happen against the actual row. ## The read path A lookup for `key = X` runs three steps: 1. Compute `h(X)` and derive the bucket number. Pure CPU work, no I/O. 2. Read that one bucket page (plus its overflow chain, if any). 3. Compare each candidate entry's key against `X` and discard non-matches, then follow surviving row pointers to fetch rows. Step 3 exists because hashing is many-to-one: distinct keys can produce the same bucket, and even the same hash value. A hash index therefore *narrows*, it does not *decide*; the equality test on the real key is still required for correctness. ## Why the cost is constant A B+tree lookup descends from root to leaf, touching one page per level — typically three or four page reads for a large table, growing logarithmically as the table grows. A hash lookup touches one bucket regardless of whether the table has a thousand rows or a billion. That is the entire structural argument for hash indexes: O(1) equality probes instead of O(log n). In practice the advantage is narrower than it looks, because a B+tree's upper levels are almost always resident in the buffer cache, so its "three or four reads" are often one physical read plus a few cached ones. Hash indexes win on CPU comparisons and on very large keys more than on I/O. ## What the layout costs you Bucket numbers are deliberately unordered — a good hash scatters neighbouring keys to distant buckets. Consequences: - **No range scans.** Keys just above `X` are not stored anywhere near `X`. - **No sorted output.** Reading buckets in order yields effectively random key order, so the index cannot satisfy an ordering requirement or feed a merge join. - **No prefix or partial-key matching.** Hashing the whole value means a prefix hashes to something unrelated. - **No min/max shortcut.** There is no leftmost or rightmost entry. It also means the index is only usable for the exact expression that was indexed, on the full key, with an equality comparison. ## Sizing and rebuilds Because `N` appears in the bucket calculation, growing the index is not free. Static hashing fixes `N` at build time and degrades as data grows (long overflow chains); dynamic schemes such as linear or extendible hashing split buckets incrementally so the structure grows without a full rebuild. Real engines use dynamic schemes, but a badly loaded hash index still degrades toward linear scanning of an overflow chain, at which point the O(1) promise evaporates. ## Where you actually meet them Hash indexes are common inside in-memory engines and caches, as internal join structures (a hash join builds a transient hash table on the fly), and as an optional on-disk index type in several relational engines. On disk they are a niche choice; the default index type in essentially every mainstream relational engine is the B+tree, because it serves equality *and* everything else.
- If the hash lookup already found the bucket, why does the engine still compare the key?Hashing is many-to-one, so a bucket can hold entries for several distinct keys, and two distinct keys can even share a hash value. Returning everything in the bucket would return wrong rows. The comparison against the real key value is what makes the result correct; the hash only reduces the search space.
- Does a hash index help a query that fetches many rows for one key value?Yes — duplicates of the same key hash to the same bucket, so all their pointers are found together. The remaining cost is the row fetches themselves, which are scattered across the table. If the key has very low cardinality, that scatter can make a full table scan cheaper than the index, exactly as it would with a B+tree.
A coat-check: your ticket number tells the attendant exactly which rack to walk to, so the queue length does not matter. But nothing on that rack tells you which coats were checked in just after yours — the numbering deliberately scrambles arrival order.
saying these in an interview costs you the question
- Saying the hash index stores rows in sorted order somewhere so ranges still work
- Claiming a hash lookup is always faster than a B+tree lookup, ignoring cached upper tree levels
- Thinking a hash match alone proves equality, with no key comparison needed
- Believing the row pointer is the hash value itself
- Assuming bucket count grows automatically with zero cost