<返回更多

线上MongoDB查询慢,如何通过索引优化直降响应时间?

2024-04-29  51CTO  
加入收藏

作者 | 吴守阳

审校 | 重楼

背景

线上某个页面的响应速度异常缓慢,达到了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")
});

其中:

ExplAIn执行计划

winningPlan

"stage": "SHARDING_FILTER",
 "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索引。

rejectedPlans

"stage": "SHARDING_FILTER",
 "inputStage": {
 "stage": "FETCH",
 "filter": {
 "company_id": {
 "$eq": 13272
 }
 },
 "inputStage": {
 "stage": "IXSCAN",
 "keyPattern": {
 "create_time": 1
 },
 "indexName": "create_time_1",
 "isMultiKey": false,
 "multiKeyPaths": {
 "create_time": [ ]
 },
 "isUnique": false,
 "isSparse": false,
 "isPartial": false,
 "indexVersion": NumberInt("2"),
 "direction": "forward",
 "indexBounds": {
 "create_time": [
 "[new Date(1712764800000), new Date(1713369599000)]"
 ]
 }
 }
 }
 },
 {
 "stage": "SHARDING_FILTER",
 "inputStage": {
 "stage": "FETCH",
 "filter": {
 "$and": [
 {
 "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": 1
 },
 "indexName": "company_id_1",
 "isMultiKey": false,
 "multiKeyPaths": {
 "company_id": [ ]
 },

这部分显示的是被拒绝的执行计划列表,不会用到company_id_1、create_time_1索引。

executionStats

"nReturned": NumberInt("229707"),
 "executionTimeMillis": NumberInt("15668"),
 "totalKeysExamined": NumberInt("238012"),
 "totalDocsExamined": NumberInt("238012"),
 "executionStages": {
 "stage": "SINGLE_SHARD",
 "nReturned": NumberInt("229707"),
 "executionTimeMillis": NumberInt("15668"),
 "totalKeysExamined": NumberInt("238012"),
 "totalDocsExamined": NumberInt("238012"),
 "totalChildMillis": NumberLong("15667"),
 "shards": [
 {
 "shardName": "d-m5eee03fdeaeaee4",
 "executionSuccess": true,
 "executionStages": {
 "stage": "SHARDING_FILTER",
 "nReturned": NumberInt("229707"),
 "executionTimeMillisEstimate": NumberInt("14996"),
 "works": NumberInt("238013"),
 "advanced": NumberInt("229707"),
 "needTime": NumberInt("8305"),
 "needYield": NumberInt("0"),
 "saveState": NumberInt("1980"),
 "restoreState": NumberInt("1980"),
 "iseoF": NumberInt("1"),
 "chunkSkips": NumberInt("0"),
 "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")
 }
 }
 ]
 },
 "nReturned": NumberInt("229707"),
 "executionTimeMillisEstimate": NumberInt("14595"),
 "works": NumberInt("238013"),
 "advanced": NumberInt("229707"),
 "needTime": NumberInt("8305"),
 "needYield": NumberInt("0"),
 "saveState": NumberInt("1980"),
 "restoreState": NumberInt("1980"),
 "isEOF": NumberInt("1"),
 "docsExamined": NumberInt("238012"),
 "alreadyHasObj": NumberInt("0"),
 "inputStage": {
 "stage": "IXSCAN",
 "nReturned": NumberInt("238012"),
 "executionTimeMillisEstimate": NumberInt("251"),
 "works": NumberInt("238013"),
 "advanced": NumberInt("238012"),
 "needTime": NumberInt("0"),
 "needYield": NumberInt("0"),
 "saveState": NumberInt("1980"),
 "restoreState": NumberInt("1980"),
 "isEOF": NumberInt("1"),
 "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]"
 ]
 },
 "keysExamined": NumberInt("238012"),
 "seeks": NumberInt("1"),
 "dupsTested": NumberInt("0"),
 "dupsDropped": NumberInt("0")

这部分显示的是查询的执行统计信息。

索引分析

通过explain的执行计划,可以看出索引的使用上存在问题。按理说company_id、create_time都已创建索引,为什么没有使用上?是什么原因导致它失效,没有用上create_time索引?

下面列举了失效的情况:

问题原因

造成执行耗时过长的主要原因是索引失效的问题,在涉及多个条件的查询中,MongoDB 会尝试使用覆盖索引(Covered Index)来尽可能地减少在磁盘上的文档检索。然而,在这种情况下,create_time_1 索引不能单独满足查询条件,因此 MongoDB 还需要查找和合并从 company_id_1 索引中过滤出来的文档。这种额外的查找和合并过程会增加查询的成本,并且降低性能。

优化方案

创建新的复合索引company_id_create_time,让其走company_id_hashed到company_id_create_time的链路。添加新的索引后,相同的语句执行时间只需要400ms,能满足业务的需求。

结论

要多关注索引在什么情况下会失效?复合索引的先后顺序,不是每个条件字段都建个单个普通索引,查询语句都会使用上,不要存在这种误区,有时候复合索引才是最完美的组合。

执行计划详解

1、queryPlanner:包含了MongoDB查询的执行计划。

作者介绍

吴守阳,51CTO社区编辑,拥有8年DBA工作经验,熟练管理MySQLredis、MongoDB等开源数据库。精通性能优化、备份恢复和高可用性架构设计。善于故障排除和自动化运维,保障系统稳定可靠。具备良好的团队合作和沟通能力,致力于为企业提供高效可靠的数据库解决方案。

关键词:MongoDB      点击(2)
声明:本站部分内容来自互联网,如有版权侵犯或其他问题请与我们联系,我们将立即删除或处理。
▍相关推荐
更多MongoDB相关>>>