中年engineerの独り言 - crumbjp

LinuxとApacheの憂鬱

MongoDB で isExist

MongoDBでドキュメントがあるか無いか?を調べたい時

原文
Checking if a document exists -- MongoDB slow findOne vs find

参考になったので和訳した

One of the biggest optimisations to make MongoDB writes faster is to avoid moving documents by doing updates in place to a preallocated document. You only want to preallocate if the document doesn’t exist and to check this you need to do a query.

MongoDBにおいて最も大きな書き込み最適化の方法はアップデートによるドキュメントの移動を避ける事だ。
プリアロケートするにはドキュメントの有無をチェックするクエリーを投げる必要がある。

詳細はリンク先で解説されているのだが要約すると
MongoDB(に限らず大抵のストレージエンジン)は一旦作ったレコードが大きくなるのが嫌なのだ。

今の場所に収まらなくなると別の場所え移動しなければならずコストが高い。
MongoDBはフィールドが追加されたり、フィールドのタイプが変わる(1.0(float)->1(int)など)だけでも移動が起きうる。
なので新規レコードは同じ形をしたスケルトンとなるドキュメントを追加投入しなければならない。(=preallocate)
またアップデートする際には変更をかけるフィールドのみを指定する。
よって新規レコードと既存レコードのアップデートは完全に別処理にしなければならずレコード有無チェックするクエリーが必要になる。

#FF0000;">$setOnInsert:MongoDB2.4新機能。upsertと組み合わせて使いInsertの時だけ特定フィールドに値を設定する事ができる。
上記の理由でドキュメント有無チェックする必要がなくクエリーを2度打つ必要も無い為、完全にこちらを使うべき。
他にもisExistしたい場合はあるのでこの記事が無駄になった訳ではない

The findOne() method seems like the right choice for this because you can query the relevant index and check to see if a document or None is returned, ideally using a covered index.

レコード有無チェックにfindOne()を使うのは一見正しい選択に思える。インデックスも効くしNoneかドキュメントが返却される。理想的にはcovered indexが適用される。

#FF0000;">covered index:一般的なRDBMSでも採用されている手法。
covered index返却カラムが使用したインデックスに全て含まれていた場合、インデックスの値を直接返却する最適化。
インデックスからドキュメントを引いて値を取得する処理がスキップできる。

However, it is significantly faster to use find() + limit() because findOne() will always read + return the document if it exists. find() just returns a cursor (or not) and only reads the data if you iterate through the cursor.

So instead of:

1 db.collection.findOne({_id: "myId"}, {_id: 1})

you should use:

1 db.collection.find({_id: "myId"}, {_id: 1}).limit(1)

しかしながら、実際はfind()+limit()を使う方が遥かに早い。これはfindOne()はレコードを発見した場合read + returnの処理であるのに対しfind()はcursorだけを返却するからで、実際にデータを読み込むのはcursorをiterateしたタイミングである。

だから

1 db.collection.findOne({_id: "myId"}, {_id: 1})

の代わりに此方を使うべき。

1 db.collection.find({_id: "myId"}, {_id: 1}).limit(1)

By making this change I saw a change in performance of 2 orders of magnitude:

Query performance after switching from findOne() to find(). Can’t even see the response time of the find() call.
Same graph with the findOne series hidden. Less than 1ms response time.

Same graph with the findOne series hidden. Less than 1ms response time.

これにより2桁ものパフォーマンスの差が出た。

findOne()からfind()へ切り替えた後、find()のレスポンスタイムは1ms以下でグラフでは見る事が出来ない。