TECH BLOG
技術ブログ
  • 2025-07-30 PostgreSQLPostgreSQL ナレッジ

    全件スキャン時に利用されるリングバッファ

リングバッファについて

「リングバッファ」は、特定のテーブルデータが、共有バッファ(shared_buffers)を占有しないようにするための仕組みです。
PostgreSQL では、ディスクからテーブルやインデックスのデータを読み込む際に、共有バッファにデータをキャッシュします。
リングバッファは、共有バッファの一部分を専用に使い、その限られた範囲の中でページを循環的に上書きしながら利用されます。

PostgreSQL は、大きなテーブルを全件スキャン(シーケンシャルスキャン)する場合などにリングバッファを利用し、共有バッファが特定のテーブルのデータで埋め尽くされてしまうことを防止します。
仮に、リングバッファという仕組みが存在しなければ、他のテーブルやインデックスのデータが追い出され、全体の性能が低下する原因になります。

本記事では、全件スキャン時に利用されるリングバッファについて説明します。
VACUUM やバルク書き込み時に利用されるリングバッファとは、説明が違う部分もありますのでご注意ください。

公式ドキュメントの説明

PostgreSQL 公式ドキュメント上から、このリングバッファについて説明している記述を見つけられませんでした。
代わりに src/backend/storage/buffer/README に、リングバッファについての説明を一部引用します。
※ 以降で引用しているコメントやソースコードは、PostgreSQL 17.5 のものです。

Buffer Ring Replacement Strategy

When running a query that needs to access a large number of pages just once,
such as VACUUM or a large sequential scan, a different strategy is used.
A page that has been touched only by such a scan is unlikely to be needed
again soon, so instead of running the normal clock sweep algorithm and
blowing out the entire buffer cache, a small ring of buffers is allocated
using the normal clock sweep algorithm and those buffers are reused for the
whole scan. This also implies that much of the write traffic caused by such
a statement will be done by the backend itself and not pushed off onto other
processes.

VACUUM や大規模な SeqScan のように多くのページにアクセスする場合には、通常のバッファキャッシュを圧迫しないように、専用の小さなバッファ領域を使って処理されます。というような内容がコメントに記載されています。

リングバッファのサイズ

リングバッファのサイズはデフォルトでは 256 KB(32 ページ)で、パラメータなどで変更することはできません。
ちなみに、先ほど引用した続きに「For sequential scans, a 256KB ring is used.」という説明があります。
src/backend/storage/buffer/freelist.c で実際にサイズを決定しています。
該当部分を下記に引用します。

/*

  • GetAccessStrategy -- create a BufferAccessStrategy object
  • The object is allocated in the current memory context.
    */
    BufferAccessStrategy
    GetAccessStrategy(BufferAccessStrategyType btype)
    {
    int ring_size_kb;

    /*

    • Select ring size to use. See buffer/README for rationales.
    • Note: if you change the ring size for BAS_BULKREAD, see also
    • SYNC_SCAN_REPORT_INTERVAL in access/heap/syncscan.c.
      /
      switch (btype)
      {
      case BAS_NORMAL:
      /
      if someone asks for NORMAL, just give 'em a "default" object */
      return NULL;

      case BAS_BULKREAD:
      ring_size_kb = 256;
      break;
      case BAS_BULKWRITE:
      ring_size_kb = 16 * 1024;
      break;
      case BAS_VACUUM:
      ring_size_kb = 2048;
      break;

      default:
      elog(ERROR, "unrecognized buffer access strategy: %d",
      (int) btype);
      return NULL; / keep compiler quiet /
      }

    return GetAccessStrategyWithSize(btype, ring_size_kb);
    }

上記のコードの中で、全件スキャン時のリングバッファのサイズを指定しているコードを下記に抜粋します。

        case BAS_BULKREAD:
            ring_size_kb = 256;
            break;

上記の通り、ring_size_kb に 256 を格納していますので、256 KB がリングバッファのサイズだと分かります。

リングバッファが利用される条件

