1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

B-treeインデックスのページ構成理解🐘

Last updated at Posted at 2026-01-14

昨年、テーブルのページ構成について簡単に解説したものを投稿しましたが、
今回は B-treeインデックスのページ構成について解説します。

テーブルのページ構成についてはこちら↓

データ件数にもよりますが、B-treeインデックスのページは次の 4種類で構成されます。

種類 格納情報、役割など
メタページ 「ルートページが何ページ目に在るか」「B-treeインデックスの高さ」など、当該の B-treeインデックスに関する情報が格納
ルートページ B-tree の最上段ページ。小さい B-treeインデックスではリーフページを兼ねることもある
インターナルページ 非リーフの中間ノード。OracleDB のブランチに相当
リーフページ 実際のデータと TID (タプル (レコード) の物理的な位置を示す識別子) が格納

それぞれについて、pageinspectモジュールを使用して確認していきます。

B-treeインデックスは、pgbench で作成した pgbench_accounts_pkey を使用します。
スケールファクタ = 1 で作成したので件数は 100,000件で、1~100000 が格納されています。

[postgres@pos-qiita-20260112 ~]$ pgbench -i -s 1 -q postgres
dropping old tables...
creating tables...
generating data (client-side)...
vacuuming...                                                                              
creating primary keys...
done in 0.43 s (drop tables 0.01 s, create tables 0.01 s, client-side generate 0.25 s, vacuum 0.09 s, primary keys 0.07 s).
[postgres@pos-qiita-20260112 ~]$ 
[postgres@pos-qiita-20260112 ~]$ psql -U postgres
psql (17.7)
Type "help" for help.

postgres=# \d pgbench_accounts
              Table "public.pgbench_accounts"
  Column  |     Type      | Collation | Nullable | Default 
----------+---------------+-----------+----------+---------
 aid      | integer       |           | not null | 
 bid      | integer       |           |          | 
 abalance | integer       |           |          | 
 filler   | character(84) |           |          | 
Indexes:
    "pgbench_accounts_pkey" PRIMARY KEY, btree (aid)  ★

postgres=# select count(1) from pgbench_accounts ;
 count  
--------
 100000
(1 row)

postgres=# select aid from pgbench_accounts fetch first 10 rows only;
 aid 
-----
   1
   2
   3
   4
   5
   6
   7
   8
   9
  10
(10 rows)

postgres=# select max(aid) from pgbench_accounts ;
  max   
--------
 100000
(1 row)

postgres=# 

pg_relation_filepathで pgbench_accounts_pkey のパスを特定しサイズを確認すると、
2,260,992byte なので ページ総数は 276 であることがわかります。(2,260,992/8,192=276)

postgres=# select pg_relation_filepath('pgbench_accounts_pkey');
 pg_relation_filepath 
----------------------
 base/5/16523
(1 row)

postgres=# exit
[postgres@pos-qiita-20260112 ~]$ ls -la 17/data/base/5/16523
-rw-------. 1 postgres postgres 2260992 Jan 13 07:18 17/data/base/5/16523
[postgres@pos-qiita-20260112 ~]$ 

bt_metapで対象の B-treeインデックスのメタページの内容を確認できます。
ルートページは 3ページ目にあることや、高さ (level) が 1であることがわかります。
(ルートページの物理的な位置は不定)

postgres=# select * from bt_metap('pgbench_accounts_pkey');
-[ RECORD 1 ]-------------+-------
magic                     | 340322
version                   | 4
root                      | 3  ★
level                     | 1  ★
fastroot                  | 3
fastlevel                 | 1
last_cleanup_num_delpages | 0
last_cleanup_num_tuples   | -1
allequalimage             | t

postgres=# 

bt_page_statsまたはbt_multi_page_statsで、メタページ以外のページの要約を確認できます。
bt_metapで確認した通り、3ページ目にルートページがあります。(type = 'r')

postgres=# select * from bt_multi_page_stats('pgbench_accounts_pkey',1,10);
 blkno | type | live_items | dead_items | avg_item_size | page_size | free_size | btpo_prev | btpo_next | btpo_level | btpo_flags 
