Learn
Lesson · Storage Engines and Indexes · The cost model

Hash vs Tree for Ranges

A hash index answers a point lookup in one I/O and answers a range query only by reading the whole table, because hashing destroys the order a range needs. A tree pays its height on both. On a workload of nine hundred lookups and a hundred small ranges a second that is 500 900 I/O against 3 000, and “hash is faster” turns out to be a true claim about one operation wearing the clothes of a claim about an index.

hash point 1 I/O range a full scan = rows / rows a page tree point h I/O range (h − 1) + ⌈k / rows a page⌉ 10⁶ rows, B = 500, 200 rows a page, ranges returning 100 rows point range 900 point/s + 100 range/s hash 1 5 000 500 900 I/O a second tree 3 3 3 000 I/O a second
Both engines on the same two operations, and then on the same mix

Core ideas

What this lesson establishes

Two structures, two operations, four numbers. The argument people have about this is entirely caused by quoting one of the four.

1

A hash index is fast at exactly one thing

It computes a bucket from a key and reads it: one I/O, no descent, no comparisons. The expected probe length behind that 1 is Θ(1 + α) under simple uniform hashing, with a Θ(n) worst case no hash function removes; “Hashing with Chaining” on the Algorithms path establishes both, and this lesson uses them rather than re-deriving them. What the structure spends to get that 1 is the order of the keys.

2

A range query has no hash plan at all

There is no way to ask a hash index for the keys between two values except to look at every one of them, so a range costs a full scan: 10⁶ rows at 200 rows a page is 5 000 page reads. It is not that a hash is slower at ranges. It is that it has no access path for them, and what you are pricing is the table scan the planner falls back to.

3

A tree charges its height and then walks

A B-tree descends to the first matching key and then reads leaves in key order, so a range of k rows costs about (h − 1) + ⌈k/rows per page⌉. Over a million rows at B = 500 the height is 3, and a range of a hundred rows is 3 I/Os altogether: two internal levels, then one leaf that already holds all hundred.

The material

One I/O, or the whole table

What a hash index gives up for its point lookup, and what that costs on the operation it gave up.

Definition · Point lookup and range query

A point lookup retrieves the row for one exact key. A range query retrieves every row whose key lies between two bounds, and is priced by the number of rows k it returns.

An index is ordered if it stores keys in a comparison order, and unordered if it stores them by a hash of the key. Only an ordered index has an access path for a range.

The hash index's 1 is worth taking seriously rather than waving at. Under simple uniform hashing the expected number of probes is Θ(1 + α) in the load factor α, so a table kept at a sensible load factor really does answer in about one access — and the worst case, where every key collides, really is Θ(n). Those are Algorithms' results, proved in “Hashing with Chaining”; what this lesson adds is that on disk the unit is a page read and the constant is therefore expensive.

10⁶ rows B = 500 200 rows a page ranges return 100 rows hash point 1 I/O range 10⁶ / 200 = 5 000 I/O (full scan) tree height 500² = 250 000 < 10⁶ ≤ 125 000 000 = 500³ → h = 3 point 3 I/O range (3 − 1) + ⌈100/200⌉ = 3 I/O mix 900 point/s + 100 range/s hash 900(1) + 100(5 000) = 500 900 I/O a second tree 900(3) + 100(3) = 3 000 I/O a second

The hash index does one hundred and sixty-seven times the I/O of the tree on that mix, and it is genuinely three times cheaper on the operation it is good at. Both facts come out of the same four numbers. Which one you quote is a choice about what you want the answer to be.

Where the ordering reverses

Set the two totals equal and solve for the range rate r at a fixed point rate. With 900 point lookups a second: 900(1) + r(5 000) = 900(3) + r(3), so r(5 000 − 3) = 900(3 − 1) and r = 1800/4997 ≈ 0.360 range scans a second. One range query every three seconds is enough to make the tree the cheaper engine on this table.

Example · A workload where the hash genuinely wins

Remove the ranges entirely. At 900 point lookups a second and nothing else, the hash index does 900 I/O a second and the tree does 2 700 — three times as many, exactly the ratio of 1 to h. The true claim behind “hash is faster” is alive; it is just very small, and it is a claim about one operation.

The same misconception exists in memory, where “a hash table is always the fastest map” is corrected by pointing out that it cannot answer predecessor, range or ordered iteration at all. “Choosing a Structure” on the Algorithms path is that statement. This lesson is the same fact with a disk attached, and the disk is what turns a structural limitation into a number: not “unsupported” but 5 000 I/O.

