ITの教科書検索
入門読了 約10分

インデックスとB-treeとは ― データベースを速くする仕組み

インデックスは、列の値から行の場所をすぐに引ける索引です。B-treeという木構造で全件を読まずに探せる仕組みと、書き込みが遅くなる理由、効く列・効かない列が分かります。

目次
  1. インデックスとは何か
  2. B-treeインデックスの仕組み
  3. 木をたどって絞り込む様子
  4. 行数が増えるほど差が広がる
  5. 更新のコストが増える理由
  6. インデックスが効く列・効きにくい列
  7. B-tree以外のインデックス
  8. 練習問題

インデックスとは、検索を速くするための索引

WHEREで絞り込むSQLが遅いとき、まず検討するのがインデックスです。データベースのインデックスとは、列の値から、その値を持つ行の場所をすぐに引けるようにした索引です。索引が無いと、DBMSは条件に合う行を探すために、表の全ての行を順番に確認します。この確認の仕方をフルスキャンと呼びます。

B-treeという木構造の索引なら、DBMSが読み書きする単位であるページに100件入ると仮定して、1億件の表でも4ページを読むだけで目的の行に届きます。ただし、索引を付けると書き込みは遅くなり、付けても効かない列もあります。

WHERE句で行を絞り込む書き方は、SQLの基本の記事で説明しています。索引は、3層スキーマでは記憶装置への置き方を決める内部スキーマに含まれます。

B-treeインデックスは、ソート済みの木構造

B-treeインデックスは、列の値をソートして持つ、バランスの取れた木構造のインデックスです。木の一番上のページをルートページ、一番下のページをリーフページと呼びます。

ルートページには、子ページへの分かれ道を示す区切りの値が入っています。探したい値をこの区切りと比べれば、確認する必要のない子ページを早い段階で除外できます。次の図で、9件の小さな例を使って、フルスキャンとB-treeの動きを比べます。

フルスキャン:先頭から1件ずつ、最後まで確認(50を探す)1012023034045056067078089095件目で見つかるが、ほかに50が無いか9件目まで確認するB-treeインデックス:ルートから該当する子ページだけをたどるルート:40 / 701ページ目:50と比較40未満40以上70未満70以上10 20 30読まない40 50 602ページ目:50が見つかる70 80 90読まないフルスキャン:先頭から1件ずつ、最後まで確認1102203304405506607708809905件目で見つかるが、ほかに50が無いか9件目まで確認するB-treeインデックス:ルートから該当する子ページだけをたどる1ページ目:50と比較ルート:40 / 7040未満10 20 30読まない40以上70未満40 50 602ページ目:50が見つかる70以上70 80 90読まない
同じ9件のデータから50を探す例です。フルスキャンは5件目で50を見つけたあとも、9件すべてを確認します。B-treeインデックスは、ルートページで40・70と比較して該当する子ページだけに進み、2ページを読んで見つけます。

フルスキャンは、5件目で50を見つけたあとも、ほかに50が無いかを確かめるため9件すべてを確認します。B-treeは、ルートページで40・70と比較して真ん中のリーフページだけに進み、2ページを読んで50を見つけます。左右2つのリーフページは読みません。

木をたどる様子を1段ずつ見る

次の図では、先ほどと同じ9件の例で、ルートからリーフまでをたどる手順を1段ずつ確認できます。

STEP 1 / 3
ルート:40 / 7050 を 40・70 と比較中10 20 3040未満40 50 6040以上70未満70 80 9070以上ここまでに読んだページ:1ページルート:40 / 7050 を 40・70 と比較中10 20 3040未満40 50 6040以上70未満70 80 9070以上ここまでに読んだページ:1ページ
ステップ1:ルートページで比較する
探す値は50です。ルートページには、区切りの値として40と70が入っています。50をこの2つの値と比べ、どの子ページに進むかをここで決めます。

読んだページは、ルートとリーフを合わせて2ページでした。残り2つのリーフページは、比較の対象にもなりません。

行数が増えるほど、フルスキャンとの差が広がる

