NetSuite处理75万条库存记录生成CSV遇阻,求解决方案
75万条库存CSV导出问题解决方案
针对UI中Saved Search无法加载、MapReduce脚本getInputData停滞15小时的问题,核心原因是超大数据集下一次性加载所有数据引发的性能瓶颈,以下是具体优化方案:
一、修复getInputData停滞:改用分页搜索
直接返回已保存搜索对象会让NetSuite在getInputData阶段尝试一次性加载75万条数据,必然导致超时或停滞。改为分页获取搜索结果,让数据分批流入Map阶段:
function getInputData() { const savedSearch = search.load({ id: 'customsearch_st_itemsaved_search_report_' }); // 每页加载1000条(NetSuite单页最大限制),避免一次性加载全部数据 return savedSearch.createPagedData({pageSize: 1000}); }
二、Map阶段稳定性与效率优化
原代码字段提取未处理关联字段的value/text属性,且CSV转义逻辑冗余,优化后提升稳定性和处理速度:
function map(context) { const result = JSON.parse(context.value); // 安全提取字段值,兼容关联字段的结构 const getFieldValue = (fieldId) => { const fieldData = result.values[fieldId]; if (!fieldData) return ''; return typeof fieldData === 'object' ? (fieldData.value || fieldData.text || '') : fieldData.toString(); }; const row = [ getFieldValue('itemid'), getFieldValue('custitem_pricefrom'), getFieldValue('custitemwine_owner'), getFieldValue('tranid.transaction'), getFieldValue('rate.transaction'), getFieldValue('formulanumeric'), getFieldValue('statusref.transaction'), getFieldValue('inventorynumber.inventoryDetail') ]; // 仅在必要时添加CSV引号,减少字符串处理开销 const csvSafeRow = row.map(val => { const strVal = val.toString(); return strVal.includes(',') || strVal.includes('"') || strVal.includes('\n') ? `"${strVal.replace(/"/g, '""')}"` : strVal; }); // 按批次拆分key,避免单个Reduce节点处理过多数据 const pageIndex = parseInt(context.key.split('_')[1]); const batchKey = `csv_batch_${Math.floor(pageIndex / 100)}`; // 每100页(10万条)一个批次 context.write({ key: batchKey, value: csvSafeRow.join(',') }); log.debug('Processed Row', `Page ${pageIndex}, Row ${context.key}`); }
三、Reduce阶段内存优化
原代码将所有行存入数组再分批保存,易引发内存溢出。优化后按批次生成文件,同时避免多Reduce节点生成重复文件名:
function reduce(context) { const chunkSize = ROW_LIMIT; const header = HEADER.join(','); let lines = [header]; let part = 1; // 从key中提取批次编号,确保文件名唯一 const batchNum = context.key.split('_')[2]; const filePrefix = `ItemSearch_Export_Batch${batchNum}_`; context.values.forEach((line, index) => { lines.push(line); if ((index + 1) % chunkSize === 0) { saveCSV(lines, filePrefix + part); lines = [header]; part++; } }); // 保存剩余未达批次量的数据 if (lines.length > 1) { saveCSV(lines, filePrefix + part); } log.audit('Batch Completed', `Batch ${batchNum} processed ${context.values.length} rows`); }
四、额外优化建议
- 精简已保存搜索:移除搜索中不必要的关联字段、复杂公式,仅保留所需的8个字段;若业务允许,按日期/仓库等维度拆分搜索,分批次导出后合并文件。
- 调整脚本部署配置:在脚本部署页面设置并发数为4-8,启用“允许并发执行”选项,提升整体处理速度。
- 日志监控:保留关键步骤的日志输出,方便排查执行过程中的异常节点。
修改后的完整脚本
/** * @NApiVersion 2.1 * @NScriptType MapReduceScript */ define(['N/search', 'N/file', 'N/log'], function(search, file, log) { const ROW_LIMIT = 100000; const HEADER = [ 'Item ID', 'Price From', 'Wine Owner', 'Document Number', 'Item Rate', 'Price - Rate Diff', 'Status', 'Inventory Number' ]; function getInputData() { const savedSearch = search.load({ id: 'customsearch_st_itemsaved_search_report_' }); return savedSearch.createPagedData({pageSize: 1000}); } function map(context) { const result = JSON.parse(context.value); const getFieldValue = (fieldId) => { const fieldData = result.values[fieldId]; if (!fieldData) return ''; return typeof fieldData === 'object' ? (fieldData.value || fieldData.text || '') : fieldData.toString(); }; const row = [ getFieldValue('itemid'), getFieldValue('custitem_pricefrom'), getFieldValue('custitemwine_owner'), getFieldValue('tranid.transaction'), getFieldValue('rate.transaction'), getFieldValue('formulanumeric'), getFieldValue('statusref.transaction'), getFieldValue('inventorynumber.inventoryDetail') ]; const csvSafeRow = row.map(val => { const strVal = val.toString(); return strVal.includes(',') || strVal.includes('"') || strVal.includes('\n') ? `"${strVal.replace(/"/g, '""')}"` : strVal; }); const pageIndex = parseInt(context.key.split('_')[1]); const batchKey = `csv_batch_${Math.floor(pageIndex / 100)}`; context.write({ key: batchKey, value: csvSafeRow.join(',') }); log.debug('Processed Row', `Page ${pageIndex}, Row ${context.key}`); } function reduce(context) { const chunkSize = ROW_LIMIT; const header = HEADER.join(','); let lines = [header]; let part = 1; const batchNum = context.key.split('_')[2]; const filePrefix = `ItemSearch_Export_Batch${batchNum}_`; context.values.forEach((line, index) => { lines.push(line); if ((index + 1) % chunkSize === 0) { saveCSV(lines, filePrefix + part); lines = [header]; part++; } }); if (lines.length > 1) { saveCSV(lines, filePrefix + part); } log.audit('Batch Completed', `Batch ${batchNum} processed ${context.values.length} rows`); } function saveCSV(lines, part) { const fileName = `${part}.csv`; const csvFile = file.create({ name: fileName, fileType: file.Type.CSV, contents: lines.join('\n'), folder: 194669 }); const fileId = csvFile.save(); log.audit(`Saved File ${fileName}`, `File ID: ${fileId}`); } return { getInputData, map, reduce }; });
内容的提问来源于stack exchange,提问作者Jidnesh Madhavi
相关产品推荐
相关产品推荐

