MongoDB の Partial Indexes を試してみた

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

こんにちは、菊池です。

MongoDB 3.2で追加された機能である、Partial Indexes(部分インデックス)を試してみました。

Partial Indexes

 

NOSQLであるMongoDBでも、クエリに対する応答性能を向上させるためには、いかにI/Oを減らすがが重要となります。RDBと同様に、インデックスを使ってコレクションの全走査をできるだけ発生させないのが有効な手段となります。しかし、むやみにインデックスを付与するとIndexが肥大化してメモリを圧迫し、結果的にDiskへのI/Oが増加するという問題もあります。

Ver. 3.2で追加された部分インデックスを使うことで、あらかじめクエリで絞り込む条件が明確な場合には、インデックスのサイズを抑えることが可能です。

インデックス作成時に条件を設定することで、インデックスの作成対象をコレクション内の全ドキュメントではなく、一部のドキュメントに限定することができます。

付与できる条件は以下の通りです。

  • イコール( key:value か $eq で指定)
  • $exists: true
  • $gt、$gte、$lt、$lte
  • $type
  • $and

やってみた

部分インデックスの有無、条件の一致/不一致による実行計画を確認してみました。

まずはテストデータを投入します。

db.users.insert({ userid:10000, name:"hoge1", age: 19 });
db.users.insert({ userid:20000, name:"hoge2", age: 33 });
db.users.insert({ userid:30000, name:"hoge3", age: 41 });
db.users.insert({ userid:40000, name:"hoge4", age: 24 });
db.users.insert({ userid:50000, name:"hoge5", age: 50 });
db.users.insert({ userid:60000, name:"hoge6", age: 16 });

この時点ではインデックスは生成されていません。これに対して、まずクエリの実行計画をみてみます。

> db.users.find({ userid:40000 }).explain()
{
       	"queryPlanner" : {
       		"plannerVersion" : 1,
       		"namespace" : "test.users",
       		"indexFilterSet" : false,
       		"parsedQuery" : {
       			"userid" : {
       				"$eq" : 40000
       			}
       		},
       		"winningPlan" : {
       			"stage" : "COLLSCAN",
       			"filter" : {
       				"userid" : {
       					"$eq" : 40000
       				}
       			},
       			"direction" : "forward"
       		},
       		"rejectedPlans" : [ ]
       	},
       	"serverInfo" : {
       		"host" : "ip-172-31-17-87",
       		"port" : 27017,
       		"version" : "3.4.2",
       		"gitVersion" : "3f76e40c105fc223b3e5aac3e20dcd026b83b38b"
       	},
       	"ok" : 1
}
>

"stage" : "COLLSCAN"はCollection Scanで全走査を意味します。

それでは部分インデックスを作成します。今回は、ageが30を超えるドキュメントに対して、useridにインデックスを作成します。

> db.users.createIndex( { userid: 1 }, { partialFilterExpression: { age: { $gt: 30 } } })
{
       	"createdCollectionAutomatically" : false,
       	"numIndexesBefore" : 1,
       	"numIndexesAfter" : 2,
       	"ok" : 1
}
>

作成されました。先ほどと同じクエリの実行計画をみてみます。

> db.users.find({ userid:40000 }).explain()
{
       	"queryPlanner" : {
       		"plannerVersion" : 1,
       		"namespace" : "test.users",
       		"indexFilterSet" : false,
       		"parsedQuery" : {
       			"userid" : {
       				"$eq" : 40000
       			}
       		},
       		"winningPlan" : {
       			"stage" : "COLLSCAN",
       			"filter" : {
       				"userid" : {
       					"$eq" : 40000
       				}
       			},
       			"direction" : "forward"
       		},
       		"rejectedPlans" : [ ]
       	},
       	"serverInfo" : {
       		"host" : "ip-172-31-17-87",
       		"port" : 27017,
       		"version" : "3.4.2",
       		"gitVersion" : "3f76e40c105fc223b3e5aac3e20dcd026b83b38b"
       	},
       	"ok" : 1
}
>

このクエリでは、検索対象が全ドキュメントのため、先ほどの部分インデックスは利用できません。では、部分インデックスの範囲に含まれる、ageが40を超えるドキュメントを絞り込む条件を加えた場合のクエリを見てみます。

> db.users.find({ userid:{ $gte: 30000 }, age: { $gt: 40 } }).explain()
{
       	"queryPlanner" : {
       		"plannerVersion" : 1,
       		"namespace" : "test.users",
       		"indexFilterSet" : false,
       		"parsedQuery" : {
       			"$and" : [
       				{
       					"age" : {
       						"$gt" : 40
       					}
       				},
       				{
       					"userid" : {
       						"$gte" : 30000
       					}
       				}
       			]
       		},
       		"winningPlan" : {
       			"stage" : "FETCH",
       			"filter" : {
       				"age" : {
       					"$gt" : 40
       				}
       			},
       			"inputStage" : {
       				"stage" : "IXSCAN",
       				"keyPattern" : {
       					"userid" : 1
       				},
       				"indexName" : "userid_1",
       				"isMultiKey" : false,
       				"multiKeyPaths" : {
       					"userid" : [ ]
       				},
       				"isUnique" : false,
       				"isSparse" : false,
       				"isPartial" : true,
       				"indexVersion" : 2,
       				"direction" : "forward",
       				"indexBounds" : {
       					"userid" : [
       						"[30000.0, inf.0]"
       					]
       				}
       			}
       		},
       		"rejectedPlans" : [ ]
       	},
       	"serverInfo" : {
       		"host" : "ip-172-31-17-87",
       		"port" : 27017,
       		"version" : "3.4.2",
       		"gitVersion" : "3f76e40c105fc223b3e5aac3e20dcd026b83b38b"
       	},
       	"ok" : 1
}
>

"stage" : "IXSCAN"となりました。これはインデックスを使った検索を実行することを意味します。同じように絞り込み条件を入れても、部分インデックスの範囲を超える場合には以下のようになります。

> db.users.find({ userid:{ $gte: 30000 }, age: { $lt: 40 } }).explain()
{
       	"queryPlanner" : {
       		"plannerVersion" : 1,
       		"namespace" : "test.users",
       		"indexFilterSet" : false,
       		"parsedQuery" : {
       			"$and" : [
       				{
       					"age" : {
       						"$lt" : 40
       					}
       				},
       				{
       					"userid" : {
       						"$gte" : 30000
       					}
       				}
       			]
       		},
       		"winningPlan" : {
       			"stage" : "COLLSCAN",
       			"filter" : {
       				"$and" : [
       					{
       						"age" : {
       							"$lt" : 40
       						}
       					},
       					{
       						"userid" : {
       							"$gte" : 30000
       						}
       					}
       				]
       			},
       			"direction" : "forward"
       		},
       		"rejectedPlans" : [ ]
       	},
       	"serverInfo" : {
       		"host" : "ip-172-31-17-87",
       		"port" : 27017,
       		"version" : "3.4.2",
       		"gitVersion" : "3f76e40c105fc223b3e5aac3e20dcd026b83b38b"
       	},
       	"ok" : 1
}
>

"stage" : "COLLSCAN"となりまずので、この場合は全走査となってしまいます。

まとめ

Ver. 3.2で追加された部分インデックスを紹介しました。

ワークロードからあらかじめクエリの絞り込み条件を指定できる場合には、インデックス量削減のために有効な機能かと思います。