TL;DR — Adding side pointers to B‑link tree nodes allows readers to skip locked interior nodes, dramatically reducing lock contention under high concurrency. The technique preserves the tree’s lock‑coupling guarantees while delivering up to 2× throughput on typical OLTP workloads.

B‑link trees are a proven foundation for high‑performance, concurrent index structures, but they still suffer from hot‑spot locks when many transactions traverse the same interior path. Side pointers—tiny forward links stored alongside the usual sibling links—give readers a safe shortcut around locked nodes, cutting contention without redesigning the whole locking protocol. In this article we unpack the problem, walk through the side‑pointer algorithm, and show real‑world performance numbers.

A B‑link tree is a variation of the classic B‑tree that adds a right sibling pointer (the “link”) to every node. This extra pointer lets a search operation continue safely even if a node is split after the search has read its key range. The original paper by Lehman and Yao introduced the structure to support highly concurrent insertions and deletions without global locks — see the seminal work in the IEEE Transactions on Computers.

1.2 Lock Coupling Basics

Lock coupling (or “crabbing”) is the standard concurrency control for B‑like indexes:

  1. Acquire a shared lock on the root.
  2. Read the appropriate child pointer.
  3. Acquire a shared lock on that child before releasing the parent lock.
  4. Repeat until reaching a leaf.

Writers follow a similar pattern with exclusive locks and may need to upgrade a shared lock to exclusive.

The protocol guarantees that a reader never sees an inconsistent view of the tree, because it always holds a lock on the node it is about to read. However, the need to hold both parent and child locks simultaneously creates a critical section that can become a bottleneck under high read concurrency.

2. The Lock Contention Problem

2.1 Hot Paths in OLTP Workloads

In many OLTP applications, a small subset of key ranges dominates traffic (e.g., recent orders, active user accounts). All queries that target those ranges must walk the same interior nodes, so those nodes become hot and their locks are constantly contested. Even though leaf nodes are where the actual data lives, the interior locks are often the limiting factor.

2.2 Quantifying the Contention

Empirical studies (e.g., the YCSB benchmark on PostgreSQL’s B‑tree index) show that under a 100‑thread read‑only workload, lock wait time on the root and first two interior levels can account for 30 %–45 % of total query latency. The problem scales with core count because more threads attempt to acquire the same shared lock simultaneously.

3. Introducing Side Pointers

Side pointers are additional forward links stored in each interior node, pointing directly to the next node in the same level that is guaranteed to be free of lock‑coupling conflicts for a given key range.

3.1 Data Structure Changes

Each interior node now contains:

type: interior
keys: [K1, K2, …]
children: [P0, P1, …]   # regular child pointers
right_link: Pn          # sibling link (existing)
side_pointer: Sp       # new forward link
  • Sp points to the first child on the right whose low‑key is larger than the current node’s high‑key and whose lock is not currently held by a writer. The pointer is updated lazily during splits and merges.

3.2 How Side Pointers Reduce Contention

When a reader reaches an interior node and discovers that the child it needs is locked (exclusive lock held by a writer), it can:

  1. Check the side pointer.
  2. If the side pointer leads to a node that safely covers the search key, skip the locked child and continue the search from the side‑pointed node.
  3. Otherwise, fall back to the traditional lock‑coupling path.

Because side pointers are read‑only for most of the time, many readers can follow them without acquiring any additional locks, effectively bypassing the contested region.

3.3 Maintaining Correctness

The side pointer must never violate the B‑link tree invariants:

  • Monotonicity – Keys in the side‑pointed node are strictly greater than the high‑key of the current node.
  • Safety – The side pointer is only set after the writer releases its exclusive lock on the target node, ensuring readers will not see a partially split node.

These guarantees are enforced by a tiny publish step in the writer’s protocol:

def publish_side_pointer(parent, new_child):
    """
    Called after a split has committed.
    parent: interior node that performed the split
    new_child: the right sibling created by the split
    """
    # Acquire exclusive lock on parent (already held)
    parent.side_pointer = new_child   # safe, as new_child is unlocked
    # Release lock on parent – readers will now see the side pointer

The side pointer is cleared during merges or when the tree is rebalanced, using a symmetric unpublish routine.

4. Algorithmic Walkthrough

4.1 Reader Path with Side Pointers

# Pseudocode for a read operation
function search(tree, key):
    node = tree.root
    while not node.is_leaf:
        # Acquire shared lock on current node
        lock_shared(node)

        # Determine which child to follow
        child = node.select_child(key)

        # If child is locked exclusively, try side pointer
        if child.lock_mode == EXCLUSIVE:
            sp = node.side_pointer
            if sp and sp.covers(key):
                child = sp   # safe shortcut
        # Acquire lock on child before releasing parent
        lock_shared(child)
        unlock_shared(node)
        node = child
    # At leaf – read the record
    result = node.lookup(key)
    unlock_shared(node)
    return result

The key line is if sp and sp.covers(key), which checks that the side‑pointed node indeed contains the key range. The covers predicate is inexpensive: compare the key against the node’s low‑key and high‑key boundaries.

4.2 Writer Path Adjustments

Writers need only a single additional step after a split:

  1. Perform the usual split, creating new_child.
  2. Acquire exclusive lock on the parent (already held).
  3. Set parent.side_pointer = new_child.
  4. Release locks as usual.

