0. 概要
サーバーのパフォーマンスを向上させる方法は主に以下の5つがあります。
- クエリチューニングを行い、データベースクエリの処理時間を短縮する
- ネットワークやディスクのI/O回数を減らす
- メモリを活用する
- 効率的なアルゴリズムを適用する
- 効率的なアーキテクチャを適用する
このうち、4番と5番は経験豊富な開発者でないと実際に使うのは難しく、私自身も現場で適用した経験はまだありません。
それ以外の1~3番は日常の開発でよく適用している方法で、どのように活用しているか時間があるときに随時まとめようと思っています。
後輩や新入社員にぜひ伝えたい内容なので、サーバー周りを勉強し始めたばかりの方にも読んでいただけると役に立つと思います。
1. インデックスを使う
基本的なことですが、一番重要だと思うので最初のテーマに選びました。
インデックスとは、追加の書き込み(Write)処理と保存領域を活用し、データベースの読み取り(Read)速度を向上させる方法です。
例として、USER テーブルがあるとします。そして USER_NAME にインデックスを適用すると、次のようなテーブル構造が生成されます。
上の図を見ると、インデックステーブルにデータを格納する際、値がソートされた状態で保存されています。これは、インデックステーブルが高速な検索を可能にする B+Tree 構造を選んでいるためです。
今回の記事では「インデックステーブルは高速検索のためにデータをソートして保存している」くらいの認識でOKです。
本題に戻ると、MySQLのようなRDBMSでは主キー(PK)を使って実際のデータがどこに保存されているかを特定できます。そしてインデックステーブルは、特定のカラム値に対応するPKを識別する役割を果たします。
したがって、インデックスが適用されたカラムをWHERE句に使用すれば、テーブル全体をスキャンすることなく必要なデータがどこにあるか分かるため、ディスクI/O回数を大幅に削減できます。
その仕組みについては少し複雑なので、別にまとめておきました。
インデックスがディスクI/Oを削減する仕組み
ディスクに保存されているデータベースのテーブルから、WHERE
条件を使って特定のデータだけを取り出す場合、RDBMSはまずディスク上のデータをメモリに読み込み、そこから条件に合うデータをフィルタリングして返します。
-
インデックスがない場合
必要なデータの位置がわからないので、テーブルの全レコードをメモリに読み込んでから条件に合うデータを探すことになります。[ディスク→メモリ] への読み込みは一度に転送できる量が限られているので、全データを移すにはディスクアクセス回数が多くなります。
-
インデックスを使う場合
インデックステーブルを通じて、特定のカラム値を持つレコードのPKがどこにあるかを知ることができます。
たとえば、下のような USER_TABLE があり、
USER_NAME
が "Nami" のデータを検索するとします。図のように、インデックステーブルは "Nami" のレコードが PK = 1, 3, 5 であることを教えてくれます。
PKを使って実際のデータの保存場所を一意に特定できるので、データベースはテーブルの全データを読む必要なく、PKが 1, 3, 5 のレコードだけを [ディスク→メモリ] に読み込めば済みます。
要するに、必要なデータだけを読み込むので、大量のデータをすべて読み込むよりもディスクアクセス回数が劇的に少なくなり、パフォーマンスが向上します。
一般に「クエリチューニングを行う」というと、多くの場合、このディスクI/Oをいかに減らすかがメインになります。
インデックスは追加のストレージ領域を使うため、「勝手にどのテーブルにも全部つけよう」というわけにはいきませんが、インデックスの有無で数十倍ものパフォーマンス差が出ることも珍しくないので、事実上必須と言えるでしょう。
次に、インデックスを使用する際の注意点について見ていきましょう
1-1. インデックス使用時の注意点(1つ目)
前述のように、インデックスは追加のストレージスペースを使います。そのため、テーブルのすべてのカラムにインデックスをかけるのではなく、「検索条件に頻繁に使われるカラム」に限定して適用するのが望ましいです。
私の場合は、日付系のカラムやIDのようにWHERE句でよく使われるカラムにインデックスを付与することが多いです。
1-2. インデックス使用時の注意点(2つ目)
インデックスがあっても、実際には使用されないケースがあります。
RDBMSがクエリを実行する際、まずは オプティマイザ (Optimizer) という仕組みが最適な実行計画を立てます。ここでインデックスを使うかどうかも決定するのですが、大きく分けて次の3つの理由でインデックスが使われないことがあります。
A. WHERE句の計算(関数/演算)がカラムに直接かかっている場合
たとえば、USER_AGE
というカラムにインデックスがかかっていると仮定して、下記2つのクエリを見てください。
SELECT *
FROM USER
WHERE USER_AGE = 19
;
SELECT *
FROM USER
WHERE USER_AGE + 1 = 20
;
結果的にはどちらも「USER_AGE が 19 の行」を求めるクエリですが、1つ目のクエリはインデックスを正常に使えるのに対し、2つ目のクエリはインデックスを使用しません。
なぜなら、インデックステーブルには USER_AGE
の値しか保存されておらず、USER_AGE + 1
という値までは保持していないからです。よって2つ目のクエリの場合、テーブル全件を読み込んでから USER_AGE + 1
の計算をして一致する行をフィルタリングする必要があります。
このように、検索条件にカラム値を変化させる計算が含まれているとインデックスが使われなくなります。
B. 取得対象データが非常に多いと判断された場合
1から1000までの数字が書かれたレンガがあり、その中から必要なレンガだけを運ぶとします。
- 花子さんは数字を読むことができますが、一度に1個しか運べません。
- ユキさんは数字は読めませんが、一度に50個までまとめて運べます。
もし「数字が1と500のレンガを持ってきて」と言われたら、花子さんが1つずつ正確に運ぶほうが早いでしょう。
しかし「数字が1~500のレンガを全部持ってきて」と言われた場合は、ユキさんがまとめて運ぶほうが早いです。
データベースも同様で、「取得対象となる行が少ない」とOptimizerが判断すれば、必要な行だけ読み込むためにインデックスを使います。しかし「ほぼテーブルの半分以上の行が必要だ」など、大量のデータを取得するときは、まとめて一気にテーブルを読み込むフルスキャンのほうが速いと判断されるため、インデックスを使わない場合があります。
C. カラムのデータ分布が広くない場合
Bのケースに近い理由です。
例えば、USER_SEX
カラムにインデックスがあると仮定して、次のクエリを考えます。
SELECT *
FROM USER
WHERE USER_SEX = 'M'
一般的に男女比は 1:1 程度なので、このクエリを実行するとユーザデータの50%程度を取得することになるかもしれません。
このようにデータの分布が広くない場合、インデックスを使用しても多くのデータを取得する必要があるため、Optimizerは一度にデータを読み込む方式を選択することになります。
ここまで、インデックスの基本的な説明と「インデックスが効かない3つのケース」について触れました。開発者が思っているのと違う形でインデックスが使われないこともあるので、実際には EXPLAIN
コマンドなどでクエリの実行計画をチェックしてみるのがよいでしょう。
(EXPLAIN
に関しては別記事を参照してみてください)
次回の記事では、内容はシンプルですが、よくミスするネットワークI/Oを減らす方法について説明する予定です。
最後までお読みいただきありがとうございました。