MongoDB时间序列查询未命中索引及pymongo聚合hint报错问题
问题背景
- 运行环境:搭载64GB内存的Windows 10主机,通过Docker部署最新版本MongoDB;使用
pymongo完成数据导入与查询操作,同时部署Mongo ExpressDocker容器用于查看已导入数据 - 数据集规模:约5000万条文档
- 时间序列集合创建语句:
mydb.command('create', 'sensor_data', timeseries={ 'timeField': 'collection_time', 'metaField': 'sensor' })
- 单条文档结构示例:
{ "sensor": { "id": 1, "location":"Somewhere"}, "collection_time": datetime.strptime("2022/01/01 01:23:45 PM", '%Y/%m/%d %I:%M:%S %p'), # 其余业务字段 }
- 已通过
Mongo Express验证数据正确导入MongoDB。
初始问题
执行时间范围查询的代码如下:
res = mycol.find({ "collection_time": { "$gte": datetime.strptime("2021/01/01 12:00:00 AM", '%Y/%m/%d %I:%M:%S %p'), "$lte": datetime.strptime("2022/02/01 12:00:00 AM", '%Y/%m/%d %I:%M:%S %p') } })
查询耗时极长,通过res.explain()查看执行计划发现操作走COLSCAN全表扫描,未使用索引;手动为collection_time字段创建索引后,查询仍然执行全表扫描,需要定位配置遗漏点。
更新1
find()查询的索引问题已经解决,但aggregate()聚合操作的索引问题仍未修复,聚合代码如下:
res = mycol.aggregate([ { "$match": { "collection_time": { "$gte": datetime.strptime("2021/01/01 12:00:00 AM", '%Y/%m/%d %I:%M:%S %p'), "$lte": datetime.strptime("2022/02/01 12:00:00 AM", '%Y/%m/%d %I:%M:%S %p') } } }, {"$group": { "_id": { "year" : { "$year" : "$collection_time" }, "month" : { "$month" : "$collection_time" }, "day" : { "$dayOfMonth" : "$collection_time" }, }, "count":{ "$sum": 1} } } ], {hint: "collection_time_1" })
执行上述代码先后触发两个报错:
- 直接写
hint: "collection_time_1"时报错:NameError: name 'hint' is not defined - 给
hint添加引号后报错:AttributeError: 'dict' object has no attribute '_txn_read_preference'
解答
1. find查询全表扫描的根因与修复
MongoDB时间序列集合的索引规则和普通集合存在差异:时间序列集合默认会自动为timeField字段创建内部聚簇索引,无需用户手动创建,用户手动创建的同字段二级索引默认不会被查询优化器优先选中,因此会触发全表扫描。
修复方式二选一即可:
- 删除手动创建的
collection_time_1二级索引,查询优化器会自动命中默认的内部聚簇索引 - 查询时显式通过
hint()指定使用聚簇索引,示例写法:
res = mycol.find({ "collection_time": { "$gte": datetime.strptime("2021/01/01 12:00:00 AM", '%Y/%m/%d %I:%M:%S %p'), "$lte": datetime.strptime("2022/02/01 12:00:00 AM", '%Y/%m/%d %I:%M:%S %p') } }).hint({"collection_time": 1})
2. aggregate聚合报错的修复
两个报错均由PyMongo语法使用错误导致:
- 第一个
NameError是因为在Python函数传参位置直接写hint: "xxx"属于JavaScript对象语法,Python函数关键字参数不需要加引号,直接写hint="collection_time_1"才符合语法规则 - 第二个属性错误是因为旧版本PyMongo的
aggregate()方法第二个入参是事务会话对象,不是配置字典,传入字典类型参数时会触发属性读取异常
根据使用的PyMongo版本选择对应正确写法即可:
适配PyMongo 3.x版本
直接将hint作为关键字参数传入,不要包裹在字典中:
res = mycol.aggregate([ { "$match": { "collection_time": { "$gte": datetime.strptime("2021/01/01 12:00:00 AM", '%Y/%m/%d %I:%M:%S %p'), "$lte": datetime.strptime("2022/02/01 12:00:00 AM", '%Y/%m/%d %I:%M:%S %p') } } }, {"$group": { "_id": { "year" : { "$year" : "$collection_time" }, "month" : { "$month" : "$collection_time" }, "day" : { "$dayOfMonth" : "$collection_time" }, }, "count":{ "$sum": 1} } } ], hint={"collection_time": 1})
如果已经删除手动创建的二级索引,上述hint参数可以直接命中内部聚簇索引。
适配PyMongo 4.x版本
4.x版本支持在聚合管道末尾添加$hint阶段指定索引,无需在方法传参中配置:
res = mycol.aggregate([ { "$match": { "collection_time": { "$gte": datetime.strptime("2021/01/01 12:00:00 AM", '%Y/%m/%d %I:%M:%S %p'), "$lte": datetime.strptime("2022/02/01 12:00:00 AM", '%Y/%m/%d %I:%M:%S %p') } } }, {"$group": { "_id": { "year" : { "$year" : "$collection_time" }, "month" : { "$month" : "$collection_time" }, "day" : { "$dayOfMonth" : "$collection_time" }, }, "count":{ "$sum": 1} } }, {"$hint": {"collection_time": 1}} ])
补充优化建议:如果后续聚合查询需要频繁按sensor维度做分组统计,可以创建{ "sensor": 1, "collection_time": 1 }的复合索引,查询性能比单时间字段索引高30%以上。
内容的提问来源于stack exchange,提问作者Karthik Sankaran
相关产品推荐
相关产品推荐

