震撼揭秘:线上MongoDB慢查询终极优化实战解析

背景

研发反馈指出,线上某个页面的响应速度异常缓慢,达到了16秒,严重影响了业务的正常运行。经过与研发的沟通得知,该页面调用的数据集合只会保留7天的数据,集合有6000万条记录。针对过期数据的处理,使用了根据 create_time 字段创建的过期索引,以自动使数据失效。此外,数据集合还通过 company_id 字段进行了哈希分片。

问题排查

慢语句分析

在后台拿到了慢查询语句,如下:

db.visitor.find({
"company_id": 13272,
"create_time": {
"$gte": ISODate("2024-04-11T00:00:00.000+0800"),
"$lte": ISODate("2024-04-11T23:59:59.000+0800")
}
});

db.visitor.find({
"company_id": 13272,
"create_time": {
"$gte": ISODate("2024-04-12T00:00:00.000+0800"),
"$lte": ISODate("2024-04-18T23:59:59.000+0800")
}
});

很简单的一个查询,语句上没有再优化的必要了,如果索引都在不应该出现这种十多秒的耗时,接下来开始分析索引。

索引分析

索引如下:
db.getCollection("visitor").createIndex({
"company_id": "hashed"
}, {
name: "company_id_hashed"
});
db.getCollection("visitor").createIndex({
"company_id": NumberInt("1")
}, {
name: "company_id_1"
});
db.getCollection("visitor").createIndex({
"create_time": NumberInt("1")
}, {
name: "create_time_1",
expireAfterSeconds: NumberInt("604800")
});
  • company_id_hashed:创建集合分片使用的hash索引
  • company_id_1:普通查询的索引
  • create_time_1:过期时间的索引

根据研发团队的反馈和对数据的分析,我们发现当前集合使用 company_id_hashed 索引进行分片存在问题。哈希索引对等值查询最为友好,但对于范围查询支持不佳。由于 company_id 是公司维度字段,相同数据较多,因此使用哈希分片并不合适。建议直接创建 company_id 和 create_time 的联合范围分片键。这样不仅能够友好地支持范围查询,还能更细粒度地拆分数据,提高查询和写入的效率。

针对当前情况就这点数据量,按理说会用到索引的,不应该执行耗时16s,接下来执行计划分析。

Explain执行计划

winningPlan

"inputStage": {
                            "stage": "FETCH",
                            "filter": {
                                "$and": [
                                    {
                                        "company_id": {
                                            "$eq": 13272
                                        }
                                    },
                                    {
                                        "create_time": {
                                            "$lte": ISODate("2024-04-17T15:59:59.000Z")
                                        }
                                    },
                                    {
                                        "create_time": {
                                            "$gte": ISODate("2024-04-10T16:00:00.000Z")
                                        }
                                    }
                                ]
                            },
                            "inputStage": {
                                "stage": "IXSCAN",
                                "keyPattern": {
                                    "company_id": "hashed"
                                },
                                "indexName": "company_id_hashed",
                                "isMultiKey": false,
                                "isUnique": false,
                                "isSparse": false,
                                "isPartial": false,
                                "indexVersion": NumberInt("2"),
                                "direction": "forward",
                                "indexBounds": {
                                    "company_id": [
                                        "[7977521071453068053, 7977521071453068053]"

这部分显示只用到了company_id_hashed索引,没有用到create_time_1索引。