knowledge

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. O(n)O(n) in the number of entries.
  • Average lookup, insert, delete. O(1)O(1) under uniform hashing, because only a small collision set is inspected.
  • Worst case. O(n)O(n), 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 NN primary buckets, the bucket for key kk is

bucket(k)=h(k)modN.\text{bucket}(k) = h(k) \bmod N.

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.
01N-1overflowh(k)h(k) mod NBucket 0primary pagek1, k4k7Bucket 1primary pagek2, k5k8Bucket N-1primary pagek3, k6k9Overflowextra pagek10k11
Static hashing: a fixed set of primary buckets, plus overflow pages when a bucket fills up.

The operations are direct:

  1. Compute h(k)modNh(k) \bmod N.
  2. Go to the corresponding primary bucket page.
  3. 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:
k1k2h(k1)h(k2).k_1 \neq k_2 \Rightarrow h(k_1) \neq h(k_2).

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 rr, hashing uses the search-key field:

  1. Convert the key to an integer-like hash value.
  2. Reduce the hash value to the bucket range 0,,N10,\dots,N-1.

A simple pattern is

h(k)=ak+b,bucket(k)=h(k)modN,h(k) = a \cdot k + b, \qquad \text{bucket}(k) = h(k) \bmod N,

with constants aa and bb. Good hashing spreads keys evenly, is fast to compute, and does not try to preserve key order.

NameTypical use or property
CRC-64Widely used in networking for error detection.
MurmurHashFast, general-purpose.
CityHashTuned for strings; strong on short keys (under about 64 bytes).
XXHashVery 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 11 slot each time.
  • Quadratic probing. Use displacements 12,22,32,1^2,2^2,3^2,\dots.
  • 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

sloti(k)=(h(k)+i)modN,i=0,1,2,\text{slot}_i(k) = \bigl(h(k) + i\bigr) \bmod N, \qquad i = 0,1,2,\dots

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:

  1. Compute the home slot h(k)modNh(k) \bmod N.
  2. Compare the stored key with kk.
  3. If the slot has a different key, move to the next slot in the probe sequence.
  4. Stop when kk 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.

CDEBACDE012home3456after deleting C:Dtombstone keeps the probe chain intact
Linear probing: collisions displace keys to the next free slot; deletions leave tombstones.

Example 1. Five insertions colliding at slot 2.

Inserting in order A,B,C,D,EA,B,C,D,E shows how later keys are displaced forward from their home slots along the probe run.

KeyHomeFinalWhat happens
A22Slot 2 is free, so AA is stored there.
B00Slot 0 is free, so BB is stored there.
C23Slot 2 holds AA, so probe the next slot.
D34Slot 3 is occupied, so move one more step.
E25Slots 2, 3, 4 are occupied, so scan to slot 5.

The same probe supports all operations:

  1. Lookup. Stop at the key or a truly empty slot.
  2. Insertion. Stop at the first reusable slot.
  3. Deletion. Find the key first, then remove it without breaking later probes.

Deletion is the subtle operation. If CC, DD, and EE lie in one probe run, deleting CC cannot make its slot truly empty. Otherwise a later lookup for DD 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.

ActionReason
Delete CCThe slot must not become fully empty, or lookups for keys farther along the same probe run stop too early.
Get DDThe search continues past the tombstone, because DD may appear later in the run.
Insert FFThe 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:

  1. Hash the key to a directory slot.
  2. Follow the pointer to the bucket chain.
  3. Scan the chain until the key is found or the chain ends.
  4. Insert or delete in that chain.
Bucket pointersBucket 0B | valBucket 1A | valC | valBucket 2OverflowD | val
Chained hashing: each directory slot points to one bucket; collisions extend the bucket through overflow pages.

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 dd, the directory has 2d2^d 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 \ell under global depth dd is pointed to by 2d2^{d-\ell} directory entries.

Assume the directory is indexed with the least significant bits of the hash. At global depth 22, h(r)=5=1012h(r)=5=101_2 goes to entry 0101, and h(r)=7=1112h(r)=7=111_2 goes to entry 1111.

