MongoDB 做聚合的时候好像选错了索引
MongoDB seems to choose the wrong index when doing aggregate
在 Amazon EC2(3.14.33-26.47.amzn1.x86_64, t2.medium: 2 vcpus, 4G内存)。
和一个集合"access_log"(大约 40,000,000 条记录,每天 1,000,000 条),以及上面的一些索引:
...
db.access_log.ensureIndex({ visit_dt: 1, 'username': 1 })
db.access_log.ensureIndex({ visit_dt: 1, 'file': 1 })
...
在执行以下 "aggregate" 时,它非常慢(需要几个小时):
db.access_log.aggregate([
{ "$match": { "visit_dt": { "$gte": ISODate('2015-03-09'), "$lt": ISODate('2015-03-11') } } },
{ "$project": { "file": 1, "_id": 0 } },
{ "$group": { "_id": "$file", "count": { "$sum": 1 } } },
{ "$sort": { "count": -1 } }
])
此聚合需要的所有字段都包含在第二个索引中({ visit_dt: 1, 'file': 1 }, 即"visit_dt_1_file_1").
所以我很困惑为什么mongodb不使用这个索引,而是另一个。
在解释计划时,总是得到以下信息,我根本看不懂。
你能帮忙吗?非常感谢!
> db.access_log.aggregate([
... { "$match": { "visit_dt": { "$gte": ISODate('2015-03-09'), "$lt": ISODate('2015-03-11') } } },
... { "$project": { "file": 1, "_id": 0 } },
... { "$group": { "_id": "$file", "count": { "$sum": 1 } } },
... { "$sort": { "count": -1 } }
... ], { explain: true } );
{
"stages" : [
{
"$cursor" : {
"query" : {
"visit_dt" : {
"$gte" : ISODate("2015-03-09T00:00:00Z"),
"$lt" : ISODate("2015-03-11T00:00:00Z")
}
},
"fields" : {
"file" : 1,
"_id" : 0
},
"queryPlanner" : {
"plannerVersion" : 1,
"namespace" : "xxxx.access_log",
"indexFilterSet" : false,
"parsedQuery" : {
"$and" : [
{
"visit_dt" : {
"$lt" : ISODate("2015-03-11T00:00:00Z")
}
},
{
"visit_dt" : {
"$gte" : ISODate("2015-03-09T00:00:00Z")
}
}
]
},
"winningPlan" : {
"stage" : "FETCH",
"inputStage" : {
"stage" : "IXSCAN",
"keyPattern" : {
"visit_dt" : 1,
"username" : 1
},
"indexName" : "visit_dt_1_username_1",
"isMultiKey" : false,
"direction" : "forward",
"indexBounds" : {
"visit_dt" : [
"[new Date(1425859200000), new Date(1426032000000))"
],
"username" : [
"[MinKey, MaxKey]"
]
}
}
},
"rejectedPlans" : [
...
{
"stage" : "FETCH",
"inputStage" : {
"stage" : "IXSCAN",
"keyPattern" : {
"visit_dt" : 1,
"file" : 1
},
"indexName" : "visit_dt_1_file_1",
"isMultiKey" : false,
"direction" : "forward",
"indexBounds" : {
"visit_dt" : [
"[new Date(1425859200000), new Date(1426032000000))"
],
"file" : [
"[MinKey, MaxKey]"
]
}
}
},
...
]
}
}
},
{
"$project" : {
"_id" : false,
"file" : true
}
},
{
"$group" : {
"_id" : "$file",
"count" : {
"$sum" : {
"$const" : 1
}
}
}
},
{
"$sort" : {
"sortKey" : {
"count" : -1
}
}
}
],
"ok" : 1
}
你可以找到一些资料here
If the query planner selects an index, the explain result includes a
IXSCAN stage. The stage includes information such as the index key
pattern, direction of traversal, and index bounds.
您的 explain()
输出表明发生了 IXSCAN,因此看来您的索引正在按预期工作。
尝试 运行 聚合命令而不进行排序或分组,您很可能会看到更好的结果 - 如果是这样,您可以将问题缩小到这些操作之一。
如果不是这种情况,您还应该在 运行 此查询时尝试监视系统内存。最有可能发生的是 Mongo 无法在内存中保留 40,000,000 条记录的索引,因此它正在从磁盘交换索引数据(非常慢)而 运行查询。
您可能想阅读 the docs regarding $sort
performance:
$sort operator can take advantage of an index when placed at the beginning of the pipeline or placed before the $project, $unwind, and $group aggregation operators. If $project, $unwind, or $group occur prior to the $sort operation, $sort cannot use any indexes.
此外,请记住它被称为 'aggregation pipeline' 是有原因的。匹配后排序到哪里并不重要。所以解决方案应该很简单:
db.access_log.aggregate([
{
"$match": {
"visit_dt": {
"$gte": ISODate('2015-03-09'),
"$lt": ISODate('2015-03-11')
},
"file": {"$exists": true }
}
},
{ "$sort": { "file": 1 } },
{ "$project": { "file": 1, "_id": 0 } },
{ "$group": { "_id": "$file", "count": { "$sum": 1 } } },
{ "$sort": { "count": -1 } }
])
当保证文件字段存在于每条记录中时,检查文件字段是否存在可能是不必要的。这并没有什么坏处,因为该字段上有一个索引。附加排序也是如此:因为我们确保只有包含文件字段的文档进入管道,所以应该使用索引。
感谢@Markus W Mahlberg。
我将查询更改如下:
db.access_log.aggregate([
{
"$match": {
"visit_dt": {
"$gte": ISODate('2015-03-09'),
"$lt": ISODate('2015-03-11')
},
}
},
{ "$sort": { "visit_dt": 1, "file": 1 } },
{ "$project": { "file": 1, "_id": 0 } },
{ "$group": { "_id": "$file", "count": { "$sum": 1 } } },
{ "$sort": { "count": -1 } }
], { explain: true })
然后得到正确的执行计划:
...
"winningPlan" : {
"stage" : "FETCH",
"inputStage" : {
"stage" : "IXSCAN",
"keyPattern" : {
"visit_dt" : 1,
"file" : 1
},
"indexName" : "visit_dt_1_file_1",
"isMultiKey" : false,
"direction" : "forward",
"indexBounds" : {
"visit_dt" : [
"[new Date(1425859200000), new Date(1426032000000))"
],
"file" : [
"[MinKey, MaxKey]"
]
}
}
},
"rejectedPlans" : [ ]
...
虽然还是有点慢,但我想这只是因为我的CPU、Mem、Disks。
非常感谢!
在 Amazon EC2(3.14.33-26.47.amzn1.x86_64, t2.medium: 2 vcpus, 4G内存)。
和一个集合"access_log"(大约 40,000,000 条记录,每天 1,000,000 条),以及上面的一些索引:
...
db.access_log.ensureIndex({ visit_dt: 1, 'username': 1 })
db.access_log.ensureIndex({ visit_dt: 1, 'file': 1 })
...
在执行以下 "aggregate" 时,它非常慢(需要几个小时):
db.access_log.aggregate([
{ "$match": { "visit_dt": { "$gte": ISODate('2015-03-09'), "$lt": ISODate('2015-03-11') } } },
{ "$project": { "file": 1, "_id": 0 } },
{ "$group": { "_id": "$file", "count": { "$sum": 1 } } },
{ "$sort": { "count": -1 } }
])
此聚合需要的所有字段都包含在第二个索引中({ visit_dt: 1, 'file': 1 }, 即"visit_dt_1_file_1").
所以我很困惑为什么mongodb不使用这个索引,而是另一个。
在解释计划时,总是得到以下信息,我根本看不懂。
你能帮忙吗?非常感谢!
> db.access_log.aggregate([
... { "$match": { "visit_dt": { "$gte": ISODate('2015-03-09'), "$lt": ISODate('2015-03-11') } } },
... { "$project": { "file": 1, "_id": 0 } },
... { "$group": { "_id": "$file", "count": { "$sum": 1 } } },
... { "$sort": { "count": -1 } }
... ], { explain: true } );
{
"stages" : [
{
"$cursor" : {
"query" : {
"visit_dt" : {
"$gte" : ISODate("2015-03-09T00:00:00Z"),
"$lt" : ISODate("2015-03-11T00:00:00Z")
}
},
"fields" : {
"file" : 1,
"_id" : 0
},
"queryPlanner" : {
"plannerVersion" : 1,
"namespace" : "xxxx.access_log",
"indexFilterSet" : false,
"parsedQuery" : {
"$and" : [
{
"visit_dt" : {
"$lt" : ISODate("2015-03-11T00:00:00Z")
}
},
{
"visit_dt" : {
"$gte" : ISODate("2015-03-09T00:00:00Z")
}
}
]
},
"winningPlan" : {
"stage" : "FETCH",
"inputStage" : {
"stage" : "IXSCAN",
"keyPattern" : {
"visit_dt" : 1,
"username" : 1
},
"indexName" : "visit_dt_1_username_1",
"isMultiKey" : false,
"direction" : "forward",
"indexBounds" : {
"visit_dt" : [
"[new Date(1425859200000), new Date(1426032000000))"
],
"username" : [
"[MinKey, MaxKey]"
]
}
}
},
"rejectedPlans" : [
...
{
"stage" : "FETCH",
"inputStage" : {
"stage" : "IXSCAN",
"keyPattern" : {
"visit_dt" : 1,
"file" : 1
},
"indexName" : "visit_dt_1_file_1",
"isMultiKey" : false,
"direction" : "forward",
"indexBounds" : {
"visit_dt" : [
"[new Date(1425859200000), new Date(1426032000000))"
],
"file" : [
"[MinKey, MaxKey]"
]
}
}
},
...
]
}
}
},
{
"$project" : {
"_id" : false,
"file" : true
}
},
{
"$group" : {
"_id" : "$file",
"count" : {
"$sum" : {
"$const" : 1
}
}
}
},
{
"$sort" : {
"sortKey" : {
"count" : -1
}
}
}
],
"ok" : 1
}
你可以找到一些资料here
If the query planner selects an index, the explain result includes a IXSCAN stage. The stage includes information such as the index key pattern, direction of traversal, and index bounds.
您的 explain()
输出表明发生了 IXSCAN,因此看来您的索引正在按预期工作。
尝试 运行 聚合命令而不进行排序或分组,您很可能会看到更好的结果 - 如果是这样,您可以将问题缩小到这些操作之一。
如果不是这种情况,您还应该在 运行 此查询时尝试监视系统内存。最有可能发生的是 Mongo 无法在内存中保留 40,000,000 条记录的索引,因此它正在从磁盘交换索引数据(非常慢)而 运行查询。
您可能想阅读 the docs regarding $sort
performance:
$sort operator can take advantage of an index when placed at the beginning of the pipeline or placed before the $project, $unwind, and $group aggregation operators. If $project, $unwind, or $group occur prior to the $sort operation, $sort cannot use any indexes.
此外,请记住它被称为 'aggregation pipeline' 是有原因的。匹配后排序到哪里并不重要。所以解决方案应该很简单:
db.access_log.aggregate([
{
"$match": {
"visit_dt": {
"$gte": ISODate('2015-03-09'),
"$lt": ISODate('2015-03-11')
},
"file": {"$exists": true }
}
},
{ "$sort": { "file": 1 } },
{ "$project": { "file": 1, "_id": 0 } },
{ "$group": { "_id": "$file", "count": { "$sum": 1 } } },
{ "$sort": { "count": -1 } }
])
当保证文件字段存在于每条记录中时,检查文件字段是否存在可能是不必要的。这并没有什么坏处,因为该字段上有一个索引。附加排序也是如此:因为我们确保只有包含文件字段的文档进入管道,所以应该使用索引。
感谢@Markus W Mahlberg。
我将查询更改如下:
db.access_log.aggregate([
{
"$match": {
"visit_dt": {
"$gte": ISODate('2015-03-09'),
"$lt": ISODate('2015-03-11')
},
}
},
{ "$sort": { "visit_dt": 1, "file": 1 } },
{ "$project": { "file": 1, "_id": 0 } },
{ "$group": { "_id": "$file", "count": { "$sum": 1 } } },
{ "$sort": { "count": -1 } }
], { explain: true })
然后得到正确的执行计划:
...
"winningPlan" : {
"stage" : "FETCH",
"inputStage" : {
"stage" : "IXSCAN",
"keyPattern" : {
"visit_dt" : 1,
"file" : 1
},
"indexName" : "visit_dt_1_file_1",
"isMultiKey" : false,
"direction" : "forward",
"indexBounds" : {
"visit_dt" : [
"[new Date(1425859200000), new Date(1426032000000))"
],
"file" : [
"[MinKey, MaxKey]"
]
}
}
},
"rejectedPlans" : [ ]
...
虽然还是有点慢,但我想这只是因为我的CPU、Mem、Disks。
非常感谢!