根本原因是未控制游标拉取行为:默认find()返回的cursor在全量消费时会一次性加载所有文档到内存并解码为python字典,导致内存激增;应使用batch_size()配合islice分块迭代或limit/sort分页。

Python 从 MongoDB 游标读取百万级文档时触发 MemoryError 或被系统 OOM Killer 杀掉,根本原因不是数据量本身太大,而是你**没控制游标的数据拉取行为**——默认的 find() 返回的 Cursor 对象在你调用 list()、next() 全量消费或转成 list(cursor) 时,会一次性把所有文档加载进内存并构建 Python 字典对象。
为什么 list(cursor) 和 cursor.next() 循环仍可能 OOM
很多人以为“我没用 cursor.fetchall(),只是 for doc in cursor”,但问题常出在隐式全量加载:
-
for doc in cursor本身是惰性的,但如果你在循环里不断append(doc)到一个列表,等于自己造了个全量缓存; -
cursor.next()每次只取一个,但如果游标未设置batch_size,MongoDB 驱动(PyMongo)默认一次从服务器拉取 101 个文档到本地缓冲区——遇到百万文档,缓冲区会反复填满、堆积,尤其当处理逻辑慢(如写文件、发 HTTP 请求),缓冲区滞留文档数飙升; - 每个 BSON 文档解码为 Python
dict后,内存开销通常是原始 BSON 的 1.5–2.5 倍(字符串对象、字典哈希表、引用计数等);100 万条平均 2KB 的文档,解码后轻松突破 3GB 内存。
必须用 cursor.batch_size() + 显式分块迭代
PyMongo 的游标本身不提供“按块返回列表”的接口,但你可以用 batch_size 控制每次网络往返获取的文档数,并配合 itertools.islice 实现可控分块:
- 创建游标时显式设
batch_size=1000:cursor = collection.find(...).batch_size(1000),这能减少单次网络 payload 和本地缓冲压力; - 不要用
for doc in cursor直接遍历,改用while+islice分批消费:from itertools import islice<br>while True:<br> batch = list(islice(cursor, 1000))<br> if not batch:<br> break<br> process_batch(batch)
- 注意:
islice(cursor, N)不会预取下一批,它只是从当前游标位置切 N 个——前提是游标尚未耗尽且未被提前关闭; - 避免在
process_batch()里做耗时 I/O(如同步 HTTP 调用),否则缓冲区积压加剧;真要并发处理,请用ThreadPoolExecutor提交 batch,而非在主线程里卡住游标。
更稳的选择:用 cursor.limit() + skip() 分页(仅限小规模或无高并发写场景)
虽然 skip() 在大数据集上性能差(需扫描跳过的文档),但它能彻底隔离每批内存,适合离线导出、后台任务等对延迟不敏感的场景:
- 用
limit固定每批数量,skip移动起始位置:collection.find().skip(0).limit(5000)、.skip(5000).limit(5000)…… - 必须配合排序(如
sort('_id')),否则分页结果可能因写入乱序而漏/重; - 跳过 99 万条再取 5000 条,MongoDB 仍要扫描前 99 万条 —— 所以只适用于总文档数
- 比
batch_size更重,但内存曲线绝对平直:每批最多占 5000 个 dict 的内存,不会累积。
真正决定 OOM 的,从来不是“有没有百万数据”,而是“你让多少数据同时活在内存里”。PyMongo 游标是懒的,但你的代码可以很贪——别让 list()、append() 或没节制的 for 循环毁掉这个惰性优势。最易忽略的一点:即使你用了分块,如果 process_batch() 返回了一个大对象并被变量持有,那内存也不会自动释放,得靠作用域退出或显式 del。
Python免费学习笔记(深入):立即使用
在学习笔记中,你将探索 Python 的核心概念和高级技巧!