2global depth000110112Bucket A4 12 32 161Bucket B1 5 7 132Bucket C10
Extendible hashing: several directory entries may point to the same bucket when the local depth is below the global depth.

Bucket A has local depth 22, so it is identified by two final bits. Bucket B has local depth 11, so both directory entries ending in 11 point to it.

Insertion is:

  1. Use the last dd hash bits to find the directory entry, where dd is the global depth.
  2. Follow the pointer to the target bucket.
  3. If the bucket has room, insert the entry.
  4. If the bucket is full and its local depth is less than the global depth, split that bucket.
  5. If the bucket is full and its local depth equals the global depth, double the directory, then split that bucket.

Splitting a bucket is:

  1. Increase the bucket's local depth by 11.
  2. Allocate one new bucket with the same local depth.
  3. Redistribute the old entries and the new entry using the new local-depth bit.
  4. 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 h(x)=xh(x)=x.

Insert 21=(10101)221=(10101)_2 with h(x)=xh(x)=x. The last two bits are 0101, so the entry targets bucket B. B is full, and its local depth 11 is below the global depth 22, so the bucket splits without directory doubling.

The split separates the old shared bucket:

  • 7=11127=111_2 ends in 1111 and moves to new bucket D.
  • 1,5,13,211,5,13,21 end in 0101 and stay in bucket B.
2global depth000110112Bucket A4 12 32 162Bucket B1 5 13 2121 just inserted2Bucket C102Bucket D (new)7
After inserting 21: bucket B splits into B and D; entry 11 now points to D instead of B.

Insert 19=(10011)219=(10011)_2 and 15=(01111)215=(01111)_2. Both end in 1111, so both go to bucket D if it has room.

Insert 20=(10100)220=(10100)_2. The last two bits are 0000, so it targets bucket A. If A is full and its local depth already equals the global depth, the directory doubles from 222^2 to 232^3 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.

overflowsplit via h1next012348already split5, 9, 1367, 1120new image bucket17
Linear hashing: next marks the bucket to split; the new bucket receives entries that hash differently under the next-level function.

Linear hashing uses one hash family:

hi(k)=h(k)mod(2iN),h_i(k) = h(k) \bmod \left(2^i N\right),

where NN is the initial bucket count and ii is the current level. Each level doubles the hash range.

With N=4N=4: h0(k)=h(k)mod4h_0(k)=h(k)\bmod 4, h1(k)=h(k)mod8h_1(k)=h(k)\bmod 8, and h2(k)=h(k)mod16h_2(k)=h(k)\bmod 16.

The state is:

  • Level. The current round number. At level ii, the table is moving from range 2iN2^iN toward 2i+1N2^{i+1}N.
  • Next. The pointer to the next bucket that must be split.

A round starts with

Nlevel=N2levelN_{\text{level}} = N \cdot 2^{\text{level}}

base buckets.

  • Buckets 0,,next10,\dots,\text{next}-1 have already split in the current round.
  • Buckets next,,Nlevel1\text{next},\dots,N_{\text{level}}-1 have not split yet.
  • A round ends when next reaches NlevelN_{\text{level}}. Then level increases by 11 and next resets to 00.

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:

  1. Compute b=hlevel(k)b=h_{\text{level}}(k).
  2. If bnextb \geq \text{next}, search bucket bb.
  3. If b<nextb < \text{next}, bucket bb has already split, so recompute b=hlevel+1(k)b=h_{\text{level}+1}(k) and search that bucket.

Insertion uses the same bucket choice:

  1. Find the target bucket by the search rule.
  2. If the bucket has space, insert the entry.
  3. If the bucket is full, place the entry in an overflow page.
  4. Split bucket next using hlevel+1h_{\text{level}+1}.
  5. Advance next; if the round is complete, increment level and reset next to 00.

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 N=4N=4 buckets and next=0\text{next}=0, holding bucket 0:{8,20}0:\{8,20\}, bucket 1:{5,9,13}1:\{5,9,13\} (full), bucket 2:{6}2:\{6\}, bucket 3:{7,11}3:\{7,11\}.

