The data structures that make databases and filesystems fast. Every time you query a SQL database, B-Trees are doing the heavy lifting underneath.
๐ด AdvancedA B-Tree is a self-balancing search tree designed for systems that read and write large blocks of data at a time โ like hard drives, SSDs, and network storage. While a binary search tree (BST โ a tree where each node stores one key and has at most two children, with all left-subtree keys smaller and all right-subtree keys larger) stores one key per node and has two children, a B-Tree node can hold many keys and have many children.
Think of a library's card catalog. Each drawer (node) contains many cards (keys) arranged in alphabetical order. The labels on the front of each drawer tell you which range of topics it covers โ so you know exactly which drawer to open without checking every single one. You might only need to open 2-3 drawers to find any book in the entire library. That's how a B-Tree works: each node contains a sorted range of keys, and child pointers (references to subtrees) tell you which child to visit next.
Rudolf Bayer and Edward McCreight invented the B-Tree in 1970 at Boeing Scientific Research Labs, publishing it in 1972. They were solving a practical problem: how to efficiently search and maintain large ordered datasets stored on slow magnetic disk drives. The "B" probably stands for "Bayer," "balanced," "Boeing," or "broad" โ the original paper never says, and both inventors have declined to clarify. It's computer science's longest-running naming mystery.
The B-Tree was revolutionary because it minimized the number of disk accesses needed for any operation. Before B-Trees, databases used structures like ISAM (Indexed Sequential Access Method) that degraded over time and required periodic reorganization. B-Trees stay balanced automatically โ every path from root to leaf has the same length, always.
A B-Tree of order m (also called minimum degree t, where m = 2t) follows these rules:
Binary search trees work great when everything lives in RAM (Random Access Memory โ the fast, volatile memory where running programs store their data). Every pointer dereference takes nanoseconds. But databases store data on disk (HDDs or SSDs), and disk access is orders of magnitude slower:
Every time you follow a pointer to a child node in a tree, that's potentially a disk read (a seek โ the physical operation of reading data from a specific location on disk). A balanced BST with 1 million keys has about 20 levels (logโ(1,000,000) โ 20), meaning 20 disk reads to find a single value. At 10ms per read on an HDD, that's 200ms for one lookup. Unacceptable for a database handling thousands of queries per second.
A B-Tree with order 1000 storing the same million keys? About 2 levels (logโโโโ(1,000,000) โ 2). Two disk reads. 20ms total. That's a 10ร speedup, and the root is usually cached in RAM, so in practice it's often just 1 disk read.
Searching a B-Tree is similar to a BST search, but at each node you do a binary search within the node's sorted keys (or linear scan for small nodes) to find either the target key or the right child pointer to follow:
Tree height is O(logm n) where m is the order โ the base of the logarithm is much larger than 2, making the tree extremely shallow.
New keys always get inserted into leaf nodes. B-Trees grow from the bottom up, not from the top down.
The beauty of this scheme: the tree stays perfectly balanced because growth happens at the root, not the leaves. Every leaf is always at the same depth.
Deletion is the most complex operation. The goal: remove a key while maintaining all B-Tree properties (minimum occupancy, sorted order, balanced leaves). There are several cases:
Case 1: Key is in a leaf with plenty of keys โ just remove it. The node still has at least โm/2โ-1 keys, so all properties hold.
Case 2: Key is in a leaf at minimum occupancy โ removing it would leave the node underfull. You need to fix this:
Case 3: Key is in an internal node โ you can't just remove it (it's a separator between children). Replace it with its in-order predecessor (the largest key in the left child's subtree) or in-order successor (the smallest key in the right child's subtree), both of which are always in a leaf. Then delete that key from the leaf (which reduces to Case 1 or 2).
Most real databases (MySQL/InnoDB, PostgreSQL, SQLite, Oracle, SQL Server) actually use B+ Trees, not plain B-Trees. The B+ Tree is a refinement that optimizes for the access patterns databases care about most: range queries and sequential scans.
Consider SELECT * FROM users WHERE age BETWEEN 25 AND 35. With a B-Tree, matching records could be scattered across internal and leaf nodes at different levels. You'd need multiple traversals. With a B+ Tree: find the leaf for age=25, then walk the linked list of leaves until age exceeds 35. Sequential, cache-friendly, and fast.
Full table scans are also faster with B+ Trees โ just read all the leaves left to right. The internal nodes are never needed for a scan.
| Operation | Time | Disk Reads | Notes |
|---|---|---|---|
| Search | O(log n) | O(logm n) | Binary search within each node + follow child |
| Insert | O(log n) | O(logm n) | May trigger splits cascading up the tree |
| Delete | O(log n) | O(logm n) | May trigger merges or rotations cascading up |
| Range Query (B+) | O(log n + k) | O(logm n + k/m) | k = number of results; sequential leaf scan |
| Min / Max | O(log n) | O(logm n) | Follow leftmost/rightmost child at each level |
| Full Scan (B+) | O(n) | O(n/m) | Walk all leaves via linked list โ skip internal nodes |
Space: O(n). Tree height: O(logm n) where m is the order. With m=1000 and n=1 billion: height โ 3 levels. With the root cached in RAM, that's 2 disk reads to find any record among a billion.
class BTreeNode:
"""A single node in a B-Tree.
Each node stores:
- keys: sorted list of keys (at most 2t-1 keys)
- children: child pointers (at most 2t children for internal nodes)
- leaf: boolean indicating whether this is a leaf node
"""
def __init__(self, leaf=True):
self.keys = [] # Sorted list of keys
self.children = [] # Child pointers (len = len(keys) + 1 for internal nodes)
self.leaf = leaf # Leaf nodes have no children
class BTree:
"""B-Tree of minimum degree t.
Each node holds between t-1 and 2t-1 keys.
Each internal node has between t and 2t children.
Common degree values:
- t=2 โ 2-3-4 tree (1-3 keys, 2-4 children) โ good for learning
- t=100 โ typical for databases (199 keys per node)
- t=500+ โ typical for filesystem metadata
Tree height with t=100 and 1 billion keys: ~5 levels.
Each level = 1 disk read, so 5 reads to find anything.
"""
def __init__(self, t=2):
self.root = BTreeNode()
self.t = t # Minimum degree
def search(self, key, node=None):
"""Search for key in the B-Tree. Returns (node, index) or None.
At each node, we binary search within the keys (O(log t) per node),
then follow the appropriate child pointer. Total: O(tยทlog_t(n))
which simplifies to O(log n).
"""
if node is None:
node = self.root
# Find the first key >= target (could use bisect for O(log t))
i = 0
while i < len(node.keys) and key > node.keys[i]:
i += 1
# Found the key in this node
if i < len(node.keys) and key == node.keys[i]:
return (node, i)
# Hit a leaf without finding the key
if node.leaf:
return None
# Recurse into the appropriate child
return self.search(key, node.children[i])
def insert(self, key):
"""Insert a key into the B-Tree.
Strategy: proactively split full nodes on the way DOWN.
This ensures we never need to split on the way back up,
simplifying the code significantly.
"""
root = self.root
# Special case: if root is full, split it first
# This is the ONLY way the tree grows taller
if len(root.keys) == 2 * self.t - 1:
new_root = BTreeNode(leaf=False)
new_root.children.append(self.root)
self._split_child(new_root, 0)
self.root = new_root
self._insert_non_full(self.root, key)
def _insert_non_full(self, node, key):
"""Insert key into a node that is guaranteed to have room.
If the node is a leaf, insert directly.
If internal, find the right child and recurse.
Split any full child BEFORE descending into it.
"""
i = len(node.keys) - 1
if node.leaf:
# Insert key in sorted position within this leaf
node.keys.append(None) # Extend by one
while i >= 0 and key < node.keys[i]:
node.keys[i + 1] = node.keys[i] # Shift right
i -= 1
node.keys[i + 1] = key
else:
# Find the child that should receive this key
while i >= 0 and key < node.keys[i]:
i -= 1
i += 1
# Proactive split: if that child is full, split BEFORE descending
if len(node.children[i].keys) == 2 * self.t - 1:
self._split_child(node, i)
# After split, the median moved up. Check which child to use.
if key > node.keys[i]:
i += 1
self._insert_non_full(node.children[i], key)
def _split_child(self, parent, i):
"""Split the i-th child of parent (which must be full).
The full child has 2t-1 keys. After split:
- Median key moves up to parent at position i
- Left half stays as original child (t-1 keys)
- Right half becomes new child at position i+1 (t-1 keys)
"""
t = self.t
full_child = parent.children[i]
new_node = BTreeNode(leaf=full_child.leaf)
# Median key goes up to parent
parent.keys.insert(i, full_child.keys[t - 1])
parent.children.insert(i + 1, new_node)
# Right half of keys goes to new node
new_node.keys = full_child.keys[t:]
full_child.keys = full_child.keys[:t - 1] # Left half stays
# If not a leaf, split children too
if not full_child.leaf:
new_node.children = full_child.children[t:]
full_child.children = full_child.children[:t]
def traverse(self, node=None):
"""In-order traversal โ returns all keys in sorted order."""
if node is None:
node = self.root
result = []
for i in range(len(node.keys)):
if not node.leaf:
result.extend(self.traverse(node.children[i]))
result.append(node.keys[i])
if not node.leaf:
result.extend(self.traverse(node.children[-1]))
return result
def height(self, node=None):
"""Height of the tree (0 for a single-node tree)."""
if node is None:
node = self.root
if node.leaf:
return 0
return 1 + self.height(node.children[0])
# Usage
bt = BTree(t=2) # 2-3-4 tree (1-3 keys per node)
for val in [10, 20, 5, 6, 12, 30, 7, 17]:
bt.insert(val)
print("Sorted:", bt.traverse()) # [5, 6, 7, 10, 12, 17, 20, 30]
print("Height:", bt.height()) # Depends on splits
print("Search 12:", bt.search(12)) # (node, index) tuple
print("Search 15:", bt.search(15)) # None
class BTreeNode {
constructor(leaf = true) {
this.keys = []; // Sorted keys (at most 2t-1)
this.children = []; // Child pointers (at most 2t for internal nodes)
this.leaf = leaf; // True if this node has no children
}
}
class BTree {
/**
* B-Tree of minimum degree t.
* - Each non-root node holds at least t-1 keys, at most 2t-1 keys.
* - Each internal non-root node has at least t children, at most 2t children.
* - The root may have as few as 1 key (or 0 if the tree is empty).
*
* Common values:
* t=2 โ 2-3-4 tree (learning)
* t=100 โ databases
* t=500+ โ filesystems
*/
constructor(t = 2) {
this.root = new BTreeNode();
this.t = t;
}
search(node, key) {
// Binary search within this node's sorted keys
let lo = 0, hi = node.keys.length - 1;
while (lo <= hi) {
const mid = (lo + hi) >> 1;
if (node.keys[mid] === key) return { node, index: mid };
if (node.keys[mid] < key) lo = mid + 1;
else hi = mid - 1;
}
// lo = index of the child to follow (between keys[lo-1] and keys[lo])
if (node.leaf) return null;
return this.search(node.children[lo], key);
}
insert(key) {
const root = this.root;
// If the root is full, split it BEFORE inserting.
// This is the ONLY way the tree grows taller.
if (root.keys.length === 2 * this.t - 1) {
const newRoot = new BTreeNode(false);
newRoot.children.push(this.root);
this._splitChild(newRoot, 0);
this.root = newRoot;
}
this._insertNonFull(this.root, key);
}
_insertNonFull(node, key) {
let i = node.keys.length - 1;
if (node.leaf) {
// Insert in sorted order within this leaf
node.keys.push(null); // Make room
while (i >= 0 && key < node.keys[i]) {
node.keys[i + 1] = node.keys[i]; // Shift right
i--;
}
node.keys[i + 1] = key;
} else {
// Find the correct child to descend into
while (i >= 0 && key < node.keys[i]) i--;
i++;
// Proactive split: if that child is full, split BEFORE descending
if (node.children[i].keys.length === 2 * this.t - 1) {
this._splitChild(node, i);
// After split, median moved up. Decide which half to descend into.
if (key > node.keys[i]) i++;
}
this._insertNonFull(node.children[i], key);
}
}
_splitChild(parent, i) {
const t = this.t;
const fullChild = parent.children[i];
const newNode = new BTreeNode(fullChild.leaf);
// Median key goes up to parent
parent.keys.splice(i, 0, fullChild.keys[t - 1]);
parent.children.splice(i + 1, 0, newNode);
// Right half of keys โ new node, left half stays
newNode.keys = fullChild.keys.splice(t); // keys from index t onward
fullChild.keys.pop(); // remove the median (already promoted)
// If not a leaf, split children too
if (!fullChild.leaf) {
newNode.children = fullChild.children.splice(t);
}
}
// In-order traversal โ sorted keys
traverse(node = this.root) {
const result = [];
for (let i = 0; i < node.keys.length; i++) {
if (!node.leaf) result.push(...this.traverse(node.children[i]));
result.push(node.keys[i]);
}
if (!node.leaf) result.push(...this.traverse(node.children[node.keys.length]));
return result;
}
height(node = this.root) {
if (node.leaf) return 0;
return 1 + this.height(node.children[0]);
}
}
// Usage
const bt = new BTree(2); // 2-3-4 tree
for (const val of [10, 20, 5, 6, 12, 30, 7, 17]) {
bt.insert(val);
}
console.log("Sorted:", bt.traverse()); // [5, 6, 7, 10, 12, 17, 20, 30]
console.log("Height:", bt.height());
console.log("Search 12:", bt.search(bt.root, 12)); // { node, index }
console.log("Search 15:", bt.search(bt.root, 15)); // null
Deletion is the most complex B-Tree operation. Here's a full implementation that handles all three cases: deleting from a leaf with spare keys, deleting from an internal node (replaced by predecessor/successor), and fixing underfull nodes via borrowing or merging.
class BTreeWithDelete(BTree):
"""Extends BTree with full deletion support.
Deletion cases:
1. Key is in a leaf with > t-1 keys โ just remove it
2. Key is in an internal node โ replace with predecessor/successor
3. Key is in a leaf at minimum occupancy โ borrow or merge first
The implementation uses a "proactive fix" approach: ensure every
node we descend into has at least t keys (one more than minimum),
so deletion from a leaf never causes underflow.
"""
def delete(self, key):
"""Delete a key from the B-Tree."""
if not self.root.keys:
return # Empty tree
self._delete(self.root, key)
# If root has no keys but has a child, shrink the tree
# This is the ONLY way the tree gets shorter
if not self.root.keys and not self.root.leaf:
self.root = self.root.children[0]
def _delete(self, node, key):
t = self.t
i = 0
while i < len(node.keys) and key > node.keys[i]:
i += 1
if node.leaf:
# Case 1: Key is in this leaf โ remove directly
if i < len(node.keys) and node.keys[i] == key:
node.keys.pop(i)
return
if i < len(node.keys) and node.keys[i] == key:
# Case 2: Key is in this internal node
self._delete_internal(node, i)
else:
# Case 3: Key is in a subtree โ ensure child has enough keys
if len(node.children[i].keys) < t:
self._fix_child(node, i)
# After fix, recalculate position (merge may have changed it)
if i > len(node.keys):
i -= 1
# Recheck if key moved to current node after merge
if i < len(node.keys) and node.keys[i] == key:
self._delete_internal(node, i)
return
self._delete(node.children[i], key)
def _delete_internal(self, node, i):
"""Delete node.keys[i] from an internal node.
Replace with predecessor or successor from a child."""
t = self.t
if len(node.children[i].keys) >= t:
# Use in-order predecessor (rightmost key in left subtree)
pred = self._get_predecessor(node.children[i])
node.keys[i] = pred
self._delete(node.children[i], pred)
elif len(node.children[i + 1].keys) >= t:
# Use in-order successor (leftmost key in right subtree)
succ = self._get_successor(node.children[i + 1])
node.keys[i] = succ
self._delete(node.children[i + 1], succ)
else:
# Both children at minimum โ merge them
self._merge(node, i)
self._delete(node.children[i], node.keys[i] if i < len(node.keys) else node.children[i].keys[t - 1])
def _get_predecessor(self, node):
"""Get the largest key in this subtree."""
while not node.leaf:
node = node.children[-1]
return node.keys[-1]
def _get_successor(self, node):
"""Get the smallest key in this subtree."""
while not node.leaf:
node = node.children[0]
return node.keys[0]
def _fix_child(self, parent, i):
"""Ensure parent.children[i] has at least t keys.
Try borrowing from a sibling first, then merge."""
t = self.t
if i > 0 and len(parent.children[i - 1].keys) >= t:
# Borrow from left sibling (rotate right)
child = parent.children[i]
left = parent.children[i - 1]
child.keys.insert(0, parent.keys[i - 1])
parent.keys[i - 1] = left.keys.pop()
if not left.leaf:
child.children.insert(0, left.children.pop())
elif i < len(parent.children) - 1 and len(parent.children[i + 1].keys) >= t:
# Borrow from right sibling (rotate left)
child = parent.children[i]
right = parent.children[i + 1]
child.keys.append(parent.keys[i])
parent.keys[i] = right.keys.pop(0)
if not right.leaf:
child.children.append(right.children.pop(0))
else:
# Neither sibling can spare a key โ merge
if i < len(parent.children) - 1:
self._merge(parent, i)
else:
self._merge(parent, i - 1)
def _merge(self, parent, i):
"""Merge parent.children[i] and parent.children[i+1].
Pulls parent.keys[i] down into the merged node."""
t = self.t
left = parent.children[i]
right = parent.children[i + 1]
# Pull the separating key down from parent
left.keys.append(parent.keys.pop(i))
# Move all keys and children from right to left
left.keys.extend(right.keys)
left.children.extend(right.children)
# Remove the right child from parent
parent.children.pop(i + 1)
# Usage
bt = BTreeWithDelete(t=2)
for val in [10, 20, 5, 6, 12, 30, 7, 17, 3, 8]:
bt.insert(val)
print("Before:", bt.traverse()) # [3, 5, 6, 7, 8, 10, 12, 17, 20, 30]
bt.delete(6)
bt.delete(20)
bt.delete(10)
print("After:", bt.traverse()) # [3, 5, 7, 8, 12, 17, 30]
Let's insert the keys [10, 20, 30, 40, 50, 25] into a B-Tree of minimum degree t=2 (a 2-3-4 tree, where each node holds 1-3 keys and has 2-4 children). Walk through every split.
Insert 10: Tree is empty. Root becomes a leaf with key [10].
[10]
Insert 20: Root has room (max 3 keys). Insert in sorted order โ [10, 20].
[10, 20]
Insert 30: Root has room โ [10, 20, 30]. Now the root is full (3 keys = 2t-1).
[10, 20, 30] โ full!
Insert 40: Root is full. Must split BEFORE inserting. Median key (20) moves up to a new root. Left child gets [10], right child gets [30]. Then insert 40 into the right child โ [30, 40].
[20]
/ \
[10] [30, 40]
The tree grew taller! This is the only way height increases โ the root splits and a new root is created above it.
Insert 50: Search leads to right child [30, 40]. It has room โ [30, 40, 50]. Now that child is full.
[20]
/ \
[10] [30, 40, 50] โ full!
Insert 25: Search leads to right child [30, 40, 50], which is full. Proactive split: median (40) moves up to root. Right child becomes [30] and [50]. Now root is [20, 40]. Insert 25 into the child that covers the range 20-40, which is [30]. It becomes [25, 30].
[20, 40]
/ | \
[10] [25,30] [50]
All leaves are at the same depth. Every node respects the min/max key constraints. The tree is perfectly balanced โ this invariant never breaks, no matter what sequence of insertions you perform.
Every SQL database you've ever used stores its indexes as B+ Trees. When you run CREATE INDEX idx_users_email ON users(email), the database builds a B+ Tree where leaf nodes contain (email โ row_pointer) mappings and internal nodes are pure navigation. A lookup like SELECT * FROM users WHERE email = '[email protected]' traverses 2-3 levels of the tree instead of scanning millions of rows. PostgreSQL's default index type is B-Tree (really B+ Tree). MySQL/InnoDB's clustered index stores actual row data in the B+ Tree leaves, sorted by primary key.
Filesystems use B-Trees (or variants) to organize directory entries and file metadata. In ext4 (the default Linux filesystem), large directories use HTree โ a B-Tree variant with hashed keys. NTFS uses B+ Trees for its Master File Table ($MFT) indexes. Apple's APFS uses B-Trees for everything: directories, extent maps, snapshot metadata. The Btrfs filesystem ("B-Tree filesystem") is literally named after the data structure โ it uses copy-on-write B-Trees for all its metadata, which enables snapshots, checksumming, and online defragmentation.
MongoDB's default storage engine (WiredTiger) uses B-Trees for its indexes. When you create an index on a MongoDB collection, it's a B-Tree mapping field values to document IDs. Browser-based IndexedDB (used for offline web apps) stores its object stores as B-Trees. Redis Sorted Sets use skip lists (a probabilistic B-Tree analog) for similar reasons โ fast ordered operations with logarithmic lookup.
Git uses a different tree structure (Merkle trees), but many version control and backup systems use B-Trees internally. ZFS (the filesystem) uses B-Trees for its metadata. Time Machine on macOS relies on APFS's B-Tree snapshots to create instant backups without copying data. The SQLite database that powers many applications (including Firefox's bookmarks, Chrome's history, and iOS's system databases) stores everything in B-Trees.
Here's a simplified simulation of how a database uses a B+ Tree index to speed up queries. The index maps column values to row IDs, avoiding full table scans.
class SimpleDBIndex:
"""Simulates a database B+ Tree index for a single column.
Real database indexes (PostgreSQL, MySQL/InnoDB, SQLite) work on
the same principle: maintain a B+ Tree on the indexed column,
where leaf nodes store (column_value โ row_id) mappings.
Without an index: SELECT WHERE age=25 scans ALL rows (O(n)).
With an index: follows the tree to the exact leaf (O(log n)).
"""
def __init__(self, column_name):
self.column_name = column_name
self.btree = BTree(t=3) # Small t for demonstration
self.value_to_rows = {} # value โ set of row IDs
def index_row(self, value, row_id):
"""Add a row to the index. Called on INSERT or when creating the index."""
if value not in self.value_to_rows:
self.btree.insert(value)
self.value_to_rows[value] = set()
self.value_to_rows[value].add(row_id)
def lookup_exact(self, value):
"""Find all rows where column = value. O(log n).
This is what happens when you run:
SELECT * FROM users WHERE age = 25
"""
result = self.btree.search(value)
if result is None:
return set()
return self.value_to_rows.get(value, set())
def lookup_range(self, min_val, max_val):
"""Find all rows where min_val <= column <= max_val.
This is what happens when you run:
SELECT * FROM users WHERE age BETWEEN 25 AND 35
In a real B+ Tree, this walks the leaf linked list.
Here we simulate it with the in-order traversal.
"""
all_keys = self.btree.traverse()
result = set()
for key in all_keys:
if min_val <= key <= max_val:
result.update(self.value_to_rows.get(key, set()))
elif key > max_val:
break # Keys are sorted, so we can stop early
return result
class SimpleDatabase:
"""Toy database demonstrating index vs. full scan performance."""
def __init__(self):
self.rows = [] # List of row dicts
self.indexes = {} # column_name โ SimpleDBIndex
def insert(self, row):
row_id = len(self.rows)
self.rows.append(row)
# Update all indexes
for col, index in self.indexes.items():
if col in row:
index.index_row(row[col], row_id)
return row_id
def create_index(self, column):
"""CREATE INDEX on a column. Indexes all existing rows."""
index = SimpleDBIndex(column)
for row_id, row in enumerate(self.rows):
if column in row:
index.index_row(row[column], row_id)
self.indexes[column] = index
return f"Index created on '{column}' ({len(self.rows)} rows indexed)"
def query(self, column, value):
"""SELECT * FROM table WHERE column = value."""
if column in self.indexes:
# INDEX SCAN: O(log n) via B-Tree
row_ids = self.indexes[column].lookup_exact(value)
return [self.rows[rid] for rid in row_ids]
else:
# FULL TABLE SCAN: O(n) โ checks every row
return [row for row in self.rows if row.get(column) == value]
def query_range(self, column, min_val, max_val):
"""SELECT * FROM table WHERE column BETWEEN min AND max."""
if column in self.indexes:
row_ids = self.indexes[column].lookup_range(min_val, max_val)
return [self.rows[rid] for rid in row_ids]
else:
return [row for row in self.rows
if min_val <= row.get(column, float('inf')) <= max_val]
# Demo
db = SimpleDatabase()
# Insert some users
users = [
{"name": "Alice", "age": 30, "dept": "Engineering"},
{"name": "Bob", "age": 25, "dept": "Marketing"},
{"name": "Charlie", "age": 35, "dept": "Engineering"},
{"name": "Dave", "age": 28, "dept": "Sales"},
{"name": "Eve", "age": 30, "dept": "Engineering"},
{"name": "Frank", "age": 22, "dept": "Marketing"},
]
for user in users:
db.insert(user)
# Without index: full scan
print("Without index (full scan):")
results = db.query("age", 30)
print(f" Age=30: {[r['name'] for r in results]}") # ['Alice', 'Eve']
# Create index on 'age'
print(db.create_index("age"))
# With index: B-Tree lookup
print("\nWith index (B-Tree lookup):")
results = db.query("age", 30)
print(f" Age=30: {[r['name'] for r in results]}") # ['Alice', 'Eve']
# Range query
results = db.query_range("age", 25, 30)
print(f" Age 25-30: {[r['name'] for r in results]}") # ['Bob', 'Dave', 'Alice', 'Eve']
B-Trees rarely appear directly in coding interviews (they're a system design topic), but these problems test the same concepts โ tree balancing, ordered insertion, range queries, and hierarchical search:
| Problem | Difficulty | Key Idea |
|---|---|---|
| LC 701 โ Insert into a BST | Medium | Similar insertion logic โ find the right leaf position |
| LC 450 โ Delete Node in a BST | Medium | Deletion with predecessor/successor replacement |
| LC 938 โ Range Sum of BST | Easy | Range query on a tree โ core B+ Tree operation |
| LC 230 โ Kth Smallest Element in a BST | Medium | In-order traversal โ same concept as B-Tree traversal |
| LC 1166 โ Design File System | Medium | Tree-structured paths โ B-Trees power real filesystem metadata |
| LC 98 โ Validate Binary Search Tree | Medium | The sorted-keys-within-range invariant applies to B-Trees too |
| LC 449 โ Serialize and Deserialize BST | Medium | Persistence and reconstruction โ how B-Trees are stored on disk |
| LC 222 โ Count Complete Tree Nodes | Easy | Exploiting balanced-tree structure for efficient counting |