CS Core & Software Engineering · Data Structures & Algorithms
B-Trees
Multi-way search trees of order m: node occupancy, search, insert-with-split, delete-with-borrow-or-merge, and why disk indexes use them.
Six concepts on B-trees — order and occupancy, multi-way search, insert-with-split, delete-with-borrow-or-merge, why fan-out cuts I/O, and the B+ fork. Running example: an order-3 tree grown from keys 10, 20, 30, 25, 5, 15.
- CS Core & Software Engineering
- Hard level
- 6 concepts
1Order m: many keys in one node
A B-tree of order m is a multi-way search tree. Every node has at most m children and therefore at most m-1 keys, stored in sorted order. Every node except the root has at least \lceil m/2 \rceil children and \lceil m/2 \rceil - 1 keys. All leaves sit at the same depth, so the tree is perfectly balanced in levels even though a node may hold one key or many.
The root is the occupancy exception: it may have as few as one key and two children (or be a single leaf when the tree is tiny). That is how a B-tree is allowed to start.
This topic uses the children convention for m. Some books call m the maximum number of keys instead — count children before you count keys.
Figure. Order-3 B-tree used for the rest of the topic. Root holds 10 and 20; three leaves hold 5, then 15, then 25 and 30. All leaves at one depth.
What order 3 allows
- CapAt most 3 children and 2 keys in any node.
- Floor (non-root)At least 2 children and 1 key.
- Same depthEvery leaf is at the same level — there is no AVL-style zigzag spine.
| Kind of node | Keys | Children |
|---|---|---|
| Any node, maximum | 2 | 3 |
| Non-root, minimum | 1 | 2 |
| Root, minimum | 1 (or 0 if the tree is a single empty leaf) | 2 (or 0) |
Keys versus children
A non-root node in an order-3 B-tree holds keys 10 and 20. How many children must it have, and what key ranges do those children cover?
- keys in the node2 keys \Rightarrow 3 children
- left childkeys < 10
- middle childkeys between 10 and 20
- right childkeys > 20
Pro tip. A node with k keys always has k+1 children if it is internal. Drawing k children is the usual off-by-one.
In a B-tree of order m (max children), a non-root node has at least how many keys?
- \lceil m/2 \rceil - 1
- m - 1
- \lceil m/2 \rceil
Minimum children is \lceil m/2 \rceil, and keys are one fewer than children. m-1 is the maximum, not the minimum. \lceil m/2 \rceil is the child floor, not the key floor.
2Search follows one child per node
Search in a B-tree is binary-search-tree search with a wider node. At a node you scan (or binary-search) the sorted keys. If the target equals a key, you are done — in a classical B-tree the record pointer may sit right there. If the target is less than the first key you take the leftmost child; if it sits between two keys you take the child between them; if it is larger than the last key you take the rightmost child.
You visit O(\log_m n) nodes. Work inside a node is O(m) for a linear scan, or O(\log m) if you binary-search the keys. For disk, the node count is the I/O count, which is why a large m is the point.
Figure. Search for 25: root says go right of 20; the right leaf holds 25. Same tree as the occupancy figure — two nodes on the path.
Search for 25
- At the rootKeys 10, 20. 25 > 20, so take the right child.
- At the right leafKeys 25, 30. 25 is present — hit.
- A missSearch for 17 would take the middle child [15] and die there. Insert would hang 17 in that leaf.
Search 25 and 17
In the order-3 tree with root [10,20] and leaves [5], [15], [25,30], how many nodes does a search for 25 visit, and where does a search for 17 die?
- 25 vs root [10,20]25 > 20 → right child
- 25 vs [25,30]hit; 2 nodes visited
- 17 vs root [10,20]10 < 17 < 20 → middle child
- 17 vs [15]miss; insert would go in this leaf
Pro tip. A successful classical B-tree search may stop at an internal node if the key lives there. A B+ tree always walks to a leaf, because data pointers live only in leaves.
Searching a B-tree of n keys and order m visits how many nodes?
- O(\log_m n) — one node per level, height \log_m n
- O(\log_2 n) nodes always, because each node is binary-searched
- O(n/m) — you scan every node
The path is one node per level. Binary-searching keys inside a node changes the CPU work per node, not the number of nodes (or pages) visited. You do not scan the whole tree.
3Insert splits an overfull node at its median
Insert searches to a leaf and writes the new key in sorted order. If the leaf then has m keys it is overfull. Split: take the median key, push it into the parent, and leave the keys left of the median in the old node and the keys right of the median in a new sibling. The parent gains one key and one child. If the parent overflows, split it the same way.
A split at the root is the only way the tree grows a new level — B-trees grow at the top. Leaves do not sprout downward.

