B木の基礎を学ぶ

RDBのインデックスについて理解を深めようとすると必ず行き当たるのがB木ですが、なんとなくわかっているけどきちんと理解できていなかったので学んだことを10分程度で理解できるよう具体的なケースでまとめました。

## B木(B-tree)

### 特徴

B木は木構造の一種で、全レコードを順番に探索（シーケンシャルスキャン） *O(N)* するよりも少ない計算量 *O(logN)* で目的のデータまで到達できるアルゴリズムです。

![order.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/35884/e51ff892-8bd6-3d5f-3d11-b57ab593408e.png)
計算量のイメージとしては上図のようになり、データ量が増えても計算時間がほとんど変わらないのが一番の特徴です。

以下の特徴があるため、バランス良くデータが配置され安定したパフォーマンスを発揮できます。（値によって遅いときと速いときの差が出にくい）

- 葉ノード以外はすべて2つ以上の子ノードを持つ
- M個のキーを持つ節点はM＋1個の子ノードを持つ
- 葉ノードはすべて同じレベルになる
- 子ノードには小さい値と大きい値が入り、親ノードには中間値が入る構造になる

![b-tree (15).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/35884/127d763f-5c9a-fad1-207f-bb66c9e3b1f2.png)

論理イメージとしては上図のようになり、左に小さい値、右に大きい値のノードをぶら下げる構造となっています。

### 値の検索

- ルートノードからポインタをたどる
- 葉ノードの前のノードで見つかる場合もある

![b-tree.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/35884/a686df72-f466-57b5-1123-618ec7c2d26d.png)

上図のように`13`という値を検索する場合、ルートノードが`15`なので左のポインタをたどり、`9`よりも大きいので右のポインタをたどり、目的の`13`が含まれるノードに辿り着きます。


### 値の挿入

- ノードに空きがある場合はそのまま追加する
- オーバーフローする場合は中間値を親ノードに追加して新しいノードを作成する
- オーバーフローはさらに親ノードに波及する場合があり、最終的にルートノードまで実施する

**ノードに空きがあるケース**
![b-tree (1).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/35884/40720c18-c9c3-9714-9b21-ad7d1610c59d.png)

上図は`18`を挿入する例で、検索時と同様にルートノードからたどって`17`が含まれているノードに空きがあるのでそのまま挿入してこのケースは終了となります。

**ノードに空きがなく、オーバーフローするケース**
![b-tree (2).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/35884/0a654584-b365-71b5-5205-819300601d14.png)

この状態から次に`20`を追加しようとした場合、同様に`17`が含まれているノードに追加するのが適当なのですが、空きがないため新しいノードを作る必要があります。

![b-tree (3).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/35884/c6cb4717-8fc6-05be-5c13-bc2b28f5a4d7.png)

ルールに則り`17`,`18`,`20`の中間値`18`を親ノードに追加して`17`,`20`を子ノードとして新たにひとつ作成しポインターを親ノードに追加して終了です。

**親ノードにも空きがないケース**
![b-tree (4).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/35884/3b45a309-ffe9-bd21-5136-81ced7799087.png)

この状態からさらに`32`という値を追加する場合、同様に`25`,`30`,`32`から中間値である`30`を親ノードに追加して`32`を持つ子ノードを追加しようとします。

![b-tree (5).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/35884/f18b0728-a5fa-8880-b0ff-4bcef195a5da.png)

しかし親ノードに空きがないため、`18`,`23`,`30`で同様に分割します。

![b-tree (6).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/35884/f3f0760b-3f2d-133a-a46c-e520823bf43c.png)

中間値である`23`をルートノードに追加し、新たに`30`の値を持つノードを追加して無事終了です。


### 値の削除

削除は大きく2つのケースがあり、削除対象が「葉ノードにある場合」と「葉ノード以外の場合」があります。

#### 葉ノードの場合

- ノードに2つ以上のレコードがあるならそのまま削除
- 削除する値があるノードに値が1つしかないならアンダーフロー処理する
  - 同じ親ノードにぶら下がっている別ノードから値を持ってきて、親ノードに中間値を置き換える
  - 別ノードにもってこれる値がない場合、中間値を親ノードから持ってきて、親ノードからその中間値を削除する

