Hello! In our last lesson, we completed our deep dive into B+ trees by analyzing the performance trade-offs between clustered and unclustered indexes. We concluded that while B+ trees are excellent for both point and range queries, their primary strength is in maintaining order.
Today, we'll pivot to a different class of indexing structures: hash indexes. These structures sacrifice ordering to achieve potentially faster point lookups.
Introduction
This lesson focuses on a fundamental dynamic hashing algorithm called extendible hashing. The learning outcome is to implement and analyze an extendible hashing scheme, including directory management.
Unlike static hashing schemes which require costly full-table reorganizations to grow, extendible hashing is designed to expand gracefully as your data scales. We'll explore the clever data structures that make this possible—the directory and buckets—and analyze the algorithms for insertion, splitting, and directory growth. By the end, you'll understand the mechanics of this scheme and the design considerations for a disk-based implementation.
1. From Static to Dynamic Hashing
If you've encountered hash tables in a standard data structures course, you've likely seen static hashing schemes like linear probing or simple chaining. In these schemes, the table has a fixed size. If it fills up, the only solution is to allocate a new, larger table and re-hash every single key. For a database with billions of records, this is prohibitively expensive.
Dynamic hashing schemes, like extendible hashing, solve this problem by allowing the hash table to grow incrementally.
Let's begin by watching a video that clearly explains the core components and logic of extendible hashing.
This video by Douglas Fisher provides a focused and clear visual walkthrough of extendible hashing. It's a perfect introduction to the key concepts.
Please watch the video from the beginning to 05:54. As you watch, focus on understanding these key components and their roles: Directory: An array of pointers to buckets. Global Depth (g): An integer that determines the size of the directory (2^g entries). Buckets: Pages that store the actual key-value pairs. Local Depth (l): An integer stored with each bucket, indicating how many bits are shared by all keys within it. Pay close attention to the two distinct scenarios for handling an insertion into a full bucket.
2. The Core Mechanism: Global vs. Local Depth
As the video explained, the magic of extendible hashing lies in the interplay between the global depth of the directory and the local depth of the buckets.
Let's break down the rules for insertions that cause a bucket to overflow:
-
Find the Target Bucket: Hash the key, take the first
g(global depth) bits of the hash value to get an index into the directory, and follow the pointer to the target bucketB. -
Check for Space: If bucket
Bhas space, insert the key and you're done. -
Handle Overflow: If bucket
Bis full, you must split it. Let the local depth ofBbel.-
Case 1:
l < g(Simple Split)- This is the easy case. Multiple directory entries point to
B. - Create a new bucket
B'. - Set the local depth of both
BandB'tol + 1. - Re-distribute the keys from the original bucket
B(plus the new key) betweenBandB'based on the(l+1)-th bit of their hash values. - Update the directory pointers that used to point to
Bto now point to eitherBorB'based on that same(l+1)-th bit. The directory itself does not change size.
- This is the easy case. Multiple directory entries point to
-
Case 2:
l == g(Directory Split)- This is the more complex case. Only one directory entry points to
B. Splitting the bucket requires us to first expand the directory so we have more pointers to work with. - Double the directory size and increment the global depth:
g = g + 1. All old pointers are copied over. For example, the old entry010now corresponds to new entries0010and1010, which both initially point to the same old bucket. - Now that
l < g, you can proceed as in Case 1: create a new bucketB', update local depths, redistribute keys, and update the pointers in the newly expanded directory.
- This is the more complex case. Only one directory entry points to
-