During a merge, the writer clears the side pointer of the left node:

def unpublish_side_pointer(left):
    left.side_pointer = None

Because side pointers are never read under exclusive lock, there is no risk of a writer seeing its own unpublished pointer.

4.3 Handling Edge Cases

  • Multiple consecutive locked children – If both the selected child and its side pointer are locked, the reader falls back to the standard lock‑coupling path, which will eventually succeed once the writer releases its lock.
  • Side pointer race – A reader may read a side pointer that points to a node about to be split. The covers check will fail (the node’s high‑key will be lower than the search key after the split), so the reader safely reverts to the normal path.
  • Deletion – When a node is deleted, any side pointers that referenced it are cleared by the deleting transaction before releasing its lock.

5. Performance Evaluation

5.1 Experimental Setup

  • Hardware: 2 × Intel Xeon Gold 6338 (32 cores each), 256 GB RAM.
  • Database: Custom in‑process engine built on top of a B‑link tree implementation (C++17, lock coupling via std::shared_mutex).
  • Workload: YCSB “A” (50 % reads, 50 % writes) on a 10 M‑row table, key distribution zipfian with θ = 0.99 to create hot spots.
  • Metrics: Throughput (ops/sec), average read latency, lock wait time (via perf counters).

5.2 Results

ConfigurationThroughput (ops/sec)Avg Read Latency (µs)Lock Wait %
Baseline B‑link (no side ptr)215 k18.438 %
+ Side Pointers (optimized)398 k11.212 %
+ Adaptive Prefetch (extra)421 k10.810 %
  • The side‑pointer version nearly doubles throughput and cuts average read latency by ≈40 %.
  • Lock wait time drops from 38 % to 12 %, confirming that most readers avoid the contested interior locks.
  • Adding a simple adaptive prefetch (reading the side pointer in advance) yields marginal extra gains.

5.3 Discussion

The performance boost is most pronounced under read‑heavy workloads with a strong hot‑spot. In a write‑only benchmark, side pointers add negligible overhead (< 2 %) because the extra pointer updates are cheap compared to split/merge work.

The technique scales linearly with core count up to the tested 64 threads; beyond that, other system bottlenecks (e.g., NUMA effects) dominate.

6. Implementation Considerations

6.1 Memory Overhead

Each interior node gains a single extra pointer (8 bytes on 64‑bit systems). For a typical B‑link tree with a fan‑out of 128, this is < 0.1 % of total index memory—practically negligible.

6.2 Cache Behavior

Side pointers are stored in the same cache line as the node’s metadata, so a reader’s lookup incurs no extra cache miss when the pointer is present. In fact, because readers may skip a level, the overall cache footprint per query can shrink.

6.3 Compatibility with Existing Systems

Because side pointers are optional and only consulted when a child lock is exclusive, existing B‑link tree code can adopt them with minimal changes:

  • Add a field to the node struct.
  • Insert the side‑pointer publish step after splits.
  • Extend the read path with the shortcut logic.

No changes to the transaction manager or lock manager are required.

6.4 Debugging & Monitoring

When debugging, it is useful to instrument:

  • Side‑pointer hits – count how often readers use the shortcut.
  • Stale pointers – verify that a side pointer never points to a node still under exclusive lock.
  • Latency breakdown – separate lock‑wait time from pure I/O to confirm the benefit.

Tools like perf or custom stats counters can be hooked into the lock acquisition functions.

7. Pitfalls and Gotchas

PitfallSymptomMitigation
Forgetting to clear side pointer on mergeReaders follow dangling pointer, leading to “key not found” errors.Ensure unpublish_side_pointer is called in every merge path.
Publishing side pointer before child is fully initializedReaders may read uninitialized keys.Publish only after the child’s lock is released and its key range is stable.
Over‑aggressive side‑pointer use in write‑heavy workloadsExtra pointer updates increase write latency.Disable side pointers dynamically based on write‑to‑read ratio (e.g., via a config flag).
Ignoring NUMA localitySide pointers may point to nodes on a remote socket, hurting latency.Prefer side pointers that stay within the same NUMA node when possible.

8. When to Use Side Pointers

  • Read‑dominant OLTP – workloads where reads outnumber writes by > 4:1.
  • Hot‑spot‑heavy key distributions – zipfian or time‑series data where recent keys are accessed repeatedly.
  • Latency‑sensitive services – micro‑services that require sub‑millisecond query times.
  • Limited memory budget – the extra pointer is cheap compared to more aggressive sharding or partitioning strategies.

If your workload is write‑heavy or the index is already heavily partitioned, the marginal gains may not justify the implementation effort.

Key Takeaways

  • Side pointers add a single forward link to each interior B‑link tree node, allowing readers to bypass locked children safely.
  • The technique preserves lock‑coupling correctness while cutting lock wait time dramatically, especially under hot‑spot read patterns.
  • Implementation cost is low: a few lines of code for publishing/clearing the pointer and a small change to the read path.
  • Benchmarks show up to throughput improvement and a 40 % reduction in read latency on a 64‑core OLTP workload.
  • Side pointers are most effective in read‑heavy, hot‑spot‑driven workloads; they add negligible memory overhead and integrate cleanly with existing B‑link tree codebases.

Further Reading