**ノードに2つ以上のレコードがあるケース**
![b-tree (7).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/35884/a34a1379-ddc5-4e27-ee92-bd1e646f0c8c.png)

上図のように`25`を削除する場合、同じノードに`30`という値があるため`25`を削除後も1つ以上の値があるという条件が満たされるためこれ以上は特になく終了です。

**ノードに値が1つしかないケース**
![b-tree (8).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/35884/0d0ecad9-b27a-e9df-a1ae-e37acf9f4567.png)

さらに`17`を削除しようとするとそのノードに値が1つもない状態となってしまうため、アンダーフロー処理を行います。

![b-tree (9).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/35884/a077e74b-31d0-1b21-6b9b-4f010f976391.png)

同じ親ノードにぶら下がっているノードに2つ以上の値をもっているものがあればそこからとって中間値を親ノードに置き換える処理をすればいいのですが、今回は`30`1つしか持っていないノードしかないため、親ノードにある`23`をもってきて`30`のノードにマージします。

![b-tree (10).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/35884/0b646f45-68aa-e2ee-d28c-02a9e5e8eb96.png)

親ノードの`23`は削除するため今度はさらに親ノードであるルートノードから`15`を持ってきます。

![b-tree (11).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/35884/52f9e1c5-878d-e870-25ae-dffd91d248a8.png)

ルートノードにあった`15`を削除したためルートノードが1段すくなくなり終了です。

#### 葉ノード以外の場合

- 右側の葉ノードにある、削除する値とその次に大きな値を置き換える
- その際にアンダーフローが発生する場合は葉ノードでアンダーフローが発生した際の処理を行う

![b-tree (12).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/35884/f2e31346-f718-f935-b326-d1ae17ecc6b0.png)

上図のようにルートノードの`15`を削除するケースを考えます。

![b-tree (13).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/35884/8e3463df-0c58-4912-fa23-7e81be86c2ed.png)

ルールに則り、右側のポインタにぶら下がっている一番小さい値`17`を持ってきて`15`があったところに格納します。

![b-tree (14).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/35884/ff5d85b3-c486-2330-aafb-9599f16b83a8.png)

元々`17`があったノードに値がなくなったためアンダーフロー処理を行い、`23`,`25`,`30`のうちの中間値である`25`を親ノードにして`23`,`25`を子ノードに格納して終了です。

## B+木(B+ -tree)

B木の発展版で、MySQLやPostgreSQL、Oracle等メジャーなRDBで広く使われているアルゴリズムです。
検索、挿入、削除いずれも基本的にはB木と同じロジックとなるためここではB木の違いと検索の説明までに留めます。

### B木との違い

B木で構成されたインデックス部と、データを葉ノードに格納した構造となっています。
このためB+木では比較的大きなデータも扱えるようになり、ページあたりのポインタも増やしやすくなるためB木と比較して検索効率も高いようです。
また、葉ノードにシーケンシャルなデータが格納されているため範囲検索が得意なことも特徴です。

### 値の検索

![b+tree.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/35884/cac91636-80b1-a825-3776-f92160c88cdc.png)

B木と同様にポインターをたどりますが、図にあるようにデータは一番下の葉ノードに存在します。

### 範囲検索

![b+tree (1).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/35884/2b5b354c-bbf3-1082-b80e-7afc407c68b6.png)

例として`12`〜`23`の範囲検索を行った場合、まず小さい方の値である`12`を検索し、そこから右のポインタをたどって`23`で終了します。
葉ノードがポインターで繋がっているためB木よりも効率的に範囲検索が行えることがわかります。

# おわりに

メジャーなRDBに広く使われているB木ですが、基礎を固めておくと様々なケースで応用が効き理解が深まりそうですね:wink:

# 参考

- [計算量オーダーについて](https://qiita.com/asksaito/items/59e0d48408f1eab081b5)
- [データベース概論Ⅰ_16.物理的データ格納方式(2)](https://www.youtube.com/watch?v=L2lHBpy7tEI)
- [データベース概論_17.物理的データ格納方式(3)/問合せ処理(1)](https://www.youtube.com/watch?v=RMzMluhNrlE&t=557s)