What the count assumes: that pages hold their nominal number of rows, that nothing is cached, and that the range is contiguous in the index's key order. Cache the tree's top two levels and its point lookup falls from 3 to 1, at which point the hash index's advantage on the operation it is good at disappears entirely — which is worth knowing before anyone builds an argument on the 1 against 3.

Interactive

Hash against tree, counted in I/Os

One I/O for a point lookup, the whole table for a range

One I/O, or the whole tableA hash destroys the order a range needs; a tree pays its height and then walks
point lookupsrange scanstree totalcrossover

Method

Counting a workload in I/Os

Four costs, then two products, then a comparison. The order stops you from quoting the wrong one of the four.

1

Write all four costs before looking at the workload

Point and range, hash and tree. Any claim about an index made from fewer than four numbers is a claim about one operation, and it will be defended as though it were about the index.

2

Count a range in pages, not in rows

A range returning k rows at p rows a page is ⌈k/p⌉ leaf pages, and a full scan of n rows is ⌈n/p⌉ pages. Rows are what the query asks for; pages are what the device delivers.

3

Multiply each cost by its rate and add

point rate × I/O a point + range rate × I/O a range, for each engine, in I/O a second. Now the two engines are comparable, which they were not when the units were operations.

4

Solve for the rate that ties them

Set the two totals equal and solve for whichever rate is in doubt. The crossover is more useful than the winner: it tells you how much of the workload would have to change to change the answer, and here that is one range query every three seconds.

Two engines, four costs

Score 0 / 0

900 point lookups and 100 ranges a second over a million rows

Four costs, two totals, and then the rate at which the two totals meet.

10⁶ rows 4 kB page holds 500 index entries or 200 rows ranges return 100 rows 900 point lookups/s 100 ranges/s tree height 500² = 250 000 < 10⁶ ≤ 125 000 000 = 500³ → h = 3 hash index B-tree point lookup 1 I/O 3 I/O range of 100 rows 5 000 I/O (full scan) 3 I/O = 2 + ⌈100/200⌉ 900 point lookups/s 900 2 700 100 range scans/s 500 000 300 the mix 500 900 3 000 crossover 900(1) + r(5 000) = 900(3) + r(3) r(4 997) = 1 800 r = 1 800/4 997 = 0.360 ranges a second

The 5 000 contains no hash parameter. It is 10⁶/200: rows divided by rows a page, which is what a scan costs when there is no access path. Doubling the buckets, improving the hash function or removing every collision leaves it exactly where it is.

The crossover is the honest summary. At these page geometries the tree wins any workload with more than about one range scan every three seconds, and the hash index wins below that. Neither engine is faster; one of them is cheaper at your mix, and the mix is measurable.

For a faded rehearsal, widen the range to 1 000 rows and keep everything else. The supplied first move is that the tree's range cost becomes 2 + ⌈1 000/200⌉. Compute it, recompute both totals, and then say what happened to the crossover rate and why it moved in the direction it did. Run the lab at your numbers before opening the quiz.

Common mistakes

1
Saying that one index is faster than anotherThere is no such quantity. The four costs here are 1, 5 000, 3 and 3, and every honest sentence names an operation: faster at point lookups, by a factor of three, on an uncached tree. A claim without the operation in it cannot be checked, and it is usually the true claim about the wrong half of the workload.
2
Counting a range in rowsA range returning 100 rows out of pages holding 200 is one page read, not a hundred. The device delivers pages, so a range is ⌈k/rows per page⌉ leaves however few rows it returns — which is also why widening a range from ten rows to a hundred can cost nothing at all.
3
Expecting a better hash to fix the rangeThe 5 000 is 10⁶/200: rows divided by rows a page, containing no hash parameter of any kind. It is the cost of having no access path. More buckets, a better function or zero collisions leave it untouched, because the problem is not collisions — it is that the order was thrown away on purpose.

Check your understanding

✓
Finish when you answer “which is faster” with a question about the mix.

You should be able to write down all four costs for a stated page geometry, total a workload in I/O a second on both engines, and solve for the rate at which the answer changes.

Note: Both engines here were priced for readers. “The Write Cost of an Index” prices the same indexes for the writer, and the arithmetic is a division rather than an addition — which is why the answer surprises people who have only ever been shown the read side.

Kept in this browser only, for you. Nothing is sent anywhere and nothing checks it. Carry it to another device.

Feedback and recommendations

Something to change about this lesson? Write it down, tick it off when it is handled, and export the list when you want to act on it. Kept in this browser only: nothing is sent anywhere, and nobody is notified.

No recommendations yet for this lesson.