MongoDBノート - $regex を使ったクエリでのインデックスの使われ方

昨日の記事で

Mongoidノート - validates_uniqueness_of で :case_sensitive => false を指定すると…… - Alone Like a Rhinoceros Horn


simple prefix queries (also called rooted regexps) like /^prefix/ のような場合を除きインデックスは使われない(使えない)ので、:case_sensitive => false を指定した場合、


validates_uniqueness_of は validation に際して、フィールドの値がユニークであることを保証するために、$regex を用いたクエリでコレクションの全ドキュメントをスキャンすることになる。

と書いた。

公式のドキュメント

によれば、simple prefix queries (also called rooted regexps) like /^prefix/ な場合はインデックスを使うと記載があるものの、そうでない場合については、クエリを最適化するともしないとも書かれていないので、実際に全ドキュメントをスキャンするのかどうか検証してみることにした。

もしかしたら、/^pattern$/i に対し、スキャンの範囲を [p, q) と [P, Q) に絞る、みたいな最適化をやっているかも知れない。

ということで、1002件のデータが登録された usersコレクションで実験してみる。( n はユーザー名)

MongoDB のバージョンは

% mongod --version                                                                                 
db version v2.0.0, pdfile version 4.5
Thu Dec 15 20:18:28 git version: 695c67dff0ffc361b8568a13366f027caa406222


まずは普通にインデックスを効かせたクエリ

> db.users.find( { n: 'mills_hector' } ).explain();
{
	"cursor" : "BtreeCursor n_1",
	"nscanned" : 1,
	"nscannedObjects" : 1,
	"n" : 1,
	"millis" : 0,
	"nYields" : 0,
	"nChunkSkips" : 0,
	"isMultiKey" : false,
	"indexOnly" : false,
	"indexBounds" : {
		"n" : [
			[
				"mills_hector",
				"mills_hector"
			]
		]
	}
}

インデックスがちゃんと使われていることが確認できる。

/^prefix/

次に、インデックスを使うとされている simple prefix queries (also called rooted regexps)

> db.users.find( { n: { $regex: /^mills_hector/ } } ).explain();
{
	"cursor" : "BtreeCursor n_1",
	"nscanned" : 1,
	"nscannedObjects" : 1,
	"n" : 1,
	"millis" : 0,
	"nYields" : 0,
	"nChunkSkips" : 0,
	"isMultiKey" : false,
	"indexOnly" : false,
	"indexBounds" : {
		"n" : [
			[
				"mills_hector",
				"mills_hectos"
			]
		]
	}
}

ちゃんとインデックスが効いている。indexBounds にも注目。

/^pattern$/i

今回検証したかった、/^pattern$/i

Mongoid で validates_uniqueness_of にて :case_sensitive => false を指定した場合、validation時にこれと同様のクエリが発行される。

> db.users.find( { n: { $regex: /^mills_hector$/i } } ).explain();
{
	"cursor" : "BtreeCursor n_1",
	"nscanned" : 1002,
	"nscannedObjects" : 1,
	"n" : 1,
	"millis" : 5,
	"nYields" : 0,
	"nChunkSkips" : 0,
	"isMultiKey" : false,
	"indexOnly" : false,
	"indexBounds" : {
		"n" : [
			[
				"",
				{
					
				}
			]
		]
	}
}

nscannedObjects は 1 だが、nscanned が 1002

Explain - MongoDB


nscanned

Number of items (documents or index entries) examined. Items might be objects or index keys. If a "covered index" is involved, nscanned may be higher than nscannedObjects.


nscannedObjects

Number of documents scanned.

とあることからもわかる通り、これはインデックスエントリの全件がスキャンされたことを示している。
ドキュメント全件分のスキャンが必要になるものの、対象フィールドに対してインデックスがはってあるため、スキャンはインデックス上のエントリに対して行われる。(なので、実際にスキャンされたドキュメントの数を示す nscannedObjects は 1 になっている)

インデックスを削除すると……
> db.users.dropIndex({n:1});
{ "nIndexesWas" : 2, "ok" : 1 }
> db.users.find( { n: { $regex: /^mills_hector$/i } } ).explain();
{
	"cursor" : "BasicCursor",
	"nscanned" : 1002,
	"nscannedObjects" : 1002,
	"n" : 1,
	"millis" : 3,
	"nYields" : 0,
	"nChunkSkips" : 0,
	"isMultiKey" : false,
	"indexOnly" : false,
	"indexBounds" : {
		
	}
}

cursor が BasicCursor となり、nscannedObjects も 1002 になった。これは全ドキュメントを実際にスキャンしたことを示している。

i を付けない場合は?

(インデックスを再度設定して続行)

> db.users.find( { n: { $regex: /^mills_hector$/ } } ).explain();
{
	"cursor" : "BtreeCursor n_1",
	"nscanned" : 1,
	"nscannedObjects" : 1,
	"n" : 1,
	"millis" : 0,
	"nYields" : 0,
	"nChunkSkips" : 0,
	"isMultiKey" : false,
	"indexOnly" : false,
	"indexBounds" : {
		"n" : [
			[
				"mills_hector",
				"mills_hectos"
			]
		]
	}
}

先頭にマッチする ^ があるのでこれは /^prefix/ と同じ。indexBounds に注目。

^ をなくすと?
> db.users.find( { n: { $regex: /mills_hector$/ } } ).explain();
{
	"cursor" : "BtreeCursor n_1",
	"nscanned" : 1002,
	"nscannedObjects" : 1,
	"n" : 1,
	"millis" : 5,
	"nYields" : 0,
	"nChunkSkips" : 0,
	"isMultiKey" : false,
	"indexOnly" : false,
	"indexBounds" : {
		"n" : [
			[
				"",
				{
					
				}
			]
		]
	}
}

nscanned が 1002
^ がなければ indexBoudns によるスキャン範囲の絞り込みができなくなるため、インデックスエントリの全件をスキャンしなければならなくなった。

まとめ
  • $regex を使ったクエリでは、/^prefix/ な場合を除き全件スキャン
    • インデックスあり → コレクションの全インデックスエントリをスキャン
    • インデックスなし → コレクションの全ドキュメントをスキャン
  • explain() 便利!