Oracle Buffer Cache の内部構造を徹底解説:Touch Count、DBWn、Checkpoint Queue まで

はじめに

この記事では、Oracle DatabaseのBuffer Cacheの内部構造を解説します。

InnoDB Buffer Pool編PostgreSQL Shared Buffers編に続く第3弾です。Oracleはクローズドソースなのでソースコードは読めませんが、公式ドキュメント、X$BH(内部ビュー)、V$BH(動的パフォーマンスビュー)、そしてOracle Internals系の文献をもとに、InnoDB・PostgreSQLと同等の深さで解説します。

この記事で扱う内容:

  • Pin/Unpinの仕組み
  • メモリ構成(Buffer Header、Hash Chain、LRU List、Checkpoint Queue)
  • Touch Countアルゴリズム(Hot/Cold領域の分割)
  • フルテーブルスキャン耐性の仕組み
  • DBWnによるダーティページの書き戻し
  • ラッチとロックによる同時実行制御

対象バージョンはOracle 19c〜23aiです。


そもそもBuffer Cacheって何?

InnoDB・PostgreSQLと同じ役割です。ディスク上のデータブロックをメモリにキャッシュして、物理I/Oを最小化する仕組みです。OracleではSGA(System Global Area)の一部として確保されます。

-- Buffer Cacheのサイズ設定
ALTER SYSTEM SET db_cache_size = 4G;
-- または自動メモリ管理(AMM)に任せる
ALTER SYSTEM SET memory_target = 16G;

Oracleのブロックサイズはデフォルト8KB(PostgreSQLと同じ、InnoDBの16KBの半分)です。

3つのプール

OracleのBuffer Cacheは用途別に分けられます:

  • Default Pool(デフォルト)
    • 通常のブロックがキャッシュされる場所
    • ほとんどのブロックはここに入る
  • Keep Pool(オプション)
    • 頻繁にアクセスされるテーブルを常駐させたい場合に使う
    • ALTER TABLE t STORAGE (BUFFER_POOL KEEP);
  • Recycle Pool(オプション)
    • アクセス頻度が低い大きなテーブル用
    • キャッシュを長く占有させたくない場合に使う

Keep/Recycle Poolは明示的にテーブルに割り当てる必要があります。InnoDBやPostgreSQLにはこの概念はありません。


Pin/Unpin:ブロックの借り方・返し方

OracleでもInnoDB・PostgreSQLと同じく、ブロックを使用中はPinして追い出しを防ぎます。

ブロック取得の流れ

ユーザープロセスがブロックを必要とするとき、内部的には以下の流れで処理されます:

ユーザープロセスがブロックを要求
    │
    ├─ 1. Buffer Tagを計算(data file番号 + block番号)
    │
    ├─ 2. Hash関数でBucket番号を算出
    │
    ├─ 3. Cache Buffer Chains ラッチを取得
    │     → Hash Chainを辿ってBuffer Headerを検索
    │
    ├─ ヒット:
    │   ├─ Buffer HeaderのPin countをインクリメント
    │   ├─ Touch countをインクリメント(後述)
    │   └─ ブロックの内容にアクセス
    │
    └─ ミス:
        ├─ Free Listまたは LRU Listから空きバッファを確保
        ├─ ディスクからブロックを読み込み
        ├─ Hash Chainに登録
        └─ Pinしてアクセス

Buffer Header

各バッファにはBuffer Headerという管理構造体が紐づいています。X$BH内部ビューで確認できます:

-- Buffer Headerの主要フィールド(X$BH)
SELECT
    HLADDR,    -- Hash Chain Latchのアドレス
    FILE#,     -- データファイル番号
    DBABLK,    -- ブロック番号
    CLASS#,    -- ブロックの種類(data, undo, headerなど)
    STATE,     -- 状態(0=FREE, 1=XCUR, 2=SCUR, 3=CR, ...)
    TCH,       -- Touch Count(アクセス回数)
    TIM,       -- 最終アクセス時刻
    BA,        -- バッファのメモリアドレス
    DIRTY_QUEUE, -- Checkpoint Queueに載っているか
    NXT_HASH,  -- Hash Chainの次のBuffer Header
    PRV_HASH   -- Hash Chainの前のBuffer Header
FROM X$BH
WHERE ROWNUM <= 10;

InnoDBのbuf_block_t、PostgreSQLのBufferDescに相当しますが、Oracleの特徴はBuffer HeaderがSGA内のShared Poolに格納される点です(バッファデータ自体はBuffer Cache内)。

InnoDBとの比較

