【问题标题】:Fastest way to insert 1 million rows in SQL Server [duplicate]在 SQL Server 中插入 100 万行的最快方法 [重复]
【发布时间】:2014-09-12 16:12:12
【问题描述】:

我正在编写一个存储过程来将行插入表中。问题在于,在某些操作中,我们可能想要插入超过 100 万行,并且我们想让它变得更快。另一件事是,在其中一个列中,它是Nvarchar(MAX)。我们可能希望在此列中放置平均 1000 个字符。

首先,我写了一个 prc 来逐行插入。然后我生成一些随机数据以插入 NVARCHAR(MAX) 列是 1000 个字符的字符串。然后使用循环调用 prc 来插入行。如果我使用 SQL 服务器登录数据库服务器进行插入,性能非常糟糕,需要 48 分钟。如果我在我的桌面使用C#连接到服务器(这是我们通常想要做的),大约需要90多分钟。

然后,我将 prc 更改为将表类型参数作为输入。我以某种方式准备了行并将它们放入表类型参数中并通过以下命令进行插入:

INSERT INTO tableA SELECT * from @tableTypeParameterB

我尝试将批量大小设置为 1000 行和 3000 行(将 1000-3000 行放入 @tableTypeParameterB 中以插入一次)。性能还是很差的。如果我在 SQL 服务器中运行插入 100 万行大约需要 3 分钟,如果我使用 C# 程序从我的桌面连接大约需要 10 分钟。

tableA 有一个包含 2 列的聚集索引。

我的目标是尽可能快地进行插入(我的想法目标是在 1 分钟内)。有什么办法可以优化吗?


只是更新:

我尝试了下面一些人建议的批量复制插入。我尝试使用 SQLBULKCOPY 一次插入 1000 行和 10000 行。插入 100 万行仍然需要 10 分钟(每行有 1000 个字符的列)。没有性能提升。还有其他建议吗?


基于 cmets 要求的更新。

数据实际上来自 UI。用户将使用 UI 更改为批量选择,我们说,一百万行并将一列从旧值更改为新值。此操作将在一个单独的过程中完成。但这里我们需要做的是让中间层服务从 UI 中获取旧值和新值并将它们插入到表中。旧值和新值最多可达 4000 个字符,平均为 1000 个字符。我认为长字符串旧值/新值会降低速度,因为当我将测试数据旧值/新值更改为 20-50 个字符时,无论使用 SQLBulkCopy 还是表类型变量,插入都非常快

【问题讨论】:

  • 你应该看看 SqlBulkCopy
  • 必须按顺序插入吗?
  • 您是从某个文件中读取数据并插入吗?
  • 目前我只是在做例子。我从另一个表中读取 1000 行并将其存储在数据表对象中。然后通过类似“插入tableA Select @i, Column1, Column2, Column3 ... ColumnN from @tmpTableTypePrameter”的命令将它们插入到tableA中1000次
  • 我使用 BCP 在大约 20 秒内将来自这个拼图挑战的 1,000,000 百万行测试数据插入到笔记本电脑上的 SQL Serve 中:ask.sqlservercentral.com/questions/1227/…

标签: c# sql sql-server


【解决方案1】:

如果您更喜欢使用 SQL,我认为您正在寻找的是 Bulk Insert

或者还有ADO.NET for Batch Operations 选项,因此您将逻辑保留在您的C# 应用程序中。 This article也很全。

更新

是的,恐怕批量插入仅适用于导入的文件(从数据库中)。

我有一个 Java 项目的经验,我们需要插入数百万行(顺便说一句,数据来自应用程序外部)。

数据库是Oracle,所以我们当然使用了Oracle的多行插入。事实证明,Java 批量更新比 Oracle 的多值插入(所谓的“批量更新”)快很多

我的建议是:

如果您要操作的数据来自应用程序外部(如果它还没有在数据库中),我会说只使用 ADO.NET 批量插入。我认为这是你的情况。

注意:请记住,批量插入通常使用相同的查询进行操作。这就是让他们如此快速的原因。

【讨论】:

  • 批量插入只能从某些本地文件路径位置插入?实际上我们在现实生活中要做的是用户将在 UI 中提供这些行。然后中间层读取行并调用过程来插入行。所以我能想到的就是使用表类型参数作为存储过程的输入。然后要求中间层服务准备参数并进行批量插入。但是性能还是不好。我用谷歌搜索了批量插入,它似乎只能从文件位置插入,这可能不是我想要的。
  • 批量插入(编辑:通过 .Net 代码中的 SqlBulkCopy)适用于任何 DataTable,不必来自文件。见:msdn.microsoft.com/en-us/library/…
  • @Blorgbeard 我认为 Mandy 指的是 SQL 命令,而不是 .NET API。
  • @Evandro 你可能是对的。但是 SqlBulk 从中间层服务复制行会很快,并且可能是可以接受的。
  • 我还需要批量插入吗?我不认为 BulkInsert 100 万行是可以接受的,因为这会导致过多的网络流量。我尝试通过执行 SqlBulkCopy 一次插入 1000 行,但它仍然很慢
【解决方案2】:

在循环中调用 prc 会导致多次往返 SQL。