リングバッファを使用するかどうかは、PostgreSQL の内部的な判断によります。
そのため、SQL 実行時に明示的に「リングバッファを使用する/しない」という指定はできません。
リングバッファが利用される条件としては、「テーブルのサイズが共有バッファの 1/4 を超える場合」となります。
ソースコードより該当の記述を引用します。

src/backend/access/heap/heapam.c

if (!RelationUsesLocalBuffers(scan->rs_base.rs_rd) &&
    scan->rs_nblocks > NBuffers / 4)
{
    allow_strat = (scan->rs_base.rs_flags & SO_ALLOW_STRAT) != 0;
    allow_sync = (scan->rs_base.rs_flags & SO_ALLOW_SYNC) != 0;
}
else
    allow_strat = allow_sync = false;

if (allow_strat)
{
    /* During a rescan, keep the previous strategy object. */
    if (scan->rs_strategy == NULL)
        scan->rs_strategy = GetAccessStrategy(BAS_BULKREAD);
}
else
{
    if (scan->rs_strategy != NULL)
        FreeAccessStrategy(scan->rs_strategy);
    scan->rs_strategy = NULL;
}

一度目の if 文で、「scan->rs_nblocks > NBuffers / 4」しているのが、該当部分です。
scan->rs_nblocks は読み込むブロック数(サイズ)で、NBuffers がが共有バッファのサイズです。
全件スキャンで読み込むサイズが、共有バッファの 1/4 のサイズを超えるかどうか、を判定しています。
二度目の if 文内で、「scan->rs_strategy = GetAccessStrategy(BAS_BULKREAD);」というのが、前述のリングバッファのサイズを決定している記述となります。

実際に検証して挙動を確認

実際に SQL を実行して挙動を確認します。

検証した環境

検証に利用した環境は

  • RockyLinux 9.6
  • PostgreSQL 17.5

となります。

検証手順

検証には pgbench の pgbench_accounts テーブルを全件スキャンし、そのときの共有バッファのヒット数を確認します。

1. 環境の確認
postgres=# SELECT version();
                                                 version                                                  
----------------------------------------------------------------------------------------------------------
 PostgreSQL 17.5 on x86_64-pc-linux-gnu, compiled by gcc (GCC) 11.5.0 20240719 (Red Hat 11.5.0-5), 64-bit
(1 row)

postgres=# SELECT COUNT(1) FROM pgbench_accounts;
  count  
---------
 1000000
(1 row)

postgres=# show shared_buffers;
 shared_buffers 
----------------
 128MB
  • pgbench_accounts テーブルには 1,000,000 件のレコードがあります。
  • shared_buffers は初期値の 128 MBです。
2. 全件スキャンして挙動を確認(shared_buffers = 128 MB)

全件スキャンを行う前に、PostgreSQL を再起動して共有バッファの内容を消去しておきます。

# pgbench_accounts テーブルを全件スキャン(1回目)
 postgres=# EXPLAIN (ANALYZE, BUFFERS) SELECT * FROM pgbench_accounts;
                                                         QUERY PLAN                                                          
-----------------------------------------------------------------------------------------------------------------------------
 Seq Scan on pgbench_accounts  (cost=0.00..26394.00 rows=1000000 width=97) (actual time=0.056..244.577 rows=1000000 loops=1)
   Buffers: shared read=16394
 Planning:
   Buffers: shared hit=44 read=11
 Planning Time: 1.035 ms
 Execution Time: 382.115 ms
(6 rows)

# pgbench_accounts テーブルを全件スキャン(2回目)
postgres=# EXPLAIN (ANALYZE, BUFFERS) SELECT * FROM pgbench_accounts;
                                                         QUERY PLAN                                                          
-----------------------------------------------------------------------------------------------------------------------------
 Seq Scan on pgbench_accounts  (cost=0.00..26394.00 rows=1000000 width=97) (actual time=0.073..246.558 rows=1000000 loops=1)
   Buffers: shared hit=32 read=16362
 Planning Time: 0.097 ms
 Execution Time: 383.914 ms
(4 rows)

