Data Intensive Systems: Lecture 5
B+ trees in depth: node structure and fan-out, search, insertion with leaf and internal splits, deletion with redistribution and merging, range scans over clustered vs unclustered data, and latch crabbing.
Access Methods and Data Entries
Access methods lead the engine to the right page instead of scanning all of them: a hash index computes a bucket and answers equality, a tree index keeps entries sorted and answers ranges and ordered scans as well. Whatever the family, the index must say how to reach the underlying record. That payload is the data entry.
Data entry (). The information stored in the index for a search-key value . It tells the DBMS how to reach the matching record or records. There are three classic forms, and the choice is orthogonal to whether the index is a hash or a tree.
| Alt. | Form of entry | Meaning |
|---|---|---|
| 1 | the data record itself | the index and the data file are the same structure, so no extra jump is needed |
| 2 | one key and one pointer per matching record; works even with duplicate keys | |
| 3 | one key and a list of pointers; compact when many records share a key |
Alternatives 2 and 3 cost less to maintain than Alternative 1, and a file with several indexes can give only one of them Alternative 1, because the data can be physically organized only one way at a time. Alternative 3 saves space under heavy duplication, but its entries become variable-sized: one long RID list can spread a single entry across several pages.
Example 1. Two access paths over one file.
An employee file is hashed on age, so its buckets hold the actual records: this is Alternative 1. A second index on salary should not duplicate those records, so it stores pairs or entries instead.
Why Ordered Trees
Consider a range query:
SELECT *
FROM Student
WHERE gpa > 3.0If the file were physically sorted on gpa, the DBMS could binary-search to the first match and then scan forward. The catch is that keeping the whole relation sorted is expensive: every insert and delete disturbs the order. The fix is to sort a smaller index file instead of the data, search the index first, and follow it to the data pages. Keeping the index ordered rather than the relation is exactly the idea behind the -tree.
-tree. A self-balancing, ordered, page-based tree. Search, insertion, deletion, and sequential access all run in , where is the fan-out and is the number of leaves.
Fan-out. The maximum number of child pointers an internal node can hold. Large fan-out is what keeps a disk-based tree shallow: one node is one page, each internal page packs many separator keys and pointers, and all leaves are chained in sorted order for range scans.
A -tree is an -way search tree, and its shape is pinned down by a few invariants.
- Balanced. Every leaf is at the same depth.
- Minimum occupancy. Every node except the root is at least half full.
- Inner node. Has between and children; a node with children holds separator keys.
- Leaf node. Holds between and keys.
- Root exception. The root alone may hold fewer entries than an ordinary internal node.
Internal nodes store only separator keys and child pointers; leaves store the actual data entries and are connected left to right by sibling pointers. The tree decides how to find an entry; the chosen alternative decides what the entry contains.
At implementation level a node is a sorted array of entries inside one page. An internal node with child pointers stores separator keys , where separates the subtree under from the subtree under . In a leaf, the value beside each key is a record pointer, a RID list, or the record itself, by the data-entry alternative, and prev/next links chain the leaves in key order.
Search
Search always starts at the root. At each internal node, key comparisons select one child pointer, and the descent stops at a leaf. The leaf then settles the query: the key is present, the key is absent (it would have to sit between two stored neighbors), or it is the first of a range that the sibling pointers continue. A lookup therefore returns either the matching entry or the exact leaf position where the key would belong, and this lower-bound behavior is what insertion and range scans reuse.
| Query | Leaf reached | Outcome |
|---|---|---|
find 5 | present | |
find 15 | absent: it would sit between 14 and 16 | |
| keys | first match, then follow sibling links right |
Equality search descends to one leaf and checks it. Range search descends once, then scans leaf pages in order.
Insertion
Leaf split. When the target leaf is full, the DBMS allocates a new leaf, redistributes the entries evenly, and inserts a separator into the parent. If the parent also overflows, the split propagates upward.
Example 2. Insert 8.
The search reaches the first leaf because , but is full. A new leaf is allocated, the values are split into and , and separator (the new right leaf's first key) is copied into the parent. The parent has room, so the insertion stops there.
Before
After
Root split. When an insertion reaches an overflowing root, the DBMS splits it and creates a new root above. This is the only case where the height of a -tree grows.
Leaf splits and internal splits move keys upward in different ways, and this distinction is worth holding onto.
| Case | What moves up | Reason |
|---|---|---|
| Leaf split | separator is copied up | the leaf stores real data entries, so the key must stay; the parent only needs a routing copy |
| Internal split | middle separator is pushed up | internal keys are pure routing, so the promoted separator appears once, only in the parent |
A leaf split copies up; an internal split pushes up.
Example 3. Insert 21 and split to the root.
Inserting into the full leaf splits it into and , copying into the parent. The parent now overflows, so an internal split pushes up into a brand-new root. The tree is one level taller.
Before inserting 21
After split propagation
Redistributing entries differently could sometimes delay a root split, but most implementations use the local split rule because it is predictable and has low maintenance cost. The recurring lesson is that insertions begin locally at one leaf, yet their structural effect may travel all the way up.
Example 4. Insert 28, 6, and 25.
Inserting fits the rightmost leaf with no split. Inserting overflows into and , copying up. Inserting overflows into and , copying up; the parent then overflows and an internal split pushes up, giving the final two-level tree.
Deletion
Deletion. Remove a key from its leaf. If the leaf still meets the minimum-occupancy rule, the work is done. Otherwise the tree rebalances by redistribution or merge.
The standard pattern descends to the leaf holding the entry and removes it. If is still at least half full, stop. If not, try redistribution: borrow an entry from a sibling under the same parent. If no sibling can spare one, merge with a sibling and delete the corresponding separator from the parent. An underflowing parent triggers the same logic one level up, and if the root is left with a single child, that root is removed and the tree shrinks by one level.
Example 5. Delete 19, then 20.
Removing leaves , which is legal. Removing leaves , which underflows, so the leaf borrows from its right sibling : the boundary shifts to give and , and the parent separator updates from to . A leaf-level boundary key is copied up, because leaves keep the actual entries.
After deleting 19
After deleting 20 and redistributing
Example 6. Delete 24 and shrink the tree.
Removing turns into . Its sibling is already at minimum, so borrowing fails and the two merge into . The parent loses a separator and underflows, so the merge propagates up, the old root is removed, and the tree height drops by one. Deletion can do the opposite of insertion: insertion may raise the height, deletion may lower it.
Before deleting 24
After merge and tree shrink
Redistribution is not only for leaves; internal nodes can redistribute too, but the mechanics differ. At the non-leaf level the move goes through the parent: one separator from the parent drops into the underfull node, one separator from the fuller sibling rises into the parent, and the child pointer travels with that separator. It is a push-through-parent rotation, not a direct sideways shift.
Example 7. Non-leaf redistribution.
The underfull internal node receives a separator pushed down from the parent, while a separator from the richer sibling is pushed up to replace it, carrying its child pointer along.
Before non-leaf redistribution
After pushing through the parent
Range Scans and Clustering
A -tree shines on ordered scans because the leaf chain already holds keys in order. To read many records in key order, the DBMS descends to the leftmost relevant leaf and then follows leaf links rightward. The traversal is identical regardless of how the data sits on disk; what changes is how leaf entries map to table pages.
Clustered index. An index whose logical order matches the physical order of the table data, so nearby entries point to nearby records. The table is stored in the sort order of one chosen key, either heap-organized but kept in that order or index-organized with the rows in the leaves. Because a file has only one physical order, a table has at most one clustered organization.
With a clustered index, consecutive leaf entries land on nearby table pages, so a scan reads data pages in mostly sequential order. With an unclustered index, consecutive entries point to tuples scattered across the file, so following the leaf order can trigger many random reads.
Clustering makes ordered scans and large ranges cheap, but it costs on updates: keeping physical order may force page splits, record movement, or reorganization. Systems differ here. Some always organize a table around a clustered primary key and invent a hidden row identifier when none is declared; others keep ordinary heap files and let no index dictate the data order.
For an unclustered index, a common optimization first collects the matching RIDs from the leaves, sorts them by table page id, then fetches pages in page order. This abandons key order during fetching but saves many random reads when many tuples qualify.
Design Choices
Real systems share the core -tree but make many low-level choices, starting with node size. A larger node raises fan-out and shrinks the tree, but takes longer to search once loaded. As a rule, the slower the device, the larger the best node.
| Storage medium | Typical size | Why |
|---|---|---|
| HDD | MB | large transfers amortize seeks and rotation |
| SSD | KB | cheap random access needs less batching |
| In-memory | B | cache behavior dominates, so smaller is better |
These are only guidelines: long leaf scans favor larger nodes, while repeated root-to-leaf lookups favor smaller nodes that fit caches.
The half-full rule is likewise a guarantee about the ideal structure, not always a strict policy after every delete. Many systems delay merging because inserts usually outnumber deletes (a sparse page may soon refill), eager merging causes extra page movement and separator churn, and average occupancy settles near . Some prefer periodic rebuilding over aggressive merge-on-delete.
Variable-length keys raise the question of how to pack them into fixed-size pages.
- Pointer-based keys. Store a pointer to the real value instead of the value.
- Variable-length nodes. Manage free space inside the page, like a slotted page.
- Padding. Pad every key to its type maximum: direct but wasteful.
- Key indirection. A small array of offsets into a key area inside the node.
The trade-off is the usual one: a fixed-width layout wastes more space, while a compact layout complicates memory management.
Once a node is in memory, the DBMS still has to find the right entry inside it.
| Method | Idea | Attractive when |
|---|---|---|
| Linear | scan keys left to right | small nodes; direct and SIMD-friendly |
| Binary | compare against the middle key | larger nodes with cheap comparisons |
| Interpolation | estimate position from key spread | keys roughly uniformly distributed |
Interpolation search estimates the landing spot rather than always probing the middle:
where is the search key. It works best when key values are spread fairly evenly.
Concurrent Access
Concurrency is harder in a tree than in isolated heap pages, because pages depend on one another structurally. Latching one page protects it against concurrent modification, but a split or merge also rewires parent-child relationships, so several pages must be synchronized together.
Lock coupling (latch crabbing). During a top-down traversal, a thread latches a parent, then latches the child it is about to enter, and only then releases the parent. The traversal keeps a short, overlapping chain of latches as it descends.
Because latches are taken in a fixed top-down order, ordinary traversals cannot deadlock. The pattern works for search and for updates that do not split or merge: latch the root, latch the next level, release the parent, and continue downward in canonical order.
Inserts are trickier, because a split may propagate upward: a leaf split inserts a separator into the parent, that parent may split, and the cascade can reach the root. By then, naive coupling has already released those ancestors.
The common fix is optimistic coupling: first attempt the insert with ordinary lock coupling; if no split propagates through inner nodes, it succeeds; otherwise release the latches and restart with a conservative strategy that keeps the needed ancestors protected, then execute on the second pass. This reduces concurrency during the rare restarted operation, but the expensive case is uncommon and correctness stays manageable.
Shape in Practice
In practice, -trees are shallow. Textbooks often parameterize a tree by , where an internal node has between and children. With and an average fill of about , the average fan-out is
which already gives enormous capacity.
| Height | Approximate entries |
|---|---|
| 3 | |
| 4 |
A tree of height 3 or 4 covers even large relations.
| Level | Size (8 KB pages) |
|---|---|
| 1 | page KB |
| 2 | pages MB |
| 3 | pages MB |
The top levels are small enough to stay resident.
So in practice the root is almost always in memory, often one or more upper levels too, and only the lower levels need regular I/O.