以下の内容はhttps://uga-box.hatenablog.com/entry/2026/02/04/000000より取得しました。


【DB】B-treeインデックス

以前にB-treeインデックスを調べていたが、どんな概念なのかまでは掘り下げでなく、分かった気のまま分かってなかったのでまた調べた

uga-box.hatenablog.com

基本概念

B-tree は1972年に Rudolf Bayer と Edward McCreight によって発明された 自己平衡木構造

「B」の由来は諸説あり、Balanced、Bayer、Boeing(開発された場所)などが挙げられるが、公式な説明はない

特徴

特性 説明
自己平衡 挿入・削除時に自動的にバランスを維持
多分岐 二分木と異なり、1ノードが複数の子を持つ
ディスク最適化 ディスクI/Oを最小化する設計
計算量 検索・挿入・削除すべて O(log n)

B-tree の構造

基本構成

                    [Root Node]
                [  30 |    70  |   90  ]
               /         |           \          \
              /          |            \          \
[10|20]    [40|50|60]  [75|80]  [95|100]
    ↓                ↓                ↓            ↓
[Data]        [Data]        [Data]    [Data]

構成要素

要素 説明
ルートノード 木の最上位。検索の起点
内部ノード(ブランチ) キーと子ノードへのポインタを格納
リーフノード 実際のデータまたはデータへのポインタを格納
キー ソートされた値。検索・比較に使用

B-tree の規則

次数 m の B-tree は以下の規則に従う:

  1. すべてのリーフノードは同じ深さ(高さが均一)
  2. ルート以外の各ノードは最低 ⌈m/2⌉ 個の子を持つ
  3. 各ノードは最大 m 個の子を持つ
  4. ノード内のキーは常にソートされている

検索の仕組み

検索アルゴリズム

email = 'user@example.com' を検索する場合:

1. ルートノードから開始
2. ノード内のキーと比較
   - 一致 → 発見
   - 小さい → 左の子ノードへ
   - 大きい → 右の子ノードへ
3. リーフノードに到達するまで繰り返し

具体例:値 45 を検索

Step 1: ルート [30 | 70 | 90]
        → 45 は 30 < 45 < 70 なので2番目の子へ

Step 2: 内部ノード [40 | 50 | 60]
        → 45 は 40 < 45 < 50 なので2番目の子へ

Step 3: リーフノード到達、データ取得

わずか3回のノードアクセスで目的のデータに到達

なぜ B-tree はディスクに最適なのか

ディスクI/Oのコスト

操作 速度
メモリアクセス ~100 ナノ秒
SSD ランダムリード ~100 マイクロ秒(1,000倍遅い)
HDD ランダムリード ~10 ミリ秒(100,000倍遅い)

ディスクアクセスは圧倒的に遅い

最小化することで性能を向上させる

B-tree の最適化ポイント

  1. ノードサイズ = ディスクページサイズ

    • 一般的に 4KB〜16KB
    • 1回のI/Oで多くのキーを読み込める
  2. 高いファンアウト(分岐数)

    • 1ノードに数百〜数千のキーを格納可能
    • 木の高さを低く保てる
  3. 低い木の高さ

    • 100万件でも高さは3〜4程度
    • I/O回数 = 木の高さ

計算例

ノードサイズ 16KB、キーサイズ 8バイト、ポインタ 8バイトの場合:

1ノードあたりのキー数 ≈ 16KB / 16バイト ≈ 1,000個
高さ2: 1,000 × 1,000 = 100万件
高さ3: 1,000 × 1,000 × 1,000 = 10億件

10億件のデータでも 最大3回のディスクアクセス で検索可能

B-tree と B+tree の違い

この2つあるの知らなかった

実際のデータベースでは B-tree の変種である B+tree がよく使われるらしい

構造の違い

特性 B-tree B+tree
データ格納場所 全ノード リーフノードのみ
リーフの連結 なし リンクリストで連結
内部ノード キー + データ キーのみ

B+tree の構造

                    [Root Node]
                [  30 |    70  |   90  ]
               /         |           \          \
              /          |            \          \
