[DMDB] Buffer cache

This commit is contained in:
2026-07-14 16:07:53 +02:00
parent a957a20244
commit 5d5a3b5d1b
7 changed files with 91 additions and 1 deletions
+1
View File
@@ -2,3 +2,4 @@
\newacronym{dbms}{DBMS}{Database Management System} \newacronym{dbms}{DBMS}{Database Management System}
\newacronym{simd}{SIMD}{Single Instruction Multiple Data} \newacronym{simd}{SIMD}{Single Instruction Multiple Data}
\newacronym{avxa}{AVX}{Advanced Vector Extensions, also see \gls{avx}} \newacronym{avxa}{AVX}{Advanced Vector Extensions, also see \gls{avx}}
\newacronym{ra}{RA}{Relational Algebra, see Section~\ref{sec:relational-algebra}}
@@ -6,7 +6,7 @@ We can set \texttt{I = *} if we want to return all columns for a table.
To rename, we can set \texttt{I = Column as Name, Column2 as Name2}, etc To rename, we can set \texttt{I = Column as Name, Column2 as Name2}, etc
% TODO: Make RA an acronym, too % TODO: Make RA an acronym, too
\inlinetheorem Every SPJR RA expression can be written in \texttt{SELECT ... FROM ... WHERE ...} form: \inlinetheorem Every SPJR \acrshort{RA} expression can be written in \texttt{SELECT ... FROM ... WHERE ...} form:
\begin{tables}{lll}{Operation & Notation & SQL} \begin{tables}{lll}{Operation & Notation & SQL}
Selection & $\sigma_c(R)$ & \texttt{SELECT * FROM R WHERE c;} \\ Selection & $\sigma_c(R)$ & \texttt{SELECT * FROM R WHERE c;} \\
Projection & $\Pi_{A_1, \ldots, A_n} R$ & \texttt{SELECT A1, \ldots, An FROM R;} \\ Projection & $\Pi_{A_1, \ldots, A_n} R$ & \texttt{SELECT A1, \ldots, An FROM R;} \\
@@ -0,0 +1,10 @@
\subsubsection{Buffer Caches}
To be quick (as covered in SPCA in Semester 3), you need data to be available in memory, and even better still in the CPU caches.
This is no different for database systems.
Here we are however not dealing with the hardware side of caching, but caching commonly used pages and metadata in main memory,
as opposed to streaming them from disk on each access. However, database systems often write dirty blocks back to disk when dirty (i.e. use write-through caches),
or when in need of space.
Thus, in principle, buffer caches are similar to OS virtual memory and paging mechanisms, however, unlike OSes,
the database system knows the access patterns and can (thus) optimize much more.
@@ -0,0 +1,34 @@
\paragraph{Buffer manager}
Databases use locks (used to avoid conflicting updates to the data) and latches (avoid conflicting updates to data structures of system) to manage buffers.
The buffer cache latches are used to avoid conflicting accesses to hash buckets with the block headers and cover several hash buckets, the number of which is configurable.
The reason that there isn't a latch per bucket, or even per block header is to reduce the space devoted to the engine data structures.
Of course, as with any other kind of locking, only one process can hold a latch and they typically span several linked lists of block headers
and the typical performance bottlenecks also apply.
To mitigate these performance issues, we can
\begin{itemize}
\item reduce the number of data in a block
\item Configure a DB with more latches and less buckets per latch
\item Multiple buffer pools
\item Tune queries to reduce full table scans
\item Avoid many concurrent queries that access the same data
\item Avoid concurrent transactions
\end{itemize}
The link list in which a block header resides is found using hashing on some kind of block header.
Then, the linked list is traversed to find the block, which is expensive, which is why the length of these lists should be reduced as much as possible.
Block headers contain the following data:
\begin{itemize}
\item Block number or ID
\item Format
\item LSN (Log Sequence Number), can be similar to git commit refs, sometimes also simply a counter
\item Checksum for integrity
\item Latches / status / etc flags
\item Buffer replacement information (for replacement policies)
\end{itemize}
Status information of the block includes relevant data for management of the buffer, such as pinning (i.e. pinning it into memory), usage counts, clean / dirty flag
and more.
@@ -0,0 +1,12 @@
\paragraph{Cache replacement policies}
This is mostly a recap from SPCA, but again, the DB can be more aggressive at optimizing, because it has much more information on the data accesses.
A commonly used strategy is Least Recently Used (LRU).
A buffer is evicted, when space is needed and it is the least recently used buffer in the buffer pool.
As much sense as this policy makes in operating systems, in DBMS, it doesn't really work (but is still used in some systems).
There are a number of possible issues with LRU:
\begin{itemize}
\item \bi{Table Scan Flooding}: If a large table is loaded to be scanned just once, it will flush out the entire cache, also pages that are used \textit{very often}.
\item \bi{Index Range Scan}: A range scan using an index will pollute the cache with random pages
\end{itemize}
@@ -0,0 +1,33 @@
\paragraph{Optimizations}
An easy way to avoid polluting the cache is to put those pages towards the bottom of the LRU list,
that way they are evicted quickly after use.
Some other options (also for performance improvements):
\begin{itemize}
\item don't cache large tables
\item Keep frequently used blocks in separate buffers
\item mark some pages as to be evicted immediately after use
\item keep statistics on usage of tables and let the system decide automatically which blocks to evict
\item evict clean pages first, because they take less time to evict (as we don't need to write)
\end{itemize}
It is important to keep cache pollution low, as other optimizations, such as pre-fetching (or read-ahead) can be used to
already pre-populate caches from the query plan before the data is even requested.
Some systems (such as Oracle DB) use Touch Counts (Hot/Cold lists), which work by inserting a new block in the middle.
When a block is touched, move it up the list, once it exceeds the next block's counts.
This will automatically keep the very commonly used pages in cache.
To avoid issues with counting, an access can be set to only count after a (configurable) number of seconds has passed.
\subparagraph{Second chance}
This concept uses no list, the counters are kept in the blocks and the buffer is treated as a circular buffer.
When a block is accessed, set the counter to 1, when the eviction head passes by, set the counter to 0 if it is 1, if it is 0, evict it.
\subparagraph{Clock Sweep}
Very similar to second chance, but instead uses counters, because some blocks are accessed very frequently.
\subparagraph{2Q}
This approach uses two lists. We have a FIFO queue for blocks that don't need to be kept and an LRU list for blocks that were accessed several times.
If a block in the FIFO queue is accessed again, it is moved to the LRU list. The lowest element of the LRU list is then either moved to the FIFO queue or evicted.