-------+------+------------+------------+---------------+-----------+-----------+-----------+-----------+------------+------------
     1 | l    |        367 |          0 |            16 |      8192 |       808 |         0 |         2 |          0 |          1
     2 | l    |        367 |          0 |            16 |      8192 |       808 |         1 |         4 |          0 |          1
     3 | r ★ |        274 |          0 |            15 |      8192 |      2676 |         0 |         0 |          1 |          2
     4 | l    |        367 |          0 |            16 |      8192 |       808 |         2 |         5 |          0 |          1
     5 | l    |        367 |          0 |            16 |      8192 |       808 |         4 |         6 |          0 |          1
     6 | l    |        367 |          0 |            16 |      8192 |       808 |         5 |         7 |          0 |          1
     7 | l    |        367 |          0 |            16 |      8192 |       808 |         6 |         8 |          0 |          1
     8 | l    |        367 |          0 |            16 |      8192 |       808 |         7 |         9 |          0 |          1
     9 | l    |        367 |          0 |            16 |      8192 |       808 |         8 |        10 |          0 |          1
    10 | l    |        367 |          0 |            16 |      8192 |       808 |         9 |        11 |          0 |          1
(10 rows)

postgres=# 

bt_page_itemsでページの詳細を確認できます。
ルートページの詳細を確認すると、リーフページに格納されているデータの境界値がわかります。(data列)
2ページ目 (itemoffset: 2) の data列の値 (6f 01 00 00 00 00 00 00) を 10進数に変換すると 367 ですので、1ページ目には 366 までが格納されていることになります。

postgres=# select * from bt_page_items('pgbench_accounts_pkey',3);
 itemoffset |  ctid   | itemlen | nulls | vars |          data ★        | dead | htid | tids 
------------+---------+---------+-------+------+-------------------------+------+------+------
          1 | (1,0)   |       8 | f     | f    |                         |      |      | 
          2 | (2,1)   |      16 | f     | f    | 6f 01 00 00 00 00 00 00 |      |      | 
          3 | (4,1)   |      16 | f     | f    | dd 02 00 00 00 00 00 00 |      |      | 
          4 | (5,1)   |      16 | f     | f    | 4b 04 00 00 00 00 00 00 |      |      | 
          5 | (6,1)   |      16 | f     | f    | b9 05 00 00 00 00 00 00 |      |      | 
          6 | (7,1)   |      16 | f     | f    | 27 07 00 00 00 00 00 00 |      |      | 
          7 | (8,1)   |      16 | f     | f    | 95 08 00 00 00 00 00 00 |      |      | 
          8 | (9,1)   |      16 | f     | f    | 03 0a 00 00 00 00 00 00 |      |      | 
          9 | (10,1)  |      16 | f     | f    | 71 0b 00 00 00 00 00 00 |      |      | 
         10 | (11,1)  |      16 | f     | f    | df 0c 00 00 00 00 00 00 |      |      | 
(snip) 
        270 | (271,1) |      16 | f     | f    | 97 80 01 00 00 00 00 00 |      |      | 
        271 | (272,1) |      16 | f     | f    | 05 82 01 00 00 00 00 00 |      |      | 
        272 | (273,1) |      16 | f     | f    | 73 83 01 00 00 00 00 00 |      |      | 
        273 | (274,1) |      16 | f     | f    | e1 84 01 00 00 00 00 00 |      |      | 
        274 | (275,1) |      16 | f     | f    | 4f 86 01 00 00 00 00 00 |      |      | 
(274 rows)

bt_page_itemsで 1ページ目 (リーフページ) を確認すると、1~366 (1~16E) の値が格納されていることがわかります。
HTID を参考にテーブルページを確認すると、レコードを参照できます。

postgres=# select * from bt_page_items('pgbench_accounts_pkey',1);
 itemoffset |  ctid  | itemlen | nulls | vars |          data           | dead |  htid  | tids 
