Asp.Net MVC导出搜索结果至Excel致应用卡顿的优化方案咨询
解决Asp.Net MVC大数据量导出Excel卡顿的问题
你遇到的问题非常典型:把30万条实体对象存入Session会导致内存占用暴增,不仅会让你的应用卡顿,甚至可能引发OutOfMemoryException。Session本来就不是用来存储大量数据的,而且ToList()会一次性把所有数据加载到内存,这是性能瓶颈的核心原因。下面给你一套完整的优化方案,包括ClosedXML的具体实现,以及是否需要存储过程的分析:
一、核心优化思路:放弃Session存储全量数据
不要把整个搜索结果集加载到内存里,而是复用查询逻辑,在导出时直接从数据库获取数据。这样既避免了Session的内存压力,也不会一次性加载几十万条数据到内存。
第一步:重构查询逻辑,抽成可复用的方法
把你在Index里构建查询的代码抽成一个单独的私有方法,这样Index(分页展示)和导出Action都能复用,避免重复代码:
private IQueryable<APPET1> BuildSearchQuery(string search, string cagecode, string status, string docNumber, string remark) { var query = db.APPET1 // 注意:如果导出不需要关联表的所有数据,这里可以去掉不必要的Include! .Include(t => t.Status) .Include(t => t.APPETCCode); DateTime searchDate; if (!string.IsNullOrEmpty(search)) { bool isDateSearch = DateTime.TryParse(search, out searchDate); if (isDateSearch) { query = query.Where(s => s.Date_Received == searchDate); } else { query = query.Where(t => t.Doc_Number.Contains(search) || t.Status.Status1.Contains(search) || t.Remark.Contains(search) || t.CCode.Contains(search)); } } if (!string.IsNullOrEmpty(status)) { query = query.Where(t => t.Status.Status1.Contains(status)); } if (!string.IsNullOrEmpty(docNumber)) { query = query.Where(t => t.Doc_Number.Contains(docNumber)); } if (!string.IsNullOrEmpty(remark)) { query = query.Where(t => t.Remark.Contains(remark)); } // 保持IQueryable,不要调用ToList(),延迟执行查询 return query; }
第二步:修改Index Action,去掉Session存储
原来的Index是做分页展示的,只需要加载当前页的数据,不需要把全量数据存到Session:
public ActionResult Index(string search, string cagecode, string sortBy, int? page, string docNumber, string remark, string status) { APPETViewModel viewModel = new APPETViewModel(); var query = BuildSearchQuery(search, cagecode, status, docNumber, remark); // 处理排序(根据你的需求实现) if (!string.IsNullOrEmpty(sortBy)) { switch (sortBy) { case "docNumber": query = query.OrderBy(t => t.Doc_Number); break; case "dateReceived": query = query.OrderBy(t => t.Date_Received); break; // 其他排序逻辑 } } // 分页(这里用PagedList.Mvc举例,你可以用自己的分页组件) int pageSize = 20; int pageNumber = page ?? 1; viewModel.SearchResults = query.ToPagedList(pageNumber, pageSize); // 填充ViewModel的其他数据 var stats = db.Status.Select(s => s.Status1); viewModel.Statuses = new SelectList(stats); viewModel.Search = search; viewModel.DocNumber = docNumber; viewModel.Remark = remark; viewModel.Status = status; return View(viewModel); }
二、用ClosedXML实现高效导出
ClosedXML支持从IQueryable逐行读取数据并写入Excel,不需要一次性加载全量数据到内存。下面是导出Action的实现:
首先,确保你已经安装了ClosedXML NuGet包:
Install-Package ClosedXML
然后实现导出Action:
public ActionResult ExportToExcel(string search, string cagecode, string status, string docNumber, string remark) { var query = BuildSearchQuery(search, cagecode, status, docNumber, remark); // 只选择需要导出的字段!不要加载整个实体,减少数据传输量 var exportData = query.Select(t => new { DocNumber = t.Doc_Number, ReceivedDate = t.Date_Received, Status = t.Status.Status1, Remark = t.Remark, CCode = t.CCode, // 加上你需要导出的其他字段 }); using (var workbook = new XLWorkbook()) { var worksheet = workbook.Worksheets.Add("APPET查询结果"); // 写入表头 worksheet.Cell(1, 1).Value = "文档编号"; worksheet.Cell(1, 2).Value = "接收日期"; worksheet.Cell(1, 3).Value = "状态"; worksheet.Cell(1, 4).Value = "备注"; worksheet.Cell(1, 5).Value = "CCode"; // 对应其他字段的表头 // 逐行写入数据,EF会从数据库逐行读取,不会一次性加载所有数据 int rowIndex = 2; foreach (var item in exportData) { worksheet.Cell(rowIndex, 1).Value = item.DocNumber; worksheet.Cell(rowIndex, 2).Value = item.ReceivedDate; worksheet.Cell(rowIndex, 3).Value = item.Status; worksheet.Cell(rowIndex, 4).Value = item.Remark; worksheet.Cell(rowIndex, 5).Value = item.CCode; rowIndex++; } // 自动调整列宽,提升Excel可读性 worksheet.Columns().AdjustToContents(); // 准备输出流,返回文件 using (var stream = new MemoryStream()) { workbook.SaveAs(stream); stream.Position = 0; string fileName = $"APPET结果_{DateTime.Now:yyyyMMddHHmmss}.xlsx"; return File(stream, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", fileName); } } }
三、是否需要存储过程?
大部分情况下不需要。EF的IQueryable会根据你的Lambda表达式生成高效的SQL,尤其是当你只选择需要的字段时。但如果你的查询逻辑非常复杂(比如多表关联、复杂条件判断),或者EF生成的SQL性能不佳,可以考虑用存储过程:
- 在数据库中创建存储过程,接收查询参数,返回需要导出的字段。
- 在EF中调用存储过程,获取IQueryable或者IEnumerable的结果。
- 同样用ClosedXML逐行写入Excel即可。
四、额外优化建议
- 添加数据库索引:给经常用于查询的字段(比如
Doc_Number、Date_Received、Status1)添加索引,提升查询速度。 - 优化Contains查询:
Contains会生成LIKE '%xxx%',无法利用索引。如果业务允许,改用StartsWith,或者给字段添加全文索引。 - 异步导出(可选):如果数据量超大(比如百万级),可以考虑把导出任务放到后台队列(比如用Hangfire),导出完成后通知用户下载,避免用户长时间等待。
这样修改后,你的应用就不会再因为存储大量数据到Session而卡顿,导出功能也能高效运行。
内容的提问来源于stack exchange,提问作者PhatLee
相关产品推荐
相关产品推荐

