Hello! Let's dive into the practical side of building a database system.
In our last lesson, we established the crucial role of the buffer pool manager. We saw it as the gatekeeper between the high-speed world of main memory and the slower, persistent world of disk. We covered its key data structures—the page table, pin counts, and dirty flags—and established why a DBMS must manage its own memory rather than relying on the OS.
We concluded by identifying a critical challenge: when the buffer pool is full, how do we decide which page to evict? A poor choice can cripple performance. Today, we answer that question by focusing on a specific, widely-used page replacement policy. Our learning outcome is to implement a buffer pool manager using a CLOCK replacement policy. This lesson will bridge the gap from theory to practice, giving you the architectural blueprint to write the C++ code for this essential database component.
1. The CLOCK Replacement Policy: An Efficient LRU Approximation
The ideal page to evict is one that won't be needed for the longest time in the future. Since we can't predict the future, a common heuristic is to evict the Least Recently Used (LRU) page. While effective, a precise LRU implementation can be a performance bottleneck itself, as it often requires updating a data structure (like a linked list or priority queue) on every single page access, which demands heavy-handed locking in a concurrent system.
The CLOCK algorithm is an ingenious and more efficient approximation of LRU.

The algorithm maintains a circular buffer of the frames in the pool and a "clock hand" that points to a frame to be considered for eviction. Each frame has one extra piece of metadata: a reference bit.
Let's watch a detailed explanation of how this works.
05 - Buffer Pools + Memory Management (CMU Databases Systems / Fall 2019)
This video from Carnegie Mellon University first briefly discusses LRU and then provides a clear, step-by-step walkthrough of the CLOCK algorithm.
Watch from 59:54 to 1:03:37. First, you'll see why a basic LRU can be inefficient. Then, the video dives into the CLOCK algorithm, explaining the 'clock hand' and the 'reference bit'. Pay close attention to how the algorithm decides whether to evict a page or give it a 'second chance'.
To reinforce this, let's see another excellent, concise explanation that includes C++ style pseudocode.
The Berkeley CS186 lecture series offers another clear walkthrough of the CLOCK algorithm, which is very helpful for understanding the logic before implementation.
Watch the entire video (0:00 to 4:04). It visualizes the process of finding a victim page and provides pseudocode that breaks down the logic for handling pinned pages, referenced pages, and replaceable pages.
The core logic for finding a victim page is a sweep of the clock hand:
- Examine the frame under the clock hand.
- If its pin count is > 0, it's in use. We cannot evict it. Advance the clock hand.
- If its pin count is 0 and its reference bit is 1, the page has been used recently. Give it a "second chance": set the reference bit to 0 and advance the clock hand.
- If its pin count is 0 and its reference bit is 0, we have found our victim. This page is not in use and has not been referenced since the last sweep. Select this frame for replacement.
After a page is loaded into a frame, its reference bit is set to 1, indicating it has been recently "referenced".
Test your understanding!
Imagine a buffer pool with 4 frames (F0, F1, F2, F3), initially empty. The clock hand starts at F0. Consider the following sequence of operations:
Fetch(P1),Fetch(P2),Fetch(P3),Fetch(P4). (These pages are loaded into F0, F1, F2, F3 and remain pinned).Unpin(P2),Unpin(P4).Fetch(P5). The buffer pool is full, so we must find a victim.
Trace the clock hand and describe what happens. Which page is evicted?
Show answer
- After the initial fetches, the state is: F0={P1, pin=1, ref=1}, F1={P2, pin=1, ref=1}, F2={P3, pin=1, ref=1}, F3={P4, pin=1, ref=1}.
- After unpinning, the state is: F0={P1, pin=1, ref=1}, F1={P2, pin=0, ref=1}, F2={P3, pin=1, ref=1}, F3={P4, pin=0, ref=1}.
- To fetch P5, the eviction process starts with the clock hand at F0:
- Hand at F0:
P1haspin > 0. Skip. Advance hand. - Hand at F1:
P2haspin == 0andref == 1. Give it a second chance: setrefto 0. Advance hand. (F1 state is now {P2, pin=0, ref=0}) - Hand at F2:
P3haspin > 0. Skip. Advance hand. - Hand at F3:
P4haspin == 0andref == 1. Give it a second chance: setrefto 0. Advance hand. (F3 state is now {P4, pin=0, ref=0}) - Hand at F0:
P1haspin > 0. Skip. Advance hand. - Hand at F1:
P2haspin == 0andref == 0. Victim found!P2is evicted.P5is loaded into F1. The frame's metadata is reset (pin=1, ref=1). The clock hand is advanced to F2.
- Hand at F0:
2. Structuring the C++ Implementation
Now let's design the code. A clean, modular design separates the replacement policy logic from the overall buffer pool management. We'll conceptualize this with two main classes: a ClockReplacer and a BufferPoolManager.
This excellent blog post provides a high-level view of this architecture.
How Buffer Pool Works: An Implementation In Go
Before diving into C++ specifics, let's look at a clear overview of the components. Although the code in this post by Bruno Calza is in Go, the architectural principles of separating the BufferPoolManager and ClockReplacer are universal and extremely well-explained.
Read the sections 'The buffer pool manager' and 'The Clock Replacement algorithm'. Focus on the distinct APIs for the BufferPoolManager and the ClockReplacer. This separation of concerns is a key principle in building robust systems.
With this architecture in mind, we can look at a concrete C++ implementation guide. The following resource from a Swarthmore College lab is a perfect blueprint for what you need to build.
CS44: Lab 1: Building a Buffer Manager
This lab guide provides a detailed blueprint for implementing a buffer manager in C++. It aligns perfectly with your background and our learning goal, outlining the specific classes and methods you would write.
First, read the sections 'Buffer Replacement Policy using the Clock Algorithm' and 'Implementing the Buffer Manager' to get an overview of the three key C++ classes: BufferManager, Frame, and BufferHashTable. Then, focus on the 'Class Methods' subsection within the BufferManager description. Pay close attention to the specifications for allocateFrame(), readPage(), and unPinPage(). These three methods are the heart of the buffer manager and contain the core logic for interacting with the clock replacer.
Let's synthesize this into a plan of action. Your implementation would consist of:
-
A
ClockReplacerClass: This class isolates the replacement logic. It might be implemented with astd::vectororstd::arrayto represent the frames and an index for the clock hand.Victim(FrameId* frame_id): Implements the clock sweep to find an unpinned frame with a reference bit of 0. Returns the ID of the victim frame.Pin(FrameId frame_id): Called when a page is pinned. This method should effectively remove the frame from consideration by theVictim()method.Unpin(FrameId frame_id): Called when a page's pin count drops to zero. This method makes the frame a candidate for eviction.
-
A
BufferPoolManagerClass: This is the main class that orchestrates everything, using theClockReplacer, a page table (e.g.,std::unordered_map), and an array of frames.
Here’s how the core methods would work:
FetchPage(page_id)
- Acquire Latch: Lock a mutex to protect shared data structures.
- Check Page Table: Look up
page_id. - Cache Hit:
- Get the
frame_idfrom the page table. - Increment the frame's pin count.
- Call
replacer->Pin(frame_id)because the frame is now in use. - Release latch and return a pointer to the page in the frame.
- Get the
- Cache Miss:
- Find a frame to place the new page in, either from a list of free frames or by calling
replacer->Victim(&victim_frame_id). - If no frame is available (e.g.,
Victim()fails because all pages are pinned), the request fails. - If a victim was chosen and its dirty flag is set, write the victim page to disk.
- Remove the victim page's entry from the page table.
- Read the requested page from disk into the chosen frame.
- Update the frame's metadata:
pin_count=1,dirty=false, set the page ID. - Call
replacer->Pin(frame_id)for the new frame. - Insert the new
page_id -> frame_idmapping into the page table. - Release latch and return a pointer to the page.
- Find a frame to place the new page in, either from a list of free frames or by calling
UnpinPage(page_id, is_dirty)
- Acquire Latch.
- Find Frame: Look up
page_idin the page table to getframe_id. - Update Metadata:
- If
is_dirtyis true, set the frame's dirty flag to true. - Decrement the frame's pin count.
- If
- Check for Eviction Candidacy:
- If the pin count is now 0, call
replacer->Unpin(frame_id).
- If the pin count is now 0, call
- Release Latch.
3. A Note on Workloads: Sequential Flooding
CLOCK is a fantastic general-purpose algorithm, but it's not perfect. One scenario where it performs poorly is during large, sequential scans of a table. The scan will read in page after page, "flooding" the buffer pool and potentially evicting valuable, non-scan-related pages that will be needed again soon.
05 - Buffer Pools + Memory Management (CMU Databases Systems / Fall 2019)
It's important to understand the limitations of any algorithm. This final clip from the CMU lecture explains 'sequential flooding' and hints at more advanced policies designed to cope with it.
Watch from 1:03:37 to 1:05:00. The goal isn't to memorize the advanced policies, but to understand why a simple CLOCK/LRU approach can be suboptimal for scan-heavy workloads.
This is a classic trade-off in systems design. More sophisticated policies exist (e.g., LRU-K, 2Q, ARC) that try to distinguish between frequently-used ("hot") pages and pages accessed once in a scan, but they come with added complexity. For most systems, CLOCK provides a robust and efficient foundation.
Conclusion
Today we've laid out the complete design for a buffer pool manager using the CLOCK replacement policy. You have a clear path from the high-level algorithm to a concrete C++ class structure.
Key Takeaways:
- CLOCK is an efficient approximation of LRU that avoids high per-access overhead by using a circular scan and a single reference bit.
- The algorithm gives pages a "second chance" before eviction, making it more robust than simple FIFO (First-In, First-Out).
- A clean implementation separates the logic into a
ClockReplacerclass and aBufferPoolManagerclass. - The
BufferPoolManagerorchestrates the process: using a page table for lookups, calling the replacer to find victims on a cache miss, and managing pin counts and dirty flags. - All shared internal data structures must be protected by latches (mutexes) for safe concurrent access.
Preview of the Next Lesson:
Now that we have designed our buffer pool manager, the next logical step is to analyze its behavior. How does the size of the buffer pool affect hit rates? What is the performance impact of having many pages pinned simultaneously? In the next lesson, we will analyze the impact of buffer pool size and page pinning on query performance, moving from implementation to performance analysis.