【问题标题】:Fastest way to convert SqlDataReader to list of objects in C#在 C# 中将 SqlDataReader 转换为对象列表的最快方法
【发布时间】:2020-09-18 13:34:42
【问题描述】:

我尝试使用 C# 从 SQL Server 数据库中获取大量数据。我从数据库中得到了大约 30 万行数据(我相信这离最坏的情况不远了。)可能包含数亿。

我认为问题不在于数据库的大小,因为 的command.ExecuteReader(); 大约只需要不到一秒钟。

我试过这段代码:

public List<ResultPulser> GetReportResult(SqlConnection opCon, SqlCommand command,
    int minReport,int maxReport,int machineNumber)
{
    List<ResultPulser> results = new List<ResultPulser>();

    using (DataContext dc = new DataContext(opCon))
    {
        try
        {
            command.CommandText = "select * from ResultPulser " +
                "where CAST(SUBSTRING([ReportNumber], 0, 8) as int) = @machineNumber and " +
                "CAST(SUBSTRING([ReportNumber],8,LEN([ReportNumber])) as int) BETWEEN @minReport AND @maxReport";
            command.Parameters.Clear();
            command.Parameters.AddWithValue("@minReport", minReport);
            command.Parameters.AddWithValue("@maxReport", maxReport);
            command.Parameters.AddWithValue("@machineNumber", machineNumber);

            Stopwatch SW1 = Stopwatch.StartNew();
            SqlDataReader reader = command.ExecuteReader();
            SW1.Stop();

            DataTable table = new DataTable();
            Stopwatch SW2 = Stopwatch.StartNew();
            table.Load(reader);
            SW2.Stop();

            Stopwatch SW3 = Stopwatch.StartNew();
            ResultPulser[] report = new ResultPulser[table.Rows.Count];

            for (int i = 0; i < table.Rows.Count; i++)
            {
                DataRow dr = table.Rows[i];
                report[i] = new ResultPulser(Convert.ToInt64(dr[0]), dr[1].ToString().Trim(),
                    dr[2].ToString().Trim(), Convert.ToDateTime(dr[3]), Convert.ToDouble(dr[4]),
                    Convert.ToDouble(dr[5]), Convert.ToDouble(dr[6]), Convert.ToDouble(dr[7]),
                    Convert.ToDouble(dr[8]), Convert.ToDouble(dr[9]), Convert.ToInt64(dr[10]),
                    Convert.ToInt64(dr[11]), Convert.ToInt64(dr[12]), Convert.ToBoolean(dr[13]),
                    Convert.ToInt32(dr[14]));
            }
            SW3.Stop();

            reader.Close();
            return report.ToList();
        }
        catch (Exception ex)
        {
            LocalPulserDBManagerInstance.WriteLog(ex.StackTrace, ex.Message);
            throw ex;
        }
    }
}

但下一行 table.Load(reader); 大约需要 20 秒才能完成。

我也试过这样:

public List<ResultPulser> GetReportResult(SqlConnection opCon, SqlCommand command,
            int minReport,int maxReport,int machineNumber)
{
            List<ResultPulser> results = new List<ResultPulser>();

            using (DataContext dc = new DataContext(opCon))
            {
                try
                {
                    command.CommandText = "select * from ResultPulser " +
                        "where CAST(SUBSTRING([ReportNumber], 0, 8) as int) = @machineNumber and " +
                        "CAST(SUBSTRING([ReportNumber],8,LEN([ReportNumber])) as int) BETWEEN @minReport AND @maxReport";
                    command.Parameters.Clear();
                    command.Parameters.AddWithValue("@minReport", minReport);
                    command.Parameters.AddWithValue("@maxReport", maxReport);
                    command.Parameters.AddWithValue("@machineNumber", machineNumber);

                    Stopwatch SW1 = Stopwatch.StartNew();
                    SqlDataReader reader = command.ExecuteReader();
                    SW1.Stop();

                    DataTable table = new DataTable();
                    Stopwatch SW2 = Stopwatch.StartNew();
                    while (reader.Read())
                    {
                        results.Add(new ResultPulser(reader.GetInt64(0), reader.GetString(1).Trim(), reader.GetString(2).Trim(),
                            reader.GetDateTime(3), reader.GetDouble(4), reader.GetDouble(5), reader.GetDouble(6),
                            reader.GetDouble(7), reader.GetDouble(8), reader.GetDouble(9), reader.GetInt64(10),
                            reader.GetInt64(11), reader.GetInt64(12), reader.GetBoolean(13), reader.GetInt32(14)));
                    }
                    SW2.Stop();


                    reader.Close();
                    return results;
                }
                catch (Exception ex)
                {
                    LocalPulserDBManagerInstance.WriteLog(ex.StackTrace, ex.Message);
                    throw ex;
                }
            }
 }

