Data Intensive Systems: Lecture 6
Hashing and sorting for data systems: static and dynamic hashing (chained, extendible, linear), open addressing with linear probing, and external merge sort.
Hash Tables
Hash indexes, hash joins, and hash aggregation all stand on one structure.
Hash table. An unordered associative array that maps keys to values. A hash function computes where a key belongs, so the DBMS can jump to a small part of the structure instead of searching keys in sorted order.
- Space. in the number of entries.
- Average lookup, insert, delete. under uniform hashing, because only a small collision set is inspected.
- Worst case. , when many keys collide and the DBMS must inspect many extra slots or pages.
Hash tables serve equality predicates well. They do not preserve key order, so they are weak for ordered scans and range predicates.
A query such as id = 42 can use a hash index directly. A query such as id BETWEEN 40 AND 60 needs ordered access, usually a B-tree or a scan.
Static Hash Tables
Static hash file. A hash file whose number of primary buckets is fixed when the file is created.
For primary buckets, the bucket for key is
Bucket. The unit that receives every entry whose hash maps to the same bucket number. On disk, a bucket is one primary page plus, when it fills, a chain of overflow pages.
- Primary page. The normal page for that bucket.
- Overflow chain. Extra pages linked from the primary page when the bucket fills.
- Membership rule. Keys with the same bucket number are stored in the same bucket chain.
The operations are direct:
- Compute .
- Go to the corresponding primary bucket page.
- Scan the primary page and any overflow pages in that bucket chain.
The weakness is fixed capacity. If one bucket receives too many keys, its overflow chain grows and the lookup is no longer close to constant time.
The idealized picture assumes:
- the number of entries is known in advance;
- the number of entries stays fixed;
- each search key is unique;
- the hash function is perfect:
Real files grow and shrink, search keys may repeat, and collisions are unavoidable because a large key space is mapped into a smaller bucket space.
Hashing therefore has two separate choices:
- Hash function. Computes a numeric hash value from a key.
- Hashing scheme. Stores entries and resolves collisions after two keys land together.
Hash Functions
Hash function. A function that takes a fixed- or variable-length key and produces a fixed-size output.
For a record , hashing uses the search-key field:
- Convert the key to an integer-like hash value.
- Reduce the hash value to the bucket range .
A simple pattern is
with constants and . Good hashing spreads keys evenly, is fast to compute, and does not try to preserve key order.
| Name | Typical use or property |
|---|---|
| CRC-64 | Widely used in networking for error detection. |
| MurmurHash | Fast, general-purpose. |
| CityHash | Tuned for strings; strong on short keys (under about 64 bytes). |
| XXHash | Very fast, with good support for parallel hashing. |
Open Addressing and Linear Probing
Open addressing. A collision-handling scheme where all entries live inside one array of slots. If the home slot is occupied, the DBMS follows a probe sequence to alternative slots.
Common probe sequences differ only in the next position they try:
- Linear probing. Step by slot each time.
- Quadratic probing. Use displacements .
- Double hashing. Compute the step size from a second hash function.
Linear probing. A static open-addressing scheme where, if the home slot is occupied, the DBMS checks the next slot, then the next, until it finds the key or a free slot. The probe sequence is
Each slot stores both key and value. The key is needed because a probe must compare the searched key against the keys it passes.
Lookup is:
- Compute the home slot .
- Compare the stored key with .
- If the slot has a different key, move to the next slot in the probe sequence.
- Stop when is found or when a truly empty slot is reached.
A truly empty slot proves absence because no later key in that probe run could have jumped over it.
Example 1. Five insertions colliding at slot 2.
Inserting in order shows how later keys are displaced forward from their home slots along the probe run.
| Key | Home | Final | What happens |
|---|---|---|---|
| A | 2 | 2 | Slot 2 is free, so is stored there. |
| B | 0 | 0 | Slot 0 is free, so is stored there. |
| C | 2 | 3 | Slot 2 holds , so probe the next slot. |
| D | 3 | 4 | Slot 3 is occupied, so move one more step. |
| E | 2 | 5 | Slots 2, 3, 4 are occupied, so scan to slot 5. |
The same probe supports all operations:
- Lookup. Stop at the key or a truly empty slot.
- Insertion. Stop at the first reusable slot.
- Deletion. Find the key first, then remove it without breaking later probes.
Deletion is the subtle operation. If , , and lie in one probe run, deleting cannot make its slot truly empty. Otherwise a later lookup for may stop at that empty slot and wrongly report absence.
Tombstone. A marker meaning the slot once held an entry that has been logically deleted. It is not treated as a free slot during lookup, so probing continues past it; a new insertion may reuse it.
| Action | Reason |
|---|---|
| Delete | The slot must not become fully empty, or lookups for keys farther along the same probe run stop too early. |
| Get | The search continues past the tombstone, because may appear later in the run. |
| Insert | The tombstone slot is reusable, so deletion still frees space. |
Tombstones preserve correctness, but too many of them lengthen probe runs, so the table needs periodic cleanup or rebuilding. Other open-addressing designs (cuckoo, hopscotch, Robin Hood, Swiss tables) solve the same problem with different trade-offs in speed, memory, probe length, and update cost.
A probe run may only ever be interrupted by a truly empty slot; everything a deletion leaves behind must look "still occupied" to lookups.
Non-unique search key. A key value that may match more than one record.
Two storage choices are common:
- Separate value list. Store one copy of the key in the hash structure and keep matching values or RIDs in a list.
- Redundant keys. Store one hash-table entry per matching value, even when the key repeats.
Why Static Hashing Breaks Down
Static hashing is fast when the size estimate is right. It breaks down when that estimate becomes wrong.
- Growth creates long overflow chains or probe runs.
- Shrinkage leaves unused primary space.
- Resizing usually means rebuilding many or all pages.
- Rebuilding can block the index during reorganization.
Dynamic hashing. A family of schemes that resize incrementally instead of rebuilding the whole hash file at once.
Chained Hashing
Chained hashing. Each directory slot points to a bucket, and all entries hashing to that slot stay in its bucket; when a bucket fills, it continues through overflow pages linked into a chain.
The structure has two levels:
- Directory. An array of pointers indexed by the hash value.
- Bucket chain. The primary bucket page plus any overflow pages for that directory slot.
Operations use the same path:
- Hash the key to a directory slot.
- Follow the pointer to the bucket chain.
- Scan the chain until the key is found or the chain ends.
- Insert or delete in that chain.
Chaining avoids probe runs across the whole table, but one bucket chain can still grow too long. Splitting the overflowing bucket is the next improvement.
Extendible Hashing
Extendible hashing. A dynamic scheme with a directory of pointers to buckets. It splits only the bucket that overflows and doubles the directory only when the split needs one more hash bit.
Two depths control the structure:
- Global depth. The number of hash bits used to index the directory. At depth , the directory has entries.
- Local depth. The number of hash bits that identify one bucket. If a bucket's local depth is below the global depth, several directory entries point to that bucket.
The gap is the sharing: a bucket with local depth under global depth is pointed to by directory entries.
Assume the directory is indexed with the least significant bits of the hash. At global depth , goes to entry , and goes to entry .
Bucket A has local depth , so it is identified by two final bits. Bucket B has local depth , so both directory entries ending in point to it.
Insertion is:
- Use the last hash bits to find the directory entry, where is the global depth.
- Follow the pointer to the target bucket.
- If the bucket has room, insert the entry.
- If the bucket is full and its local depth is less than the global depth, split that bucket.
- If the bucket is full and its local depth equals the global depth, double the directory, then split that bucket.
Splitting a bucket is:
- Increase the bucket's local depth by .
- Allocate one new bucket with the same local depth.
- Redistribute the old entries and the new entry using the new local-depth bit.
- Update only the directory entries that should now point to the new bucket.
The benefit is locality. The DBMS touches the overflowing bucket and the directory entries that reference it, not the whole hash file.
Example 2. Inserting into the current directory, with .
Insert with . The last two bits are , so the entry targets bucket B. B is full, and its local depth is below the global depth , so the bucket splits without directory doubling.
The split separates the old shared bucket:
- ends in and moves to new bucket D.
- end in and stay in bucket B.
Insert and . Both end in , so both go to bucket D if it has room.
Insert . The last two bits are , so it targets bucket A. If A is full and its local depth already equals the global depth, the directory doubles from to entries before A splits.
A directory can use least significant bits (LSB) or most significant bits (MSB). The logical rule is the same. The implementation differs: with LSB, doubling copies the old directory and appends the copy; with MSB, earlier pointer positions must be rewritten more carefully.
Linear Hashing
Linear hashing. A dynamic scheme with no directory. It permits temporary overflow pages but controls them by splitting buckets gradually in a fixed round-robin order rather than splitting the bucket that overflowed.
Linear hashing keeps a pointer called next. When any bucket overflows, the DBMS splits bucket next, then advances the pointer. The bucket that overflows is not necessarily the bucket that splits.
Linear hashing uses one hash family:
where is the initial bucket count and is the current level. Each level doubles the hash range.
With : , , and .
The state is:
- Level. The current round number. At level , the table is moving from range toward .
- Next. The pointer to the next bucket that must be split.
A round starts with
base buckets.
- Buckets have already split in the current round.
- Buckets have not split yet.
- A round ends when
nextreaches . Thenlevelincreases by andnextresets to .
Behind the next pointer the world uses one more bit; in front of it the old function is still right.
Search chooses the hash function from next:
- Compute .
- If , search bucket .
- If , bucket has already split, so recompute and search that bucket.
Insertion uses the same bucket choice:
- Find the target bucket by the search rule.
- If the bucket has space, insert the entry.
- If the bucket is full, place the entry in an overflow page.
- Split bucket
nextusing . - Advance
next; if the round is complete, incrementleveland resetnextto .
Linear hashing grows without a directory. Rehashing is spread across insertions, and only one bucket is reorganized per split.
Example 3. One overflow triggers a split.
Start with buckets and , holding bucket , bucket (full), bucket , bucket .
Searching for gives . Since , bucket has not split, so no recomputation is needed.
Inserting gives . Bucket is full, so goes to an overflow page. That overflow triggers a split of bucket , not bucket .
The split uses :
- , so stays in bucket .
- , so moves to new bucket .
nextadvances to .
| Bucket | Contents | Comment |
|---|---|---|
| 0 | already split this round | |
| 1 | and overflow | not split yet |
| 2 | not split yet | |
| 3 | not split yet | |
| 4 | new bucket from the split |
After the split, searching for first gives . Since , bucket has split, so recompute and search bucket . Searching for gives , and , so bucket has not split and no recomputation is needed.
The split pointer eventually reaches every bucket, so overflow chains are reduced in round-robin order. Linear hashing still depends on a good hash function, and temporary overflow pages can lengthen lookups.
Disk-Oriented Sorting
A disk-oriented DBMS cannot assume that a table or query result fits in main memory. Algorithms run through the buffer pool and may spill data to disk. Good disk algorithms therefore favor large sequential reads and writes.
Sorting is needed for:
ORDER BY;- duplicate elimination for
DISTINCT; - grouping for
GROUP BY; - efficient bulk-loading of a B-tree.
External sorting. Sorting a file that is too large to fit in memory by creating sorted runs on disk and repeatedly merging them.
Run. A sorted piece of the file produced during external sorting.
2-way external sort. A baseline external sort where each merge step combines two runs into one larger run.
The algorithm is:
- Pass 0. Read one page, sort it in memory, and write it back as a one-page sorted run. Repeat for all pages.
- Merge passes. Merge pairs of runs. Each pass doubles the run length.
- Stop. Finish when one sorted run remains.
A 2-way merge needs three buffer pages: one input page per run and one output page.
Each pass reads and writes every page exactly once, so a pass costs I/Os. The number of passes is , giving a total of
I/Os.
External merge sort with buffers. The practical version uses all available buffer pages.
Pass 0 creates
initial runs.
The algorithm is:
- Pass 0. Read up to pages, sort them in memory, and write one sorted run.
- Merge passes. Use input buffers and one output buffer, so each merge pass combines up to runs.
- Stop. Finish when one run remains.
The pass count is
The total I/O cost is
Every pass reads and writes every page exactly once, so the cost is always per pass; all sorting optimization is about reducing the number of passes.
A larger buffer pool creates fewer initial runs and gives each merge pass a larger fan-in.
Example 4. Sorting pages with buffers.
| Pass | Runs after the pass | What happens |
|---|---|---|
| 0 | sorted runs of pages, except the last with pages. | |
| 1 | Merge runs at a time into runs; most are pages, the last is . | |
| 2 | Merge again into runs of and pages. | |
| 3 | A final merge produces one sorted file of pages. |
The pass count is , matching the four rows.
Double buffering. An I/O technique that prefetches the next page or run fragment while the current buffer is being processed.
Double buffering overlaps CPU work with disk transfer. It can reduce elapsed time, but it reserves extra buffers, so the effective merge fan-in may be smaller than .
The pass count for different file and buffer sizes shows how sharply memory helps.
| 100 | 7 | 4 | 3 | 2 | 1 | 1 |
| 1,000 | 10 | 5 | 4 | 3 | 2 | 2 |
| 10,000 | 13 | 7 | 5 | 4 | 2 | 2 |
| 100,000 | 17 | 9 | 6 | 5 | 3 | 3 |
| 1,000,000 | 20 | 10 | 7 | 5 | 3 | 3 |
| 10,000,000 | 23 | 12 | 8 | 6 | 4 | 3 |
| 100,000,000 | 26 | 14 | 9 | 7 | 4 | 4 |
| 1,000,000,000 | 30 | 15 | 10 | 8 | 5 | 4 |
With little memory, sorting needs many passes. With more memory, pass count drops sharply, and each avoided pass saves about I/Os.
Sorting with a B+-Tree
When a table already has a B-tree index on the sort attribute, the DBMS may avoid external sorting by scanning the index in key order.
The index-order scan is:
- Go to the left-most leaf page.
- Scan leaf pages from left to right.
- Fetch records in the order given by the leaf entries.
Clustering determines whether this is good:
- Clustered B-tree. Usually strong for sorting, because records are stored close to leaf order and reads are mostly sequential.
- Unclustered B-tree. Usually weak for large sorts, because leaf entries are sorted but record fetches may be random. In the worst case, the cost is close to one I/O per record.