【问题标题】:EPPlus save two million rows with 200+ columns datatable to multiple excel filesEPPlus 将包含 200 多列数据表的 200 万行保存到多个 Excel 文件中
【发布时间】:2015-10-21 16:01:43
【问题描述】:

我有使用 EPPlus 将 SQL 表中的所有记录保存到 Excel 工作表的功能。 如果我导出少量数据一切正常,但有 200 多列和 500 000 多行我得到 OutOfMemory 异常。

我想修改我的代码,使每个文件能够保存 50 000 条记录。

这是我适用于小数据的代码:

private Task SaveAsync(string tableName)
{

    return Task.Run(() =>
    {
        try
        {
            using (var conn = new SqlConnection(_connectionString))
            {
                using (var cmd = new SqlCommand(string.Format(DataQuery, tableName), conn))
                {
                    cmd.CommandType = CommandType.Text;
                    cmd.CommandTimeout = 360;
                    conn.Open();
                    using (SqlDataReader sdr = cmd.ExecuteReader())
                    {
                        var fileName = string.Format(TargetFile, tableName);
                        if (File.Exists(fileName))
                        {
                            File.Delete(fileName);
                        }

                        sdr.Read();
                        var numberOfRecordsInTable = sdr.GetInt32(0);

                        sdr.NextResult();

                        using (ExcelPackage pck = new ExcelPackage(new FileInfo(fileName)))
                        {
                            ExcelWorksheet ws = pck.Workbook.Worksheets.Add("Results");

                            int count = sdr.FieldCount;
                            int col = 1, row = 1;

                            for (int i = 0; i < count; i++)
                            {
                                ws.SetValue(row, col++, sdr.GetName(i));
                            }
                            row++;
                            col = 1;
                            while (sdr.Read())
                            {
                                for (int i = 0; i < count; i++)
                                {
                                    var val = sdr.GetValue(i);
                                    ws.SetValue(row, col++, val);
                                }
                                row++;
                                col = 1;
                            }
                            //autosize
                            ws.Cells[ws.Dimension.Address].AutoFitColumns();
                            //autofiltr
                            ws.Cells[1, 1, 1, count].AutoFilter = true;
                        }
                    }
                    conn.Close();
                }
            }
        }
        catch (Exception e)
        {
            Debug.WriteLine("Error at: " + Thread.CurrentThread.ManagedThreadId);
            Debug.WriteLine(e);
        }
    });
}

以及我修改后的代码,每个文件拆分 50 000 条记录:

private Task SaveAsync2(string tableName)
{
    return Task.Run(() =>
    {
        try
        {
            using (var conn = new SqlConnection(_connectionString))
            {
                using (var cmd = new SqlCommand(string.Format(DataQuery, tableName), conn))
                {
                    cmd.CommandType = CommandType.Text;
                    cmd.CommandTimeout = 360;
                    conn.Open();
                    using (SqlDataReader sdr = cmd.ExecuteReader())
                    {

                        var fileName = string.Format(TargetFile, tableName,"");
                        if (File.Exists(fileName))
                        {
                            File.Delete(fileName);
                        }

                        sdr.Read();
                        var max = sdr.GetInt32(0);
                        int filesCount = 1;
                        if (max > 50000)
                        {
                            fileName = string.Format(TargetFile, tableName, filesCount);
                        }

                        sdr.NextResult();

                        ExcelPackage pck = new ExcelPackage(new FileInfo(fileName));
                        ExcelWorksheet ws = pck.Workbook.Worksheets.Add("RESULTS");

                        int count = sdr.FieldCount;

                        int col = 1, row = 1;

                        for (int i = 0; i < count; i++)
                        {
                            ws.SetValue(row, col++, sdr.GetName(i));
                        }
                        row++;
                        col = 1;
                        while (sdr.Read())
                        {
                            for (int i = 0; i < count; i++)
                            {
                                var val = sdr.GetValue(i);
                                ws.SetValue(row, col++, val);
                            }
                            row++;
                            col = 1;

                            if (row > 50000)
                            {
                                pck.Save();
                                filesCount++;
                                fileName = string.Format(TargetFile, tableName, filesCount);

                                pck = new ExcelPackage(new FileInfo(fileName));
                                ws = pck.Workbook.Worksheets.Add("RESULTS");

                                count = sdr.FieldCount;

                                col = 1;
                                row = 1;

                                for (int i = 0; i < count; i++)
                                {
                                    ws.SetValue(row, col++, sdr.GetName(i));
                                }
                                row++;
                                col = 1;
                            }
                        }

                        //autosize
                        ws.Cells[ws.Dimension.Address].AutoFitColumns();
                        //autofiltr
                        ws.Cells[1, 1, 1, count].AutoFilter = true;

                        pck.Save();
                    }
                }
                conn.Close();

            }
        }
        catch (Exception e)
        {
            Debug.WriteLine("Error at: " + Thread.CurrentThread.ManagedThreadId);
            Debug.WriteLine(e);
        }
    });
}

