必须用.explain("executionStats")而非queryPlanner,因后者仅显示计划,前者才提供真实扫描数、索引使用数和执行时间等关键指标;复合索引字段顺序错误、聚合管道$match位置不当、游标skip分页等均会导致性能骤降。

为什么 find() 越查越慢?先看执行计划
不加 explain("executionStats") 就调优,等于蒙眼开车。PyMongo 的 find() 或 aggregate() 返回游标,但真正耗时的是底层扫描行为——你得知道它到底扫了几个文档、用了哪个索引、有没有回表。
- 必须用
.explain("executionStats"),不是"queryPlanner":后者只告诉你“打算怎么查”,前者才给出真实扫描数(totalDocsExamined)、索引使用数(totalKeysExamined)和耗时(executionTimeMillis) - 关键指标阈值参考:
totalDocsExamined≈nReturned是理想状态(全走索引);如果前者是后者的 10 倍以上,说明大量无效扫描 - 常见陷阱:在测试环境跑
explain,但生产数据分布不同(比如某字段取值倾斜),结果失真;务必在生产副本或同分布数据集上验证
复合索引字段顺序错了,性能直接掉一档
写 create_index([("status", 1), ("created_at", -1)]) 很容易,但如果你实际查询是 {"created_at": {"$gte": ...}, "status": "done"},这个索引大概率被忽略——MongoDB 只能利用索引前缀匹配。
- 规则很简单:把等值查询字段放前面,范围查询字段放后面。例如查
{"type": "order", "paid_at": {"$gt": t1}},索引应为[("type", 1), ("paid_at", 1)] - 排序字段也要纳入索引:如果还带
.sort([("amount", -1)]),索引得扩展成[("type", 1), ("paid_at", 1), ("amount", -1)],否则会额外排序(stage: "SORT"出现在 explain 里就是警告) - 别迷信“越多越好”:一个集合上超过 5 个索引,写入压力和内存占用会明显上升;优先覆盖高频查询,用
db.collection.getIndexes()定期清理未使用的索引
聚合管道里 $match 放错位置,等于白建索引
聚合不是 SQL,阶段顺序直接影响索引能否生效。aggregate([{"$sort": ...}, {"$match": ...}]) 这种写法会让 MongoDB 先全量排序再过滤,索引完全失效。
-
$match和$limit必须尽可能往前放:越早筛出数据,后续阶段处理量越小。理想结构是[{"$match": {...}}, {"$sort": ...}, {"$project": ...}] - 注意隐式全量操作:比如
$lookup后没跟$match,会导致关联结果全部加载进内存;大集合关联时,务必在$lookup的pipeline参数里嵌套$match - 避免在聚合里算复杂表达式:像
{"$add": ["$a", "$b"]}这类操作无法走索引;如果用于过滤,提前在应用层计算好值,改用等值$match
游标遍历大数据集,skip() 是性能杀手
百万级数据用 .skip(99000).limit(100) 做分页?MongoDB 得先跳过前 9.9 万条——每条都得读、判断、丢弃,CPU 和 I/O 双爆。
立即学习“Python免费学习笔记(深入)”;
- 替代方案只有两种:游标分页(推荐):用上一页最后一条的
_id或时间戳作为下一页查询条件,例如{"created_at": {"$lt": last_time}, "status": "done"};预生成页码映射:对稳定数据集,用后台任务提前算好各页起始_id存 Redis - 别依赖
estimated_document_count()做分页总页数:它只是集合元数据估算,不准;真要总数,且数据变动不频繁,用count_documents({})并加缓存 - 批量拉取时控制游标生命周期:
list(collection.find(...).limit(10000))会一次性把 1 万条全 load 到内存;改成迭代器 + 批处理,例如每次next()100 条,处理完再取下一批
explain 看真实数据、用生产流量压测、盯着 totalDocsExamined 调。



