不确定您使用了哪种批处理方法,但您应该查看表值参数:Docs are here。您仍然需要批量写入。

您还需要考虑服务器上的内存。批处理(比如一次 10K)可能会慢一些,但可能会降低服务器上的内存压力,因为您一次要缓冲和处理一组数据。

表值参数提供了一种编组多行的简单方法 从客户端应用程序到 SQL Server 的数据,而无需 多次往返或特殊的服务器端逻辑来处理 数据。您可以使用表值参数来封装数据行 在客户端应用程序中,并以单个方式将数据发送到服务器 参数化命令。传入的数据行存储在一个表中 然后可以使用 Transact-SQL 操作的变量。

另一个选项是bulk insert。 TVP 受益于重复使用,因此这取决于您的使用模式。第一个链接有一个关于比较的注释:

使用表值参数与其他使用方式相当 基于集合的变量;但是,经常使用表值参数 对于大型数据集可以更快。与批量操作相比 启动成本高于表值参数,表值 参数在插入少于 1000 行时表现良好。

重用的表值参数受益于临时表 缓存。此表缓存可实现比同等产品更好的可扩展性 批量插入操作。

这里的另一个比较:Performance of bcp/BULK INSERT vs. Table-Valued Parameters

【讨论】:

    【解决方案3】:

    这是我之前使用 SqlBulkCopy 的示例。授予它我只处理大约 10,000 条记录,但它确实在查询运行后几秒钟插入了它们。我的字段名称相同,所以很容易。您可能必须修改 DataTable 字段名称。希望这会有所帮助。

    private void UpdateMemberRecords(Int32 memberId)
        {
    
        string sql = string.Format("select * from Member where mem_id > {0}", memberId);
        try {
            DataTable dt = new DataTable();
            using (SqlDataAdapter da = new SqlDataAdapter(new SqlCommand(sql, _sourceDb))) {
                da.Fill(dt);
            }
    
            Console.WriteLine("Member Count: {0}", dt.Rows.Count);
    
            using (SqlBulkCopy sqlBulk = new SqlBulkCopy(ConfigurationManager.AppSettings("DestDb"), SqlBulkCopyOptions.KeepIdentity)) {
                sqlBulk.BulkCopyTimeout = 600;
                sqlBulk.DestinationTableName = "Member";
                sqlBulk.WriteToServer(dt);
            }
        } catch (Exception ex) {
            throw;
        }
    }
    

    【讨论】:

    • 有人会将 int32 更改为字符串(例如,将此代码用于 guid)和你好 SQL 注入 :)
    • @Alex,如果你不能信任自己的程序员,这是你最不用担心的。
    • @PRMan 很不成熟的结论
    【解决方案4】:

    如果你有SQL2014,那么In-Memory OLTP的速度是惊人的; http://msdn.microsoft.com/en-au/library/dn133186.aspx

    【讨论】:

      【解决方案5】:

      根据您的最终目标,研究实体框架(或类似的)可能是个好主意。这将 SQL 抽象出来,这样您就不必在客户端应用程序中真正担心它,事情应该是这样的。

      最终,你可能会得到这样的结果:

      using (DatabaseContext db = new DatabaseContext())
      {
          for (int i = 0; i < 1000000; i++)
          {
              db.Table.Add(new Row(){ /* column data goes here */});
          }
          db.SaveChanges();
      }
      

      这里的关键部分(归结为许多其他答案)是实体框架处理构建实际的插入语句并将其提交到数据库。

      在上面的代码中,在调用SaveChanges 并发送所有内容之前,实际上不会将任何内容发送到数据库。

      我不太记得在哪里找到了它,但有研究表明,每隔一段时间打电话给SaveChanges 是值得的。从记忆中,我认为每 1000 个条目是提交到数据库的一个不错的选择。与每 100 个条目相比,提交每个条目并没有提供太多的性能优势,而 10000 则超过了限制。不过不要相信我的话,数字可能是错误的。不过,您似乎对事物的测试方面掌握得很好,所以请尝试一下。

      【讨论】:

      • 抽象出 SQL 会带来最佳性能吗?您的代码与 BCP 或 TVP 相比如何?我想你会发现它的代码更少但不是最优的(手头的问题)
      • @bryanmac 我没有对这种情况进行基准测试,没有。我只是从个人经验/研究中知道,在循环外使用SaveChanges()(或在有条件的内部,例如每 1000 次添加一次)会比在循环内使用它显着提高性能。如前所述,我相信 EF 将其归结为 T-SQL,您的回答中提到了这一点。
      • 阅读上面帖子的第三条评论。是的,它生成 T-SQL,但生成的是通用 T-SQL 与 TVP 所需的特定类型的 T-SQL。
      • 好的,这是一个在 100k Add() 调用后调用 SaveChanges() 的基准。所以一次插入了 10 万条记录。我发现的研究表明这是次优的,最好更频繁地进行。我相信每隔一段时间重新创建上下文也很好,因为它会减少上下文中存储的内容。我并不是说这是最好的解决方案,但出于可维护性的目的,我更喜欢这个选项,我认为它应该得到 OP 所追求的性能改进。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2023-03-21
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多