MongoDBのスロークエリの最適化をするまでの流れ
前回New Relicを入れてデータを見ているとちょいちょい response timeが遅くなってることがわかりました。
やはりというかなんと言うか、 アプリのデータの永続化に使っているMongoDBで遅いクエリが発行されているようなので、これを調査して対応をするまで流れをメモしました。
ここではクエリと呼んでますが、mongo的には正確にはオペレーションと呼ぶのが正しいのかな?
スロークエリを調べる
コンソールからmongoシェルに入って以下を実行すればスロークエリのロギングが始まります。
db.setProfilingLevel(1,20)
これを実行すると20ms以上掛かってるクエリがsystem.profileというcollectionに保存されて、普通のcollectionと同じくfindで確認することができます。
system.profile.find();
setProfillingLevelの第一引数は出力する内容のレベルを指定するものです。 0がプロファイリングを行わない(ログを何も記録しない)、1がスロークエリの記録、2が全てのクエリの記録です。
第二引数はslowmsで、スロークエリとして扱う閾値をミリ秒で指定をします。
実際に使うときには、1か2で必要なだけ記録して、必要がなくなったら0を指定して元に戻す感じですね。
コンソールからさくっとプロファイリングして、実際のスロークエリをcollectionの一つとしてすぐに見えるようになるのはすばらしいですね。
setProfillingLevelに関する公式ドキュメントはこちら
で、スロークエリはわかったので、MySQLとかと同じくexplainをします。
db.colleciton.find({遅い条件}).explain();
結果の項目の見方は公式ドキュメントを参照のこと。
クエリの最適化
explainの結果、私の場合、インデックスが意図したものが使われていないことがわかりました。 MySQLのforce index的なことをやるには、hintを使います。
db.collection.find().hint({_id: 1});
descでensureIndexをしている場合は、1の代わりに-1を使います。 存在しないインデックスを指定するとエラーになります。
地味にわからなかったのが複合インデックスの場合の指定の方法です。 例えば、tags: 1, updatedAt: -1で複合インデックスを作っている場合、以下のように指定します。
db.collection.find().hint({tags: 1, updatedAt: -1});
インデックスを作ったときの順序と値で指定する必要があるので注意がいります。
自分が作ったインデックスを確認するときは以下です。
db.collection.getIndexes();
結果
というところで、意図したインデックスを使ってみたのですが、私の場合むしろhintを付けるよりも遅くなるという事態に。
原因としては、普通に大小比較とソートが別々のフィールドを指定しているというよくあるインデックスが効果的に使われないパターンでした。
面白いのは、絞り込みの条件にインデックスを使うよりも、ソートの方にインデックスが使われるようにした方が速度が向上したことでした。
データ件数的には40万件ぐらいだったのですが、条件によっては絞り込まれる数が多すぎてその後のソートに時間が掛かっていたようで、むしろソートで使うフィールドだけをhintでインデックスを使った方が早くなりました。外から見た挙動から察するに多分ソートをした後に絞り込みを行っているんじゃないかなあ。
ということで、一旦hintを使ってソートにのみインデックスを使うようにして、後日データ構造を見直して大小比較を条件に使わないように更新を行いました。
今回もそうですが、基本的にはインデックスはb-treeで実装されているので、MongoDBもMySQLも遅くなる原因が似通っていて、対応も似たり寄ったりなので、MySQLの知識が使えることが多い気がしますね。