MongoDB带索引仍查询缓慢问题排查(PyMongo 4.8.0)
PyMongo查询性能异常排查:索引已存在但查询耗时过长
环境与代码背景
使用PyMongo 4.8.0,网页爬虫完成商品抓取后,在管道中创建索引,相关代码如下:
utils.py
class DecimalCodec(TypeCodec): python_type = Decimal bson_type = Decimal128 def transform_python(self, value): return Decimal128(value) def transform_bson(self, value): return value.to_decimal() decimal_codec = DecimalCodec() type_registry = TypeRegistry([decimal_codec]) codec_options = CodecOptions(type_registry=type_registry) def get_mongo_client(): return MongoClient(settings.MONGODB_SERVER, settings.MONGODB_PORT) def get_mongo_db(): return get_mongo_client().get_database( settings.MONGODB_DB, codec_options=codec_options )
爬虫Pipeline代码
class BasePipeline: def open_spider(self, spider): self.product_collection = get_mongo_db()[settings.PRODUCT_COLLECTION_NAME] # ... 其他逻辑 self.product_collection.create_index([("import_run_id", -1)])
问题现象
- 确认索引已存在:
In [8]: list(get_mongo_db()[settings.PRODUCT_COLLECTION_NAME].list_indexes()) Out[8]: [ SON([('v', 2), ('key', SON([('_id', 1)])), ('name', '_id_')]), SON([('v', 2), ('key', SON([('import_run_id', -1)])), ('name', 'import_run_id_-1')]) ]
- 集合约含30万条数据,执行以下查询耗时56.496秒:
In [7]: t1 = time.time() ...: x = [p for p in get_mongo_db()[settings.PRODUCT_COLLECTION_NAME].find( ...: {"import_run_id": 245})]; t2 = time.time(); print(t2-t1)
疑问:是否是列表推导导致查询缓慢?
查询执行计划分析(explain结果)
提取.explain().executionStats的转换结果(RawBSON转JSON):
{ 'executionSuccess': true, // 查询执行成功 'nReturned': 5885, // 返回文档数 'executionTimeMillis': 61, // 数据库侧查询耗时(毫秒) 'totalKeysExamined': 5885, // 检查的索引键总数 'totalDocsExamined': 5885, // 检查的文档总数 'executionStages': { // 查询执行阶段详情 'stage': 'FETCH', // 文档获取阶段 'inputStage': { // 输入阶段 'stage': 'IXSCAN', // 索引扫描阶段 'keyPattern': { // 使用的索引模式 'import_run_id': -1 }, 'indexName': 'import_run_id_-1', // 索引名称 'isMultiKey': false, // 非多键索引 'multiKeyPaths': { // 多键路径为空 'import_run_id': [] }, 'isUnique': false, // 非唯一索引 'isSparse': false, // 非稀疏索引 'isPartial': false, // 非部分索引 'indexVersion': 2, // 索引版本 'direction': 'forward', // 扫描方向 'indexBounds': { // 索引扫描范围 'import_run_id': [245, 245] }, 'keysExamined': 5885, // 检查的索引键数量 'seeks': 1, // 索引查找次数 'dupsTested': 0, // 无重复键检测 'dupsDropped': 0 // 无重复键丢弃 } }, 'executionTimeMillisEstimate': 61, // 预估执行耗时 'works': 5885, // 总工作量 'advanced': 5885, // 推进的文档数 'needTime': 0, // 无需额外等待时间 'needYield': 0, // 无需让出资源 'saveState': 194, // 状态保存次数 'restoreState': 194, // 状态恢复次数 'isEOF': 1, // 已到结果末尾 'docsExamined': 5885, // 检查的文档数 'alreadyHasObj': 0, // 无缓存文档 'allPlansExecution': [] // 无其他执行计划 }
从执行计划可以看出:数据库层面的查询仅耗时61毫秒,说明慢的根源不在数据库查询本身。
结论与优化建议
- 列表推导不是核心问题,但它强制将所有查询结果加载到内存并转换为Python对象,这才是耗时主因:
- PyMongo的
find()返回游标,默认不会立即加载所有数据;列表推导会遍历游标,把BSON文档转换为Python字典,加上自定义DecimalCodec对Decimal128的转换,会产生额外开销 - 网络传输(如果MongoDB不在本地)、序列化/反序列化5885条文档的过程,是耗时的主要来源
- PyMongo的
- 优化方向:
- 只查询需要的字段:通过
find的第二个参数指定返回字段,减少数据传输量,例如:find({"import_run_id": 245}, {"_id": 1, "title": 1, "price": 1}) - 分批处理数据:避免一次性加载全量数据,使用游标分批遍历,例如:
cursor = get_mongo_db()[settings.PRODUCT_COLLECTION_NAME].find({"import_run_id": 245}).batch_size(1000) for doc in cursor: # 处理单条文档 pass - 检查网络延迟:如果MongoDB部署在远程服务器,优先排查网络链路的耗时
- 优化Codec性能:评估自定义
DecimalCodec的转换逻辑是否有优化空间,比如批量转换或简化处理
- 只查询需要的字段:通过
内容的提问来源于stack exchange,提问作者Džiugas Bižokas
相关产品推荐
相关产品推荐

