大きな画面での利用をおすすめします — アニメーションと操作がより快適になります。
入力

B+ 木

B+ 木は、順序付けたキーをページに分けて保存する。内部ノードの区切りキーが探索先を決め、葉ノードがレコードのキーを持つ。すべての葉は同じ深さにあり、キー順のリンクでもつながる。点検索は木を下り、範囲検索は開始位置を見つけてから葉のリンクをたどる。等値検索、範囲検索、インデックス順と一致する ORDER BY に使える。

B+ 木の仕組み

このレッスンの次数 m は内部ノードの最大子数を表す。安定状態のノードは最大 m−1 個のキーを持つ。次数 3 なら 2 個までで、3 個目が分割を起こす。

内部ノード [15,30] の子が担当する区間は <15、15≤key<30、≥30。区切りキーと等しければ右へ進む。レコードのコピーが右の葉にあるためだ。内部キーは探索用で、別のレコードではない。

葉があふれたら左右に分け、右葉の先頭キーを親へコピーする。そのキーは右葉にも残す。内部ノードの分割では中央の区切りキーを親へ移動し、分割後の左右の内部ノードには残さない。

葉リンクも更新する。新しい右葉の next に旧後続を保存してから、旧葉の next を新右葉へ向ける。順序を逆にすると後続を失いかねない。内部ページの分割では子ポインターもキーとともに振り分ける。親もあふれれば分割を上へ伝える。根の分割は新しい根を作り、全葉の深さを同時に一つ増やす。

ステップ実行

既定値は次数 3 で、次を実行する。

insert 10, insert 20, insert 5, insert 15, insert 25, insert 30, search 25, range 9 27

  1. 10 と 20 を挿入すると、根の葉は [10,20] になる。
  2. 5 の挿入で [5,10,20] があふれる。[5,10] と [20] に分け、20 をコピーして新しい根を作る。
  3. 15 を挿入すると左葉 [5,10,15] を [5,10] と [15] に分ける。新葉 [15] に旧後続 [20] を引き継ぎ、[5,10] から新葉へつなぐ。15 を根にコピーする。
  4. 25 を挿入すると右葉は [20,25] になる。
  5. 30 の挿入で右葉を [20,25] と [30] に分け、30 を根へコピーする。根の [15,20,30] もあふれるため、中央の 20 を新根へ移動する。左右の内部ノードにはそれぞれ 15 と 30 が残る。

最終形は次のとおり。

                 [20]
              /        \
            [15]       [30]
           /   \       /   \
      [5,10]  [15] [20,25] [30]
          └────→────→────→

葉リンクは [5,10] -> [15] -> [20,25] -> [30]。各レコードキーは葉層に一度だけ現れる。内部の 15、20、30 は区切りキーだ。

25 の検索は、根の 20 で右、内部キー 30 で左へ進み、葉 [20,25] 内を二分探索する。訪問するページは 3 枚。

閉区間 [9,27] の検索では左葉へ下り、lower bound で 10 を見つける。10 を出力し、リンク先で 15、次で 20 と 25 を出力する。次の葉の 30 は上限を超えるので終了する。結果は [10,15,20,25]、葉間の移動は 3 回。結果ごとに根へ戻ると、葉リンクによる連続走査の利点を失う。

用途

B+ 木は等値検索と順序付き範囲検索の両方に対応する。ハッシュインデックスは等値検索に向くが、ハッシュバケットだけでキー順の走査はできない。B+ 木は子数を増やして高さを抑える。実際のページは図より多くの子を持つため、多数のレコードでも数層に収まることがある。

計算量

レコード数を n、次数を m、返すレコード数を k とする。点検索が訪問するページの層数は O(log_m n)。分割なしの挿入も同じ経路を下る。分割は各層で最大一度なので、ページ層単位では O(log_m n)。範囲検索は開始位置の特定と結果の出力を合わせて O(log_m n+k) となる。

これは次数を固定したモデル、またはページ層の訪問回数としての評価だ。ページ内の二分探索やキー、ポインターの移動には別の CPU コストがある。m を変数とする場合、それらを無視できない。ページがキャッシュにあれば物理ディスク読み取りは不要なので、ページ訪問数はディスク I/O 数ではない。画面の次数 3–5 は分割を見せるための設定で、運用時の推奨値ではない。

境界ケース

  • 空の木は根の葉だけを持つ。検索は未検出、範囲結果は空になる。
  • 最初の葉分割で新根を作り、高さが一層増える。
  • モデルは一意キーのみを扱う。重複挿入では変化しない。実際のインデックスはレコード識別子などで重複を扱える。
  • 存在しないキーは選ばれた候補葉で判定でき、全葉の走査は不要。
  • 開始位置の lower bound が範囲上限を超えれば、リンクをたどらず終了する。
  • 最大キーを超える範囲は末尾の空 next で終了する。
  • range 1 99 は既定の挿入キーをすべて返す。出力件数 k のコストも必要だ。
  • 画面は最大 12 レコードを表示する。上限後も挿入ボタンは押せるが、制限を説明する。アルゴリズムの容量上限ではない。

よくある間違い

  • 葉の区切りキーを移動してレコードを消す。右葉の先頭キーはコピーする。
  • 親へ移した中央キーを内部ノードにも残し、子の区間を崩す。
  • 区切りキーと等しいとき左へ進み、右葉のレコードを見落とす。
  • 旧 next を上書きしてから後続を取得し、葉リンクを切る。
  • 範囲内の各キーを根から検索し、一度の下降と連続走査の利点を失う。
  • 図のページを PostgreSQL や MySQL の実際のバイト配置と見なす。

モデルの範囲

メモリ内の一意な整数キー、固定次数、あふれた時点の分割を扱い、削除は実装しない。バッファープール、物理 I/O、充填率、圧縮、重複キー、並行処理のラッチ、WAL、MVCC、復旧、ディスク形式は省略する。実際の葉はレコード本体やレコード位置を持つ場合があり、子数も大きい。画面で追うのは探索経路、分割、葉リンクの制約だ。