Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

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.

  • Intermediate
  • 20 minutes
  • Examples run with sql.js 1.14.2, Python 3.14.8 and Pyodide 314.0.7
  • By MySmartCoPilot
Free core, Pro depth: 7 of 20 minutes free
Free: Static hashing slows down as its overflow chains grow · Extendible hashing doubles the directory, not the file · Linear hashing splits buckets in turn, behind a pointer · 2 more sections. With a pass: 2 in-depth parts and 3 interview answers, about 13 minutes.

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:

Blocks per lookup as a static hash file fills Python · intro/static_hash.py
# 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

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:

Interactive · step through it Eight keys in 4 fixed buckets, then a lookup of 9 Each key goes to slot k mod m; watch what happens when two keys want the same slot.
Hash table of 4 slots, 0 keys. All empty.0empty1empty2empty3empty
  • being compared or looked at
  • being moved or written
  • in its final place

Pseudo-code

  1. insert(k): b ← k mod m; append k to the list in bucket b
  2. find(k): b ← k mod m; walk the list in bucket b
  3. 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.

Slots probed 0
Collisions 0
Load factor 0.00

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:

Interactive · step by step Eight inserts into an extendible hash index Step through the inserts. Each bucket shows its local depth and keys; the directory shows its global depth.
Eight inserts into an extendible hash index, step 8 of 8. Showing directory (global depth 3, 8 slots), bucket A (local depth 2: 12, 20), bucket B (local depth 3: 17, 9), bucket C (local depth 2: 27, 15), bucket D (local depth 3: 5), bucket E (local depth 2: 6).directoryglobal depth 3, 8 slotsbucket Alocal depth 2: 12, 20bucket Blocal depth 3: 17, 9bucket Clocal depth 2: 27, 15bucket Dlocal depth 3: 5bucket Elocal depth 2: 6

The drawing shows the whole scene; its 8 steps are listed below.

Every step as text
  1. 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.
  2. 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.
  3. 6 joins A. 6 is 00110 in binary. Its last bit, 0, picks slot 0, which points to A, and A has room.
  4. 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.
  5. 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.
  6. 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.
  7. 15 joins C. 15 ends in 111, a slot of C, and C has room. Nothing splits.
  8. 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:

One program · 14 languages Eight inserts into an extendible hash index one bucket read per lookup; a doubling copies every slot

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]

The directory after the eighth insert, with the slots that share each bucket:

A directory of 8 slots, 000 to 111, pointing to 5 buckets; buckets of local depth 2 have two slots each, those of depth 3 one.Extendible hashing: global depth 3slots use the last 3 bits; a bucket may use fewerdirectory000001010011100101110111buckets (2 keys each)Alocal depth 212, 20Blocal depth 317, 9Elocal depth 26Clocal depth 227, 15Dlocal depth 35

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

  1. Question 1 of 3 In the static run, 800 keys fill 100 buckets of 4 keys a block, a load of 2.0. How many blocks does a successful lookup read on average? Give two decimals.

    Type a number.

    Show the answer: Question 1 of 3

    Answer: 1.64 blocks (anything from 1.635 to 1.645 counts)

    The run printed 1.64 blocks per lookup at a load of 2.0, with 93 of the 100 buckets overflowing. At the design size, a load of 1.0, it was 1.18, and at a load of 4.0 it was 2.63.

  2. Question 2 of 3 An extendible hash index has global depth 4. How many directory slots point to a bucket whose local depth is 2?

    Type a number.

    A hint

    The bucket's keys share their last 2 bits; the directory looks at 4.

    Show the answer: Question 2 of 3

    Answer: 4 slots

    The bucket's keys share their last 2 bits, and the directory looks at 4. The 2 bits left over can take 4 values, so 2 to the power (4 − 2) = 4 slots lead to the bucket.

  3. Question 3 of 3 A key arrives for a full bucket with local depth 2, and the global depth is 3. What happens first?

    Choose one answer.

    Show the answer: Question 3 of 3

    Answer: The bucket splits on bit 2, and only directory slots change where they point

    The directory doubles only when the full bucket's local depth equals the global depth. Here two slots point to the bucket, so the split only repoints one of them, as the split at 20 did in the figure.

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:

Interactive · step by step Linear hashing splits the bucket at the pointer Step through two inserts, two splits and a lookup. Watch which bucket splits, and which one waits with an overflow page.
Linear hashing splits the bucket at the pointer, step 7 of 7. Showing bucket 0 (8), bucket 1 (9), bucket 2, bucket 3 (7, 3), bucket 4 (12), bucket 5 (5, 13). In focus: bucket 2.splitsplitbucket 08bucket 19bucket 210bucket 37, 3bucket 412bucket 55, 13split pointer

The drawing shows the whole scene; its 7 steps are listed below.

Every step as text
  1. 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.
  2. 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.
  3. 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.
  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.
  5. 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.
  6. 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.
  7. 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:

Interactive · answer and compare A hash index or a B-tree for your column? Answer three questions about the column and its queries. An index that cannot serve them is ruled out, with the reason.
Does any query on the column use a range, ORDER BY or a prefix match?
Must every value in the column be unique?
Are the values long, such as URLs or long tokens?

Answer the questions above: the table then marks what fits your case and says why the rest does not.

