Skip to main content
© 2024 NTT DATA Group Corporation
© 2024 NTT DATA Group Corporation
第49回 PostgreSQLアンカンファレンス@東京
B-Tree
のアーキテクチャ解説
2024年10月12日(土)
NTTデータグループ 藤井雅雄
© 2024 NTT DATA Group Corporation 2
自己紹介
藤井 雅雄
Database Technical Lead @ NTTデータグループ
データベース研究開発
PostgreSQL 技術支援
PostgreSQLコミッタ
レプリケーション
WAL圧縮
バックアップ進捗確認
pg_bigm(全文検索モジュール) コミッタ
fujii_masao
MasaoFujii
© 2024 NTT DATA Group Corporation 3
謝辞
本発表資料は、中村航規さん(*1)がNTTデータグループの業務の一環として作成したもの
をベースに少し手を入れたものです
PostgreSQLのB-Treeのアーキテクチャを調査し、本発表資料を作成していただいた
中村航規さんに深く感謝いたします!!
(*1)
名古屋大学大学院 情報学研究科 知能システム学専攻 修士1年 中村 航規
© 2024 NTT DATA Group Corporation 4
本講演について
講演資料は、後日、NTTデータグループのSlideShareアカウント上で公開予定です。
https://www.slideshare.net/nttdata-tech
© 2024 NTT DATA Group Corporation 5
B-Treeのアーキテクチャについて
• データ構造
• 検索
• 挿入
• 削除
© 2024 NTT DATA Group Corporation 6
B-Treeとは
• インデックスのうちの一つ
• 最も一般的なインデックス、 CREATE INDEX時USING句での指定がない場合B-Treeが使用される
• 一意インデックスとしても使われる
CREATE TABLE時にカラムのUNIQUEを宣言
CREATE UNIQUE INDEX name ON table (column [, ...]);
宣言するとそのカラムについて重複を許さない制約(一意性制約)が追加
デフォルトではNULLの重複は許される(はじきたい場合はNULLS NOT DISTINCTを宣言時に追加)
• 主キーとなるカラム(or カラムの組)、一意性制約を持つカラムには自動的にB-Treeが生成されて、制約を維持
• 対応する演算子は<、<=、=、>、>=とそれに準じるもの
• ある程度データ数が大きくないと性能改善効果なし
• インデックス管理のコストやHOT(後述)が使用できないデメリットが上回るためインデックスの使用頻度が低い場合も逆効果
• 地理情報であったり、全文検索をよく使用したりする場合や非スカラデータについては他インデックスのが優れる
(逆にそれ以外は基本的にB-Treeでよい)
© 2024 NTT DATA Group Corporation 7
B-Treeのデータ構造
メタページ
ルートページ
インターナルページ インターナルページ
リーフページ リーフページ リーフページ リーフページ
ヒープ (テーブルデータ領域)
• インデックスのページもテーブル(ヒープ)同様にデ
フォルト8KBのサイズ
• 親ページから子ページにリンク。子から親へのリンク
はない
• 左右ページへ双方向リンク
• ヒープ(テーブルデータ領域)にはリーフからのみリン
ク
• メタページという制御用のページが存在し、そこから
ルートページにリンク
• インターナルページは複数階層になる可能性があ
る
階層0
階層1
階層2
B-Treeの階層
© 2024 NTT DATA Group Corporation 8
B-Treeのデータ構造 – ファイル上の構造
メタページ
インターナルページ
リーフページ
ルートページ
リーフページ
インターナルページ
リーフページ
リーフページ
・・・
先頭
末尾
インデックスファイル上のイメージ
• メタページが常にインデックスファイルの先頭
• ルートページの位置は不定
© 2024 NTT DATA Group Corporation 9
B-Treeのデータ構造 - Fast Root
レコードの挿入や削除を繰り返した結果メタページから1階層ずつ降下するのが非効率になる場合がある
(その階層にページが1つしかない場合、そのページは削除されないため階層の数が減ることはない)
メタページは子を2つ以上持つページのうち最上層にあるものをFast Rootとしてリンクを持つ
探索や挿入操作などでメタページから降下する場合はそのリンクを用いてメタページからFast Rootへ降下する
このリンクを用いて
下の階層へ降りる
このページがFast Rootとなる
これらのリンクも
保持はされるが使われない
ページの分割や削除で
Fast Rootが変更となる場合は
Fast Rootへのリンクを変更する
© 2024 NTT DATA Group Corporation 10
B-Treeのデータ構造 – ページ内の構造
30 1 10 20
30 20 25 27
10 1 5 7 20 10 13 17
• 親ページにはリンクする子ページの最小キー値が入る
• 右端以外の各ページには、1番目のアイテムとして high key が入る
• high keyには、右隣ページの最小キー値が入る
• high keyにより、そのページのキー値の範囲を知ることができ、検索を高速化できる
• ページ内で各キー値は常にソート済。ページ内の探索にはバイナリサーチを使用
※実際には1つのページにもっと大量の値が格納される デフォルト8KB
インターナルページ
リーフページ リーフページ リーフページ
xx high key
凡例
© 2024 NTT DATA Group Corporation 11
検索 メタページ
インターナルページ
インターナルページ インターナルページ
リーフページ リーフページ リーフページ リーフページ
ヒープ (テーブルデータ領域)
① メタページから検索開始
ルートページ ② Fast Rootへ移動
③ インターナルページ内で検索条件のキーを
見つけて、そのリンクから子ページに移動
※リーフに辿り着くまで繰り返す
④ リーフページを右に移動しながら、
検索条件のキーを見つけて、その
リンクからヒープにアクセスして、
テーブルデータを取得
⑤ テーブルデータが削除済の(全Txが見えない)場合は、リーフのそのキーに削除フラグを立てる(以降、削除フラグのある
キーは検索からスキップできる)
© 2024 NTT DATA Group Corporation 12
検索
1 30 80
30 1 10 20 80 30 50 70 80 90 99
10 1 5 6 20 10 13 15 25 20 21 22
id name
・・・
13 東京
15 新潟
20 長野
・・・
リーフページ
インターナルページ
ルートページ
B-TreeのIndex Scanでの検索例
SELECT * FROM tbl WHERE id = 13
ヒープ
(テーブルデータ領域)
Index Scan:リーフページからヒープへアクセスして該当するテーブルデータを取得
© 2024 NTT DATA Group Corporation 13
検索
1 30 80
30 1 10 20 80 30 50 70 80 90 99
10 1 5 6 20 10 13 15 25 20 21 22
id name
・・・
6 山形
10 群馬
13 東京
・・・
リーフページ
インターナルページ
ルートページ
B-TreeのIndexScanでの検索例
SELECT * FROM tbl WHERE id >= 6 AND id <= 13
ヒープ
(テーブルデータ領域)
Index Scan:リーフページからヒープへアクセスして該当するテーブルデータを取得
© 2024 NTT DATA Group Corporation 14
検索
Index Only Scan:リーフページにある情報だけで検索を完結して、ヒープへのアクセスなし
リーフページにある情報だけで問い合わせに対応できるときはIndex Only Scanが使われる可能性がある
1 30 80
30 1 10 20 80 30 50 70 80 90 99
10 1 5 6 20 10 13 15 25 20 21 22
id name
・・・
・・・
インターナルページ
ルートページ
B-TreeのIndex Only Scanでの検索例
SELECT id FROM tbl WHERE id = 13
ヒープ
(テーブルデータ領域)
リーフページ
© 2024 NTT DATA Group Corporation 15
挿入
• 検索と同様に、挿入するキー値でリーフまで辿り、右に移動しながら挿入先のリーフページを見つける
• 挿入先のリーフページに空き領域があれば、そこに挿入。その際、ページ内のキー値がソート済になるように適宜キー値を移動
• リーフページ内に十分な空き領域がない場合は、不要なキー値を削除して空き領域を確保できないか試し、それでも無理な場
合はページをスプリットする
1 30 80
30 1 10 20 80 30 50 70 80 90 99
10 1 5 20 10 15 25 20 21
20 10 13 15
インターナルページ
ルートページ
リーフページ
• 空き領域に13を挿入
• ソート済になるように移動
B-Treeの挿入例
INSERT INTO tbl VALUES (13, '..');
※キー値13の挿入
© 2024 NTT DATA Group Corporation 16
挿入 – スプリット
リーフページに空きがないとき:後述する分割を避ける操作を行う、それでも無理なら分割へ
(実際はdeleteされたものが残っているため空きがないことが多い、
その場合についてはできるだけ分割を避ける操作で空き領域を作り分割回避)
例:13を挿入
1 10
10 1 5 10 15 17 19
挿入したいが空きのない
ページを分割
© 2024 NTT DATA Group Corporation 17
挿入 – スプリット
1 10 17
• 上の階層にも空きがない場合は上の階層についても同様の分
割を行っていく
• ルートページも空きがなければルートページを分割してインターナ
ルページとし、上の階層に新たなルートページを追加
• 同じインデックス値のものはできるだけ同じページに入るように分
割
親から新しいページ
へのリンクを追加
1 10
1 10 17
13を挿入
© 2024 NTT DATA Group Corporation 18
挿入 – スプリット
前ページの図では を分割して としている
実際の分割後の配分は分割対象ページがその階層の一番右のページかどうかで異なる
• 一番右のページでないとき 100%
分割後50パーセントずつ埋まっているように分割
50% 50%
• 一番右のページのとき
分割後の左ページがFILLFACTORパーセント埋まるように分割
残りは右ページに 100%
一番右のページが分割されるとき、timestampなど単純増加する FILLFACTER% (100-FILLFACTER)%
カラムのことが多い
分割後右ページに多くのタプルが入ることを想定しているためこのような実装となっている
※FILLFACTOR:初期インデックス構築時と一番右ページの分割の時に
リーフページにどの割合まで埋めるかを決めるパラメータ デフォルト90
10 15 17 19 17 10 15 17 19
© 2024 NTT DATA Group Corporation 19
スプリットを避ける操作
挿入時にページが埋まっていて挿入できないとき、できるだけSplitを操作を避けるため3つの操作が行われる
単純削除(Simple deletion)、ボトムアップ削除(Bottom-Up deletion)、重複削除(deduplication)
単純削除:削除フラグがついたキー値をすべて削除
それに加えて削除フラグがついたものの近くのタプルで削除できるものが見つかれば削除
(削除フラグがついたものの近くのタプルを調べる操作は非常に安価)
ページの占有ロックを取り、該当のタプルを削除
ボトムアップ削除:更新されたものについて古いバージョンのものの削除を目指す
(バージョン14以降で実装)
重複削除:重複したインデックスタプルを効率の良い表現に変換しサイズを縮小
(バージョン13以降で実装)
上から順に行い、挿入可能な領域が空けば終了し挿入へ
これらの操作後にクイックソートを行い空き領域は詰めて、ソート済みのものが並ぶ
© 2024 NTT DATA Group Corporation 20
単純削除 (Simple Deletion)
20 10 11 13 15 18
18 13 43 47 11 10 32 37 15 5
福井 東京 熊本 沖縄 埼玉 群馬 島根 香川 新潟 秋田
削
除
済
削
除
済
削
除
済
削
除
済
ヒープ
(テーブルデータ領域)
リーフページ
ページ1 ページ2
削
除
フ
ラ
グ
20 10 11 15
単純削除
• 削除フラグのあるキー「13」をリーフから削除
• 削除フラグのあるキー「13」と同じヒープのページをリンクしている
キー「11」「18」について、ヒープ側で削除済のキー「18」をリーフ
から削除
© 2024 NTT DATA Group Corporation 21
ボトムアップ削除 (Bottom-up Index Deletion)
Tableに対してインデックスが2個以上あり、あるインデックス対象の要素が更新されたものとする
それ以外のインデックスについてボトムアップ削除で削除を目指す
インデックスがないカラムの要素が更新されたとき → HOT(Heap Only Tuple)が適用される、インデックスに対する処理なし
インデックスがあるカラムの要素が更新されたとき → 削除+挿入操作として処理
(削除処理は前述のように何も行わないので実質は挿入操作のみ行われる)
※更新は削除+挿入として考えることができる
・更新されたカラムのインデックス → 単純削除で削除
・更新されていないカラムのインデックス → 単純削除で削除されなかった場合、ボトムアップ削除で削除を目指す
※単純削除はスキャンなどで該当タプルを参照して“known dead”を示すフラグがつけられるか、
“known dead”とされたタプルの近傍タプルで調べられて削除可能と分かった場合しか削除されない
つまり、削除可能なタプルがすべて削除されるわけではない
© 2024 NTT DATA Group Corporation 22
HOT (Heap Only Tuple)
インデックスがないカラムの更新の際に更新前レコードから更新後レコードへのポインタを保存しておくことで
インデックスの更新なしで更新操作が可能→高速な更新を実現
idへのインデックス
赤線のポインタを保持しておくことでTableが更新されても
インデックスに対して処理を行わずにレコードへたどることができる
HOTの処理がない場合はインデックスについても削除+挿入の操作が必要
Table
・・・
・・・
id = 10, key = 1
・・・
id = 10, key = 2
更新
10
© 2024 NTT DATA Group Corporation 23
ボトムアップ削除 (Bottom-up Index Deletion)
かなり複雑な処理をしているかつ資料が少なく、調べきれていないため要点のみをまとめる
• キー値が重複しているものについて削除可能かどうかを調べる
ボトムアップ削除で削除を目指しているのは更新されたが、別インデックスのカラムのみ更新されたもの
そのため、更新前と更新後はキー値が重複している
• すべての削除可能なタプルが削除されるわけではない
必要なコストとボトムアップ削除のメリットのバランスがとられており、見合わないと判断されれば途中で打ち切られる
• ヒューリスティックな手法
“most objects die young”という経験的な観察に基づいている
1つ以上削除されるとも限らない
それでもうまく動き、病的なほど大量な更新が起こらない限り不必要なページ分割は起こらない
© 2024 NTT DATA Group Corporation 24
ボトムアップ削除 (Bottom-up Index Deletion)
UPDATE tbl SET id2 = 99 WHERE id1 = 1;
UPDATE tbl SET id2 = 44 WHERE id1 = 1;
UPDATE tbl SET id2 = 66 WHERE id1 = 1;
id1 id2
1 11
1 99
1 44
1 66
1 1 1 1 11 44 66 99
id1カラムへのB-Treeインデックスのリーフ id2カラムへのB-Treeインデックスのリーフ
ボトムアップ削除では重複して削除
済の可能性の高いキーを削除する
© 2024 NTT DATA Group Corporation 25
重複削除
リーフページについて同じキー値をもつエントリをまとめることで効率の良い表現へ
例:インデックスの対象カラム:a
重複削除なし
重複削除あり
Table
a = 50, b = 15
a = 40, b = 50
a = 40, b = 1
a = 10, b = 40
a = 30, b = 20
a = 40, b = 5
a = 20, b = 60
40 40 40
40
© 2024 NTT DATA Group Corporation 26
削除
論理的にはB-Treeをたどり値を見つけ、その値を削除すればよいが・・・ 上の層については更新しなくてもよい?
例:10を削除
1 30 80
30 1 10 20 80 30 50 70 80 90 99
10 1 5 20 10 15 25 20 21
20 15
B-Treeとしては上の層を更新するほうが正
確だが、探索能力としては別に変わらない
実際も更新していないと思われる
© 2024 NTT DATA Group Corporation 27
削除
実際のPostgreSQL内では
DELETEやUPDATEが起きてもB-Treeに対して即座に変更操作はしない
その後のScanなどでヒープを参照して、すべてのトランザクションに対してデッドであるタプルを見つけた場合に
そのアイテム“known dead”を示すにフラグを立てる
“known dead”のものについてはヒープへの参照をスキップ
このタプルについて処理を行うのは挿入時にページが分割されそうになったタイミング
単純削除で空き領域を作る(後述)
メリット
• まとめて削除できるので効率が良い
• 空き領域を作る操作が起こるまで占有ロックがいらないため高速に
記載されている会社名、商品名、又はサービス名は、
各社の登録商標又は商標です。
© 2024 NTT DATA Group Corporation 29
ページレイアウト
インターナルページとリーフページは標準のページフォーマットに準じる
ソートされた順番で格納される
VACUUMされた場合はその領域が詰められる
色付きの部分はhigh keyが入る(階層で一番右のページを除く、この場合は詰める)
出典:PostgreSQL 16.4文書
© 2024 NTT DATA Group Corporation 30
Index Only ScanとIndex Scanの違い
リーフページからヒープへのアクセスがあるかどうかが違い
Index Only Scan:リーフページにある情報だけで完結するのでヒープへのアクセスなし
(例:インデックス対象のカラムのみSELECTする場合など)
Index Scan:リーフページからヒープへアクセスして該当するタプルの情報を取得
リーフページにある情報だけで問い合わせに応答できるときはIndex Only Scanが用いられる
この部分のアクセスが
必要かどうかの違い
© 2024 NTT DATA Group Corporation 31
検索
Bitmap Index Scan:インデックスから得られた候補行をメモリー上にビットマップ化
ビットマップを用いてスキップしながらSeq Scanを行う → I/O操作が少ない
※ディスク上のデータはIndexで並んでいるわけではないので、
対象行が多いとリーフページからポインタが伸びたレコードをヒープ上で見つけるためのI/O操作が多くなりこちらのが速くなる
例:WHERE a >10
Table
a = 20
a = 1
a = 100
a = 5
a = 40
BitMap
1
0
1
0
1
赤の矢印部分でヒープ内の
I/O操作発生
© 2024 NTT DATA Group Corporation 32
よりBitmap Index Scanが強力になるとき
AND や OR を条件に含む場合、bit演算を組み合わせてより高速に
例:WHERE a > 10 AND b > 10 (a, bそれぞれのインデックスを持っている前提)
Table
a = 50, b = 15
a = 5, b = 50
a = 15, b = 1
a = 1, b = 40
a = 30, b = 20
a = 40, b = 5
a = 20, b = 60
a > 10
1
0
1
0
1
1
1
b > 10
1
1
0
1
1
0
1
BitmapAnd
1
0
0
0
1
0
1
© 2024 NTT DATA Group Corporation 33
検索中のロック
検索しているページを読んでいる間に変更されないようにする以外のロックを保持しない(共有ロックのみ保持)
下の階層でsplitが起きていてもhigh keyを見ることで分割を検知できる
例:WHERE a = 15
(インデックスの対象カラム:a)
25 1 10 20
10 1 5 6 14 10 11 20 14 15
High keyが20でないので分割さ
れたと判定、右ページを見る
というページが分割されてこのようになったとする
20 10 any any
© 2024 NTT DATA Group Corporation 34
挿入操作の物理構造
ページをたどっていき挿入するページを見つける
そして挿入するページを探索し、挿入位置を決定 (挿入後にソートするわけではない)
例:3を挿入
• この時_bt_binsrch()の結果をそのまま利用するため、通常は既存の同じキー値の前に挿入される
• 分割を避ける操作を行った場合は操作後にクイックソートを行う
ソート済みのものについて挿入位置を決定する操作が行われる
• できるだけ同じインデックスのものは同じページに収まるようにする(分割操作のときも)
PageHeaderData ItemID1 ItemID5 ・・・
・・・ 5 1 Special
PageHeaderData ItemID1 ItemID3 ItemID5 ・・・
5 3 1 Special
・・・
© 2024 NTT DATA Group Corporation 35
リーフページの要素が空になったら
リーフページ内のアイテムが完全に空になったときにページの削除の可能性
ただし、一番右のリーフページは削除されない(high keyの処理など探索アルゴリズムの単純化のため)
また親ページの一番右のページもそれが唯一の子ページでない限りは削除されない(削除される場合は親ページとともに)
1 30 80
30 1 10 20 80 30 50 70 80 90 99
10 1 5 20 25 20 21
© 2024 NTT DATA Group Corporation 36
リーフページの削除
ステップ1:親ページと対象ページをロックし、親ページから対象ページへのリンクを右ページへのリンクに変更
30 1 10 20
10 1 5 20 25 20 21
30 1 10
10 1 5 20 25 20 21
※20からのリンクは使われなくなる
ため図上では消している
© 2024 NTT DATA Group Corporation 37
リーフページの削除
ステップ2:左兄弟ページ、対象ページ、右兄弟ページの順でロックをとり、左右リンクを更新
30 1 10
10 1 5 20 25 20 21
30 1 10
10 1 5 25 20 21
© 2024 NTT DATA Group Corporation 38
リーフページの削除
親ページの唯一の子ページの場合:親ページごと削除
(親ページが祖父母ページの唯一の子ページである場合は祖父母ページもという形で
複数子ページをもつページの子ページまで削除)
ステップ1:リーフページのみロック、削除しない階層まで再帰的にリンクを切っていく
ステップ2:「左兄弟ページ、対象ページ、右兄弟ページの順でロックをとり、左右リンクを更新」の操作を上の階層から順にする
緑のリーフページを削除するとき
赤枠のページが削除される
ステップ1の終了 ステップ2の終了