You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.28 09:41:21