游标本身不防溢出,关键在分批拉取+及时释放+避免重复消费;batch_size是网络与内存双控开关,需按场景设为1000~5000;禁用skip/limit分页,改用基于_id或单调字段的游标分页;聚合时allowdiskuse=true须作顶层参数。

游标本身不防溢出,关键在怎么用
游标(Cursor)只是延迟执行的句柄,不是内存保护机制。直接 list(cursor) 或 cursor.next() 无节制调用,照样把全部结果加载进内存——尤其当匹配文档达数十万、字段含大文本或嵌套对象时,Python 进程很容易 OOM。
真正起作用的是:分批拉取 + 及时释放 + 避免重复消费。以下操作必须同步做:
-
batch_size显式设为合理值(如1000),驱动才会按批请求,而非依赖默认(可能高达101或动态调整) - 遍历完一批后,不保留对已读游标的引用(比如别把它存进全局 list)
- 不要对同一个游标反复调用
list()、next()、__getitem__——它是一次性耗尽的,第二次调用会抛InvalidOperation: Cursor has been exhausted - 在分片集群中,额外加
cursor.allowDiskUse()没用;它只对聚合管道生效,对find()游标完全无效
batch_size 不是“建议值”,而是网络+内存双控开关
batch_size 控制每次从 mongod/mongos 拉取多少文档到本地内存。设太小(如 10)会导致频繁 getMore 请求,拖慢整体吞吐;设太大(如 100000)则单次拉取就占满内存,尤其配合 sort() 且没索引时,可能触发 Exceeded memory limit for $sort 报错。
推荐实践:
- 普通查询:设
batch_size=1000—— 平衡网络往返与内存驻留 - 带
sort()且确认走索引:可略提高至5000,但必须先用explain("executionStats")验证 stage 是IXSCAN,无SORT节点 - 导出/ETL 场景:配合
find_arrow_all(..., batch_size=10000)流式转 Arrow RecordBatch,避免 Python 对象层开销
skip() + limit() 分页在大数据量下等于自毁
用 collection.find().skip(100000).limit(100) 查第 1001 页?MongoDB 仍要扫描前 100000 条并丢弃,CPU 和内存成本线性增长。这不是游标问题,是查询设计缺陷。
替代方案只有两个真实可行的:
- 基于
_id或时间字段的“游标分页”:记录上一页最后一条的_id(如ObjectId("...")),下一页查{"_id": {"$gt": last_id}},再配limit(100)—— 完全跳过 skip 开销 - 业务主键强单调场景(如自增
seq_no):用{"seq_no": {"$gt": last_seq}},效果等同于游标分页
注意:所有分页方式都需确保排序字段有索引,否则 $gt 查询也会变全表扫。
聚合管道里游标行为更隐蔽,容易漏掉 allowDiskUse
collection.aggregate(pipeline) 返回的 CommandCursor 看似和 find() 游标一样,但它内部可能含 $sort、$group 等内存敏感阶段。此时仅设 batch_size 不够,必须传顶层选项 allowDiskUse=True,且不能写进 pipeline 数组里。
错误写法:collection.aggregate([{"$sort": {...}}, {"allowDiskUse": True}]) → 完全无效
正确写法:collection.aggregate(pipeline, allowDiskUse=True)(PyMongo)或 db.coll.aggregate(pipeline, { allowDiskUse: true })(mongosh)
特别提醒:MongoDB Atlas 免费层(M0)禁用磁盘落盘,allowDiskUse=True 在那里永远不生效,报错也只会提示内存超限,不会说“不支持”。