InnoDBPostgreSQLOracle
取得buf_page_get_genReadBuffer内部関数(非公開)
追い出し防止buf_fix_count++refcount++Pin count++
管理構造体buf_block_tBufferDescBuffer Header(X$BH
格納場所Buffer Pool内(Chunk)共有メモリShared Pool内
ブロックサイズ16KB8KB8KB(デフォルト)

メモリの中身を覗く:Hash Chain、LRU List、Checkpoint Queue

Hash Chain:ブロックの検索

InnoDB・PostgreSQLと同じく、Oracleもハッシュテーブルでブロックを検索します。OracleではHash BucketHash Chainと呼びます。

Hash Bucket 0 → [BH: file#=1, blk#=100] → [BH: file#=3, blk#=200] → NULL
Hash Bucket 1 → [BH: file#=2, blk#=50]  → NULL
Hash Bucket 2 → [BH: file#=1, blk#=300] → [BH: file#=5, blk#=10] → NULL
...

※ BH = Buffer Header
※ 同じBucketに複数のBuffer Headerがチェインされる(衝突解決)

各Hash ChainはCache Buffer Chains ラッチで保護されています。ラッチの数はバッファ数に応じて自動的に決まり、1つのラッチが複数のHash Chainを保護します。

-- Cache Buffer Chains ラッチの競合を確認
SELECT name, gets, misses, sleeps
FROM V$LATCH
WHERE name = 'cache buffer chains';

このラッチの競合(latch: cache buffer chains待機イベント)は、Oracleのパフォーマンスチューニングで最も頻繁に遭遇する問題の一つです。同じブロックに大量のセッションが同時アクセスする「ホットブロック」問題が原因であることが多いです。

LRU List:Hot領域とCold領域

OracleのLRU Listは、InnoDBのNew/Old Sublistと似た構造で、Hot領域Cold領域に分割されています:

InnoDBのNew/Old Sublistとの違い:

  • InnoDBは5/8と3/8の分割、Oracleはデフォルト50/50
  • InnoDBはinnodb_old_blocks_timeで昇格を制御、OracleはTouch Countで制御(後述)

Checkpoint Queue(CKPT-Q)

InnoDBのFlush Listに相当するのがCheckpoint Queueです。ダーティバッファが変更された順(RBA = Redo Block Address順)に並んでいます。

InnoDBのFlush Listがoldest_modification(LSN)順なのと同じ考え方です。チェックポイントを進めるには、Checkpoint Queueの先頭(最も古いダーティバッファ)をディスクに書き出す必要があります。

4つのデータ構造の比較

役割InnoDBPostgreSQLOracle
ブロック検索Hash Mapbuf_tableHash Bucket + Chain
使用中管理LRU Listなし(配列走査)LRU List
空きブロックFree ListFree ListFree List(LRU末尾)
ダーティ管理Flush ListなしCheckpoint Queue
スキャン耐性New/Old SublistRing BufferHot/Cold + Touch Count

Touch Count:ブロックの「人気度」を測る

OracleのLRU管理の核心は**Touch Count(TCH)**です。PostgreSQLのusage_countに近い概念ですが、より洗練されています。

Touch Countの基本

バッファがアクセスされるたびに、Buffer HeaderのTouch Count(X$BH.TCH)がインクリメントされます。ただし、短時間に何度もアクセスされた場合は1回としかカウントしません(3秒間隔のウィンドウがあると言われています)。

-- Touch Countの分布を確認
SELECT tch, COUNT(*) AS buffers
FROM X$BH
WHERE state != 0  -- FREE以外
GROUP BY tch
ORDER BY tch;

-- 結果例:
-- TCH  BUFFERS
-- ---  -------
--   0    15000   ← 一度もアクセスされていない or 最近読み込まれたばかり
--   1     8000
--   2     5000
--   3     3000
--  10     1000   ← ホットなブロック
--  50      200   ← 非常にホットなブロック

Hot領域への昇格

新しく読み込まれたブロックは、LRU ListのCold領域の中間地点(Midpoint)に挿入されます。その後、Touch Countが一定の閾値を超えると、Hot領域に昇格します。

1. ブロックがディスクから読み込まれる
   → Cold領域のMidpointに挿入(TCH = 0)

2. アクセスされるたびに TCH++

3. TCH が閾値を超える
   → Hot領域のMRU端に移動

4. Hot領域内でもアクセスされるたびにMRU端に近づく

5. アクセスされなくなると徐々にLRU端に向かって移動
   → 最終的にCold領域に落ちる
   → さらにアクセスされなければ追い出し対象に

InnoDBとの比較

InnoDBのNew/Old Sublist方式との違いが面白いです:

  • InnoDB: 時間ベースの昇格
    • Old Sublistに入ったページが昇格するにはinnodb_old_blocks_time(デフォルト1秒)経過後に再アクセスされる必要がある
    • 「1秒以内の再アクセスは無視」という時間フィルタ
  • Oracle: 回数ベースの昇格(Touch Count)
    • アクセス回数(TCH)が閾値を超えたら昇格
    • 3秒以内の連続アクセスは1回とカウント
    • 「何回アクセスされたか」という頻度フィルタ
  • PostgreSQL: 減衰ベース(usage_count)
    • アクセスでusage_count++(上限5)
    • Clock Sweepが通過するたびに usage_count–
    • 「最近どれだけアクセスされたか」の減衰カウンタ

3つのDBMSとも「頻繁にアクセスされるブロックを優遇する」という目標は同じですが、アプローチが異なります。

フルテーブルスキャン耐性

Oracleのフルテーブルスキャン(FTS)対策は、バージョンによって進化してきました:

Oracle 9.2以前:
FTSのブロックはLRU端に挿入(すぐ追い出される)
→ 小さなウィンドウ内でのみキャッシュ

Oracle 9.2以降:
FTSのブロックもMidpointに挿入
ただし、テーブルサイズによって挙動が変わる:

テーブルサイズ < small_table_threshold(Buffer Cacheの2%)
→ 通常通りキャッシュ(小さいテーブルは全部載せてOK)
テーブルサイズ > Buffer Cacheの一定割合
→ Direct Path Read(Buffer Cacheをバイパス)
PGAに直接読み込む
→ Buffer Cacheへの影響はゼロ

Oracle 11g以降のオプティマイザは、大きなテーブルのFTSに対してDirect Path Readを選択することが多くなりました。これはBuffer Cacheを完全にバイパスしてPGA(プロセス専用メモリ)に直接読み込む方式です。

-- Direct Path Readが使われているか確認
SELECT name, value
FROM V$SYSSTAT
WHERE name IN ('physical reads', 'physical reads direct');

PostgreSQLのRing Bufferが「小さなリングで使い回す」アプローチなのに対し、Oracleは「そもそもBuffer Cacheを通さない」というより大胆なアプローチです。


DBWnによるダーティページの書き戻し

InnoDBのpage cleaner、PostgreSQLのBGWriterに相当するのが、Oracleの**DBWn(Database Writer)**プロセスです。

DBWnの基本

DBWnはバックグラウンドプロセスで、ダーティバッファをデータファイルに書き戻します。複数のDBWnプロセスを起動できます(DB_WRITER_PROCESSESパラメータ)。

-- DBWnプロセス数の確認
SHOW PARAMETER db_writer_processes;
-- デフォルト: 1(CPUコア数に応じて自動調整されることもある)

DBWnが起動するタイミング

DBWnは以下の条件で書き出しを行います:

  1. サーバープロセスがFreeバッファを見つけられない、LRU Listを走査してもクリーンなバッファが見つからない
    → DBWnにシグナルを送って書き出しを要求
    → InnoDBのSingle Flushに相当する状況
  2. Checkpoint Queueが長くなりすぎたチェックポイントを進めるために、古いダーティバッファを書き出す必要がある
    → InnoDBのSync Flushに相当
  3. 定期的なタイムアウト(3秒間隔)
    → InnoDBのBatch Flush、PostgreSQLのBGWriterに相当
  4. テーブルスペースのオフライン/読み取り専用化
  5. DROP TABLE / TRUNCATE TABLE
  6. ALTER TABLESPACE BEGIN BACKUP │

Checkpoint Queue からの書き出し

DBWnがCheckpoint Queueから書き出すとき、キューの先頭(最も古いダーティバッファ)から順に処理します:

これはInnoDBのFlush Listからのフラッシュと同じ考え方です。Checkpoint Queueの先頭が進むことで、REDOログの再利用可能な領域が増えます。

LRU Listからの書き出し

サーバープロセスがFreeバッファを必要としているとき、DBWnはLRU ListのCold端からダーティバッファを書き出します:

LRU List
  Hot端 ←────────────────→ Cold端
  [...] [...] [...] [dirty] [dirty] [clean] [dirty]
                                              ↑
                              DBWnがここから書き出す

CKPT(Checkpoint)プロセスとの役割分担

OracleにはDBWnとは別にCKPTプロセスがあります:

DBWn: ダーティバッファをデータファイルに書き出す
→ 実際のI/Oを行う
CKPT: チェックポイント情報を制御ファイルとデータファイルヘッダに記録する
→ DBWnに書き出しを指示する
→ 自分ではI/Oしない

InnoDBではCheckpointerの役割がpage cleanerに統合されていますが、Oracleでは明確に分離されています。

3つのDBMSのフラッシュ比較

InnoDBPostgreSQLOracle
書き出しプロセスpage cleanerBGWriterDBWn
チェックポイント同上CheckpointerCKPT + DBWn
ダーティ管理Flush List(LSN順)なし(配列走査)Checkpoint Queue(RBA順)
書き出し量の決定計算式(4要素)バッファ割り当て速度予測内部アルゴリズム(非公開)
緊急フラッシュSingle Flushvictim書き出しサーバープロセスがDBWnに要求

ラッチとロック:Oracleの同時実行制御

Oracleでは、メモリ構造の保護に**ラッチ(Latch)ロック(Lock)**を使い分けます。ラッチはCPUレベルの超短期ロック(スピンロック)、ロックはより長期の排他制御です。

Buffer Cacheに関わる主要なラッチ

  1. Cache Buffer Chains ラッチ
    • 保護対象:Hash Chain(ブロック検索時)
    • 粒度:複数のHash Chainで1つのラッチを共有
    • 競合時の待機イベント:latch: cache buffer chains
    • InnoDB の Hash Map Lock、PostgreSQL の BufMappingLock に相当
  2. Cache Buffer LRU Chain ラッチ
    • 保護対象:LRU Listの操作(挿入・削除・移動)
    • 競合時の待機イベント:latch: cache buffers lru chain
    • InnoDB の LRU List Mutex に相当
  3. Checkpoint Queue ラッチ
    • 保護対象:Checkpoint Queueの操作
    • 競合時の待機イベント:latch: checkpoint queue latch
    • InnoDB の Flush List Mutex に相当

Buffer Lock(Buffer Pin)

ラッチとは別に、バッファの内容を保護するBuffer Lockがあります:

  • Buffer Lock モード
    • Shared (S): 読み取り用。複数セッションが同時に保持可能
    • Exclusive (X): 書き込み用。1セッションだけが保持
    • 競合時の待機イベント:buffer busy waits
    • InnoDB の Page Frame Lock、PostgreSQL の Buffer Content Lock に相当

buffer busy waitsはOracleのパフォーマンスチューニングでよく見る待機イベントです。同じブロックに対する読み書きの競合が原因です。

ラッチ競合の確認方法

-- ラッチの競合状況を確認
SELECT name, gets, misses, sleeps,
       ROUND(misses/NULLIF(gets,0)*100, 2) AS miss_pct
FROM V$LATCH
WHERE name IN (
    'cache buffer chains',
    'cache buffers lru chain',
    'checkpoint queue latch'
)
ORDER BY misses DESC;

-- ホットブロックの特定(cache buffer chains競合時)
SELECT o.object_name, o.object_type, bh.tch, COUNT(*) AS buffers
FROM X$BH bh
JOIN DBA_OBJECTS o ON o.data_object_id = bh.OBJ
WHERE bh.tch > 10
GROUP BY o.object_name, o.object_type, bh.tch
ORDER BY bh.tch DESC;

3つのDBMSのロック比較

役割InnoDBPostgreSQLOracle
ハッシュテーブルHash Map Lock(16分割)BufMappingLock(128分割)Cache Buffer Chains ラッチ
LRU/リストLRU List Mutexbuffer_strategy_lockCache Buffer LRU Chain ラッチ
バッファヘッダBlock MutexBM_LOCKED ビットラッチ内で処理
ページデータPage Frame LockContent LockBuffer Lock (S/X)
ダーティリストFlush List MutexなしCheckpoint Queue ラッチ
競合の可視化SHOW ENGINE INNODB STATUSpg_stat_activityV$LATCH, V$SESSION_WAIT

Oracleの強みは待機イベントの可視性です。V$SESSION_WAITV$ACTIVE_SESSION_HISTORY(ASH)で、どのセッションがどのラッチで待っているかをリアルタイムに確認できます。InnoDBやPostgreSQLでは同等の情報を得るのがやや難しいです。


まとめ

この記事では、Oracle Buffer Cacheの内部構造を、InnoDB・PostgreSQLとの比較を交えて解説しました。

テーマOracleInnoDBPostgreSQL
置換アルゴリズムLRU + Touch CountLRU(New/Old Sublist)Clock Sweep
スキャン耐性Direct Path ReadNew/Old SublistRing Buffer
ダーティ管理Checkpoint QueueFlush Listなし
書き出しDBWn + CKPTpage cleanerBGWriter + Checkpointer
プール分割Default/Keep/Recycle単一(インスタンス分割)単一
可観測性X$BH, V$LATCH, ASHSHOW ENGINE STATUSpg_buffercache

3つのDBMSを比較して見えてくるのは、同じ問題(ディスクI/Oの最小化、スキャン耐性、同時実行制御)に対して、それぞれ異なるアプローチを取っていることです:

  • InnoDB:リスト構造を積極的に使い、情報を整理して持つ
  • PostgreSQL:構造を最小限にして、必要なときに走査する
  • Oracle:豊富なメタデータ(Touch Count、待機イベント)で可観測性を重視する

どれが「正解」ということではなく、各DBMSの全体設計(ストレージエンジン、WAL/REDO設計、プロセスモデル)に合わせた合理的な選択です。

参考

コメント

タイトルとURLをコピーしました