読者です 読者をやめる 読者になる 読者になる

BizStationブログ

ビズステーション株式会社の公式ブログです。

MySQL パフォーマンスとtransactd その2の1

MySQL Transactd

その2はselect * from tablename where fieldname = xxxです。長くなるのでまずは2の1から。

なんとも簡単なSQL文ですが、テーブルの定義やデータの状況によって全くパフォーマンスが異なってきます。

使用するインデックス解析

MySQLはまずSQL文を解析し、fieldnameフィールドをキーセグメントの先頭に持つインデックスが存在するか調べます。存在すれば、そのインデックスを使用したオペレーションhandler::ha_index_read_map(HA_READ_KEY_EXACT)を使い操作を組み立てます。無ければ、hanndler::ha_rnd_next()かhandler::ha_index_next()を使ったレコードスキャンをします。

実は、このインデックスの選択と、handler::ha_index_read_map()オペレーションが使えるかどうかが最も重要なのです。handler::ha_index_read_map()は、インデックスを使って一発で目的のレコードを取得します。そうでなければ、目的のレコードがテーブルのどこにあるのかわからないので、フルスキャンすることになります。

もし、MySQLオプティマイザがうまくインデックスを見つけられないとき(今回の例のような単純なものの場合はありえませんが)は、USE INDEX (index_list)構文を使ってどのインデックスを使うのか指定します。(どのインデックスが使われているかは、EXPLAINコマンドのkeyで確認できます。)

transactdでインデックスを指定する

transactdでは、インデックスを使ってテーブルアクセスする際には、必ずどのインデックスを使うかをプログラマtable::setKeyNum()で指定します。インデックスを使わないアクセスメソッドもありますが、それを使うということは「テーブルをスキャンすることをプログラマが選択している」ということになりますので、予想外にテーブルスキャンしてしまうことはありません。

そのため、当然ですが、インデックスがうまく選択されているかの心配や確認は必要なくなります。書いた通りです。

MySQLでfieldnameフィールドのインデックスがない場合

インデックスがないフィールドから目的の値をもつレコードを探すには、全レコードスキャンします。スキャンの範囲は、先頭レコードから最後のレコードです。

インデックスが付いていないと重複値の許可の有無もないので、1つ見つかってもやめることなく全レコードアタックして調べていきます。100万行もあったら、前回のcount(*)と同じように恐ろしい結果が待っています。レコード数が少ないうちは瞬時に結果が返りますが、多くなるとレコード数に比例して遅くなります。

SQL文でこれ以上遅い例はないでしょう。遅い理由のほとんどはこのようなフルレコードスキャンです。(MySQLのhandlerを使ったスキャンの操作はMySQL パフォーマンスとtransactd その1をご覧ください。)

Lmit句を追加すると、スキャンの範囲を少し狭くすることができるかも知れません。たとえば、Limit 1とすれば、該当する行が1個見つかったときに検索を中止できます。しかし、それが最後のレコードだった場合は、やはり全レコードスキャンなので、範囲を狭くすることができる「かも知れない」です。

transactdでfieldnameフィールドのインデックスがない場合(クライントフィルター)

transactdでは、インデックスを使うメソッドと使わないメソッドが分かれています。使わない場合はtable::stepFirst()で最初から始めて、table::stepNext()で次のレコードを順次取得し、条件に合った値を持つかどうかをクライント(プログラマ)が自分で調べます。(このようにクライントで値をチェックし、フィルタリングすることを、「クライアントフィルター」と呼んでいます。)

サーバー側では、クライアントのstepFirst()stepNext()の呼び出しごとにhandler::ha_rnd_next()が呼ばれ、先頭から順次カーソルを移動しながらレコードを返します。

インデックスがない場合、コーディングでは先頭から最後までのループを書かざるを得ません。そのため、コードからパフォーマンスを想像することが容易になります。また、重複があるかどうかを事前にプログラマが知っているなら、最初に対象レコードが見つかった時点でループをやめることもできます。さらに、検索レコード数が多くなリ過ぎたら、パフォーマンスを優先し検索を諦めるといった追加条件を加えてフルスキャンを防止することも容易です。ユーザーインタフェースでのキャンセルなどもその一例です。

このクライントフィルターはプログラムとしてはごく自然で解りやすいものです。しかしstepNext()の度にサーバーと通信するため、通信のオーバーヘッドが発生します。このオーバーヘッドはループが多くなればなるほど無視できないものなります。そこでSQLのように通信が少なくて済む方法が次に紹介する「サーバーフィルター」です。

transactdでfieldnameフィールドのインデックスがない場合(サーバーフィルター)

transactdではもう一つ、「サーバーフィルター」という検索方法があります。SQLと同様にサーバー側でフィルタリングするので、通信回数を大幅に削減できます。

実際の手順は、①まずtable::setFilter("fieldname = xxxx", rejectCount, maxRecords)のようにフィルターを指定して、②table::seekFirst()で検索開始レコードに移動し、③table::find()で検索を開始します。サーバー側では、handler::ha_index_next()を使って1レコードずつ、レコードが検索対象か調べます。

スキャンの開始位置はプログラマseekオペレーションで指定できます。今回の例は全レコードですので、seekFirst()で先頭に移動します。スキャンの終了はsetFilterrejectCountmaxRecordsで決まります。

maxRecordsSQLlimitとほぼ同じです。指定した数のレコードが見つかると検索を中止します。

面白いのはrejectCountです。マッチしなかったレコードがrejectCountに達すると検索を中止します。たとえば1000とすると、マッチしないレコードを合計1000レコードスキャンしたところで検索を中止します。もし、全レコードを最後まで検索したいのなら、rejectCountにゼロを指定します。

transactdのサーバーフィルターでは、プログラマが「検索の開始位置」と「1回の検索でのおおよそのスキャン数」をフィルター指定時に決めることができます。また、続きから検索を再開することもできますので、フルスキャンするにしても、定期的にユーザーのキャンセルを確認するといったことも可能です。

まとめ (select * from tablename where fieldname = xxx)

MySQLは、インデックスを使えるかどうかを最初に判断します。それによって、その後のスキャン操作が異なってきます。

インデックスがない場合は、全レコードをスキャンします。インデックスを用意したのなら、それが使われているかEXPLAINで確認しましょう。

インデックスが無いことを承知でそのフィールドだけのwhere文を組み立てるのなら、将来も含めたレコード数を想定して使いましょう。具体的には、数十レコード程度なら気にせずこのままでも良いでしょう。数百より多い場合は、ハードウェア(ディスク、メモリなど)や同時アクセス数とパフォーマンスの要求レベルに応じてインデックスの追加やスキャンするレコードを削減するための条件追加も検討しましょう。

transactdのインデックスを使わない検索

transactdを利用したアクセスでも、SQLと同様に全レコードアクセスするしかありません。

クライアントフィルターによるアクセスは、コード上でループを書くことになるので、コードからパフォーマンスを想像できます。また、クライアントサイドでフィルターするので、その他の条件によって自由に途中でやめることができます

さらに、通信回数を減らしてより高速にしたいときは、サーバーフィルターを使います。サーバーフィルターによるアクセスは、通信回数を大幅に少なくできます。また、検索の開始位置と、rejectCountmaxRecordsによって1回の検索でのスキャンレコード数を制限することで終了位置をコントロールできます。パフォーマンスは、プログラマが組み立てられます。