------------+--------+---------+-------+------+-------------------------+------+--------+------
          1 | (6,1)  |      16 | f     | f    | 6f 01 00 00 00 00 00 00 |      |        | 
          2 | (0,1)  |      16 | f     | f    | 01 00 00 00 00 00 00 00 | f    | (0,1)  | 
          3 | (0,2)  |      16 | f     | f    | 02 00 00 00 00 00 00 00 | f    | (0,2)  | 
          4 | (0,3)  |      16 | f     | f    | 03 00 00 00 00 00 00 00 | f    | (0,3)  | 
          5 | (0,4)  |      16 | f     | f    | 04 00 00 00 00 00 00 00 | f    | (0,4)  | 
(snip)
        360 | (5,54) |      16 | f     | f    | 67 01 00 00 00 00 00 00 | f    | (5,54) | 
        361 | (5,55) |      16 | f     | f    | 68 01 00 00 00 00 00 00 | f    | (5,55) | 
        362 | (5,56) |      16 | f     | f    | 69 01 00 00 00 00 00 00 | f    | (5,56) | 
        363 | (5,57) |      16 | f     | f    | 6a 01 00 00 00 00 00 00 | f    | (5,57) | 
        364 | (5,58) |      16 | f     | f    | 6b 01 00 00 00 00 00 00 | f    | (5,58) | 
        365 | (5,59) |      16 | f     | f    | 6c 01 00 00 00 00 00 00 | f    | (5,59) | 
        366 | (5,60) |      16 | f     | f    | 6d 01 00 00 00 00 00 00 | f    | (5,60) | 
        367 | (5,61) |      16 | f     | f    | 6e 01 00 00 00 00 00 00 | f    | (5,61) | 
(367 rows)

postgres=# select * from heap_page_items(get_raw_page('pgbench_accounts',5)) where lp = 54;
 lp | lp_off | lp_flags | lp_len | t_xmin | t_xmax | t_field3 | t_ctid | t_infomask2 | t_infomask | t_hoff | t_b
its | t_oid |                                                                                                t_d
ata                                                                                                
----+--------+----------+--------+--------+--------+----------+--------+-------------+------------+--------+----
----+-------+---------------------------------------------------------------------------------------------------
---------------------------------------------------------------------------------------------------
 54 |   1280 |        1 |    121 |    798 |      0 |        6 | (5,54) |           4 |       2818 |     24 |    
    |       | \x670100000100000000000000ab2020202020202020202020202020202020202020202020202020202020202020202020
20202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020
(1 row)

postgres=#

データ件数を 1,000,000件に増やすと、B-treeインデックスの高さ (level) が 2となり、
インターナルページが構成されました。(type = 'i')

[postgres@pos-qiita-20260112 ~]$ pgbench -i -s 10 -q postgres
dropping old tables...
creating tables...
generating data (client-side)...
vacuuming...                                                                                
creating primary keys...
done in 3.38 s (drop tables 0.02 s, create tables 0.01 s, client-side generate 2.30 s, vacuum 0.21 s, primary keys 0.83 s).
[postgres@pos-qiita-20260112 ~]$ psql -U postgres
psql (17.7)
Type "help" for help.

postgres=# 
postgres=# select count(1) from pgbench_accounts ;
  count  
---------
 1000000
(1 row)

postgres=# select pg_relation_filepath('pgbench_accounts_pkey');
 pg_relation_filepath 
----------------------
 base/5/16545
(1 row)

postgres=# exit
[postgres@pos-qiita-20260112 ~]$ ls -la 17/data/base/5/16545 
-rw-------. 1 postgres postgres 22487040 Jan 13 11:16 17/data/base/5/16545
[postgres@pos-qiita-20260112 ~]$ psql -U postgres
psql (17.7)
Type "help" for help.

postgres=# \x
Expanded display is on.
postgres=# select * from bt_metap('pgbench_accounts_pkey');
-[ RECORD 1 ]-------------+-------
magic                     | 340322
version                   | 4
root                      | 290
level                     | 2  ★
fastroot                  | 290
fastlevel                 | 2
last_cleanup_num_delpages | 0
last_cleanup_num_tuples   | -1
allequalimage             | t

postgres=# \x
Expanded display is off.
postgres=# select type,count(1) from bt_multi_page_stats('pgbench_accounts_pkey',1,-1) group by type;
 type | count 
------+-------
 r    |     1
 l    |  2733
 i    |    10  ★
(3 rows)

postgres=# 
1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?