DBMSは、表もインデックスもページという単位で読み書きします。下の図は、表のページにもインデックスのページにも1ページに100件が入ると仮定して、読むページの数を行数ごとに計算したものです。フルスキャンは表の全ページを読み、B-treeはルートからリーフまで1段に1ページずつ読みます。

前提:表のページにもインデックスのページにも、1ページに100件が入ると仮定します。フルスキャンは表の全ページを読みます。B-treeは、ルートからリーフまでのページを1段に1つずつ読みます。

フルスキャン
100 ページ
表の全ページ
vs
B-treeインデックス
2 ページ
木の高さ

10,000件では、B-treeが読むページはフルスキャンのおよそ50分の1です。

行数が1億件になっても、B-treeは4ページを読むだけで絞り込めます。フルスキャンは、行数に比例して読むページが増え、1億件では100万ページになります。1ページに入る件数は、行やキーの大きさで変わる仮定の値です。それでも、行数が増えても木の高さは緩やかにしか増えないという傾向は変わりません。

インデックスは、更新のたびに保守される

インデックスを作ると、検索は速くなりますが、書き込みは遅くなります。行を追加・更新・削除するたびに、DBMSは索引も合わせて更新する必要があるからです。

PostgreSQLの公式ドキュメントも、「インデックスが作成された後、システムでは、テーブルとインデックスとの間で常に同期を取っておく必要があります。これにより、データ操作の処理にオーバーヘッドが加わります」と説明しています。

MySQLのInnoDBは、表に行を挿入するとき、後の挿入や更新に備えてページの1/16を空けておこうとします。削除が進んでページの使用率が既定の50%を下回ると、隣のページとの併合を試みます。PostgreSQLの実装では、ページが一杯になるとそのページを2つに分割します。分割がルートページまで及ぶと、新しいルートページが作られ、木の高さが1段増えます。

書き込みのたびに、表の本体に加えて、作成したインデックスの数だけ索引の更新が増えます。そのため、PostgreSQLの公式ドキュメントは、めったに使われないインデックスは削除したほうがよいとしています。

インデックスが効く列・効きにくい列

WHERE句の条件に使う列や、JOINで表をつなぐ列には、インデックスがよく効きます。MySQLの公式ドキュメントも、インデックスの用途として、WHERE句に合う行を素早く見つけることと、JOINでほかの表から行を取り出すことを挙げています。文字列のLIKEでは、前方一致ならインデックスを使える場合がありますが、%で始まるパターンでは使えません。詳しくはLIKEの記事で説明しています。

条件で絞り込んだあとに残る行が少ない列ほど、効果が大きくなります。MySQLは、複数のインデックスから選べるとき、見つかる行が最も少なくなるインデックスを通常は使うと説明しています。

一方で、小さな表や、条件に合う行の割合が大きい列には、インデックスは向きません。PostgreSQLの公式ドキュメントは、10万行から1000行を選ぶ場合はインデックスの候補になると説明しています。しかし100行から1行を選ぶ場合は、その100行がおそらく1つのディスクページに収まるため、ページを順に読むほうが速いとしています。前の節の図で、100件ではどちらも1ページだったのと同じ状況です。

どの列に索引を付けるかは、データベース設計の段階のうち物理設計で決めます。

B-tree以外のインデックス

B-tree以外にも、ビットマップインデックス・ハッシュインデックス・クラスタ化インデックス・カバリングインデックス・転置インデックスといった方式があります。それぞれの仕組みの説明は省きます。

参考にした資料

練習問題(全5問)

Q1データベースのインデックスの役割として、最も適切な説明はどれ?
Q2B-treeインデックスで、ルートページは何をする?
Q3フルスキャンとB-treeインデックスの手数について、正しい説明はどれ?
Q4インデックスを作成すると、行の追加・更新・削除(INSERT・UPDATE・DELETE)に起こることは?
Q5インデックスが効きにくい列として適切なものはどれ?

選んでから「採点する」を押すと、解説つきで答え合わせができます。

関連記事

おすすめの本 PR

セキュリティとデータベースは、基本情報技術者試験の対策本でまとめて学べます。

目的別の本の選び方を見る

データベースの記事を続けて読む

データベース 全36本中34本目