在这种情况下,此代码部分大约需要 17-16 秒...

  while (reader.Read())
  {
      results.Add(new ResultPulser(reader.GetInt64(0), reader.GetString(1).Trim(), reader.GetString(2).Trim(),
                            reader.GetDateTime(3), reader.GetDouble(4), reader.GetDouble(5), reader.GetDouble(6),
                            reader.GetDouble(7), reader.GetDouble(8), reader.GetDouble(9), reader.GetInt64(10),
                            reader.GetInt64(11), reader.GetInt64(12), reader.GetBoolean(13), reader.GetInt32(14)));
                    }

如何优化我的代码以使其更快?

【问题讨论】:

  • 创建 300,000 个对象的列表后,您将如何处理它?
  • 第一个问题,正如 Caius 上面所说的,真的,你需要一次性将这么多数据加载到内存中吗?如果答案是肯定的,那么将这么多数据传输到您的程序中需要真正可衡量的时间。
  • 确实,我问'cos,如果答案是“我要把它们放在一个 ComboBox 中,让用户选择一个!”那么您需要查看您的 UI/UX 要求。如果是“我要将它们写入磁盘/将它们发送到套接字”,那么答案是直接流式传输它们,一次一个,而不是将它们加载到内存中。很少有充分的理由在内存中拥有如此庞大的数据集。如果每个对象都是 1 kb,那么它是 300meg 加上仅用于列表。另外,您知道 list 在内部使用大小为 16 的数组并在需要更多空间时将其加倍(复制每个元素)吗?数百万无用的复制操作..
  • 这太可笑了。您已经说过查询很快;实施我建议的改进,这样您就不会削弱报告编号上的索引,确保报告编号上有一个索引,并经常查询数据库以获取您需要的小数据项。不要仅仅因为您可能需要几千个并且不想进行一百个数据库查询就下载 300,000 个项目。还要考虑您的用户将如何处理他的 300,000 行报告;大概总结一下,因为谁想要/可以查看 300,000 行数据并理解它。这整个事情是一个半生不熟的XY问题..
  • (并考虑到,在上下文中,您的用户将花费几分钟或几小时来查看 300,000 行,生成数据的 7 秒是沧海一粟。节省服务器资源并将它们写入一个接一个的文件;你不需要所有这些在内存中。如果你在c#中汇总数据,请使用SQL在数据库中汇总它,这样X百兆字节就不会通过网络传输——可能是最慢的部分)

标签: c# sql sql-server optimization


【解决方案1】:

这是你的问题:

"where CAST(SUBSTRING([ReportNumber], 0, 8) as int) = @machineNumber and " +
"CAST(SUBSTRING([ReportNumber],8,LEN([ReportNumber])) as int) BETWEEN @minReport AND @maxReport";

通过使用这些CAST(SUBSTRING(...)) 表达式,您将强制对该表(可能很大)进行全面扫描。 您需要对其进行更改/优化,使其在 WHERE 中包含裸列名称。

我从谓词中假设 ReportNumber 列具有以下形式的值 嗯嗯嗯嗯嗯嗯嗯+ 其中MMMMMMMM 是机器编号,R+ 是报告编号的一位或多位数字。

我不确定 ReportNumber 列的数据类型是什么。它可以是bigint 或某种字符串。从查询来看,它似乎是一个字符串,但您不妨使用隐式转换。

无论如何,优化这一点并使其达到以毫秒为单位测量的经过时间的潜力很大。

我只能给你一个起点,因为没有足够的信息。

第一部分很简单:

"where CAST(SUBSTRING([ReportNumber], 0, 8) as int) = @machineNumber"

变成

"where ReportNumber LIKE @machineNumber"

所以这变成了前缀搜索,并且您更改 C# 部分以将参数作为前缀搜索传递给 LIKE:

string machineNumberLikeString = machineNumber.ToString() + "%";

然后像这样将它传递给查询:

command.Parameters.AddWithValue("@machineNumber", machineNumberLikeString);

然后在 SQL 方面,您可以创建一个索引来支持快速搜索此查询的数据(但前提是它经常使用 - 这是主观的,取决于您的工作负载和要求)。请记住,索引有其成本,收益必须大于它。

CREATE INDEX IX_ResultPulser_ReportNumber
    ON ResultPulser (ReportNumber);

假设 ReportNumber 列是某种字符串数据类型,例如,它将起作用。 VARCHAR 或 NVARCHAR。如果它是数字,它可能会或可能不会工作。需要修改策略。但是我没时间了,我没有足够的数据和表格信息。

编辑:如果您在一个查询中提取 30 万行,这可能无济于事。 SQL Server 将需要 300k 查找来获取您的 SELECT * 的剩余列。您可以考虑将此表聚集在 ReportNumber 列上(CREATE CLUSTERED INDEX...)。

最后一个警告:不要在生产代码中使用SELECT *(选择星号)。始终明确指定您需要哪些列。

希望对您有所帮助。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2014-10-30
    • 2011-03-11
    • 2017-04-23
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2017-10-07
    相关资源
    最近更新 更多