Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 9

B+ Tree Operations: Insertion and Deletion

Recommended resources

Hello! Let's dive into the mechanics of how a B+ Tree maintains its structure.

Introduction

In our last lesson, we established why the B+ Tree is the go-to index structure for disk-based databases. We learned that its short, wide structure, high fanout, and linked-list leaf nodes are all designed to minimize costly disk I/O, especially when compared to a traditional binary search tree.

A key property we discussed is that a B+ Tree is always balanced. This means the path from the root to any leaf is the same length. Additionally, every node (except possibly the root) must be at least half-full. Today's learning outcome is to implement insertion and deletion operations in a B+ tree, handling node splits and merges. We will explore the algorithms that ensure these critical properties are maintained as the tree grows and shrinks. This lesson is all about the dynamic nature of the B+ tree.


1. B+ Tree Insertion

The insertion algorithm always starts the same way: traverse the tree from the root to find the appropriate leaf node where the new key should be placed. Once you've found the target leaf, there are two possibilities.

The Simple Case: Space is Available

If the leaf node is not full (i.e., it contains fewer than max_size - 1 keys), the process is simple:

  1. Insert the new key-value pair into the leaf node.
  2. Keep the keys in the leaf node sorted.
  3. You're done! No other nodes in the tree are affected.

The Complex Case: Node Splitting

The more interesting case occurs when the target leaf node is already full. You cannot simply add another key, as this would violate the node's size constraint. This is where node splitting comes in.

The core idea is to split the full node into two, distributing the keys between them, and then updating the parent to reflect this change. This process can propagate up the tree.

Let's watch a detailed walkthrough of the insertion algorithm, focusing on how splits are handled.

#08 - B+Trees: The Best Data Structure in the World (CMU Intro to Database Systems)

This segment from the CMU Introduction to Database Systems course provides a clear, step-by-step explanation of the B+ Tree insertion algorithm, complete with animated diagrams that show how splits propagate up to the root.

Watch the segment from 37:12 to 45:22. Pay close attention to: The initial traversal to find the correct leaf. The process of splitting a full leaf node into two separate nodes. How the middle key is 'copied up' or 'pushed up' to the parent node to act as a new separator. The recursive nature of splitting: what happens when you try to push a key up to a parent node that is also full.

