别用 list(collection.find()),改用 batch_size 迭代或 _id/时间戳游标分页;因 list() 会全量加载并转 dict,内存翻 2–3 倍致 oom;batch_size 控制单批数量,游标分页避免 skip 性能衰减,排序必须走索引。

直接结论:别用 list(collection.find()) 加载全部数据,改用带 batch_size 的游标迭代,配合 _id 范围分页或时间戳游标分页。
为什么 list(collection.find()) 会内存爆炸
它把整个结果集一次性拉进 Python 进程内存,不管数据是 10 万还是 5000 万条。MongoDB 返回的是 BSON 流,PyMongo 默认会缓存一批(通常是 101 条)在本地,但 list() 会强制遍历完全部并存成 Python 字典列表——每个文档转成 dict 后内存占用常翻 2–3 倍。你看到的 OOM(Out of Memory)往往就发生在这一步。
常见错误现象:
- 程序运行几分钟后被系统 kill(Linux 下常报
Killed,无 Python 异常) - 任务管理器/htop 显示 Python 进程内存飙升到 10GB+,CPU 却不高
- PyCharm 或 Jupyter 中卡死、无响应
用 batch_size 控制每次拉取量
这是最轻量、最通用的缓解方式。它不改变查询逻辑,只控制网络和内存批次大小。
实操建议:
- 显式设置
batch_size=1000(默认是 101,太小;设太大又可能单批超时) - 搭配
no_cursor_timeout=True防止游标过期(仅限临时脚本;生产环境优先用游标分页) - 避免在循环内做耗时操作(如 HTTP 请求、复杂计算),否则容易触发游标超时(10 分钟限制)
示例:
cursor = collection.find({"status": "done"}, no_cursor_timeout=True).batch_size(1000)
for doc in cursor:
process(doc) # 快速处理,别在这里 sleep() 或 requests.post()
cursor.close() # 显式关闭,释放服务器端游标
用 _id 或时间字段做游标分页(推荐用于千万级+)
比 skip/limit 稳定得多。skip 在大数据集上性能随页码线性下降,第 1000 页要跳过 999000 条,MongoDB 得扫描并丢弃它们。
使用场景:
- 全量导出、ETL 同步、离线分析
- 需要稳定吞吐、可中断续跑的任务
- 集合有递增
_id(ObjectId)或有单调时间字段(如created_at)
关键点:
- 首次查:取最小
_id或最早时间,然后循环查{"_id": {"$gt": last_id}} - 每次查完记录最后一条的
_id,作为下次查询起点 - 务必对
_id或时间字段建索引(collection.create_index("_id")通常已有)
示例(基于 _id):
last_id = None
while True:
query = {"status": "done"}
if last_id:
query["_id"] = {"$gt": last_id}
batch = list(collection.find(query).sort("_id").limit(5000))
if not batch:
break
for doc in batch:
process(doc)
last_id = batch[-1]["_id"]
遇到排序 + 大结果集时的内存陷阱
执行 .sort().limit() 时,如果没走索引,MongoDB 会在内存里排序整个中间结果集。默认内存上限 100MB(104857600 bytes),超了就报 QueryExceededMemoryLimitNoDiskUseAllowed。
容易踩的坑:
- 以为加了
limit就安全——错,sort是在limit之前执行的 - 在没索引的字段上
sort,哪怕只limit 100,也可能因全表扫描导致排序内存爆掉 -
allowDiskUse=True只对aggregate有效,对find().sort()无效
正确做法:
- 先确保排序字段有索引:
collection.create_index("updated_at", -1) - 若必须用非索引字段排序且数据大,改用
aggregate并显式传allowDiskUse=True
示例(安全替代):
pipeline = [
{"$match": {"status": "done"}},
{"$sort": {"updated_at": -1}},
{"$limit": 2000}
]
for doc in collection.aggregate(pipeline, allowDiskUse=True):
process(doc)
真正麻烦的不是数据多,而是“一边读一边等”——比如处理单条要 2 秒,又没控制批次,100 条就超 10 分钟游标时限。游标分页 + 批次处理 + 索引兜底,这三样缺一不可。
Python免费学习笔记(深入):立即使用
在学习笔记中,你将探索 Python 的核心概念和高级技巧!