基本上这可以正常工作,但在我的代码的第一个版本中,我使用了 using 语句中的所有内容,而在第二个版本中,我调用了两次相同的代码。

  1. 如何修复我的代码以删除重复代码并将所有内容放入其中。
  2. 我可以将下一组(50 000 条记录)添加为新工作表而不是创建新文件吗?
  3. 将数据保存到文件时 EPPlus 限制是多少? rows x columns?我发现 EPPlus 应该处理超过百万行的信息,但不像我拥有的​​那么多列。我认为我可以用单列导出数百万行,但是对于 200 多列对我来说,50 000 行是有限的。我想知道是否有数量(行 x 列)会限制我的导出工作正常。我希望该导出功能是通用的,因此当我传递包含 50 列的数据表时,它将导出例如每个文件 100 000 行,而对于 2 列,它将导出每个文件 50 万行。

【问题讨论】:

    标签: c# .net-4.5 epplus epplus-4


    【解决方案1】:

    我过去曾遇到过 EPPlus 的内存限制,最终生成多个 .xlsx 文件作为解决方法(类似于您的方法)。另一种选择是将编译器设置更改为仅针对 64 位(如果您可以在不支持 32 位平台的情况下通过)。我记得,EPPlus 是为“任何 CPU”编译的,所以如果您可以将代码更改为以“x64”为目标,这可能会放宽内存限制并允许您生成单个 .xlsx 文件。以 x64 为目标可能对我来说是可行的,但直到事后我才想到它,所以我从来没有机会进行测试。

    更新: 我刚刚使用 EPPlus 3.1.3 进行了快速测试,创建了 500,000 行,每行 70 列。在生成内存不足异常之前,我的 32 位应用程序能够生成大约 119,000 行。将目标切换到 x64 后,它成功生成了所有 500,000 行,尽管它花了很长时间。创建实际工作表只需要几分钟,但 ExcelPackage.SaveAs() 需要将近 20 分钟。 RAM 消耗也相当高(大约 11GB 的 RAM)。生成的 .xlsx 为 220MB,32 位 Excel 无法打开(内存不足)。 底线: 以 x64 为目标可能不是一个可行的解决方案;最好将输出拆分为多个 .xlsx 文件。

    我很想删除这个答案,因为它已经证明是一个死胡同,但决定留下它以防它帮助其他人将来避免这条路。

    【讨论】:

    • 这可能行得通!我没有尝试将目标更改为 x64,我将立即尝试!
    • 我尝试将目标平台更改为 x64,但之后我的应用程序在单击导出后挂起。更改目标平台是我所做的唯一更改。
    • @misiu:可能是它没有挂起,只是速度很慢。请参阅上面的新信息。
    • 感谢您检查这个,是的,留下这个答案,因为它可能会帮助有类似问题的人。
    • 谢谢 cbranch 和@Misiu。它确实帮助了我。
    【解决方案2】:

    不幸的是,没有简单的方法可以将这么多数据与 Epplus 合并到一个文件中。基本上,整个文件在打开时都会被加载到内存中——要么全部加载,要么全部加载。理论上,您可以生成 XLSX 包含的 XML 文件(它们是重命名的 zip 文件)并手动插入它,因为它的内存占用会更小,但这是一个不小的壮举。

    对于您当前的代码,如果您想避免使用 using 语句,您总是可以手动调用 .dispose()。但我了解您希望避免重复代码。像这样的东西怎么样(但在复制所有对象数据时注意内存使用情况):

    const int max = 10;
    var loop = 0;
    
    using (var sdr = cmd.ExecuteReader())
    {
        var fieldcount = sdr.FieldCount;
    
        var getfi = new Func<int, FileInfo>(i =>
        {
            var fi = new FileInfo(String.Format(@"c:\temp\Multi_Files{0}.xlsx", i));
            if (fi.Exists) fi.Delete();
            return fi;
        });
    
        var savefile = new Action<FileInfo, List<Object[]>>((info, rows) =>
        {
            using (var pck = new ExcelPackage(info))
            {
                var wb = pck.Workbook;
                var ws = wb.Worksheets.Add("RESULTS");
                for (var row = 0; row < rows.Count; row++)
                    for (var col = 0; col < fieldcount; col++)
                        ws.SetValue(row + 1, col + 1, rows[row][col]);
                pck.Save();
            }
        });
    
        var rowlist = new List<Object[]>();
    
        while (sdr.Read())
        {
            var rowdata = new Object[sdr.FieldCount];
            sdr.GetValues(rowdata);
            rowlist.Add(rowdata);
    
            if (rowlist.Count == max)
            {
                savefile(getfi(++loop), rowlist);
                rowlist.Clear();
            }
        }
        if (rowlist.Count > 0)
            savefile(getfi(++loop), rowlist);
    }
    

    【讨论】:

    • 谢谢你。我不知道我可以以这种方式使用FuncAction。我将尝试使用您的答案修改我的代码。现在您正在创建临时列表来存储传递给savefile 操作的数据。没有额外的临时变量可以完成相同的功能吗?
    • EPPlus 限制是多少? row x columns?我发现 EPPlus 应该处理超过百万行的信息,但不能处理那么多列。我认为我可以用单列导出数百万行,但对于我来说,107 列的 50 000 行是限制的。我想知道是否有数量(行 x 列)会限制我的导出工作正常。我希望该导出功能是通用的,因此当我传递包含 50 列的数据表时,它将导出例如每个文件 100 000 行,而对于 2 列,它将导出每个文件 50 万行。
    • @Misiu 是的,它的临时变量是这种方法的缺点,但由于您使用的是顺序阅读器,如果没有一些重复的代码,很难绕过它。至于实际限制,很难确切地说,因为它不仅是 r x c 的函数,而且是内容的函数,即字符串与数字。甚至字符串也是变体,因为 Excel 使用字符串池,因此字符串的唯一性也会影响它。我通常会遇到大约 60k 行和 150 列的情况。
    • Emie 我刚刚回到这个问题,因为很多时候我得到OutOfMemoryException 我认为这是因为rowdata 变量存储了然后添加到工作表中的数据。我将在这个问题上开始赏金,因为我需要更好的东西,不会像现在那样抛出太多异常。这并不意味着您的代码不好,我从中吸取了教训,再次感谢您的帮助。
    • 我的最终解决方案基于此代码,感谢您的帮助!
    【解决方案3】:

    由于您正在创建一个新的 excel 文件(如果我错了,请纠正我),您可以简单地编写一个包含一些特定内容的 XML 文件。如果 .xml 文件包含正确的内容,则 Excel 支持它们。

    您可以简单地在内存中创建 XML 文件的内容,然后将这些内容写入 .XML 文件。您不需要 EPPlus 包,因此您绕过了 EPPlus 包的限制。

    当然,您必须手动确定需要在 .XML 文件中写入的内容。你们中将使用格式和公式,这可能很复杂。

    看这里:

    【讨论】:

    • 感谢链接,但我可以忍受 EPPlus 的限制,这就是我将整个数据表切割成几个文件的原因。我正在添加格式和其他内容,所以我想避免手动执行此操作。
    【解决方案4】:

    没有任何技巧的简单解决方案(未经测试,但意图应该很清楚)

    using (var conn = new SqlConnection(_connectionString))
    {
        int filesCount = 1;
        int col = 1, row = 1;
        string fileName = String.Empty;
        int count;
        ExcelPackage pck;
        ExcelWorksheet ws;
    
        using (var cmd = new SqlCommand(string.Format(DataQuery, tableName), conn))
        {
             cmd.CommandType = CommandType.Text;
             cmd.CommandTimeout = 360;
             conn.Open();
             using (SqlDataReader sdr = cmd.ExecuteReader())
             {
                  while (sdr.Read())
                  {
                       if (row == 1)
                       {
                           fileName = string.Format(TargetFile, tableName, filesCount);
                           if (File.Exists(fileName))
                           {
                                File.Delete(fileName);
                           }
                           pck = new ExcelPackage(new FileInfo(fileName));
                           ws = pck.Workbook.Worksheets.Add("RESULTS");
                       }
    
                       count = sdr.FieldCount; 
                       for (int i = 0; i < count; i++)
                       {
                           var val = sdr.GetValue(i);
                           ws.SetValue(row, col++, val);
                       }
                       row++;
                       col = 1;
    
                       if (row >= 50000)
                       {
                            ws.Cells[ws.Dimension.Address].AutoFitColumns();
                            ws.Cells[1, 1, 1, count].AutoFilter = true;
                            pck.Save();
                            row = 1;
                            filesCount+
                       }
                   }
              }
              if (row > 1)
              {
                   ws.Cells[ws.Dimension.Address].AutoFitColumns();
                   ws.Cells[1, 1, 1, count].AutoFilter = true;
                   pck.Save();
              }
         }
    }
    conn.Close();
    

    【讨论】:

    • 我做了一些非常相似的事情,但不幸的是我在保存第 7 部分后得到了OutOfMemoryException(当 EPPlus 试图保存第 8 部分时)。我可以在我的问题中添加整个错误,这有什么帮助
    • 我已经试过了。我正在处理 pck 和 ws。我首先将每个文件的限制设置为 300000,然后出现异常,现在我正在尝试获取每个文件的行数,该行数可以在不抛出异常的情况下工作。在完善所有内容之后,我可能会在 codereview 上提出问题。理想情况下,我希望每个文件放置数百万行,但 EPPlus 不会处理那么多,尤其是 100 多列。
    • 也许使用 Excel 互操作是一种解决方案 - 它比使用 epplus 更复杂(并且有阴暗面,因为您需要安装 Excel),但可以处理 Excel 可以处理的所有文件(虽然也可能有大小限制),或者通过 odbc 编写 Excel 文件
    • 感谢您的建议,但据我所知,Interop 不会并行工作,我需要能够(将来)一次形成多个表的解决方案
    • @Misiu 你能举一个行数据的例子吗?或者至少是你的行的平均大小? (有多少双精度值、日期值、字符串(大小)...
    猜你喜欢
    • 2020-12-22
    • 2011-05-26
    • 2014-03-02
    • 1970-01-01
    • 1970-01-01
    • 2020-07-02
    • 2020-07-22
    • 1970-01-01
    • 2016-11-30
    相关资源
    最近更新 更多