遅い理由はたいていインデックスだ
一言でいうと
インデックスがなければ、ドキュメントDBもすべてを走査します。COLLSCANと表示されます。RDBのSeq Scanとまったく同じもので、対策も同じです。
なぜ必要なのか: 遅い原因はたいてい1つ
「MongoDBが遅い」という報告の大半は、データベースではなくインデックスのないクエリが原因です。ドキュメントが1万件のときは誰も気づきません。100万件になると同じコードが急に遅くなり、そのとき人はデータベースを疑います。
確認する方法はRDBと同じです。実行計画を尋ねます。
db.items.find({ name: "빨간 배낭" }).explain()
実行計画の中で見る語は2つだけです。COLLSCANなら全部を走査したということで、IXSCANならインデックスを使ったということです。
数字も一緒に見る
ステージ名だけでは半分です。explain('executionStats')を使うと、実際に何件を走査したかがわかります。
| 値 | 意味 |
|---|---|
nReturned |
実際に返したドキュメント数 |
totalDocsExamined |
それを探すために開いたドキュメント数 |
この2つの比率が、インデックスの成績表です。1件を返すために100万件を開いたなら、インデックスがないか、間違った作り方になっています。逆に2つが近ければ、インデックスがうまく効いています。
複合インデックスは順序がすべて
インデックスを作ると決めたら、次の問いはどの順序で組み合わせるかです。ドキュメントDBの複合インデックスも値でソートされた構造なので、先頭のフィールドから使うことができ、そのルールが何を使えて何を使えないかを決めます。
{ status: 1, createdAt: -1 }で作ったインデックスは、次のように使われます。
statusだけで検索: 使えます。statusで検索し、createdAtでソート: 使えます。ソートが無料になります。createdAtだけで検索: 使えません。先頭が空いているからです。
そこから、実務での順序のルールが導かれます。等価で絞り込むフィールドを前に、範囲で絞り込むかソートに使うフィールドを後ろに置きます。範囲条件が掛かったフィールドより後ろのフィールドは、探索には使えず、絞り込みにだけ使われます。
ソートがインデックスを使えないと、静かにコストが高くなります。ドキュメントDBはメモリ内でソートしますが、その量に上限があり、超えるとエラーで失敗するか一時ファイルを使います。結果が少なくても、ソート前の候補が多いと引っかかる点が、特に人を驚かせます。実行計画にソートのステージが別に見えたら、その順序をインデックスに移せるかを確認します。
配列フィールドに張るインデックスは性質が違います。配列の要素ごとにエントリが1つずつ作られるため、ドキュメント1つがインデックスに何度も入ります。要素が数百個ある配列にインデックスを張ると、その分だけ大きくなり、書き込みも遅くなります。そして配列フィールドを2つ以上、1つのインデックスにまとめることはできません。組み合わせの数が掛け算で爆発するからです。
最後に、必要な値がすべてインデックスの中にあれば、ドキュメントをまったく開きません。先ほど見たtotalDocsExaminedが0になるのがこの場合で、一覧画面のように一部のフィールドだけを表示するクエリでは、特に価値が大きくなります。
現場では
インデックスを増やすことが常に答えではありません。インデックスは書き込みを遅くし、容量を消費します。ドキュメントを1件入れるたびに、すべてのインデックスが更新されます。
だから順序があります。まず実行計画を読んで何が遅いのかを確認し、そのうえで、そのクエリ1つのためのインデックスを作ります。「念のため」に作ったインデックスは、書き込みコストだけを残して、どのクエリも速くしない場合が多いのです。
この順序を守っているかどうかが、SQLの最適化経験を問う面接で分かれるところです。