A hash index and a B-tree in PostgreSQL, against the five things that decide between them
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

  1. Question 1 of 3 In linear hashing, the load passes its limit after a key lands in bucket 5, and the split pointer is at bucket 2. Which bucket splits?

    Choose one answer.

    Show the answer: Question 1 of 3

    Answer: Bucket 2, the one at the split pointer

    Linear hashing splits buckets in a fixed order, as bucket 0 split in the figure when bucket 1 had overflowed. The address rule then only has to know how far the pointer has moved.

  2. Question 2 of 3 A linear hash file started with 4 buckets: round 0 uses k mod 4, and a bucket that has split uses k mod 8. The split pointer is at bucket 2. Which bucket holds key 13?

    Type a number.

    A hint

    Compare k mod 4 with the pointer first.

    Show the answer: Question 2 of 3

    Answer: 5 bucket

    13 mod 4 = 1, which is below the pointer, so bucket 1 has split this round and the next round's function decides: 13 mod 8 = 5.

  3. Question 3 of 3 A table must refuse duplicate API tokens, and every lookup is by the exact token. Which index does PostgreSQL need on the column?

    Choose one answer.

    Show the answer: Question 3 of 3

    Answer: A unique B-tree, because PostgreSQL's hash indexes cannot enforce uniqueness

    A hash index serves equality only, on one column, and cannot enforce uniqueness. Once a column must be unique, a unique B-tree exists anyway, and a second index on the same column would have to earn its extra writes.

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.

  1. slot(h, global_depth): the directory slot of hash value h, its last global_depth bits as a number.
  2. split(keys, local_depth): a full bucket with this local depth splits. Return (stay, move): the keys whose bit number local_depth is 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.
  3. 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.

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.

Binary & Hex Calculator Write hash values in binary to see which last bits pick their directory slot.

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.

  1. Question 1 of 10 In the static hashing run, 100 buckets of one 4-key block each hold 1,600 keys. How many blocks did a successful lookup read on average? Give two decimals.

    Type a number.

    A hint

    The run printed one line for each load; this is the last one.

    Show the answer: Question 1 of 10

    Answer: 2.63 blocks (anything from 2.625 to 2.635 counts)

    At a load of 4.0 every bucket has overflowed, chains are up to 6 blocks long, and the average lookup reads 2.63 blocks, against 1.01 when the file held 200 keys. Only rebuilding with more buckets brings it back.

  2. Question 2 of 10 At its design size, a load of 1.0, why do a third of the static file's buckets already overflow?

    Choose one answer.

    Show the answer: Question 2 of 10

    Answer: A hash spreads keys evenly only on average, so some buckets receive more than 4 keys

    Even a good hash function gives some buckets more keys than others. At a load of 1.0 the run had 35 of 100 buckets overflowing, and every overflow adds a block to some lookups.

  3. Question 3 of 10 When does an extendible hash index double its directory?

    Choose one answer.

    Show the answer: Question 3 of 10

    Answer: When a full bucket's local depth equals the global depth

    The directory doubles when the bucket that must split already has local depth equal to the global depth, as at 17, 27 and 9. Otherwise the bucket splits and some slots are repointed, as at 20.

  4. Question 4 of 10 With global depth 3, which directory slot does the hash value 20 use? Give the slot as a number.

    Type a number.

    A hint

    Write 20 in binary and keep its last 3 bits.

    Show the answer: Question 4 of 10

    Answer: 4 slot

    20 is 10100 in binary, and its last 3 bits, 100, are 4. Slot 4 pointed to bucket A, which split on its arrival.

  5. Question 5 of 10 The directory of an extendible index is in memory. How many pages does a successful lookup read?

    Choose one answer.

    Show the answer: Question 5 of 10

    Answer: One, the bucket

    Extendible hashing keeps every lookup at one bucket read. The price is the directory, which doubles as the deepest bucket deepens.

  6. Question 6 of 10 What does linear hashing do without, that extendible hashing needs?

    Choose one answer.

    Show the answer: Question 6 of 10

    Answer: A directory

    Linear hashing addresses a bucket with the round's hash function, or the next round's below the pointer, so no directory is needed. A bucket that overflows before its turn keeps an overflow page meanwhile.

  7. Question 7 of 10 In the stepped figure, bucket 1 overflowed, yet bucket 0 split. When does bucket 1's overflow page go away?

    Choose one answer.

    Show the answer: Question 7 of 10

    Answer: When the pointer reaches bucket 1 and splits it

    An overflowing bucket waits for its turn. In the figure the next split, after 3 arrived, was bucket 1's, and its keys 9, 5 and 13 spread over buckets 1 and 5.

  8. Question 8 of 10 Which conditions can a PostgreSQL hash index on the column token help with?

    Choose every answer that is right.

    Show the answer: Question 8 of 10

    Answer:

    • WHERE token = 'f3a9c2'

    A hash index stores a hash code of each value, so it can only find values equal to a given one. Ranges, sorting and prefix matches need the values in order, which a B-tree keeps and a hash index does not.

  9. Question 9 of 10 Why is a PostgreSQL hash index small for long values such as URLs?

    Choose one answer.

    Show the answer: Question 9 of 10

    Answer: It stores a 32-bit hash code instead of the value

    PostgreSQL's hash index keeps a 4-byte hash code per row instead of the value, so a long key costs no more than a short one, while a B-tree entry grows with the value it stores.

  10. Question 10 of 10 Lookups by session token use a hash index. Support staff now need to search by the first characters of a token. What changes?

    Choose one answer.

    Show the answer: Question 10 of 10

    Answer: The column needs a B-tree, because a hash index cannot serve a prefix match

    A hash index answers equality and nothing else. Once a query needs ranges, sorting or prefixes on the column, a B-tree has to serve it, and it can serve the exact lookups as well.

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.

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.

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

  1. 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.

  2. 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.

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  3. 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?

    Model answer, Pro. First step and follow-ups

  4. 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.

    Model answer, Pro. First step and follow-ups

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

Related tools

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.