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。

非常感谢!