
本文介绍通过流式读取数据库 + 流式写入 excel 的方式,避免将百万行数据一次性加载至内存,彻底解决 node.js 导出大数据 excel 时的内存溢出(oom)问题。
本文介绍通过流式读取数据库 + 流式写入 excel 的方式,避免将百万行数据一次性加载至内存,彻底解决 node.js 导出大数据 excel 时的内存溢出(oom)问题。
在 Node.js 中导出超大规模数据(如 100 万+ 行)到 Excel 时,常见误区是「全量查库 → 全量加载至内存 → 全量写入 Workbook」。正如示例代码所示,db.read_data_all() 将全部结果一次性返回为数组,再调用 worksheet.addRows(result),这会导致 V8 堆内存迅速飙升,最终触发 FATAL ERROR: Reached heap limit —— 这正是典型的内存瓶颈。
根本解法是全程流式处理(Streaming):
✅ 数据库查询以流式(stream)方式逐行/分块拉取;
✅ Excel 写入采用可写流(Writable Stream)或流式工作表(ExcelJS.stream.xlsx.createWriterStream());
✅ 零中间内存缓存,数据“边读边写”,内存占用稳定在 KB~MB 级别。
✅ 推荐方案:ExcelJS 流式写入 + 数据库流式查询
ExcelJS 原生支持流式 Excel 生成,无需临时文件,可直接通过 HTTP 响应流输出:
const express = require('express');
const ExcelJS = require('exceljs');
const app = express();
const db = require.main.require('./src/app/models/db_controller');
app.get('/download-excel', (req, res) => {
// 设置响应头(注意:必须在 writeStream.pipe(res) 前设置!)
res.setHeader('Content-Type', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet');
res.setHeader('Content-Disposition', 'attachment; filename="export_data.xlsx"');
// 创建流式 workbook(不占用内存缓冲区)
const workbook = new ExcelJS.stream.xlsx.WorkbookWriter({
stream: res, // 直接写入响应流
useStyles: false, // 可选:禁用样式进一步降内存
useStyles: false,
});
const worksheet = workbook.addWorksheet('Sheet1');
worksheet.columns = [
{ header: 'Header 1', key: 'date_time', width: 20 },
{ header: 'Header 2', key: 'shaft_seal_pressure', width: 20 },
{ header: 'Header 3', key: 'transfer_pressure', width: 20 },
{ header: 'Header 4', key: 'cip_tem', width: 15 },
{ header: 'Header 5', key: 'elect_usage', width: 15 },
];
// 关键:使用数据库流式查询(需底层驱动支持,如 mysql2、pg、oracledb)
// 此处假设 db.queryStream 返回一个 Readable Stream(每行 emit 'data' 事件)
const dbStream = db.queryStream('SELECT * FROM master_data');
let rowCount = 0;
dbStream.on('data', (row) => {
worksheet.addRow(row);
rowCount++;
// 可选:每 10000 行 flush 一次,防止内部 buffer 积压(ExcelJS v4.3.0+ 支持)
if (rowCount % 10000 === 0) worksheet.commit();
});
dbStream.on('end', () => {
// 所有行写入完成,关闭 workbook(自动触发 end)
workbook.commit()
.then(() => console.log(`✅ Excel export completed: ${rowCount} rows`))
.catch(err => {
console.error('❌ Excel commit failed:', err);
res.status(500).end();
});
});
dbStream.on('error', (err) => {
console.error('❌ DB stream error:', err);
res.status(500).end();
});
});
⚠️ 关键前提与注意事项
-
数据库驱动必须支持流式查询:
- MySQL:推荐 mysql2(.query().stream());
- PostgreSQL:pg(client.query(new QueryStream(sql)) 或 pg-cursor);
- SQL Server:mssql(request.stream = true);
- Oracle:oracledb(resultSet = true + getRows() 循环)。
若当前 db_controller 不支持流,请优先升级或封装流式接口。
禁用非必要功能:
关闭 useStyles, useSharedStrings, useCellCache(ExcelJS 默认开启)可显著降低内存压力。仅当需复杂样式/公式时再启用。不要手动 addRows(array):
即使是分块 addRows(chunk),仍会将 chunk 数组暂存于内存。务必改用 addRow(object) + commit() 控制写入节奏。超长运行时间处理:
添加 res.setTimeout(30 * 60 * 1000)(30 分钟)避免连接超时;前端建议显示进度提示(可通过 SSE 或轮询 /status 实现)。
? 总结
百万级 Excel 导出不是性能问题,而是架构问题。放弃“内存中组装再写入”的思维定式,拥抱 Streaming First 原则:
? 数据从 DB 流向应用层(无全量数组);
? 数据从应用层流向 Excel 流(无临时文件、无大对象);
? 内存峰值 ≈ 单行数据大小 × 缓冲系数(通常
经实测,该方案可稳定导出 200 万行数据,Node.js 进程内存占用稳定在 80–120 MB,CPU 利用率平滑,完全规避 OOM 风险。











