cursor.stream()是唯一可行路径,因它返回原生Readable流,自动分批拉取并即时释放内存;而toArray()会将全部文档解包至V8堆,千万级数据必致内存溢出崩溃。

直接用 find().toArray() 或 aggregate().toArray() 导出千万级数据必然崩溃——这不是配置调优问题,是内存模型的硬边界。必须走游标流式通道,且每一步都得防住超时、断连、字段膨胀和错误静默。
为什么 cursor.stream() 是唯一可行路径
Node.js 驱动 v4.0+ 的 cursor.stream() 返回原生 Readable 流,底层自动分批拉取(默认 batchSize: 1000),每批处理完立即释放内存,不累积文档引用。而 toArray() 会强制等待游标耗尽,把全部 BSON 文档解包成 JS 对象塞进 V8 堆,导出 800 万用户时内存常突破 6GB,FATAL ERROR: Ineffective mark-compacts near heap limit 直接终止进程。
常见误判:limit(50000) 并不能缓解压力——只要游标没显式关闭,MongoDB 服务端仍维持快照和连接资源,其他查询响应延迟飙升。
- 必须禁用
noCursorTimeout: true,否则空闲 10 分钟游标自动销毁,流中途报Cursor not found - 聚合管道必须显式传
{ cursor: { batchSize: 1000 } },否则部分版本回退到旧式单次返回模式 -
$project要精简到最小必要字段,避免传输passwordHash、refreshToken等敏感或冗余字段
stream() 后如何安全写入 CSV 文件
流式导出不是“把流 pipe 到 fs.createWriteStream 就完事”。CSV 格式对特殊字符(换行、逗号、双引号)有严格转义要求,直接 JSON.stringify() 或字符串拼接会破坏格式,Excel 打开后错列甚至乱码。
推荐用 fast-csv 库的 writeToStream() 方法,它内置字段转义和缓冲控制:
const csv = require('fast-csv');
const stream = collection.find(filter, { noCursorTimeout: true }).stream();
const ws = fs.createWriteStream('users.csv');
stream
.pipe(csv.format({ headers: true }))
.pipe(ws);
注意点:
DMap GL地图JSAPI(dmapgl)开发指南。用于编写、审查、调试 DMap 地图 API 代码,涵盖地图初始化、覆盖物渲染、图层管理、事件处理、控件交互等任务。当用户提及 dmap、dmapgl、jsapi‑gl 或相关地图开发需求时自动触发。
- 别用
fs.writeFileSync接流——同步写法会阻塞事件循环,流速一快就卡死 - 如果字段含中文,确保
ws选项加{ encoding: 'utf8' },否则 Windows Excel 默认用 GBK 解析,显示为乱码 - 不要在
stream.on('data', ...)里手动 push 到数组再批量写——又回到内存累积的老路
如何防止流中断后重跑全量
千万级导出常需数小时,网络抖动、MongoDB 重启、磁盘满都会导致流中断。从头重跑成本太高,必须支持断点续传。
MongoDB 本身不提供游标位置保存,但可用 _id(ObjectId)做自然续传点:
- 每次写入 CSV 前,记录当前文档的
_id到临时文件(如last_id.txt) - 重跑时读取该值,用
{ _id: { $gt: ObjectId('...') } }作为新查询条件 - 注意:ObjectId 有序但非严格时间序,若业务允许少量重复,可加
{ createdAt: { $gte: ISODate(...) } }双重保险
别依赖 skip()——跳过前 500 万条时性能断崖式下跌,skip 仍要扫描所有前置文档。
聚合管道流式导出的陷阱
用 aggregate() 做去重、分组、关联再导出很常见,但容易踩两个坑:
-
$lookup大表关联时,若未加$unwind或$limit,中间结果可能超 16MB 单文档限制,报errmsg: "aggregation result exceeds maximum document size" -
$group后未用$sort,游标返回顺序不可控,导致断点续传失效(因_id不连续) - 聚合必须显式开启游标:
collection.aggregate(pipeline, { allowDiskUsage: true, cursor: { batchSize: 1000 } }),漏掉cursor选项会退化为一次性加载
最易被忽略的是:聚合游标不支持 noCursorTimeout: true 选项,必须靠 maxTimeMS 控制单次拉取上限,例如 { maxTimeMS: 300000 } 防止某一批卡死。

