Insert 30 into [10, 20]
- Write in the leafThe only node is [10,20]. 30 belongs at the end: [10,20,30].
- OverflowOrder 3 allows 2 keys. Three keys is illegal.
- Median upMedian 20 becomes a new root. Left leaf [10], right leaf [30].
Split a full child
def split_child(parent, i, full):
mid = len(full.keys) // 2
median = full.keys[mid]
right = Node(keys=full.keys[mid + 1:],
children=full.children[mid + 1:])
full.keys = full.keys[:mid]
full.children = full.children[: mid + 1]
parent.keys.insert(i, median)
parent.children.insert(i + 1, right)Insert 10, 20, 30 then 25, 5, 15
Order-3 B-tree, start empty. After 10, 20, 30 the root is [20] with leaves [10] and [30]. Insert 25, 5, then 15. What is the tree after 15?
- 25 into [30]right leaf [25,30]; no split
- 5 into [10]left leaf [5,10]; no split
- 15 into [5,10] → [5,10,15]overflow; median 10 moves up
- root [20] becomes [10,20]leaves [5], [15], [25,30]
Pro tip. 15 did not create a new level. The median moved into an existing root that still had room. A new level appears only when the root itself splits.
Coding lab. Split the overfull order-3 leaf runs in the app, with checks on your output.
A B-tree grows a new level when
- The root splits — a new root is created above it
- A leaf splits — leaves sprout a deeper level
- Any node reaches \lceil m/2 \rceil keys
A leaf split pushes a median into the parent and stays at the same depth. Only a root split adds a level. \lceil m/2 \rceil keys is the non-root minimum, not a split trigger.
4Delete: borrow through the parent, or merge
Delete starts by locating the key. If it sits in an internal node, replace it by its in-order predecessor (or successor) in a leaf and delete that leaf copy — the same idea as BST delete, but the replacement lives in a leaf. After the leaf loses a key, check occupancy.
If the leaf still has at least \lceil m/2 \rceil - 1 keys, stop. If it underflows, try to borrow: a sibling with a spare key rotates one key through the parent separator. If no sibling can spare a key, merge the underflowing node with a sibling and pull the parent separator down into the merged node. A merge can underflow the parent, so delete walks up just as AVL delete does.

