Loading content...
Every time you execute a SQL query with a WHERE clause on an indexed column, you're invoking one of the most elegant search algorithms in computer science: the B+-tree search. This operation transforms what would be a linear scan through millions of records into a graceful descent through a handful of tree levels, delivering results in milliseconds regardless of table size.
Understanding B+-tree search isn't merely academic—it's the foundation for comprehending query performance, predicting I/O costs, and making informed decisions about index design. A Principal Engineer doesn't just use indexes; they understand precisely how the search algorithm navigates the tree structure and why this navigation is extraordinarily efficient.
By the end of this page, you will understand the complete mechanics of B+-tree search, from root traversal to leaf node inspection. You'll be able to trace search paths, calculate I/O costs, analyze time complexity, and recognize why B+-trees guarantee logarithmic search performance in the worst case.
Before diving into the algorithm, let's precisely formulate what we're trying to accomplish. The B+-tree search problem has two distinct variants:
Point Query (Equality Search):
Given a search key K, find all records where the indexed attribute equals K. For example: SELECT * FROM employees WHERE employee_id = 12345.
Range Query:
Given a range [K₁, K₂], find all records where the indexed attribute falls within this range. For example: SELECT * FROM orders WHERE order_date BETWEEN '2024-01-01' AND '2024-03-31'.
This page focuses on the point query mechanics. Range queries build upon this foundation and are covered in the next page.
In a B+-tree, all data resides in leaf nodes. Internal nodes serve purely as routing guides. This is fundamentally different from B-trees, where data can be found at any level. The B+-tree search always terminates at a leaf node, even if we encounter the search key in an internal node—we must continue to the leaf to find the actual record pointer.
Formal Problem Statement:
Given:
Find:
The algorithm must guarantee:
To understand search, we must first crystallize the B+-tree structure that the search algorithm navigates:
Internal Node Structure: Each internal node contains:
The separator key invariant holds:
Leaf Node Structure: Each leaf node contains:
| Component | Location | Role in Search | Search Interpretation |
|---|---|---|---|
| Separator Keys | Internal Nodes | Routing decisions | Compare with search key to choose path |
| Child Pointers | Internal Nodes | Navigation links | Follow to descend one level |
| Actual Keys | Leaf Nodes | Search targets | Match against search key |
| Record Pointers | Leaf Nodes | Final result | Returned when key matches |
| Sibling Pointer | Leaf Nodes | Range traversal | Used after point search for ranges |
Internal node keys are separators, not actual data values. They might be copies of leaf keys, but their sole purpose is routing. In some B+-tree implementations, when a leaf key is deleted, its copy in an internal node may remain as a separator—this doesn't affect correctness because the separator still correctly routes searches.
The B+-tree search algorithm is elegantly simple yet powerful. It consists of two phases:
Phase 1: Tree Descent (Finding the Leaf) Starting from the root, traverse down through internal nodes, selecting the appropriate child pointer at each level until reaching a leaf node.
Phase 2: Leaf Search (Finding the Key) Within the leaf node, search for the target key using binary search and return the associated record pointer(s) if found.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566
FUNCTION BPlusTreeSearch(root, searchKey K) // ================================================ // PHASE 1: TREE DESCENT - Navigate to correct leaf // ================================================ currentNode ← root WHILE currentNode is NOT a leaf node DO // Find the appropriate child pointer to follow childIndex ← FindChildIndex(currentNode, K) currentNode ← currentNode.children[childIndex] END WHILE // currentNode is now a leaf node leafNode ← currentNode // ================================================ // PHASE 2: LEAF SEARCH - Find key within leaf // ================================================ RETURN SearchInLeaf(leafNode, K)END FUNCTION FUNCTION FindChildIndex(internalNode, K) // Binary search to find correct child pointer // internalNode has keys: K₁, K₂, ..., Kₘ₋₁ // and children: P₁, P₂, ..., Pₘ keys ← internalNode.keys // m-1 keys m ← internalNode.childCount // m children // Find smallest i such that K < keys[i] // If no such i exists, use the rightmost child FOR i ← 1 TO m-1 DO IF K < keys[i] THEN RETURN i // Follow child pointer Pᵢ END IF END FOR RETURN m // K ≥ all keys, follow rightmost pointer PₘEND FUNCTION FUNCTION SearchInLeaf(leafNode, K) // Binary search within leaf node keys ← leafNode.keys n ← leafNode.keyCount // Binary search for K left ← 1 right ← n WHILE left ≤ right DO mid ← (left + right) / 2 IF keys[mid] = K THEN // Found! Return associated record pointer(s) RETURN (TRUE, leafNode.recordPointers[mid]) ELSE IF keys[mid] < K THEN left ← mid + 1 ELSE right ← mid - 1 END IF END WHILE // Key not found RETURN (FALSE, NULL)END FUNCTIONWhen duplicate keys are allowed (non-unique indexes), the search might find one occurrence, but additional duplicates may exist to the left or right in the same leaf, or in adjacent leaf nodes. A complete search for all duplicates requires scanning in both directions or using a different duplicate handling strategy (like overflow chains).
Let's trace a complete search operation through a concrete B+-tree example. Consider a B+-tree of order 4 (maximum 4 children per internal node, maximum 3 keys per leaf node) indexing employee IDs:
Tree Structure:
Search for Key = 42:
Step 1: Start at Root
Step 2: At Internal Node B
Step 3: At Leaf Node 5
| Step | Node Type | Node Contents | Comparison | Action |
|---|---|---|---|---|
| 1 | Root (Internal) | [30, 60] | 30 ≤ 42 < 60 | Follow P₂ → Node B |
| 2 | Internal | [40, 50] | 40 ≤ 42 < 50 | Follow P₂ → Leaf 5 |
| 3 | Leaf | [40, 42, 45] | 42 = 42 ✓ | Key Found! |
Search for Key = 25 (Not Present):
Step 1: Start at Root
Step 2: At Internal Node A
Step 3: At Leaf Node 3
Notice that the search correctly navigates to Leaf 3, which is where 25 would be stored if it existed. This property is crucial for insertions.
The search algorithm locates the leaf node where a key belongs, whether or not the key actually exists. This is essential for insert operations—run the search to find the correct leaf, then insert the key there. The search is thus reused as the first step of insertion.
The B+-tree search algorithm's efficiency stems from its logarithmic depth. Let's derive the time and I/O complexity rigorously.
Height Analysis:
For a B+-tree of order n (max n children per node):
With N total keys, the maximum height h satisfies:
N ≤ (n-1) × n^(h-1)
Solving for h:
h ≤ 1 + log_n(N/(n-1))
h = O(log_n N)
For typical B+-trees where n = 100-200:
| Number of Records (N) | Maximum Height | Disk I/Os for Search |
|---|---|---|
| 1,000 | 2 | 2 |
| 100,000 | 3 | 3 |
| 10,000,000 | 4 | 4 |
| 1,000,000,000 | 5 | 5 |
| 100,000,000,000 | 6 | 6 |
I/O Complexity:
Each level traversed requires one disk I/O to read a node. The search reads exactly h nodes where h is the tree height.
I/O Cost = O(log_n N) = O(height)
This is remarkably efficient. Searching among 1 billion records requires only 5 disk reads. At 10ms per random disk seek, that's 50ms—fast enough for interactive queries.
CPU Complexity:
At each internal node, we perform binary search on up to n-1 keys:
Since disk I/O dominates, the O(log N) CPU time is negligible.
The key to B+-tree efficiency is the high fanout (number of children per node). With n=100, each level of the tree can partition the search space by a factor of 100. Compare this to a binary tree where each level only halves the search space. A binary tree would require 30 levels for 1 billion records; a B+-tree with n=100 needs only 5.
Production database systems implement several optimizations to make B+-tree search even faster:
In a well-tuned system with adequate buffer pool size, the top levels of frequently-used B+-trees remain cached. For a B+-tree with height 4, if levels 0-2 are cached, searches require only 1 disk I/O (for the leaf node). This is why memory sizing is crucial for database performance.
Adaptive Search Strategies:
Some advanced systems employ adaptive techniques:
Hybrid Search: For very wide nodes (high fanout), systems may use a hybrid of binary search and linear probe. Binary search narrows to a cache-line-sized region, then linear scan identifies the exact position.
Branching Hints: After traversing to a leaf, the search path can be cached as a "hint" for subsequent searches with nearby keys. This is particularly effective for range queries and sequential access patterns.
NUMA-Aware Placement: In multi-socket systems, B+-tree nodes are placed in memory close to the processor that most frequently accesses them, reducing memory access latency.
To appreciate B+-tree search, compare it with alternative index structures:
| Structure | Search Complexity | Disk I/Os (1B records) | Strengths | Weaknesses |
|---|---|---|---|---|
| B+-tree | O(log N) | ~5 | Balanced, cache-friendly, supports ranges | Moderate insert overhead |
| Binary Search Tree | O(log N) avg, O(N) worst | ~30 (if binary) | Simple implementation | Degenerates if unbalanced |
| Hash Index | O(1) average | ~1-2 | Fastest for equality | No range queries |
| Linear Scan | O(N) | ~millions | No structure needed | Impractical at scale |
| Sorted Array | O(log N) | ~30 (binary search) | Simple, compact | Expensive inserts O(N) |
Why B+-trees Win for General-Purpose Indexing:
Guaranteed Balance: Unlike BSTs, B+-trees cannot become unbalanced. Every leaf is exactly the same distance from the root, ensuring consistent O(log N) performance regardless of insertion order.
Disk-Optimized Fanout: By storing hundreds of keys per node (matching disk block size), B+-trees minimize the number of disk accesses. A binary tree would require one disk access per comparison—catastrophic for large datasets.
Range Query Support: Unlike hash indexes, B+-trees maintain sorted order within leaves and link leaves together. After finding a start key, range queries simply follow sibling pointers—no additional tree traversals needed.
Mature Implementations: Decades of optimization have produced B+-tree implementations that exploit CPU caches, SIMD, and modern storage hierarchies. The simple algorithm enables sophisticated engineering.
Hash indexes achieve O(1) average-case point queries, faster than B+-trees. However, they cannot support range queries, inequality comparisons, or ORDER BY without sorting. Most databases default to B+-trees because they're more general-purpose. Hash indexes are used selectively for specific equality-lookup workloads.
Let's consolidate the essential concepts of B+-tree search:
What's Next:
Now that you understand point queries, the next page explores range queries—one of B+-trees' defining advantages. You'll see how the linked leaf structure enables efficient range scans after an initial search, making B+-trees ideal for BETWEEN clauses, ORDER BY, and range-based analytics.
You now understand the complete mechanics of B+-tree point query search. This algorithm runs thousands of times per second in every database system, and your knowledge of its inner workings will inform index design, query tuning, and performance analysis throughout your career.