[10|20]    [40|50|60]  [75|80]  [95|100]
    ↓                ↓                ↓            ↓
[Data] ←→ [Data] ←→ [Data] ←→[Data]
              リンクリストで連結

B+tree のメリット

  1. 内部ノードにデータがない

    • より多くのキーを格納可能
    • 木の高さがさらに低くなる
  2. リーフノードの連結

    • 範囲検索が高速
    • WHERE age BETWEEN 20 AND 30 などで威力を発揮
  3. シーケンシャルアクセス

    • ORDER BY が効率的
    • フルスキャンもリーフを順番に辿るだけ

インデックスの種類と B-tree

RDBMSでの使用例

RDBMS デフォルトインデックス 備考
MySQL (InnoDB) B+tree クラスタインデックスを採用
PostgreSQL B-tree GiST、GIN など他も選択可能
SQLite B-tree テーブル自体も B-tree で格納
Oracle B-tree B*tree(変種)を使用

クラスタインデックス vs セカンダリインデックス

クラスタインデックス(主キー)

B+tree のリーフノード = 実際の行データ

セカンダリインデックス(その他のインデックス)

B+tree のリーフノード = 主キーへの参照
→ 主キーのB+treeを再度検索(二重検索)

B-tree が得意なクエリ

完全一致検索

SELECT * FROM users WHERE id = 12345;
-- O(log n) で高速

範囲検索

SELECT * FROM orders WHERE created_at BETWEEN '2024-01-01' AND '2024-12-31';
-- リーフノードの連結リストを活用

プレフィックス検索

SELECT * FROM products WHERE name LIKE 'iPhone%';
-- 前方一致は B-tree で対応可能

ソート

SELECT * FROM users ORDER BY created_at DESC LIMIT 10;
-- インデックス順 = ソート順なので追加のソート不要

B-tree が苦手なクエリ

後方一致・中間一致

SELECT * FROM users WHERE email LIKE '%@gmail.com';
-- フルスキャンが必要
-- 対策: 全文検索インデックス、または逆順カラムを追加

否定条件

SELECT * FROM users WHERE status != 'deleted';
-- インデックスが効きにくい

関数・演算を含む条件

SELECT * FROM users WHERE YEAR(created_at) = 2024;
-- インデックスが使われない
-- 対策: WHERE created_at >= '2024-01-01' AND created_at < '2025-01-01'

カーディナリティが低いカラム

SELECT * FROM users WHERE gender = 'male';
-- 値の種類が少ない(true/false、性別など)カラムは効果が薄い

インデックス設計のベストプラクティス

1. 複合インデックスの順序

-- クエリ
SELECT * FROM orders 
WHERE user_id = 123 
  AND status = 'shipped'
ORDER BY created_at DESC;

-- 効果的なインデックス
CREATE INDEX idx_orders ON orders(user_id, status, created_at);

原則: 等価条件 → 範囲条件 → ソートの順

2. カバリングインデックス

-- クエリ
SELECT id, name FROM users WHERE email = 'user@example.com';

-- インデックスだけで完結(テーブルアクセス不要)
CREATE INDEX idx_users_email ON users(email, id, name);

3. インデックスの過剰作成を避ける

  • INSERT/UPDATE/DELETE のたびにインデックスも更新
  • インデックスが多いとストレージ消費も増加
  • 本当に必要なインデックスを見極める

実行計画で確認の仕方

PostgreSQL

EXPLAIN ANALYZE SELECT * FROM users WHERE email = 'user@example.com';
Index Scan using idx_users_email on users  (cost=0.42..8.44 rows=1 width=...)
  Index Cond: (email = 'user@example.com'::text)
  Actual time: 0.025..0.026 rows=1 loops=1

参考資料




以上の内容はhttps://uga-box.hatenablog.com/entry/2026/02/04/000000より取得しました。
このページはhttp://font.textar.tv/のウェブフォントを使用してます

不具合報告/要望等はこちらへお願いします。
モバイルやる夫Viewer Ver0.14