Delete 15 (borrow from the right)
- Drop the leaf key[15] becomes empty — below the 1-key floor.
- Left cannot lend[5] already has the minimum one key.
- Right can lend[25,30] has a spare. Separator 20 moves down; 25 moves up. Leaves become [5], [20], [30]; root becomes [10,25].
Borrow after deleting 15
Order-3 tree: root [10,20], leaves [5], [15], [25,30]. Delete 15. Report the root and the three leaves after the borrow.
- remove 15middle leaf empty — underflow
- left sibling [5]at minimum; cannot borrow
- right sibling [25,30] lends through 2020 down, 25 up
- finishroot [10,25]; leaves [5], [20], [30]
Pro tip. The borrowed key is not a sibling's edge key sliding sideways. It always passes through the parent, so search order (left < separator < right) stays true.
When a B-tree leaf underflows and a sibling has a spare key, the repair is
- Borrow: rotate a key through the parent separator
- Move the sibling's nearest key directly into the empty leaf, leaving the parent unchanged
- Always merge — borrow is only for internal nodes
Borrow always uses the parent separator as the stepping stone. Skipping the parent breaks the between-keys invariant. Merge is the fallback when no sibling has a spare.
5Fan-out cuts I/O, not just pointer hops
A binary AVL of a million keys has height about 20. A B-tree of order m = 101 (100 keys per node) has height about \log_{101}(10^6) \approx 3. On disk each node is a page, so those numbers are page reads. That is the whole reason B-trees exist: one I/O should move you across many keys.
Pick m from the page size, not from taste. A 4 KiB page holding 8-byte keys and 8-byte child pointers supports on the order of a hundred children. Shrinking m to 3 for a disk index throws the I/O advantage away.
Figure. Nodes visited to search 10^6 keys: about 20 in a binary tree, 3 in an order-101 B-tree. On disk those bars are page reads.
Height for n = 10^6
- Binary\lceil \log_2 10^6 \rceil \approx 20 levels.
- Order 101\lceil \log_{101} 10^6 \rceil = 3 because 101^2 = 10201 and 101^3 \approx 1.03 \times 10^6.
- Read as I/OThree page reads versus twenty, assuming one node per page.
A million keys
Compare worst-case nodes visited to search n = 10^6 keys in a binary tree versus a B-tree of order 101.
- \log_2 10^6\approx 19.93 → 20 levels
- 101^210201
- 101^31030301 > 10^6 → 3 levels
- I/O if one node = one page20 pages vs 3 pages
Pro tip. The O(m) scan inside a page is CPU. The O(\log_m n) walk is I/O. Disk indexes optimise the second number.
A million keys in a B-tree of order 101 need about how many node visits to search?
- 3 — because 101^3 > 10^6
- 20 — binary height, reused because each node is binary-searched
- 101 — one visit per possible child
101^3 \approx 1.03 \times 10^6, so three levels suffice. Twenty is the binary height. You follow one child per level, not 101 children.
6Filesystems and databases, and the B+ fork
B-trees (and their B+ cousins) sit under filesystems and database indexes because a node is sized to a disk page and a search is a handful of I/Os. That is a different job from an in-memory AVL: same word balanced, different unit of cost.
A classical B-tree may store a record pointer with a key at any level, so a hit can stop early. A B+ tree keeps every record pointer in the leaves, uses internal nodes only as a router, and usually links the leaves so a range scan walks a list instead of re-descending. Production databases almost always pick B+. This topic stays with the classical B-tree algorithms; the B-versus-B+ structural split is the indexing lesson, not a second copy of it.
Figure. Qualitative fit, not a shared unit: AVL for RAM maps, B-tree for disk point lookups, B+ when the workload is range scans on disk.
Which tree for which store
- RAM, ordered mapAVL or red-black — pointer hops are cheap.
- Disk pagesB-tree or B+ tree — fan-out sized to the page.
- Range scans on diskB+ tree, because leaves are linked and hold every key.
| Kind | Where keys / data live | Range scan |
|---|---|---|
| Classical B-tree | Keys (and often record pointers) at every level | Re-walk the tree |
| B+ tree | Record pointers only in leaves; internals route | Walk the leaf linked list |
A classical B-tree (not B+) may report a hit
- At an internal node, because a key there can carry a record pointer
- Only at a leaf — that is the B-tree rule
- Only after scanning every leaf, because internals store no keys
Classical B-trees store keys at every level. Restricting data pointers to leaves is the B+ rule. Internals in a B-tree do store keys.
Notes
- A B-tree of order m is a search tree in which every node has at most m children and every non-root has at least \lceil m/2 \rceil children. All leaves sit at the same depth.
- A node with k children stores k-1 keys in sorted order. Maximum keys in a node is m-1; a non-root has at least \lceil m/2 \rceil - 1 keys.
- Search scans the keys in a node and follows one child — O(m) work per node, O(\log_m n) nodes on the path.
- Insert writes the key in a leaf. An overfull node (m keys) splits: the median key moves to the parent and the remaining keys become two legal nodes.
- Delete that underflows a node either borrows a key from a sibling through the parent or merges with a sibling and pulls the parent separator down.
- High fan-out makes height a handful of pages, which is why filesystems and databases use B-trees (or B+ trees) rather than binary AVLs on disk.
Formulas
- Order m: at most m children, at most m-1 keys per node.
- Non-root occupancy: at least \lceil m/2 \rceil children and \lceil m/2 \rceil-1 keys.
- Height in nodes visited: O(\log_m n) = O(\log n / \log m).
- Split: a node with m keys sends its median to the parent and becomes two nodes of \lfloor (m-1)/2 \rfloor and \lceil (m-1)/2 \rceil keys.
- I/O cost of a search is the height in pages, not the binary height.
Exam traps & shortcuts
- Order m here means maximum children, not maximum keys — say which convention you are using before counting.
- The root is allowed to be poorer than \lceil m/2 \rceil children; every other node is not.
- Insert splits bottom-up; a split at the root is the only way the tree grows a level, so B-trees grow at the root, not at the leaves.
- B+ trees keep every record pointer in the leaves and link the leaves; a classical B-tree may store a record pointer with a key at any level. Do not swap those rules.
Reference tables
Children convention — restated from the occupancy concept.
| Quantity | Form |
|---|---|
| Max children / keys | m / m-1 |
| Non-root min children / keys | \lceil m/2 \rceil / \lceil m/2 \rceil-1 |
| Internal node with k keys | k+1 children |
| Search nodes visited | O(\log_m n) |
| Split trigger | a node that has reached m keys |
| New level | only when the root splits |
The instance the insert and delete ledgers share.
| After | Root | Leaves |
|---|---|---|
| 10, 20 | [10,20] (single node) | — |
| +30 (split) | [20] | [10], [30] |
| +25, +5, +15 | [10,20] | [5], [15], [25,30] |
| delete 15 (borrow) | [10,25] | [5], [20], [30] |
Recap
Night-before B-tree pegs.
- Order
- m = max children. Non-root has \ge \lceil m/2 \rceil children. All leaves at one depth.
- Search
- Scan keys, follow one child. O(\log_m n) nodes — that is the I/O count on disk.
- Insert
- Write in a leaf; split at the median; grow a level only at the root.
- Delete
- Borrow through the parent separator, or merge and pull the separator down.
- Why
- 10^6 keys: ~20 binary levels vs ~3 at m=101.
- B+
- Data only in linked leaves. Classical B-tree may hit at an internal node.
Practise B-Trees
Reading is free and needs no account. Practice, mocks and progress live in the app.
- A 6-question practice set that ends the chapter
- 6 quick checks with worked explanations
- Timed mocks scored with the real marking scheme
- Readiness tracked per topic, kept on your device