昨年、テーブルのページ構成について簡単に解説したものを投稿しましたが、
今回は 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=#