Searching for 66 gives 6mod4=26\bmod 4 = 2. Since 2next2\ge\text{next}, bucket 22 has not split, so no recomputation is needed.

Inserting 1717 gives 17mod4=117\bmod 4=1. Bucket 11 is full, so 1717 goes to an overflow page. That overflow triggers a split of bucket next=0\text{next}=0, not bucket 11.

The split uses h1(k)=kmod8h_1(k)=k\bmod 8:

  • 8mod8=08\bmod 8=0, so 88 stays in bucket 00.
  • 20mod8=420\bmod 8=4, so 2020 moves to new bucket 44.
  • next advances to 11.
BucketContentsComment
088already split this round
15,9,135, 9, 13 and overflow 1717not split yet
266not split yet
37,117, 11not split yet
42020new bucket from the split

After the split, searching for 2020 first gives 20mod4=020\bmod 4=0. Since 0<next0<\text{next}, bucket 00 has split, so recompute 20mod8=420\bmod 8=4 and search bucket 44. Searching for 99 gives 9mod4=19\bmod 4=1, and 1next1\ge\text{next}, so bucket 11 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.

1 fully sorted filePass 0ceil(N/B) sorted runs ofB pages eachPass 1each pass merges B-1runs into fewer, largerrunsFinal
External merge sort: pass 0 creates sorted runs; later passes merge B-1 runs at a time until one fully sorted file remains. Each pass costs 2N I/Os.

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:

  1. Pass 0. Read one page, sort it in memory, and write it back as a one-page sorted run. Repeat for all NN pages.
  2. Merge passes. Merge pairs of runs. Each pass doubles the run length.
  3. 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 2N2N I/Os. The number of passes is 1+log2N1+\lceil\log_2 N\rceil, giving a total of

2N(1+log2N)2N\left(1+\lceil\log_2 N\rceil\right)

I/Os.

External merge sort with BB buffers. The practical version uses all available buffer pages.

Pass 0 creates

R0=NBR_0 = \left\lceil \frac{N}{B} \right\rceil

initial runs.

The algorithm is:

  1. Pass 0. Read up to BB pages, sort them in memory, and write one sorted run.
  2. Merge passes. Use B1B-1 input buffers and one output buffer, so each merge pass combines up to B1B-1 runs.
  3. Stop. Finish when one run remains.

The pass count is

P=1+logB1R0.P = 1 + \left\lceil \log_{B-1} R_0 \right\rceil.

The total I/O cost is

2NP.2N \cdot P.

Every pass reads and writes every page exactly once, so the cost is always 2N2N 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 N=108N=108 pages with B=5B=5 buffers.

PassRuns after the passWhat happens
0108/5=22\lceil 108/5\rceil=222222 sorted runs of 55 pages, except the last with 33 pages.
122/4=6\lceil 22/4\rceil=6Merge 44 runs at a time into 66 runs; most are 2020 pages, the last is 88.
26/4=2\lceil 6/4\rceil=2Merge again into 22 runs of 8080 and 2828 pages.
311A final merge produces one sorted file of 108108 pages.

The pass count is 1+log422=1+3=41+\lceil\log_4 22\rceil = 1+3 = 4, 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 B1B-1.

The pass count PP for different file and buffer sizes shows how sharply memory helps.

NNB=3B=3B=5B=5B=9B=9B=17B=17B=129B=129B=257B=257
100743211
1,0001054322
10,0001375422
100,0001796533
1,000,00020107533
10,000,00023128643
100,000,00026149744
1,000,000,000301510854

With little memory, sorting needs many passes. With more memory, pass count drops sharply, and each avoided pass saves about 2N2N 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:

  1. Go to the left-most leaf page.
  2. Scan leaf pages from left to right.
  3. 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.