Rails
mongoid
mongo

Mongoのインデックスの改善についてまとめてみた

More than 3 years have passed since last update.

インデックスは闇雲に貼ればいいという物ではなく、既存のシステムにインデックスを作成するのであれば調査から行うのが定石ということで、まずはボトルネックの調査方法から紹介します。

撲滅スロークエリ

スロークエリ

MongoShell
# まずはMongoShellにログインします
$ mongo

# DBを選択します
use sample

# db.setProfilingLevel(level, slowms)
# level:1 スロークエリのみ出力
# level:2 すべて
# slowms 以下の例だと20ms以上かかっているクエリを出力する
db.setProfilingLevel(1,20)

# いくつかクエリを実行したあとに実行してみます
#  forEach(printjson) は見やすく整形するメソッド
db.system.profile.find().forEach(printjson)

system.profile.find()だと全て出力されるので新しいスロークエリ3件だけ
commandの要素を見るとqueryとあるので、この中がwhereに設定した条件
今回の例だとage: {"$gt": 0}なので、年齢 > 0という条件というのがわかる

MongoShell
> db.system.profile.find().sort({ts: -1}).limit(3).forEach(printjson)
{
    "op" : "command",
    "ns" : "user_model_development.$cmd",
    "command" : {
        "count" : "people",
        "query" : {
            "age" : {
                "$gt" : 0
            }
        }
    },
    "keyUpdates" : 0,
    "numYield" : 1,
    "lockStats" : {
        "timeLockedMicros" : {
            "r" : NumberLong(15597646),
            "w" : NumberLong(0)
        },
        "timeAcquiringMicros" : {
            "r" : NumberLong(82294),
            "w" : NumberLong(3)
        }
    },
    "responseLength" : 48,
    "millis" : 8104,
    "execStats" : {

    },
    "ts" : ISODate("2015-11-04T10:29:00.841Z"),
    "client" : "127.0.0.1",
    "allUsers" : [ ],
    "user" : ""
}

クエリを調査

クエリを調査するにはMySQLでもおなじみのexplainを使用して調査します。
先ほどのスロークエリをexplainで解析してみます。

MongoShell
> db.people.find({age: {"$gt": 0}}).explain()
{
    "cursor" : "BasicCursor",
    "isMultiKey" : false,
    "n" : 2624196,
    "nscannedObjects" : 2624196,
    "nscanned" : 2624196,
    "nscannedObjectsAllPlans" : 2624196,
    "nscannedAllPlans" : 2624196,
    "scanAndOrder" : false,
    "indexOnly" : false,
    "nYields" : 20509,
    "nChunkSkips" : 0,
    "millis" : 6404,
    "server" : "vagrant-centos64.vagrantup.com:27017",
    "filterSet" : false
}

ただRailsでMongoidを使用している人はこのクエリが一体どこから来たものなのかわからない場合があると思うので、
そういう時は直接Modelのfindやwhereに対してexplainをかけることも可能なので、それでも実行計画を見てもいいと思います。

RailsConsole
Person.where(age: {"$gt": 0}).explain

チェックする項目はcursorとnとnscannedObjectsとnscannedとmillis

  1. cursor
    cursorにBasicCursorと書いてあったらインデックスが使われていない
    BTreeCursor

  2. n
    実際にヒットしたドキュメントの件数

  3. nscannedObjects
    ドキュメント(RDBでいうところのレコード)を検索した件数

  4. nscanned
    インデックスを使用して検索した件数

  5. millis
    実行時間 短い方がうれしいね

インデックスを効率的に使用して検索できているか調査するので n / nscanned が1に近い方がいい。
次に nscannedObjects と nscanned に乖離がある場合はwhereに複数条件を指定している場合や、
ソートの指定を行っていて、インデックスでヒットしたドキュメントからBasicCursorを使ってmongoが
ドキュメントをチェックしていっていることがあるので、複合インデックスを貼ることを考えた方が良いかもしれない。
複合インデックスの張り方についてはMySQLと同じ考えなので、以前ボクが書いたMySQLの記事が役に立つと思います。
BTreeIndexのことにも触れているので参考にしてください。
http://qiita.com/kkyouhei/items/e3502ef632c48d94541d

インデックス

mongoで一般的に使用されるインデックスはMySQLでもお馴染みB-TreeIndexです。
インデックスを貼る作業はマシンにとってとてもコストのかかる処理なので、運用中のシステムに実行する場合は
バックグラウンドで実行するオプションをつけたり、アクセスが少ない時間帯にやるなどしたほうが懸命です。
バックグラウンドで実行と言っても更新などのロックがかからないというだけで、マシンにかかる負担は変わらないので、やっぱり気をつけましょう。

インデックス参照

MongoShell
db.people.getIndexes()

インデックス作成

MongoShell
db.people.ensureIndex({"field_name": 1})

# バックグラウンドで実行
db.people.ensureIndex({"field_name": 1}, { background: true})

インデックス削除

MongoShell
db.people.dropIndex({"field_name": 1})

# 全部消す
db.people.dropIndexes()