CO1 Topic 5 of 34
B-trees and B+ trees
Fat, shallow search trees built for disks and databases: each node holds many keys and fits one disk block, so a search touches only a handful of blocks even for millions of keys.
You will be able to
- State the rules of a B-tree of minimum degree t (or order m)
- Insert keys by splitting full nodes, and draw the tree after each split
- Explain how B+ trees differ from B-trees and why databases use them
- Compute the maximum and minimum number of keys for a given height
Read
- Textbook 1, Ch. 18 B-trees
- Textbook 3, §4.7 B-trees
- Textbook 4, §11.2 B-trees
Watch
Intuition
Reading one block from a disk takes as long as millions of CPU steps. A binary tree with a million keys has height about 20 — that is 20 disk reads per search. A B-tree packs many keys into each node (one node = one disk block), so a node with 100 keys has 101 children and a million keys fit in a tree of height 2 or 3.
digraph {
rankdir=TB; node [shape=record, fontname="Inter"]; edge [dir=none];
r [label="<a> | 20 | <b> | 40 | <c>"];
x [label="5 | 10 | 15"]; y [label="25 | 30"]; z [label="45 | 50 | 55"];
r:a -> x; r:b -> y; r:c -> z;
}
A B-tree with minimum degree t = 2 (a 2-3-4 tree): every node has 1–3 keys, keys between the parent's keys go to the child between them, and all leaves are on the same level.
How it works
B-tree of minimum degree t ≥ 2:
| Rule | Detail |
|---|---|
| keys per node | every node except the root has t − 1 to 2t − 1 keys (the root has 1 to 2t − 1) |
| children | an internal node with k keys has k + 1 children |
| order | keys in a node are sorted; the keys in child i lie between key i−1 and key i |
| balance | all leaves are at the same depth |
(Books that use the order m — the maximum number of children — write m = 2t: each node has at most m − 1 keys and at least ⌈m/2⌉ children.)
Search: at each node, scan (or binary-search) its keys, then descend into the one child whose range contains the key.
Insert (split on the way down): insertion always goes into a leaf. On the way down, whenever you meet a full node (2t − 1 keys), split it: its middle key moves up into the parent, and the remaining keys form two nodes of t − 1 keys each. Because full nodes are split before you enter them, the parent always has room. When the root is full, splitting it creates a new root — the only way a B-tree grows taller, so all leaves stay at the same depth.
Worked example (t = 2, so at most 3 keys per node): insert 10, 20, 30, 40, 50, 60.
| Insert | Action | Tree (levels) |
|---|---|---|
| 10, 20, 30 | fill the root leaf | [10 20 30] |
| 40 | root full → split: 20 moves up | [20] / [10] [30 40] |
| 50 | goes to [30 40] | [20] / [10] [30 40 50] |
| 60 | [30 40 50] full → split: 40 moves up | [20 40] / [10] [30] [50 60] |
B+ tree: a variation used by almost every database and file system.
- All records live in the leaves; internal nodes hold only copies of keys used for routing.
- Leaves are linked left to right, so a range query ("all keys from 30 to 70") finds the first key, then walks the leaf list.
- Internal nodes hold no data, so more keys fit per block — the tree is even shallower.
digraph {
rankdir=TB; node [shape=record, fontname="Inter"]; edge [dir=none];
r [label="<a> | 30 | <b> | 50 | <c>"];
l1 [label="10 | 20"]; l2 [label="30 | 40"]; l3 [label="50 | 60 | 70"];
r:a -> l1; r:b -> l2; r:c -> l3;
l1 -> l2 [dir=forward, style=dashed, constraint=false]; l2 -> l3 [dir=forward, style=dashed, constraint=false];
}
A B+ tree: 30 and 50 appear both as routing keys and in the leaves; dashed arrows link the leaves.
Python implementation
class BNode:
def __init__(self, leaf=True):
self.keys, self.children, self.leaf = [], [], leaf
class BTree:
def __init__(self, t):
self.t, self.root = t, BNode()
def search(self, key, node=None):
node = node or self.root
i = 0
while i < len(node.keys) and key > node.keys[i]:
i += 1
if i < len(node.keys) and node.keys[i] == key:
return True
return False if node.leaf else self.search(key, node.children[i])
def _split_child(self, parent, i):
t, full = self.t, parent.children[i]
right = BNode(full.leaf)
parent.keys.insert(i, full.keys[t - 1]) # middle key moves up
right.keys, full.keys = full.keys[t:], full.keys[:t - 1]
if not full.leaf:
right.children, full.children = full.children[t:], full.children[:t]
parent.children.insert(i + 1, right)
def insert(self, key):
if len(self.root.keys) == 2 * self.t - 1: # full root: grow upwards
old, self.root = self.root, BNode(leaf=False)
self.root.children.append(old)
self._split_child(self.root, 0)
node = self.root
while not node.leaf:
i = sum(1 for k in node.keys if key > k)
if len(node.children[i].keys) == 2 * self.t - 1:
self._split_child(node, i)
if key > node.keys[i]:
i += 1
node = node.children[i]
node.keys.insert(sum(1 for k in node.keys if key > k), key)
def levels(self):
out, level = [], [self.root]
while level:
out.append([n.keys for n in level])
level = [c for n in level for c in n.children]
return out
if __name__ == "__main__":
tree = BTree(t=2)
for k in [10, 20, 30, 40, 50, 60]:
tree.insert(k)
print(tree.levels()) # [[[20, 40]], [[10], [30], [50, 60]]]
print(tree.search(50), tree.search(35)) # True False
Complexity
With n keys and minimum degree t, the height is at most log_t((n + 1)/2).
| Operation | Disk reads | CPU time |
|---|---|---|
| search | O(h) = O(log_t n) | O(t log_t n) |
| insert | O(log_t n) | O(t log_t n) |
| delete | O(log_t n) | O(t log_t n) |
Key counts for height h (root at depth 0): - maximum: every node has 2t − 1 keys → (2t)^(h+1) − 1 keys; - minimum: root has 1 key, every other node t − 1 keys → 2t^h − 1 keys.
Common mistakes
- Splitting after inserting (bottom-up) and then forgetting that the split can cascade to the root. The top-down method avoids that by splitting full nodes on the way down.
- Wrong middle key. In a full node of 2t − 1 keys, the key at index t − 1 (0-based) moves up.
- Mixing order m and minimum degree t. Check which one the question uses: order m means at most m children (m − 1 keys).
- B+ trees: deleting the routing key. In a B+ tree a key can remain in an internal node after its record is deleted from the leaf — it is only a signpost.
Problems
- Insert 5, 3, 21, 9, 1, 13, 2, 7, 10, 12, 4, 8 into a B-tree with t = 2. Draw the tree after each split.
- A B-tree of order m = 5 (at most 4 keys per node) has height 2. What is the largest number of keys it can hold? The smallest?
- Lab: extend the
BTreeclass withinorder()and a range querybetween(lo, hi). - A disk block holds 4096 bytes; a key takes 8 bytes and a child pointer 8 bytes. What is the largest minimum degree t a node can have? How many levels does a B-tree need for 10⁹ keys?
- Explain why a B+ tree is better than a B-tree for the SQL query
SELECT * FROM marks WHERE score BETWEEN 60 AND 80.
Quiz
GATE-style practice
Questions in the GATE CSE formats: MCQ (one correct option), MSQ (one or more correct options) and NAT (type a number). Each comes with a full worked solution. These are original practice questions, not past papers; for the real thing, see the official GATE site and its previous question papers.