Skip to content
About
Main content

Hash tables

Where does a hash table fall off a cliff?

The constraint. If you can compute a position from the key, a lookup needs exactly one . That is the theoretical floor. The price: the computation is a random map, so order is destroyed. Also, by the , collisions begin after only about √m insertions into m slots. (With 1,000,000 slots, you expect the first collision after roughly 1,000 keys. The same effect makes two people in a room of 23 likely to share a birthday.) Collisions are not a flaw in your . They are the definition of squeezing a large key space into a small address space.

The dial is load factor, and its cost curve is a hyperbola

The α ("alpha") is the fraction of slots that are full. In (if the slot is taken, try the next slot, then the next), the expected number of probes is roughly:

  • ½ × (1 + 1/(1 − α)) for a successful lookup (a hit);
  • ½ × (1 + 1/(1 − α)²) for a failed lookup or an insert (a miss).

The last ten percent of a hash table costs more than the first ninety.

The shape 1/(1 − α) should look familiar to anyone who has studied . 1/(1 − ρ) is the multiplier for waiting time in the simplest queue model (one server, random arrivals, called M/M/1): at 90% , a request waits about 10 times the service time. This is not an analogy. The analysis of clustering in hash tables (due to Knuth) and queueing theory are the same mathematics, because a probe sequence is a queue for a slot. Any resource that is allocated by (everybody , the unlucky ones retry) rather than by assignment (each user gets a reserved place) costs hyperbolically as it approaches capacity. We call this the :

Chaining versus open addressing is a cache argument

Two classic collision strategies:

  • Chaining: each slot holds a linked list of the keys that landed there.
  • Open addressing: all keys live in the array itself; on collision you probe other slots (like linear probing above).

Both are O(1) amortized. But follows a pointer on every collision, and a pointer chase is a dependent cache miss: about 80 ns of waiting. scans forward inside the same or the next one. This is why every high-performance hash table built in the last decade (Abseil's Swiss tables, Folly's F14) uses open addressing with one byte of metadata per slot, scanned sixteen slots at a time with SIMD (one CPU instruction that compares 16 bytes at once). The cost model is cache lines touched. The comparison count is irrelevant.

Chaining survives where entries are large, where deletion must be trivial, or where you need stable references to values (a pointer to a value that stays valid when the table grows).

# Open addressing with linear probing, minimal version (no deletion, no resize).
def put(table, key, value):
    i = hash(key) % len(table)
    while table[i] is not None and table[i][0] != key:
        i = (i + 1) % len(table)          # next slot: usually the same cache line
    table[i] = (key, value)

def get(table, key):
    i = hash(key) % len(table)
    while table[i] is not None:
        if table[i][0] == key:
            return table[i][1]
        i = (i + 1) % len(table)
    return None                            # an empty slot ends the search

Robin Hood hashing reduces variance, not the mean

Robin Hood hashing does not lower the average probe count at all. It makes probe counts more equal. When a new key collides with an existing key, it compares how far each key is from its ideal slot, and the key that is closer to home (the "rich" key) gives up its slot to the one that is farther (the "poor" key). The mean stays the same. The tail collapses.

In a system with a budget, that trade is free. It is a direct instance of the :

It also gives early termination on negative lookups: because keys are kept in order of distance from home, a search can stop as soon as it meets a key that is closer to home than the search has traveled. This matters more than it sounds, because negative lookups (asking for something that is not there) dominate in any structure that sits behind a filter.

Cuckoo hashing buys a worst-case bound with an unbounded insert

Cuckoo hashing uses two hash functions, so each key has two candidate slots. A lookup needs at most two probes, regardless of load and regardless of an adversary choosing keys. An insert may cascade: the new key evicts the key in its slot, which moves to its other slot and evicts another key, and so on. A cascade can fail outright and force a rehash. Two functions with one slot each saturate near 50% occupancy. With four slots per bucket, occupancy can go beyond 95%, and that is what makes the design practical.

The mechanism underneath is worth extracting because it recurs outside this mechanism. Throw n balls into n bins at random with one hash function: the fullest bin holds Θ(log n / log log n) balls. With two choices (put each ball in the less full of two random bins), the fullest bin holds only Θ(log log n). That is an exponential improvement from one extra choice. It is called the power of two choices, and it also appears in load balancers: pick two servers at random, send the request to the less busy one. Same mechanism, different costume. A single random assignment concentrates badly; the minimum over two random assignments concentrates tightly. This is why it appears anywhere load is spread without a coordinator.

Growth is the problem nobody teaches

Doubling a hash table means rehashing every key: a stop-the-world O(n) pause. In a path with a latency budget, that is unacceptable. The escapes all limit how much of the mapping changes at once:

  • Incremental rehash with two live tables (Redis): move a few entries per operation from the old table to the new one.
  • Extendible hashing: a directory of pointers to buckets that doubles, while individual buckets split on their own.
  • Linear hashing: split one bucket per insert in a fixed rotation, so the table grows smoothly.

These last two are the on disk methods; the in-memory descriptions above often leave them out, but the pause problem they solve is exactly the one on this page.

The distributed answer to the same problem is consistent hashing. When a server joins a cluster, you want 1/n of the keys to move, not all of them. is not a distributed-systems invention. It is the rehash problem with the cost counted in network transfers instead of memory writes.

Where hash tables bite in data engineering

Inside a query engine, hash tables are the build side of a hash join, the state of a hash aggregation, and the partitioner of a shuffle (the step that sends each row to the worker that owns its key). In none of these is probe count the main concern. The main concern is footprint per distinct key, because it decides the number of partitions, and the number of partitions decides whether the operator spills (writes its state to disk because memory is full). A hash aggregation that fits in memory and one that differ by an order of magnitude, not by a percentage.

The trigger for that discontinuity is almost always skew: hash partitioning assumes keys are independent and nearly uniformly spread. We call this the :

The party that pays is whichever task received the hot key.