For a more dynamic walkthrough, the following segment from the CMU Database Systems lecture provides excellent animations of these splitting procedures.
F2023 #07 - Hash Tables (CMU Intro to Database Systems)
Professor Andy Pavlo provides a fast-paced, animated explanation of extendible hashing that beautifully illustrates both bucket split scenarios.
Watch the segment from 59:19 to 1:04:38. Follow the animated example of inserting keys and watch how the directory and buckets evolve, paying special attention to the case where the global depth must be incremented.
Test your understanding!
Imagine an extendible hash index with a global depth g=2. Buckets can hold a maximum of two entries. The hash function uses the least significant bits. The initial state is:
- Directory
[00, 01, 10, 11] - Bucket A (
l=1): holds keys with hash ending in...1(e.g., 50101, 91001). Directory entries01and11point to A. - Bucket B (
l=2): holds keys with hash ending in...00(e.g., 40100, 121100). Directory entry00points to B. - Bucket C (
l=2): empty. Directory entry10points to C.
What happens when you insert a key with hash 13 (1101)? Describe the state of the directory and buckets after the insertion.
Show answer
- Lookup: The hash
1101ends in01. We use theg=2LSBs, so we look at directory entry01, which points to Bucket A. - Overflow: Bucket A already contains keys 5 (
...0101) and 9 (...1001), so it's full. We must split it. - Check Depths: Bucket A has
l=1and the global depth isg=2. Sincel < g, this is a simple split (no directory doubling). - Split:
- Create a new bucket, Bucket D.
- The local depths of both A and D become
l+1 = 2. - We re-distribute keys based on the
l+1=2nd LSB (the second bit from the right).- Key 5 (
...0101): 2nd LSB is0. Stays in Bucket A. - Key 9 (
...1001): 2nd LSB is0. Stays in Bucket A. - Key 13 (
...1101): 2nd LSB is0. Goes to Bucket A.
- Key 5 (
- Wait, there's another overflow! All three keys hash to the same bucket even after splitting. This is a case for recursive splitting.
- Recursive Split: Bucket A (now with
l=2) is still full.- Check Depths: Bucket A has
l=2andg=2. Nowl == g. We must double the directory. - Directory Growth: Global depth becomes
g=3. The directory now has 8 entries[000, 001, ..., 111]. - Split Bucket A: Create new bucket, say E. Local depths of A and E become
l+1 = 3. - Re-distribute: Re-distribute keys 5 (
...0101), 9 (...1001), and 13 (...1101) based on the 3rd LSB.- Key 5 (
...101): 3rd LSB is1. Goes to Bucket E. - Key 9 (
...001): 3rd LSB is0. Stays in Bucket A. - Key 13 (
...101): 3rd LSB is1. Goes to Bucket E.
- Key 5 (
- Check Depths: Bucket A has
- Final State (simplified):
- Global depth
g=3. - Bucket A (
l=3) has key 9. Pointed to by directory entry001. - Bucket E (
l=3) has keys 5, 13. Pointed to by101. - Other directory pointers are updated accordingly. For example,
011and111still point to the original Bucket A's split image.
- Global depth
This shows how a single insertion can trigger multiple splits and directory growth.
3. Implementing a Disk-Based Extendible Hash Index
Your goal is to understand how to implement this scheme. For a disk-based database, this means managing the directory and buckets as pages that are read from and written to disk via the buffer pool.
The CMU Database Systems course includes a project to build exactly this. We won't be implementing it right now, but studying its design specification is invaluable for understanding the practical engineering details.
Project #2 - Extendible Hash Index
This project specification details the on-disk layout and logic for an extendible hash index. It's a perfect resource for turning the abstract algorithm into a concrete, implementable design.
Read 'Task #1 - Extendible Hash Table Pages' and 'Task #2 - Extendible Hashing Implementation'. As you read, focus on: Page Structure: How are the directory and buckets represented as distinct page types (Directory Page, Bucket Page)? Notice the local_depths array is stored in the directory page. Directory Management: What are the explicit rules for splitting a bucket and growing the directory? Key Detail: The spec mentions using most-significant bits for an initial header page and least-significant bits for the directory pages. This is a common design choice. Deletion & Merging: Briefly look at the rules for merging buckets on deletion. This is the inverse of splitting and is required to shrink the index.
The specification provides a clear blueprint. The logic described—splitting buckets, growing the directory, and merging on deletion—directly maps to the concepts we've discussed. This mapping from algorithm to a page-based, on-disk implementation is a core skill in database systems engineering.
4. Analysis and Comparison
Now that you understand the mechanics, let's analyze the I/O cost and compare it to B+ Trees.
-
Point Lookup: A lookup usually costs 2 I/Os: one to fetch the directory page and one to fetch the bucket page. If the directory is small enough to be cached in memory (a common scenario), the cost drops to just 1 I/O. This is often faster than a B+ Tree traversal, which costs
heightI/Os (typically 3-4). -
Insertion:
- No Split: 2 I/Os (read directory, read bucket, then write them back).
- Simple Split (
l < g): Requires more I/Os to write the new bucket page and update the directory. - Directory Split (
l == g): The most expensive case, as you need to write a whole new set of directory pages. However, since the directory doubles in size each time, these events become exponentially less frequent. The cost is amortized over many insertions.
-
Major Weakness: Range Queries
A hash index does not store keys in any meaningful order. A query likeSELECT * FROM users WHERE age BETWEEN 30 AND 40cannot be answered efficiently. You would have to do a full table scan. This is the fundamental trade-off: hash indexes excel at equality-based point lookups (WHERE id = ?) but are useless for range scans, which are a primary strength of B+ Trees.
Conclusion
In this lesson, you've learned how extendible hashing provides a dynamic index structure that grows gracefully without the need for catastrophic, full-table rehashes.
Key Takeaways:
- Structure: Extendible hashing uses a directory of pointers and buckets for data, managed by a global depth and per-bucket local depths.
- Growth: It grows by splitting full buckets. If a bucket's local depth equals the global depth, the directory itself must double in size. This amortizes the cost of resizing.
- Performance: It offers excellent performance for point lookups, often requiring just one or two I/Os.
- Trade-off: Its major limitation is the complete inability to support efficient range queries, making B+ Trees the more versatile, default choice for a general-purpose index.
Preview of the Next Lesson:
Extendible hashing's main complexity is managing the directory. What if we could design a dynamic hashing scheme that doesn't need a directory at all? In our next lesson, we will explore linear hashing, another clever scheme that achieves incremental growth using a simple "split pointer" and multiple hash functions.