To solidify this concept, here is a summary of the algorithm when a node (let's call it N) overflows:

  1. Create a new node, N'.
  2. Distribute keys: Take all the keys that were in N, plus the new key you're trying to insert. Move the second half of these sorted keys into the new node N'. N now contains the first half.
  3. Promote the middle key: Take the first key from N' (which was the middle key of the combined set) and insert it into the parent node. This key now serves as the separator, with a pointer to N'.
  4. Handle the parent recursively: If the parent node is now full after receiving the promoted key, repeat the split process on the parent. This can continue all the way to the root. If the root itself splits, you create a new root, and the tree's height increases by one.

This image provides a static, step-by-step view of an insertion that causes a leaf, and subsequently the root, to split.

B+ Tree Insertion with Node Split and Root Split
This diagram shows the insertion of key '35'. First, the leaf node [20, 25, 30] splits. The middle key '25' is promoted to the parent. Since the parent [15, 25, 40] is now full, it also splits, and its middle key '25' becomes the new root of the tree.
Test your understanding!

Consider a B+ Tree where internal and leaf nodes can hold a maximum of 3 keys (i.e., max_size = 4, order = 4). The current state of the tree is:

  • Root: [20]
  • Left Child (Leaf): [5, 10, 15]
  • Right Child (Leaf): [20, 25, 30]

You want to insert the key 17. Describe the state of the tree after this insertion. Which node splits? What key is promoted? What is the new root?

Show answer
  1. The key 17 belongs in the left leaf node [5, 10, 15].
  2. This node is full. We temporarily have [5, 10, 15, 17].
  3. The leaf node splits. The original node becomes [5, 10] and a new leaf node is created with [15, 17].
  4. The middle key, 15, is promoted to the parent (the root).
  5. The root now contains [15, 20]. It is not full, so no further split is needed.
  6. The final tree structure is:
    • Root: [15, 20]
    • Children (Leaves): [5, 10], [15, 17], [20, 25, 30]

2. B+ Tree Deletion

Deletion is the reverse process of insertion. It starts by finding the key in the correct leaf node and removing it. The complexity arises when this removal causes the node to become less than half-full, violating one of the B+ Tree's core invariants.

The Simple Case: Node Remains Half-Full

If you delete a key from a leaf node and the node still has at least ceil(max_size / 2) entries, you are done. The B+ tree properties are maintained.

The Complex Case: Node Underflow

If deleting the key causes the node to underflow, you must rebalance the tree. You have two options, which should be attempted in this order:

  1. Redistribution (Borrowing): Look at an immediate sibling (a node next to it with the same parent). If the sibling has more than the minimum number of keys, you can "borrow" a key from it. The key moves from the sibling, through the parent, into your under-full node. The separator key in the parent must be updated to reflect the new boundary. This is generally preferred as it is a less disruptive local operation.

  2. Merging: If the sibling is also at the minimum fill level and cannot spare a key, you must merge your under-full node with the sibling. This involves combining all keys from both nodes into a single node and deleting the separator key from the parent that used to sit between them.

This merge operation might cause the parent node to underflow, in which case the rebalancing logic is applied recursively up the tree. If the root's last two children are merged, the root itself is removed, and the tree's height decreases by one.

Let's watch the deletion process in detail.

#08 - B+Trees: The Best Data Structure in the World (CMU Intro to Database Systems)

Continuing with the CMU video, this segment explains the deletion algorithm, demonstrating both redistribution and merging with clear animations.

Watch the segment from 45:22 to 53:00. Focus on: What happens when a deletion causes a node to be less than half-full. The 'borrowing' or 'redistribution' mechanism from a 'rich' sibling. How the parent's separator key is updated during redistribution. The 'merging' mechanism when redistribution is not possible. How merging nodes causes a key to be deleted from the parent, which can propagate up the tree and shrink its height.

This image shows a static example of deletion followed by redistribution.

B+ Tree Deletion with Leaf Underflow and Sibling Borrowing
Here, key '25' is deleted, causing the left leaf to underflow. It borrows key '30' from its right sibling. The parent's separator key is updated from '30' to '35' to reflect the new smallest key in the rightmost leaf.

3. An Implementation Perspective

You have an applied/engineering goal, so let's connect this theory to practice. In a real C++ implementation, like the one in CMU's Bustub project, you would model these concepts with specific classes.

Project #2 - B+Tree

The CMU DB project specification for the B+ Tree index provides an excellent roadmap for implementation. Reading about the tasks will help you structure the logic we've just discussed.

Skim through the sections for 'Task #1 - B+Tree Pages', 'Task #2 - B+Tree Operations', and the 'Development Roadmap'. You don't need to read the C++ code links, but notice: How there are separate classes for BPlusTreeInternalPage and BPlusTreeLeafPage. Your split/merge logic would have different implementations for each. Task #2 explicitly requires implementing Insert() and Remove(), handling splits and merges to maintain invariants. The 'Development Roadmap' provides a great incremental approach: start with simple inserts, then simple searches, then handle splits, then simple deletes, and finally coalesces (merges). This is a practical way to build a complex data structure.

The key takeaway is that the abstract algorithms for Insert and Remove are translated into concrete methods on Page objects. Your code would fetch a page from the buffer pool, cast it to the correct type (Leaf or Internal), and then call methods like Insert, Split, Merge, etc., on that page object.


4. Handling Duplicate Keys

One final practical consideration is how to handle duplicate keys, as non-unique indexes are very common. If you simply try to insert the same key multiple times, your B+ Tree's logic might fail. There are two common solutions:

  1. Append Record ID: The most common approach is to make every key unique by appending the record's unique identifier (e.g., its RID, composed of page ID and slot number). So, if you insert key 100, the actual entry in the tree might be (100, <RID_1>), (100, <RID_2>), etc. Since each RID is unique, the composite key is always unique.
  2. Overflow Pages: An alternative is to allow duplicate keys in a leaf node and, if there are too many to fit, create special "overflow" pages that are linked from the main leaf node. This essentially creates a linked list of records for a given key. This approach is less common because it complicates the tree logic and can degrade performance if these chains become long.

F2023 #08 - B+Tree Indexes (CMU Intro to Database Systems)

This short clip from a different CMU lecture clearly explains these two approaches for handling duplicates.

Watch from 40:35 to 46:11. The video describes the pros and cons of appending record IDs versus using overflow leaf nodes.

For most systems, the first approach (appending a unique ID) is preferred because it keeps the core B+ Tree logic unchanged and guarantees logarithmic performance.


Conclusion

Today, we went deep into the dynamic operations that keep a B+ Tree balanced and efficient. While the logic for splits and merges can be intricate to implement, the underlying algorithms are systematic and designed to preserve the tree's crucial invariants at all times.

Key Takeaways:

  • Insertion: When a node is full, it splits into two. The middle key is promoted to the parent to act as a separator. This can propagate up to the root, increasing the tree's height.
  • Deletion: When a node becomes less than half-full, it must be rebalanced by either redistributing (borrowing) a key from a full-enough sibling or merging with a sibling if it's also at minimum capacity.
  • Recursive Nature: Both splits and merges can propagate up the tree, ensuring that the half-full and balance properties are maintained at every level.
  • Implementation: These algorithms translate into methods on InternalPage and LeafPage objects, which are fetched from and written back to the buffer pool.

Preview of the Next Lesson:

Now that we have a fully functional, dynamic B+ Tree, we can finally put it to use! In our next lesson, we will focus on performing point and range queries using the index. We will analyze the I/O cost of these operations and see exactly how the B+ Tree provides rapid data access.