# pgbench_accounts テーブルを全件スキャン(3回目)
postgres=# EXPLAIN (ANALYZE, BUFFERS) SELECT * FROM pgbench_accounts;
                                                         QUERY PLAN                                                          
-----------------------------------------------------------------------------------------------------------------------------
 Seq Scan on pgbench_accounts  (cost=0.00..26394.00 rows=1000000 width=97) (actual time=0.069..244.600 rows=1000000 loops=1)
   Buffers: shared hit=64 read=16330
 Planning Time: 0.095 ms
 Execution Time: 375.165 ms
(4 rows)

実行計画の Seq Scan 行の下にある「Buffers」で始まる行を確認します。
hit の数値は、共有バッファから読み込んだページ数です。
read の数値は、ディスクから読み込んだページ数です。
1 回目の全件スキャンでは

Buffers: shared read=16394

ということで、全てがディスクから読み込まれています。
2 回目の全件スキャンでは

Buffers: shared hit=32 read=16362

となっていて、32 ページのデータを共有バッファから取得できたことを確認できます。
1 ページのサイズは 8 KB なので、32 * 16 = 256 KB となり、リングバッファのサイズと合致するため、リングバッファが利用されていると判断できます。

3. 全件スキャンして挙動を確認(shared_buffers = 4 GB)

今度は、shared_buffers を 128 MB から 4 GB に増加させて、全件スキャンを行います。
ここでもキャッシュのクリア兼 shared_buffers の反映のため、PostgreSQL の再起動を行なっています。

# shared_buffers の確認
postgres=# show shared_buffers;
 shared_buffers 
----------------
 4GB
(1 row)

# pgbench_accounts テーブルを全件スキャン(1回目)
postgres=# EXPLAIN (ANALYZE, BUFFERS) SELECT * FROM pgbench_accounts;
                                                         QUERY PLAN                                                          
-----------------------------------------------------------------------------------------------------------------------------
 Seq Scan on pgbench_accounts  (cost=0.00..26394.00 rows=1000000 width=97) (actual time=0.064..378.260 rows=1000000 loops=1)
   Buffers: shared read=16394
 Planning:
   Buffers: shared hit=44 read=11
 Planning Time: 1.110 ms
 Execution Time: 516.555 ms
(6 rows)

# pgbench_accounts テーブルを全件スキャン(2回目)
postgres=# EXPLAIN (ANALYZE, BUFFERS) SELECT * FROM pgbench_accounts;
                                                         QUERY PLAN                                                          
-----------------------------------------------------------------------------------------------------------------------------
 Seq Scan on pgbench_accounts  (cost=0.00..26394.00 rows=1000000 width=97) (actual time=0.017..208.000 rows=1000000 loops=1)
   Buffers: shared hit=16394
 Planning Time: 0.107 ms
 Execution

2 回目の全件スキャンでは

Buffers: shared hit=16394

となっていて、全て共有バッファからデータを読み込めたことを表しています。
shared_buffers を増加させたことで、リングバッファが利用されなくなったと確認できました。

pg_prewarm 拡張モジュールの利用

特定のテーブルの全データを共有バッファにキャッシュさせようと全件スキャンを実行しても、前述の通り、リングバッファの影響で想定した通りにキャッシュされません。
こうした場合には、pg_prewarm 拡張モジュールを利用することで、特定のテーブルの全データを共有バッファにキャッシュさせることができます。
pg_prewarm 拡張モジュールの詳細については、公式ドキュメントを参照してください。

【 PostgreSQL 17.0 文書 - F.28. pg_prewarm - 】
https://www.postgresql.jp/document/17/html/pgprewarm.html

まとめ

以上が、全件スキャン時に利用されるリングバッファの解説となります。
公式ドキュメントにはっきりとした説明がない(見つけられない)のですが、全件スキャン時の重要な仕様だと思いますので、押さえておきたい知識です。

CATEGORY

ARCHIVE

PostgreSQLに関するご相談は
株式会社インサイトまで
お気軽にお問い合わせください。

CONTACT