MySQL パフォーマンスとtransactd その3
その3は、select * from tablename where fieldname in(a,b,c...)
です。
IN
句による検索ですが、前回までと同じようにMySQLの処理とtransactdでの処理を見ていきましょう。
使用するインデックス解析
MySQLはまずSQL文を解析し、fieldnameフィールドをキーセグメントの先頭に持つインデックスが存在するか調べます。存在すれば、そのインデックスを使用したオペレーションhandler::ha_index_read_map(HA_READ_KEY_EXACT)
を使い操作を組み立てます。無ければ、handler::ha_rnd_next()
かhandler::ha_index_next()
を使ったレコードスキャンをします。
どちらの方法であっても、a b cをfieldnameで示されたフィールドの型に従って昇順にソートした上で処理していきます。
MySQLでfieldnameフィールドのインデックスがある場合
handler::ha_index_read_map(HA_READ_KEY_EXACT)
でa b cのソート結果順に検索し結果を返します。インデックスを使っての検索なので、重複を許可しない1セグメントのキーの場合は、1つあたりの検索時間は一定で、条件の個数分で検索が終了します。重複を許可するもしくはマルチセグメントのキーの場合は、handler::ha_index_next_same
を使って同じ値のレコードをすべて返します。
もし、見つからない値があってもエラーにはならず、その値の結果が返らないだけです。
MySQLでfieldnameフィールドのインデックスが無い場合
インデックスがないと、目的のレコードがどこにあるか不明なので、handler::ha_rnd_next()
かhandler::ha_index_next()
を使って先頭からすべてのレコードを検索します。当然複数レコードが存在する場合もあるので、最後のレコードまで完全にスキャンします。100万レコードあれば100万行すべてをスキャンすることになります。
transactdでfieldnameフィールドのインデックスがある場合
transactdでは、table::setKeyNum(keyNumber)
でユニークなインデックスを指定し、table::setFilter("in a,b,c")
で、値を列挙します。フィールド名を指定しないのは、インデックスを指定しているため、そのインデックスのフィールド値を列挙することが前提となるからです。もし、setKeyNum()
でユニークなキーを指定しなかった場合は、setFilter()
でstat()
がSTATUS_FILTERSTRING_ERROR
を返します。すなわち、in句ではユニークなキーでしか検索できません。ですので、意図せず全レコードをスキャンしてしまうことはありません。
インデックスが無い場合でのinに相当する処理は"fieldname = a or fieldname = b or fieldname = c"
のフィルターを使えば実現できます。
また、複数セグメントでユニークなキーの場合は、セグメントの数だけ値を並べます。たとえば、2つのセグメントでキーを構成していて、3つのレコードを得たい場合は、"in a1,a2,b1,b2,c1,c2"
のように各レコード2つづつ値を指定します。
検索の開始はtable::find()
を呼び出します。サーバー側の処理は、クライアントから送られてきたキー番号をセットしhandler::ha_index_read_map(HA_READ_KEY_EXACT)
をキー値セットの数だけ繰り返し呼び出して結果を返します。
クライアントでは順次読み出しますが、次のレコードはfindNext()
で読み取れます。もし、サーバー側で2番目の値が見つからなかった場合は、findNext()
で2番目に来るとstat()
がSTATUS_NOT_FOUND_TI
を返し、見つからなかったことを示します。もう一度findNext()
を呼び出すと3番目の検索結果を得ることができます。SQLと違うのは、どの値が見つからなかったかを容易に知ることができる点です。また、結果の順は自動でソートされることはなく、in句で指定した順に返ります。
このような振る舞いは、in句を使ってJOINに相当する処理をするのにとても便利です。取得したがソートされていたり飛ばされていたりしていた場合、JOIN元と今取得した結果を結合する際に、どちらかをソートしたり検索するといった余計な処理が必要になります。transactdはテーブル単位での処理しかありませんので、このJOINに相当する処理がやりやすいかはとても重要なことです。
transactdでfieldnameフィールドのインデックスがない場合
これはこの前のインデックスがある場合で説明したとおり、in句では検索することはできません。代わりに"fieldname = a or fieldname = b or fieldname = c"
のようなフィルターで処理します。インデックスの無いフィールドの場合、どこにそのレコードがあるのかわからないので、全レコードフルスキャンするしかありません。フルスキャンするにしてもどれかインデックスを指定します。innodbの場合、プライマリーキーはレコードのデータも含んでいて他のキーより高速ですから、通常はそれを指定します。
まとめ
MySQLでin句で指定したfieldnameにインデックスがない場合は、全レコードスキャンします。将来も含めたレコード数を想定してパフォーマンスに問題がでないかどうかよく検討して使いましょう。ユニークなインデックスがあれば、高速に検索して結果が返ります。ただし、見つからない値があっても、どれが見つからなかったかは自分で確認が必要です。
transactdでは、fieldnameにユニークなインデックスがある場合しかin句は使用できません。検索結果はfindNext()
で値の列挙順にアクセスでき、見つからならなかったことも知ることができます。これによりJOINに相当する後処理がやりやすくなります。インデックスが無い場合は、"fieldname = a or fieldname = b or fieldname = c"
のフィルターを使います。この場合の、パフォーマンスはSQLと同様ですが、可能であれば検索の開始位置と、rejectCount
などで検索範囲をコントロールしてパフォーマンスを改善しましょう。