Database Management Systems (DBMS) Module 9 – Indexing
Hashing: static, extendible and linear
How static, extendible and linear hashing place keys in buckets, why static hashing slows down as it fills, and when a hash index beats a B-tree.
What you will learn
- Explain how overflow chains slow a static hash file's lookups as its load rises
- Simulate extendible hashing with global and local depth, splits and directory doubling
- Simulate linear hashing with a split pointer and its address rule, and compare it with extendible hashing
- Decide when a hash index beats a B-tree and when it cannot help at all
Before you start
What you’ll learn · Before you start
What you will learn
- Explain how overflow chains slow a static hash file's lookups as its load rises
- Simulate extendible hashing with global and local depth, splits and directory doubling
- Simulate linear hashing with a split pointer and its address rule, and compare it with extendible hashing
- Decide when a hash index beats a B-tree and when it cannot help at all
Before you start
On this page
A hash index puts each entry in a bucket chosen by a hash function of its key, so an equality lookup reads about one page, however large the file. Static hashing fixes the number of buckets; full buckets chain overflow pages, and lookups slow down. Extendible hashing keeps a directory of 2^d slots, chosen by the last d bits of the hash value, and splits only the bucket that overflows. Linear hashing has no directory: it splits buckets in turn, behind a split pointer, whenever the file is too full.
Static hashing slows down as its overflow chains grow
With static hashing the number of buckets M is fixed when the file is created, and a key goes to bucket h(k) mod M. Each bucket is one page; when it fills, an overflow page: A page chained to a full bucket of a hash file to hold its extra keys. A lookup in that bucket reads the chain until it finds its key. is chained to it, and a lookup reads the chain until it finds its key. The program fills 100 buckets of 4 keys more and more:
# Static hashing: 100 buckets fixed in advance, one block of 4 keys per bucket, and overflow blocks chained on when
# a bucket fills. As the file grows past its design size, lookups read more and more blocks.
import random
BUCKETS, PER_BLOCK = 100, 4
def bucket_of(key):
return (key * 2654435761) % 2**32 % BUCKETS # a multiplicative hash, then the bucket number
rng = random.Random(1)
keys = rng.sample(range(10**9), 1600)
print(f"{BUCKETS} buckets of {PER_BLOCK} keys per block")
for n in (200, 400, 800, 1600):
chains = [[] for _ in range(BUCKETS)]
for k in keys[:n]:
chains[bucket_of(k)].append(k)
# A successful lookup reads the blocks up to the one that holds its key.
reads = [i // PER_BLOCK + 1 for chain in chains for i in range(len(chain))]
longest = max(-(-len(c) // PER_BLOCK) for c in chains)
overflowing = sum(len(c) > PER_BLOCK for c in chains)
print(f" {n:>5,} keys, load {n / (BUCKETS * PER_BLOCK):.1f}: {sum(reads) / n:.2f} blocks per lookup on average, "
f"longest chain {longest} blocks, {overflowing} buckets overflowing") Output
100 buckets of 4 keys per block
200 keys, load 0.5: 1.01 blocks per lookup on average, longest chain 2 blocks, 2 buckets overflowing
400 keys, load 1.0: 1.18 blocks per lookup on average, longest chain 3 blocks, 35 buckets overflowing
800 keys, load 2.0: 1.64 blocks per lookup on average, longest chain 4 blocks, 93 buckets overflowing
1,600 keys, load 4.0: 2.63 blocks per lookup on average, longest chain 6 blocks, 100 buckets overflowing
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 static_hash.py
Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Your run, in this browser
At half capacity a lookup reads 1.01 blocks. At the design size a third of the buckets already overflow, because a hash spreads keys evenly only on average, and at four times the size a lookup reads 2.63 blocks. Only a rehash into a larger file cures it; the dynamic schemes grow a bucket at a time instead.
The visualiser shows the same on 4 fixed buckets holding the eight keys of the next section; type your own keys too:
- being compared or looked at
- being moved or written
- in its final place
Pseudo-code
-
insert(k): b ← k mod m; append k to the list in bucket b -
find(k): b ← k mod m; walk the list in bucket b -
delete(k): find k in bucket b and unlink it
A table of 4 slots with separate chaining: each slot holds a list of every key that hashes there. The home slot of a key k is k mod 4.
When it finishes: 8 keys stored.
Try your own input
Bucket 1 ends up with 5, 17 and 9, so the lookup of 9 walks to the third entry of its chain.
Extendible hashing doubles the directory, not the file
Extendible hashing keeps a directory of 2^d slots, where d is the global depth: In extendible hashing, the number of last bits of a hash value that choose a directory slot. A directory of global depth d has 2^d slots.. A key’s slot is the last d bits of its hash value. Several slots may point to one bucket: a bucket with local depth: In extendible hashing, the number of last bits that all the keys of one bucket share. With global depth d, 2^(d - j) slots point to a bucket of local depth j. j holds keys that agree on their last j bits, and 2^(d − j) slots point to it. When a bucket overflows:
- If its local depth is below the global depth, it splits on its next bit: keys with that bit set move to a new bucket, both get local depth j + 1, and half of the old bucket’s slots now point to the new one.
- If its local depth equals the global depth, the directory first doubles: every slot is copied, so the new slots point to the same buckets as their twins, and then the bucket splits.
Step through eight inserts into buckets of 2 keys:
The drawing shows the whole scene; its 8 steps are listed below.
Every step as text
- One slot, one bucket. With global depth 0 the directory has a single slot, and every key goes to bucket A, which holds 2 keys. 12 and 5 fit.
- 17: double, then split. 17 finds A full, and A's local depth equals the global depth, so the directory doubles to 2 slots first. A then splits on bit 0. Even keys stay, odd keys move to the new bucket B, and 17 lands in B.
- 6 joins A. 6 is 00110 in binary. Its last bit, 0, picks slot 0, which points to A, and A has room.
- 27: a split that moves nothing. 27 ends in 1 and finds B full at local depth 1, the global depth, so the directory doubles to 4 slots. B splits on bit 1, but 5 and 17 both have 0 there, so nothing moves, and 27, ending in 11, lands in the new bucket C.
- 9: the directory doubles again. 9 ends in 01, the slot of B, which is full at local depth 2, the global depth again. The directory doubles to 8 slots, B splits on bit 2, 5 moves to the new bucket D, and 9 joins 17 in B.
- 20: a split without doubling. 20 ends in 100, a slot of A, which is full. A's local depth is 1, below the global depth 3, so four slots point to it and A splits without doubling. 6 moves to the new bucket E, and 20 joins 12.
- 15 joins C. 15 ends in 111, a slot of C, and C has room. Nothing splits.
- The finished index. Eight slots point to five buckets. A lookup reads one slot of the directory and one bucket, and only the bucket when the directory is in memory.
The same inserts as a program in 14 languages:
Python 3.14 · main.py
# Eight hash values into an extendible hash index with buckets of 2 keys.
from dataclasses import dataclass, field
CAPACITY = 2
@dataclass
class Bucket:
name: str
depth: int # local depth: the last bits all its keys share
keys: list[int] = field(default_factory=list)
class Index:
def __init__(self) -> None:
self.depth = 0 # global depth: the last bits that choose a slot
self.slots = [Bucket("A", 0)]
self.made = 1
def insert(self, h: int) -> None:
while True:
bucket = self.slots[h & ((1 << self.depth) - 1)]
if len(bucket.keys) < CAPACITY:
bucket.keys.append(h)
return
if bucket.depth == self.depth:
self.slots += self.slots # slot i + 2^d points where slot i does
self.depth += 1
self.split(bucket)
def split(self, bucket: Bucket) -> None:
bit = 1 << bucket.depth
bucket.depth += 1
new = Bucket(
"ABCDEFGH"[self.made], bucket.depth, [k for k in bucket.keys if k & bit]
)
self.made += 1
bucket.keys = [k for k in bucket.keys if not k & bit]
for i, b in enumerate(self.slots):
if b is bucket and i & bit:
self.slots[i] = new
def show(self) -> None:
for i, b in enumerate(self.slots):
bits = format(i, f"0{self.depth}b") if self.depth else "-"
print(f" {bits} -> {b.name} (local depth {b.depth}): {b.keys}")
index = Index()
for h in [12, 5, 17, 6, 27, 9, 20, 15]:
print(f"insert {h} = {h:05b}")
index.insert(h)
index.show() In Python: self.slots += self.slots appends the list to itself, so each new slot refers to the same Bucket object as its twin.
Recorded with CPython 3.14.8 on macOS 26 arm64. It runs in your browser with Pyodide 314.0.7 (CPython 3.14.2). To run it yourself: python3 main.py
JavaScript Node 24 · main.mjs
// Eight hash values into an extendible hash index with buckets of 2 keys.
const CAPACITY = 2;
class Bucket {
constructor(name, depth, keys = []) {
this.name = name;
this.depth = depth; // local depth: the last bits all its keys share
this.keys = keys;
}
}
class Index {
depth = 0; // global depth: the last bits that choose a slot
slots = [new Bucket("A", 0)];
made = 1;
insert(h) {
for (;;) {
const bucket = this.slots[h & ((1 << this.depth) - 1)];
if (bucket.keys.length < CAPACITY) {
bucket.keys.push(h);
return;
}
if (bucket.depth === this.depth) {
this.slots = [...this.slots, ...this.slots]; // slot i + 2^d points where slot i does
this.depth += 1;
}
this.split(bucket);
}
}
split(bucket) {
const bit = 1 << bucket.depth;
bucket.depth += 1;
const fresh = new Bucket(
"ABCDEFGH"[this.made],
bucket.depth,
bucket.keys.filter((k) => k & bit),
);
this.made += 1;
bucket.keys = bucket.keys.filter((k) => !(k & bit));
this.slots = this.slots.map((b, i) => (b === bucket && i & bit ? fresh : b));
}
show() {
this.slots.forEach((b, i) => {
const bits = this.depth ? i.toString(2).padStart(this.depth, "0") : "-";
console.log(` ${bits} -> ${b.name} (local depth ${b.depth}): [${b.keys.join(", ")}]`);
});
}
}
const index = new Index();
for (const h of [12, 5, 17, 6, 27, 9, 20, 15]) {
console.log(`insert ${h} = ${h.toString(2).padStart(5, "0")}`);
index.insert(h);
index.show();
} Recorded with Node.js 24.21.0 LTS (V8 13.6) on macOS 26 arm64. It runs in your browser with QuickJS (quickjs-emscripten 0.32.0). To run it yourself: node main.mjs
Java 25 LTS · Main.java
import java.util.ArrayList;
import java.util.List;
public class Main {
// Eight hash values into an extendible hash index with buckets of 2 keys.
static final int CAPACITY = 2;
// depth is the local depth: the last bits all the bucket's keys share.
record Bucket(char name, int depth, List<Integer> keys) {}
static int depth = 0; // global depth: the last bits that choose a slot
static List<Bucket> slots = new ArrayList<>(List.of(new Bucket('A', 0, new ArrayList<>())));
static int made = 1;
static String bits(int value, int width) {
String b = Integer.toBinaryString(value);
return "0".repeat(width - b.length()) + b;
}
static void insert(int h) {
while (true) {
Bucket bucket = slots.get(h & ((1 << depth) - 1));
if (bucket.keys().size() < CAPACITY) {
bucket.keys().add(h);
return;
}
if (bucket.depth() == depth) {
slots.addAll(List.copyOf(slots)); // slot i + 2^d points where slot i does
depth++;
}
split(bucket);
}
}
static void split(Bucket bucket) {
int bit = 1 << bucket.depth();
Bucket kept = new Bucket(bucket.name(), bucket.depth() + 1, new ArrayList<>());
Bucket fresh = new Bucket((char) ('A' + made++), bucket.depth() + 1, new ArrayList<>());
for (int k : bucket.keys()) {
((k & bit) != 0 ? fresh : kept).keys().add(k);
}
for (int i = 0; i < slots.size(); i++) {
if (slots.get(i) == bucket) {
slots.set(i, (i & bit) != 0 ? fresh : kept);
}
}
}
public static void main(String[] args) {
for (int h : new int[] {12, 5, 17, 6, 27, 9, 20, 15}) {
System.out.println("insert " + h + " = " + bits(h, 5));
insert(h);
for (int i = 0; i < slots.size(); i++) {
Bucket b = slots.get(i);
String slot = depth == 0 ? "-" : bits(i, depth);
System.out.println(
" " + slot + " -> " + b.name() + " (local depth " + b.depth() + "): " + b.keys());
}
}
}
} In Java: A record cannot change, so a split builds two new buckets and points every slot of the old one at the right half.
Recorded with OpenJDK 25.0.4.1 LTS (Eclipse Temurin) on macOS 26 arm64. It runs in your browser with a TeaVM 0.16.0 build in QuickJS (quickjs-emscripten 0.32.0). To run it yourself: java Main.java
C++23 · main.cpp
#include <format>
#include <print>
#include <vector>
// Eight hash values into an extendible hash index with buckets of 2 keys.
constexpr std::size_t capacity = 2;
struct Bucket {
char name;
int depth; // local depth: the last bits all its keys share
std::vector<int> keys;
};
std::vector<Bucket> buckets{{'A', 0, {}}};
std::vector<std::size_t> slots{0}; // each slot holds the position of its bucket
int depth = 0; // global depth: the last bits that choose a slot
void split(std::size_t b) {
int bit = 1 << buckets[b].depth++;
Bucket fresh{static_cast<char>('A' + buckets.size()), buckets[b].depth, {}};
std::vector<int> kept;
for (int k : buckets[b].keys)
((k & bit) ? fresh.keys : kept).push_back(k);
buckets[b].keys = kept;
for (std::size_t i = 0; i < slots.size(); ++i)
if (slots[i] == b && (i & bit))
slots[i] = buckets.size();
buckets.push_back(fresh);
}
void insert(int h) {
for (;;) {
std::size_t b = slots[h & ((1 << depth) - 1)];
if (buckets[b].keys.size() < capacity) {
buckets[b].keys.push_back(h);
return;
}
if (buckets[b].depth == depth) {
// Slot i + 2^d points where slot i does.
std::vector<std::size_t> copy = slots;
slots.insert(slots.end(), copy.begin(), copy.end());
++depth;
}
split(b);
}
}
int main() {
for (int h : {12, 5, 17, 6, 27, 9, 20, 15}) {
std::println("insert {} = {:05b}", h, h);
insert(h);
for (std::size_t i = 0; i < slots.size(); ++i) {
const Bucket &b = buckets[slots[i]];
auto bits = depth ? std::format("{:0{}b}", i, depth) : std::string("-");
std::println(" {} -> {} (local depth {}): {}", bits, b.name, b.depth,
b.keys);
}
}
} Recorded with Apple clang 21.0.0 (C++23) and GCC 16.2.0 on macOS 26 arm64. It runs in your browser with Clang in WebAssembly (YoWASP) 22.0.0-git20542-10. To run it yourself: clang++ -std=c++23 -O2 -Wall -Wextra -Werror -pedantic -fsanitize=address,undefined main.cpp -o main && ./main
C# 14 · .NET 10 · app.cs
// Eight hash values into an extendible hash index with buckets of 2 keys.
const int Capacity = 2;
int depth = 0; // global depth: the last bits that choose a slot
List<Bucket> slots = [new Bucket('A', 0)];
int made = 1;
foreach (int h in (int[])[12, 5, 17, 6, 27, 9, 20, 15])
{
Console.WriteLine($"insert {h} = {Bits(h, 5)}");
Insert(h);
for (int i = 0; i < slots.Count; i++)
{
Bucket b = slots[i];
string slot = depth == 0 ? "-" : Bits(i, depth);
string keys = string.Join(", ", b.Keys);
Console.WriteLine($" {slot} -> {b.Name} (local depth {b.Depth}): [{keys}]");
}
}
static string Bits(int value, int width) => Convert.ToString(value, 2).PadLeft(width, '0');
void Insert(int h)
{
while (true)
{
Bucket bucket = slots[h & ((1 << depth) - 1)];
if (bucket.Keys.Count < Capacity)
{
bucket.Keys.Add(h);
return;
}
if (bucket.Depth == depth)
{
slots.AddRange([.. slots]); // slot i + 2^d points where slot i does
depth++;
}
Split(bucket);
}
}
void Split(Bucket bucket)
{
int bit = 1 << bucket.Depth++;
var fresh = new Bucket((char)('A' + made++), bucket.Depth);
fresh.Keys.AddRange(bucket.Keys.Where(k => (k & bit) != 0));
bucket.Keys.RemoveAll(k => (k & bit) != 0);
for (int i = 0; i < slots.Count; i++)
{
if (slots[i] == bucket && (i & bit) != 0)
slots[i] = fresh;
}
}
class Bucket(char name, int depth)
{
public char Name { get; } = name;
// Local depth: the last bits all its keys share.
public int Depth { get; set; } = depth;
public List<int> Keys { get; } = [];
} Recorded with C# 14 on .NET 10.0.12 (SDK 10.0.401) on macOS 26 arm64. It runs in your browser with the .NET runtime for WebAssembly 10.0.12. To run it yourself: dotnet run app.cs
C23 · main.c
#include <stdio.h>
// Eight hash values into an extendible hash index with buckets of 2 keys.
enum { CAPACITY = 2 };
typedef struct {
int depth; // local depth: the last bits all its keys share
int keys[CAPACITY], n;
} Bucket;
static Bucket buckets[8];
static int made = 1, nslots = 1, slots[16];
static int depth = 0; // global depth: the last bits that choose a slot
static const char *bits(int value, int width) {
static char out[8];
for (int j = 0; j < width; j++)
out[j] = (char)('0' + (value >> (width - 1 - j) & 1));
out[width] = '\0';
return out;
}
static void split(int b) {
Bucket *old = &buckets[b], *fresh = &buckets[made];
int bit = 1 << old->depth++, kept = 0;
fresh->depth = old->depth;
for (int i = 0; i < old->n; i++)
if (old->keys[i] & bit)
fresh->keys[fresh->n++] = old->keys[i];
else
old->keys[kept++] = old->keys[i];
old->n = kept;
for (int i = 0; i < nslots; i++)
if (slots[i] == b && (i & bit))
slots[i] = made;
made++;
}
static void insert(int h) {
for (;;) {
int b = slots[h & ((1 << depth) - 1)];
if (buckets[b].n < CAPACITY) {
buckets[b].keys[buckets[b].n++] = h;
return;
}
if (buckets[b].depth == depth) {
for (int i = 0; i < nslots; i++) // slot i + 2^d points where slot i does
slots[nslots + i] = slots[i];
nslots *= 2;
depth++;
}
split(b);
}
}
int main(void) {
static const int keys[] = {12, 5, 17, 6, 27, 9, 20, 15};
for (int k = 0; k < 8; k++) {
printf("insert %d = %s\n", keys[k], bits(keys[k], 5));
insert(keys[k]);
for (int i = 0; i < nslots; i++) {
const Bucket *to = &buckets[slots[i]];
printf(" %s -> %c (local depth %d): [", depth ? bits(i, depth) : "-",
'A' + slots[i], to->depth);
for (int j = 0; j < to->n; j++)
printf(j ? ", %d" : "%d", to->keys[j]);
printf("]\n");
}
}
} In C: Buckets are named by their position, A for the first, and the directory holds those positions, so a split repoints slots by number.
Recorded with Apple clang 21.0.0 (C23) and GCC 16.2.0 on macOS 26 arm64. It runs in your browser with Clang in WebAssembly (YoWASP) 22.0.0-git20542-10. To run it yourself: clang -std=c23 -O2 -Wall -Wextra -Werror -pedantic -fsanitize=address,undefined main.c -o main && ./main
PHP 8.5 · main.php
<?php
declare(strict_types=1);
// Eight hash values into an extendible hash index with buckets of 2 keys.
const CAPACITY = 2;
final class Bucket
{
/** @param list<int> $keys */
public function __construct(
public string $name,
public int $depth, // local depth: the last bits all its keys share
public array $keys = [],
) {}
}
final class HashIndex
{
/** @param list<Bucket> $slots */
public function __construct(
public int $depth = 0, // global depth: the last bits that choose a slot
public array $slots = [new Bucket('A', 0)],
public int $made = 1,
) {}
public function insert(int $h): void
{
while (true) {
$bucket = $this->slots[$h & ((1 << $this->depth) - 1)];
if (count($bucket->keys) < CAPACITY) {
$bucket->keys[] = $h;
return;
}
if ($bucket->depth === $this->depth) {
// Slot i + 2^d points where slot i does.
$this->slots = [...$this->slots, ...$this->slots];
$this->depth++;
}
$this->split($bucket);
}
}
private function split(Bucket $bucket): void
{
$bit = 1 << $bucket->depth++;
$fresh = new Bucket('ABCDEFGH'[$this->made++], $bucket->depth);
$moves = fn(int $k): bool => ($k & $bit) !== 0;
$fresh->keys = array_values(array_filter($bucket->keys, $moves));
$bucket->keys = array_values(array_filter($bucket->keys, fn(int $k): bool => !$moves($k)));
foreach ($this->slots as $i => $b) {
if ($b === $bucket && ($i & $bit) !== 0) {
$this->slots[$i] = $fresh;
}
}
}
}
$index = new HashIndex();
foreach ([12, 5, 17, 6, 27, 9, 20, 15] as $h) {
echo 'insert ' . $h . ' = ' . sprintf('%05b', $h) . "\n";
$index->insert($h);
foreach ($index->slots as $i => $b) {
$slot = $index->depth === 0 ? '-' : sprintf("%0{$index->depth}b", $i);
$keys = implode(', ', $b->keys);
echo " {$slot} -> {$b->name} (local depth {$b->depth}): [{$keys}]\n";
}
} Recorded with PHP 8.5.11 (CLI) on macOS 26 arm64. It runs in your browser with php-wasm 0.2.0 (PHP 8.5.2). To run it yourself: php main.php
Rust 1.99 · main.rs
// Eight hash values into an extendible hash index with buckets of 2 keys.
const CAPACITY: usize = 2;
// Buckets are named A, B, C … in the order they are made.
#[derive(Default)]
struct Bucket {
depth: u32, // local depth: the last bits all its keys share
keys: Vec<u32>,
}
#[derive(Default)]
struct Index {
depth: u32, // global depth: the last bits that choose a slot
slots: Vec<usize>, // the position of each slot's bucket in buckets
buckets: Vec<Bucket>,
}
impl Index {
fn insert(&mut self, h: u32) {
loop {
let b = self.slots[(h & ((1 << self.depth) - 1)) as usize];
if self.buckets[b].keys.len() < CAPACITY {
self.buckets[b].keys.push(h);
return;
}
if self.buckets[b].depth == self.depth {
self.slots.extend_from_within(..); // slot i + 2^d points where slot i does
self.depth += 1;
}
self.split(b);
}
}
fn split(&mut self, b: usize) {
let bit = 1 << self.buckets[b].depth;
self.buckets[b].depth += 1;
let depth = self.buckets[b].depth;
let (keys, kept) = self.buckets[b].keys.iter().partition(|&&k| k & bit != 0);
self.buckets[b].keys = kept;
let fresh = self.buckets.len();
self.buckets.push(Bucket { depth, keys });
for (i, slot) in self.slots.iter_mut().enumerate() {
if *slot == b && i & bit as usize != 0 {
*slot = fresh;
}
}
}
}
fn main() {
let mut index = Index::default();
index.slots.push(0);
index.buckets.push(Bucket::default());
for h in [12, 5, 17, 6, 27, 9, 20, 15] {
println!("insert {h} = {h:05b}");
index.insert(h);
let d = index.depth as usize;
for (i, &b) in index.slots.iter().enumerate() {
let bits = match d {
0 => "-".to_string(),
_ => format!("{i:0d$b}"),
};
let (name, Bucket { depth, keys }) = (char::from(b'A' + b as u8), &index.buckets[b]);
println!(" {bits} -> {name} (local depth {depth}): {keys:?}");
}
}
} In Rust: The directory holds positions in buckets rather than references, because two slots share one bucket and Rust gives a value one owner.
Recorded with rustc 1.99.0 (edition 2024) on macOS 26 arm64. It runs in your browser with a WebAssembly build on the WASI runner (browser_wasi_shim 0.4.2). To run it yourself: rustc --edition 2024 -O main.rs && ./main
TypeScript 6.0 · main.ts
// Eight hash values into an extendible hash index with buckets of 2 keys.
const CAPACITY = 2;
class Bucket {
name: string;
depth: number; // local depth: the last bits all its keys share
keys: number[];
constructor(name: string, depth: number, keys: number[] = []) {
this.name = name;
this.depth = depth;
this.keys = keys;
}
}
class HashIndex {
depth = 0; // global depth: the last bits that choose a slot
slots: Bucket[] = [new Bucket("A", 0)];
made = 1;
insert(h: number): void {
for (;;) {
const bucket = this.slots[h & ((1 << this.depth) - 1)];
if (bucket.keys.length < CAPACITY) {
bucket.keys.push(h);
return;
}
if (bucket.depth === this.depth) {
this.slots = [...this.slots, ...this.slots]; // slot i + 2^d points where slot i does
this.depth += 1;
}
this.split(bucket);
}
}
split(bucket: Bucket): void {
const bit = 1 << bucket.depth;
bucket.depth += 1;
const moved = bucket.keys.filter((k) => (k & bit) !== 0);
const fresh = new Bucket("ABCDEFGH"[this.made], bucket.depth, moved);
this.made += 1;
bucket.keys = bucket.keys.filter((k) => (k & bit) === 0);
this.slots = this.slots.map((b, i) => (b === bucket && (i & bit) !== 0 ? fresh : b));
}
show(): void {
this.slots.forEach((b, i) => {
const bits = this.depth ? i.toString(2).padStart(this.depth, "0") : "-";
console.log(` ${bits} -> ${b.name} (local depth ${b.depth}): [${b.keys.join(", ")}]`);
});
}
}
const index = new HashIndex();
for (const h of [12, 5, 17, 6, 27, 9, 20, 15]) {
console.log(`insert ${h} = ${h.toString(2).padStart(5, "0")}`);
index.insert(h);
index.show();
} Recorded with TypeScript 6.0.3 (strict), run by Node.js 24.21.0 on macOS 26 arm64. It runs in your browser with QuickJS (quickjs-emscripten 0.32.0) after Sucrase 3.35.1. To run it yourself: npx -p typescript@6.0.3 tsc --noEmit --strict main.ts && node main.ts
Go 1.27 · main.go
package main
import (
"fmt"
"strings"
)
// Eight hash values into an extendible hash index with buckets of 2 keys.
const capacity = 2
type bucket struct {
name byte
depth int // local depth: the last bits all its keys share
keys []int
}
var (
depth = 0 // global depth: the last bits that choose a slot
slots = []*bucket{{name: 'A'}}
made = 1
)
func insert(h int) {
for {
b := slots[h&(1<<depth-1)]
if len(b.keys) < capacity {
b.keys = append(b.keys, h)
return
}
if b.depth == depth {
slots = append(slots, slots...) // slot i + 2^d points where slot i does
depth++
}
split(b)
}
}
func split(b *bucket) {
bit := 1 << b.depth
b.depth++
fresh := &bucket{name: byte('A' + made), depth: b.depth}
made++
var kept []int
for _, k := range b.keys {
if k&bit != 0 {
fresh.keys = append(fresh.keys, k)
} else {
kept = append(kept, k)
}
}
b.keys = kept
for i, s := range slots {
if s == b && i&bit != 0 {
slots[i] = fresh
}
}
}
func main() {
for _, h := range []int{12, 5, 17, 6, 27, 9, 20, 15} {
fmt.Printf("insert %d = %05b\n", h, h)
insert(h)
for i, b := range slots {
bits := "-"
if depth > 0 {
bits = fmt.Sprintf("%0*b", depth, i)
}
keys := strings.ReplaceAll(fmt.Sprint(b.keys), " ", ", ")
fmt.Printf(" %s -> %c (local depth %d): %s\n", bits, b.name, b.depth, keys)
}
}
} In Go: append(slots, slots...) doubles the slice of pointers, so both slots of each pair point to the same bucket.
Recorded with Go 1.27.1 on macOS 26 arm64. It runs in your browser with the Go compiler in WebAssembly 1.27.1. To run it yourself: go run main.go
Swift 6.4 · main.swift
// Eight hash values into an extendible hash index with buckets of 2 keys.
let capacity = 2
struct Bucket {
let name: Character
var depth: Int // local depth: the last bits all its keys share
var keys: [Int] = []
}
var depth = 0 // global depth: the last bits that choose a slot
var buckets = [Bucket(name: "A", depth: 0)]
var slots = [0] // each slot holds the position of its bucket
func bits(_ value: Int, width: Int) -> String {
let digits = String(value, radix: 2)
return String(repeating: "0", count: width - digits.count) + digits
}
func insert(_ h: Int) {
while true {
let b = slots[h & ((1 << depth) - 1)]
if buckets[b].keys.count < capacity {
buckets[b].keys.append(h)
return
}
if buckets[b].depth == depth {
slots += slots // slot i + 2^d points where slot i does
depth += 1
}
split(b)
}
}
func split(_ b: Int) {
let bit = 1 << buckets[b].depth
buckets[b].depth += 1
let letters = Array("ABCDEFGH")
var fresh = Bucket(name: letters[buckets.count], depth: buckets[b].depth)
fresh.keys = buckets[b].keys.filter { $0 & bit != 0 }
buckets[b].keys = buckets[b].keys.filter { $0 & bit == 0 }
for i in slots.indices where slots[i] == b && i & bit != 0 {
slots[i] = buckets.count
}
buckets.append(fresh)
}
for h in [12, 5, 17, 6, 27, 9, 20, 15] {
print("insert \(h) = \(bits(h, width: 5))")
insert(h)
for (i, b) in slots.enumerated() {
let slot = depth == 0 ? "-" : bits(i, width: depth)
let keys = buckets[b].keys.map(String.init).joined(separator: ", ")
print(" \(slot) -> \(buckets[b].name) (local depth \(buckets[b].depth)): [\(keys)]")
}
} In Swift: Bucket is a struct, a value, so the directory holds positions in buckets and a split changes the bucket in place by its position.
Recorded with Apple Swift 6.4 on macOS 26 arm64. It runs in your browser with a WebAssembly build on the WASI runner (browser_wasi_shim 0.4.2). To run it yourself: swiftc -O main.swift -o main && ./main
Ruby 4.0 · main.rb
# frozen_string_literal: true
# Eight hash values into an extendible hash index with buckets of 2 keys.
CAPACITY = 2
# depth is the local depth: the last bits all the bucket's keys share.
Bucket = Struct.new(:name, :depth, :keys)
# The directory of an extendible hash index.
class HashIndex
def initialize
@depth = 0 # global depth: the last bits that choose a slot
@slots = [Bucket.new('A', 0, [])]
@made = 1
end
def insert(value)
bucket = slot_of(value)
while bucket.keys.size >= CAPACITY
double if bucket.depth == @depth
split(bucket)
bucket = slot_of(value)
end
bucket.keys << value
end
def show
@slots.each_with_index do |bucket, i|
slot = @depth.zero? ? '-' : format("%0#{@depth}b", i)
puts " #{slot} -> #{bucket.name} (local depth #{bucket.depth}): #{bucket.keys}"
end
end
private
def slot_of(value)
@slots[value & ((1 << @depth) - 1)]
end
def double
@slots += @slots # slot i + 2^d points where slot i does
@depth += 1
end
def split(bucket)
bit = 1 << bucket.depth
bucket.depth += 1
moved, bucket.keys = bucket.keys.partition { |key| key.anybits?(bit) }
fresh = Bucket.new('ABCDEFGH'[@made], bucket.depth, moved)
@made += 1
repoint(bucket, fresh, bit)
end
def repoint(bucket, fresh, bit)
@slots.each_index { |i| @slots[i] = fresh if @slots[i].equal?(bucket) && i.anybits?(bit) }
end
end
index = HashIndex.new
[12, 5, 17, 6, 27, 9, 20, 15].each do |value|
puts "insert #{value} = #{format('%05b', value)}"
index.insert(value)
index.show
end In Ruby: RuboCop keeps methods small, so split hands the slot update and its message to repoint and report.
Recorded with CRuby 4.0.7 on macOS 26 arm64. It runs in your browser with ruby.wasm 2.10.1 (Ruby 4.0.0). To run it yourself: ruby main.rb
Kotlin 2.4 · Main.kt
// Eight hash values into an extendible hash index with buckets of 2 keys.
const val CAPACITY = 2
// depth is the local depth: the last bits all the bucket's keys share.
class Bucket(
val name: Char,
var depth: Int,
var keys: List<Int> = emptyList(),
)
var depth = 0 // global depth: the last bits that choose a slot
val slots = mutableListOf(Bucket('A', 0))
var made = 1
fun insert(h: Int) {
while (true) {
val bucket = slots[h and ((1 shl depth) - 1)]
if (bucket.keys.size < CAPACITY) {
bucket.keys += h
return
}
if (bucket.depth == depth) {
slots += slots.toList() // slot i + 2^d points where slot i does
depth++
}
split(bucket)
}
}
fun split(bucket: Bucket) {
val bit = 1 shl bucket.depth
bucket.depth++
val (moved, kept) = bucket.keys.partition { it and bit != 0 }
val fresh = Bucket('A' + made++, bucket.depth, moved)
bucket.keys = kept
for (i in slots.indices) {
if (slots[i] === bucket && i and bit != 0) slots[i] = fresh
}
}
fun main() {
for (h in listOf(12, 5, 17, 6, 27, 9, 20, 15)) {
println("insert $h = ${h.toString(2).padStart(5, '0')}")
insert(h)
slots.forEachIndexed { i, b ->
val slot = if (depth == 0) "-" else i.toString(2).padStart(depth, '0')
println(" $slot -> ${b.name} (local depth ${b.depth}): ${b.keys}")
}
}
} In Kotlin: keys is a read-only List that a split replaces, so bucket.keys += h builds a new list with the key added.
Recorded with Kotlin 2.4.20 on Java 25.0.4.1 on macOS 26 arm64. It runs in your browser with a WebAssembly build on the WASI runner (browser_wasi_shim 0.4.2). To run it yourself: kotlinc Main.kt -include-runtime -d main.jar && java -jar main.jar
Dart 3.13 · main.dart
// Eight hash values into an extendible hash index with buckets of 2 keys.
const capacity = 2;
// depth is the local depth: the last bits all the bucket's keys share.
class Bucket {
Bucket(this.name, this.depth, [List<int>? keys]) : keys = keys ?? [];
final String name;
int depth;
List<int> keys;
}
var depth = 0; // global depth: the last bits that choose a slot
var slots = [Bucket('A', 0)];
var made = 1;
void insert(int h) {
while (true) {
final bucket = slots[h & ((1 << depth) - 1)];
if (bucket.keys.length < capacity) {
bucket.keys.add(h);
return;
}
if (bucket.depth == depth) {
slots = [...slots, ...slots]; // slot i + 2^d points where slot i does
depth++;
}
split(bucket);
}
}
void split(Bucket bucket) {
final bit = 1 << bucket.depth++;
final moved = bucket.keys.where((k) => k & bit != 0).toList();
final fresh = Bucket('ABCDEFGH'[made++], bucket.depth, moved);
bucket.keys = bucket.keys.where((k) => k & bit == 0).toList();
for (var i = 0; i < slots.length; i++) {
if (identical(slots[i], bucket) && i & bit != 0) slots[i] = fresh;
}
}
void main() {
for (final h in [12, 5, 17, 6, 27, 9, 20, 15]) {
print('insert $h = ${h.toRadixString(2).padLeft(5, '0')}');
insert(h);
for (final (i, b) in slots.indexed) {
final slot = depth == 0 ? '-' : i.toRadixString(2).padLeft(depth, '0');
print(' $slot -> ${b.name} (local depth ${b.depth}): ${b.keys}');
}
}
} Recorded with Dart 3.13.5 on macOS 26 arm64. It runs in your browser with a WebAssembly build by dart2wasm 3.13.5. To run it yourself: dart run main.dart
What every language printed
insert 12 = 01100 - -> A (local depth 0): [12] insert 5 = 00101 - -> A (local depth 0): [12, 5] insert 17 = 10001 0 -> A (local depth 1): [12] 1 -> B (local depth 1): [5, 17] insert 6 = 00110 0 -> A (local depth 1): [12, 6] 1 -> B (local depth 1): [5, 17] insert 27 = 11011 00 -> A (local depth 1): [12, 6] 01 -> B (local depth 2): [5, 17] 10 -> A (local depth 1): [12, 6] 11 -> C (local depth 2): [27] insert 9 = 01001 000 -> A (local depth 1): [12, 6] 001 -> B (local depth 3): [17, 9] 010 -> A (local depth 1): [12, 6] 011 -> C (local depth 2): [27] 100 -> A (local depth 1): [12, 6] 101 -> D (local depth 3): [5] 110 -> A (local depth 1): [12, 6] 111 -> C (local depth 2): [27] insert 20 = 10100 000 -> A (local depth 2): [12, 20] 001 -> B (local depth 3): [17, 9] 010 -> E (local depth 2): [6] 011 -> C (local depth 2): [27] 100 -> A (local depth 2): [12, 20] 101 -> D (local depth 3): [5] 110 -> E (local depth 2): [6] 111 -> C (local depth 2): [27] insert 15 = 01111 000 -> A (local depth 2): [12, 20] 001 -> B (local depth 3): [17, 9] 010 -> E (local depth 2): [6] 011 -> C (local depth 2): [27, 15] 100 -> A (local depth 2): [12, 20] 101 -> D (local depth 3): [5] 110 -> E (local depth 2): [6] 111 -> C (local depth 2): [27, 15]
Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs. Runs on this device, in your browser. The first run downloads JavaScript (about 0.6 MB), which is kept for the next runs. Runs on this device: a JavaScript build of this program (about 12 KB), made when the page was built. The first run also downloads the JavaScript runner (about 0.6 MB), which is kept for the next runs. To change the code, copy it and run it with the command below. Runs on this device: a WebAssembly build of this program (about 157 KB), made when the page was built. Edit runs your changes in the browser: the first edited run downloads the C and C++ compiler (about 29.1 MB), which is kept for the next runs. It is best on a computer. Runs on this device, in your browser. The first run downloads the C and C++ compiler (about 29.1 MB), which is kept for the next runs. It is best on a computer. Runs on this device: a .NET build of this program (about 5 KB), made when the page was built. The first run also downloads .NET (about 3.6 MB), which is kept for the next runs. Edit runs your changes in the browser: the first edited run downloads .NET (about 8.9 MB), which is kept for the next runs. Runs on this device, in your browser. The first run downloads .NET (about 8.9 MB), which is kept for the next runs. Runs on this device: a WebAssembly build of this program (about 9 KB), made when the page was built. Edit runs your changes in the browser: the first edited run downloads the C and C++ compiler (about 29.1 MB), which is kept for the next runs. It is best on a computer. Runs on this device, in your browser. The first run downloads the C and C++ compiler (about 29.1 MB), which is kept for the next runs. It is best on a computer. Runs on this device, in your browser. The first run downloads PHP (about 4.3 MB), which is kept for the next runs. Runs on this device: a WebAssembly build of this program (about 22 KB), made when the page was built. To change the code, copy it and run it with the command below. Runs on this device, in your browser. The first run downloads the JavaScript runner (about 0.9 MB), which is kept for the next runs. Runs on this device: a WebAssembly build of this program (about 724 KB), made when the page was built. Edit runs your changes in the browser: the first edited run downloads the Go compiler (about 18.7 MB), which is kept for the next runs. Runs on this device, in your browser. The first run downloads the Go compiler (about 18.7 MB), which is kept for the next runs. Runs on this device: a WebAssembly build of this program (about 1.7 MB), made when the page was built. To change the code, copy it and run it with the command below. Runs on this device, in your browser. The first run downloads Ruby (about 10.2 MB), which is kept for the next runs. Runs on this device: a WebAssembly build of this program (about 183 KB), made when the page was built. To change the code, copy it and run it with the command below. Runs on this device: a WebAssembly build of this program (about 15 KB), made when the page was built. To change the code, copy it and run it with the command below.Your run, in this browser
The directory after the eighth insert, with the slots that share each bucket:
The extendible hash index after the eight inserts of the example
Text description of the diagram
The directory has global depth 3, so it has 8 slots, one for each value of the last 3 bits of a hash value: 000, 001, 010, 011, 100, 101, 110 and 111. Each slot points to a bucket that holds up to 2 keys.
- Bucket A, local depth 2, holds 12 and 20. Slots 000 and 100 point to it: its keys share the last 2 bits, 00.
- Bucket B, local depth 3, holds 17 and 9. Only slot 001 points to it.
- Bucket E, local depth 2, holds 6. Slots 010 and 110 point to it.
- Bucket C, local depth 2, holds 27 and 15. Slots 011 and 111 point to it.
- Bucket D, local depth 3, holds 5. Only slot 101 points to it.
A bucket of local depth d is shared by 2 to the power (3 minus d) slots. Splitting A, E or C only repoints one of its two slots to the new bucket; splitting B or D, which already use all 3 bits, would double the directory first.
The first overflow, at 17, doubles the directory and separates odd keys from even ones. At 27 it doubles again, but the split moves nothing, because 5 and 17 agree on their second bit. At 20 the bucket of 12 and 6 splits without doubling, since its local depth, 1, was below the global depth, 3.
Key idea
Only the bucket that overflows splits, and the directory doubles only when that bucket already uses every bit the directory looks at.
Checkpoint
Check: Static hashing and its overflow chains · Extendible hashing: global and local depth
Linear hashing splits buckets in turn, behind a pointer
Linear hashing keeps the buckets in an array and splits them in order, 0, then 1, then 2, whatever bucket overflowed. A split pointer: In linear hashing, the next bucket to split. A key whose address under the round's hash function falls below it uses the next round's function instead. marks the next bucket to split; a round ends when every bucket of the round has split once, doubling their number. A key’s address comes from the round’s hash function, or from the next round’s when the first result is below the pointer. A bucket that overflows before the pointer reaches it keeps an overflow page meanwhile: a few overflow pages instead of a directory (Larson: Dynamic Hash Tables). Step through 4 buckets of 2 keys:
The drawing shows the whole scene; its 7 steps are listed below.
Every step as text
- Round 0, pointer at bucket 0. Four buckets of 2 keys, and h(k) = k mod 4. Six keys fill 6 of the 8 places, a load of 0.75. The split pointer is at bucket 0, the next bucket to split.
- 13 overflows bucket 1. 13 mod 4 = 1, and bucket 1 is full, so 13 goes to an overflow page chained to it. The load is now 7 of 8, above 0.75.
- Bucket 0 splits, not bucket 1. The load passed its limit, so the bucket at the pointer splits, whichever bucket overflowed. Bucket 0's keys are rehashed with k mod 8. 8 stays, and 12 moves to the new bucket 4.
- The pointer moves on. The pointer moves to bucket 1, which keeps its overflow page until the pointer reaches it. The load is 7 of 10 now.
- A lookup below the pointer. A lookup of 12 computes 12 mod 4 = 0. Bucket 0 is below the pointer, so it has split this round, and the next round's function decides where 12 is, 12 mod 8 = 4.
- 3 arrives, bucket 1 splits. 3 mod 4 = 3, so 3 joins bucket 3, and the load is 8 of 10, above 0.75 again. Bucket 1, now at the pointer, splits with k mod 8. 9 stays, 5 and 13 move to bucket 5, and the overflow page is gone.
- Pointer at bucket 2. The pointer is at bucket 2. Once it has passed bucket 3, the round ends with 8 buckets, every key uses k mod 8, and the pointer starts again at 0.
Bucket 0 split although bucket 1 had overflowed, because the pointer was at bucket 0.
A hash index answers equality and nothing else
PostgreSQL’s hash indexes support only equality, one column and no uniqueness, and store a 32-bit hash code instead of the value, which keeps them small for long keys such as URLs (PostgreSQL 18: Hash Indexes); they have been crash-safe since PostgreSQL 10 (release notes). Answer three questions about your column:
Answer the questions above: the table then marks what fits your case and says why the rest does not.
| Index | Equality lookups | Ranges, sorting, prefixes | Uniqueness | Several columns | Size with long values |
|---|---|---|---|---|---|
| Hash index Stores: a 4-byte hash code per row | Good one bucket, about one page | Poor cannot help: a hash keeps no order | Poor cannot enforce it in PostgreSQL | Poor one column only in PostgreSQL | Good small: 4 bytes of hash code, however long the value |
| B-tree Stores: the values, in sorted order | Good one path from the root, a few pages | Good reads the values in sorted order | Good a unique B-tree enforces it | Good several columns in one index | Mixed grows with the values it stores |
How the answers decide
- “Does any query on the column use a range, ORDER BY or a prefix match?” Yes, at least one: rules out Hash index. A hash index stores hash codes with no order, so a range, a sort or a prefix match cannot use it.
- “Must every value in the column be unique?” Yes, duplicates are refused: rules out Hash index. PostgreSQL's hash indexes cannot enforce uniqueness, so a unique B-tree is needed anyway.
- “Are the values long, such as URLs or long tokens?” Yes, tens of bytes or more and “Does any query on the column use a range, ORDER BY or a prefix match?” No, exact values only and “Must every value in the column be unique?” No: adds a caveat to B-tree. Each B-tree entry holds the whole value, so with long values it is larger than a hash index of 4-byte codes.
- “Are the values long, such as URLs or long tokens?” No, short numbers or codes: adds a caveat to Hash index. With short values a B-tree is about as small, and it also serves ranges, sorting and uniqueness.
Every verdict follows from one fact: a hash code keeps no order and says nothing about neighbouring values, so only a lookup by the exact value can use it, while a B-tree keeps the values themselves, sorted.
A hash index suits exact lookups on long values that need no ranges and no uniqueness; elsewhere a B-tree, the default, wins.
Checkpoint
Check: Linear hashing and its split pointer · When a hash index can help
Practise: the three steps of an extendible insert
The exercise asks for the three pieces of an extendible insert: a value’s slot, a bucket’s split and the test for doubling.
Exercise · Easy · Python
The three decisions of an extendible hashing insert
Write the three functions in extendible.py that an extendible hashing insert needs. Keys are given as their hash values, and the directory is indexed by their last bits.
slot(h, global_depth): the directory slot of hash valueh, its lastglobal_depthbits as a number.split(keys, local_depth): a full bucket with this local depth splits. Return(stay, move): the keys whose bit numberlocal_depthis 0 stay, those where it is 1 move to the new bucket. Keep each list in the order given. Bits are numbered from 0, the last one.doubles(local_depth, global_depth): whether the directory must double before the bucket can split.
For example, slot(12, 3) is 4, because 12 is 1100 in binary and its last 3 bits, 100, are 4.
Starter code · extendible.py
def slot(h, global_depth):
"""The directory slot of hash value h: its last global_depth bits."""
return 0
def split(keys, local_depth):
"""(stay, move): split a full bucket of this local depth on bit number local_depth."""
return [], []
def doubles(local_depth, global_depth):
"""Whether the directory must double before a bucket of this local depth can split."""
return False The sample tests · test_extendible.py
from extendible import doubles, slot, split
def test_slot():
"""The last global_depth bits of the hash value"""
assert slot(12, 3) == 4
assert slot(27, 2) == 3
assert slot(9, 0) == 0
assert slot(15, 4) == 15
def test_split_moves_keys_with_the_next_bit_set():
"""Bit number local_depth decides who moves"""
assert split([12, 6], 1) == ([12], [6])
assert split([5, 17, 9], 2) == ([17, 9], [5])
def test_split_can_move_nothing():
"""5 and 17 agree on bit 1, so both stay"""
assert split([5, 17], 1) == ([5, 17], [])
def test_doubles():
"""Only a bucket that already uses every directory bit forces a doubling"""
assert doubles(2, 2) is True
assert doubles(1, 3) is False
assert doubles(0, 0) is True A hint
h & ((1 << d) - 1) keeps the last d bits of h, and (h >> b) & 1 is bit number b. A directory of 2^g slots can give a bucket at most g bits of its own, so it must double exactly when the bucket already uses all of them.
Results of the sample tests
| Test | Result | Details |
|---|
What your code printed
The sample tests run on this device, in your browser (Pyodide): nothing is sent to mysmartcopilot.com. The first run downloads Python (about 13.5 MB), which is kept for the next runs. A check in your browser is feedback for you, not proof that the code is right for every input.
Then test yourself: the questions begin with your weakest idea, and each right answer brings a harder one.
Practice that adapts to you
Practise this lesson
10 questions about 6 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.
Extendible hashing in depth, with exam-style sequences Pro: opens with a Pro, Premium or Ultimate pass
Extendible hashing is short to state and easy to get wrong in the corner cases: splits that move nothing, keys that share many bits, and deletions that should shrink the directory. This part works through each, then checks four exam-style sequences with a program.
In this part:
- Insertion in full, with the loop that most answers miss
- Keys that share their last bits, reproduced
- Deletion: buddies merge, the directory halves
- What the directory costs, in numbers
- Four exam-style sequences, worked out by the simulator
4 runnable examples (Python) · about 6 minutes
Opening this part with your pass…
This part opens with any MySmartCoPilot pass.
- Pro pass · 1 week₹79$2.99
- 30 days₹179$5.99
- 1 year₹1,699$45
One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).
Linear hashing in depth, and the two schemes compared with numbers Pro: opens with a Pro, Premium or Ultimate pass
Linear hashing avoids the directory by giving up control over which bucket splits. This part states its address rule exactly, traces it, and measures both dynamic schemes against a static file on 100,000 keys.
In this part:
- The address rule and the split pointer
- 100,000 keys in three schemes, measured
- Hash tables inside the query engine
- Choosing between the schemes
2 runnable examples (Python) · about 8 minutes
Opening this part with your pass…
This part opens with any MySmartCoPilot pass.
- Pro pass · 1 week₹79$2.99
- 30 days₹179$5.99
- 1 year₹1,699$45
One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).
Interview questions
-
Fresher · Warm-up
In extendible hashing, what do the global depth and a bucket's local depth mean, and when does the directory double?
The global depth d is the number of hash bits the directory uses: it has 2^d slots, and a key's slot is the last d bits of its hash value. A bucket's local depth j is the number of last bits that all its keys share, so 2^(d - j) slots point to it. When a full bucket receives a key, it splits on its next bit, and both halves get local depth j + 1. If j already equals d, no spare slot exists for the new half, so the directory doubles first: every slot is copied, the global depth grows by one, and then the bucket splits. A bucket whose local depth is below the global depth splits without changing the directory's size.
-
Mid to senior
A service looks up sessions by a 64-character token about 40,000 times a second and never by range. Would you use a hash index or a B-tree in PostgreSQL? Defend the choice with index sizes, lookup costs and what you give up.
-
Mid to senior
An extendible hash index's directory has grown to 2^20 slots although the table holds only 50,000 rows in buckets of 100 entries. What could cause it, how would you confirm it, and how would you fix it?
-
Fresher
Trace linear hashing with 2 starting buckets of 2 keys and h(k) = k mod (2 x 2^round). Insert 6, 11, 3, 8, 15, 22, 14 and 9, splitting whenever the load passes 0.75. Give the buckets, the split pointer and any overflow pages after each split.
Key takeaways
- A hash index finds a key from its exact value in about one page read, and cannot serve ranges, sorting or prefixes.
- Static hashing slows down as it fills: 1.01 blocks per lookup at half capacity, 2.63 at four times its design size.
- Extendible hashing doubles its directory only when a full bucket already uses all d bits; otherwise it splits just that bucket.
- Linear hashing splits buckets in order behind a split pointer and needs no directory, at the cost of short-lived overflow pages.
Terms in this lesson
- global depth
- In extendible hashing, the number of last bits of a hash value that choose a directory slot. A directory of global depth d has 2^d slots.
- local depth
- In extendible hashing, the number of last bits that all the keys of one bucket share. With global depth d, 2^(d - j) slots point to a bucket of local depth j.
- overflow page
- A page chained to a full bucket of a hash file to hold its extra keys. A lookup in that bucket reads the chain until it finds its key.
- split pointer
- In linear hashing, the next bucket to split. A key whose address under the round's hash function falls below it uses the next round's function instead.
References
- R. Fagin, J. Nievergelt, N. Pippenger and H. R. Strong: Extendible Hashing, a Fast Access Method for Dynamic Files (ACM Transactions on Database Systems)
- P.-A. Larson: Dynamic Hash Tables (Communications of the ACM)
- PostgreSQL 18 Documentation: Hash Indexes (The PostgreSQL Global Development Group)
- PostgreSQL 18 Documentation: Index Types (The PostgreSQL Global Development Group)
- PostgreSQL 10 release notes: write-ahead logging for hash indexes (The PostgreSQL Global Development Group)
- PostgreSQL 18 Documentation: Resource Consumption (work_mem and hash_mem_multiplier) (The PostgreSQL Global Development Group)
- R. Pagh and F. F. Rodler: Cuckoo Hashing (Journal of Algorithms)
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress