Step 1 of 3 · Reading · ~3 min
Searching in a B-Tree
B-Tree Index
Why B-Trees, not binary search trees?
Every real database index — SQLite, PostgreSQL, MySQL's InnoDB — is backed by a B-tree (or the closely related B+tree), not a binary search tree. The reason is disk. A binary tree with a million keys has ~20 levels, and each level is a pointer chase that can cost a disk seek. A B-tree with a branching factor of, say, 100 needs only 3 levels to hold a million keys. Fewer levels means fewer page reads means a faster lookup. The core design idea of a B-tree is: make each node as wide as one disk page, and keep the tree shallow and perfectly balanced.
The shape of a B-tree node
A B-tree of order M (sometimes defined slightly differently across textbooks — here, order M means at most M-1 keys per node) has two kinds of nodes:
- Internal nodes hold
ksorted keys andk+1child pointers. Childicontains all keys strictly between keyi-1and keyi. - Leaf nodes hold sorted keys but no children.
For example, an internal node [10, 20] has three children: one for keys < 10, one for keys between 10 and 20, and one for keys > 20. Every leaf sits at the same depth — that's what "balanced" means for a B-tree; there's no rebalancing step needed for search itself, only for insert/delete (later lessons).
The search algorithm
Searching a B-tree is a straightforward recursive (or iterative) descent from the root:
function search(node, key):
i = binary_search(node.keys, key) // find insertion point
if node.keys[i] == key:
return FOUND
if node.is_leaf:
return NOT_FOUND
return search(node.children[i], key)
At each node you do a binary search over its (sorted, in-memory) key array — this is cheap, it's an array scan/binary search, not a disk operation. The expensive part in a real system is fetching each node from disk, which is exactly why B-trees keep nodes wide: wide nodes mean few levels mean few page fetches.
Building SEARCH_PATH for debugging
Your exercise asks for two commands: BTREE SEARCH <key> (just the verdict) and BTREE SEARCH_PATH <key> (the verdict and the path taken). The path is invaluable for debugging tree structure — it's how you'll visually confirm your insert/split logic in the next lesson actually produced a correct tree.
Print each visited node's key array, in root-to-leaf order, joined by →, ending in FOUND or NOT_FOUND:
[10,20] → [1,5] → FOUND
[10,20] → [12,15] → NOT_FOUND
Notice the second example: the search descended into [12,15] (the middle child of [10,20], holding keys between 10 and 20) and didn't find the key there, and since [12,15] is a leaf, the search terminates as NOT_FOUND without visiting anything else.
Edge cases to get right
- Root is a leaf. A brand-new tree with only a few keys is a single leaf node — your traversal must handle "root has no children" as a base case, not just recurse blindly.
- Key equals a node's key exactly. Don't fall through to a child if the key matches one already stored in an internal node — internal-node keys are real, searchable data, not just separators.
- Off-by-one child selection. With keys
[10, 20], key15must go to child index 1 (the middle child),5to child index 0, and25to child index 2. Get this index arithmetic wrong and searches silently return wrong answers for a subset of keys. - Empty tree. Searching before any insert should report
NOT_FOUNDcleanly, not crash.
Get search rock-solid first — every later B-tree lesson (insert, delete, splitting, rebalancing) depends on correctly walking root-to-leaf, and you'll reuse this exact traversal logic to locate the node where an insert or delete needs to happen.
Discussion
Ask a question, share an insight, or help someone who’s stuck.
Sign in to post a comment or reply.
Loading…