作者 | 吴守阳
审校 | 重楼
线上某个页面的响应速度异常缓慢,达到了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")
});
其中:
"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索引。
"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索引。
"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工作经验,熟练管理MySQL、redis、MongoDB等开源数据库。精通性能优化、备份恢复和高可用性架构设计。善于故障排除和自动化运维,保障系统稳定可靠。具备良好的团队合作和沟通能力,致力于为企业提供高效可靠的数据库解决方案。