【问题标题】:Slow insert performance with large amounts of data (SQL Server / C#)大量数据的插入性能缓慢(SQL Server / C#)
【发布时间】:2013-10-15 20:33:16
【问题描述】:

我正在使用实时数字化波形的电子设备(每个设备每秒生成大约 1000 512 字节数组 - 我们有 12 个设备)。我已经用 C# 为这些设备编写了一个客户端,它在大多数情况下都可以正常工作并且没有性能问题。

但是,应用程序的要求之一是存档,并且 Microsoft SQL Server 2010 被强制用作存储机制(我无法控制)。数据库布局非常简单:每天每台设备有一个表(“Archive_Dev02_20131015”等)。每个表都有一个Id 列、一个timestamp 列、一个Data 列(varbinary) 和另外20 个带有一些元数据的整数列。 Idtimestamp 上有一个聚集主键,timestamp 上有另一个单独的索引。我幼稚的方法是将客户端应用程序中的所有数据排队,然后使用SqlCommand 每隔 5 秒将所有数据插入数据库。

基本机制如下所示:

using (SqlTransaction transaction = connection.BeginTransaction()
{
    //Beginning of the insert sql statement...
    string sql = "USE [DatabaseName]\r\n" +
                 "INSERT INTO [dbo].[Archive_Dev02_20131015]\r\n" + 
                 "(\r\n" +
                 "   [Timestamp], \r\n" +
                 "   [Data], \r\n" +
                 "   [IntField1], \r\n" +
                 "   [...], \r\n" +                         
                 ") \r\n" +
                 "VALUES \r\n" +
                 "(\r\n" +
                 "   @timestamp, \r\n" + 
                 "   @data, \r\n" + 
                 "   @int1, \r\n" +
                 "   @..., \r\n" +  
                 ")";

    using (SqlCommand cmd = new SqlCommand(sql))
    {
        cmd.Connection = connection;
    cmd.Transaction = transaction;

    cmd.Parameters.Add("@timestamp", System.Data.SqlDbType.DateTime);
    cmd.Parameters.Add("@data", System.Data.SqlDbType.Binary);
    cmd.Parameters.Add("@int1", System.Data.SqlDbType.Int);

    foreach (var sample in samples)
    {
            cmd.Parameters[0].Value = amples.ReceiveDate;
            cmd.Parameters[1].Value = samples.Data;       //Data is a byte array
            cmd.Parameters[1].Size  = samples.Data.Length;
            cmd.Parameters[2].Value = sample.IntValue1;
             ...

            int affected = cmd.ExecuteNonQuery();

            if (affected != 1)
            {
                throw new Exception("Could not insert sample into the database!");
            }
          }
       }
   }

   transaction.Commit();                
}       

总结一下:一批 1 个事务,带有一个生成插入语句并执行它们的循环。

结果证明这种方法非常非常慢。在我的机器上(i5-2400 @ 3.1GHz,8GB RAM,使用 .NET 4.0 和 SQL Server 2008,镜像中有 2 个内部 HD,一切都在本地运行),从 2 个设备保存数据大约需要 2.5 秒,所以每 5 秒保存 12 台设备是不可能的。

为了比较,我编写了一个小的 SQL 脚本(实际上我提取了 C# 使用 sql server profiler 运行的代码),它直接在服务器上执行相同的操作(仍在我自己的机器上运行):

set statistics io on
go

begin transaction
go

declare @i int = 0;

while @i < 24500 begin
SET @i = @i + 1

exec sp_executesql N'USE [DatabaseName]                                                                
INSERT INTO [dbo].[Archive_Dev02_20131015]                                                      
(                                                                                      
   [Timestamp],                                                                        
   [Data],                                                                             
   [int1],                                                                       
    ...                                                    
   [int20]                                                                                
)                                                                                      
VALUES                                                                                 
(                                                                                      
   @timestamp,                                                                         
   @data,                                                                              
   @compressed,                                                                        
   @int1,                                                                           
   ...                                                                  
   @int20,                                                                   

)',N'@timestamp datetime,@data binary(118),@int1 int,...,@int20 int,',
@timestamp='2013-10-14 14:31:12.023',
@data=0xECBD07601C499625262F6DCA7B7F4AF54AD7E074A10880601324D8904010ECC188CDE692EC1D69472329AB2A81CA6556655D661640CCED9DBCF7DE7BEFBDF7DE7BEFBDF7BA3B9D4E27F7DFFF3F5C6664016CF6CE4ADAC99E2180AAC81F3F7E7C1F3F22FEEF5FE347FFFDBFF5BF1FC6F3FF040000FFFF,
@int=0,
...
@int20=0
end

commit transaction

这确实(imo,但我可能错了;))同样的事情,只是这次我使用 24500 次迭代,一次模拟 12 台设备。查询大约需要 2 秒。如果我使用与 C# 版本相同的迭代次数,查询将在不到一秒的时间内运行。

所以我的第一个问题是:为什么它在 SQL Server 上比在 C# 上运行得更快?这和连接(本地tcp)有什么关系吗?

为了让事情变得更加混乱(对我来说),这段代码在生产服务器上的运行速度是原来的两倍(IBM Bladecenter、32GB 内存、与 SAN 的光纤连接……文件系统操作非常快)。我试过查看 sql 活动监视器,写入性能从未超过 2MB/秒,但这也可能是正常的。我是 sql server 的完全新手(实际上与称职的 DBA 截然相反)。

关于如何使 C# 代码更高效的任何想法?

【问题讨论】:

  • 您是否尝试为每个命令删除 USE [DatabaseName]
  • 大概您使用for循环或foreach循环来遍历每个设备?您可以尝试在单独的线程上运行并行 for 循环(假设您不关心设备运行的顺序)。
  • 您可能想查看 SqlBulkCopy - msdn.microsoft.com/en-us/library/…
  • 我会调查建立连接所花费的时间。 (你有方便的连接字符串吗)我也会研究 SQL Bulk insert 来聚合插入语句
  • 您每秒生成 6 Mbits 的数据(512 字节 * 1000 * 12 个设备),加上开销等,您的 SQL Server 应该至少有 12 Mbit 的网络吞吐量。如果您只想每 5 秒插入 0.5 秒,那么您需要一个“管道”连接到您的 SQL Server,至少为 120 Mbit/Sec可用的网络带宽。 你有吗?请注意,您的 NIC 卡本身并不能确定这一点,它受到客户端和服务器之间最慢/负载最多的设备的限制。

标签: c# sql-server


【解决方案1】:

到目前为止,加载此类数据的最佳方法是使用表值参数和获取数据的存储过程。一个非常简单的表类型和使用它的过程的例子是:

CREATE TYPE [dbo].[StringTable]
AS TABLE ([Value] [nvarchar] (MAX) NOT NULL)
GO

CREATE PROCEDURE [dbo].[InsertStrings]
  @Paths [dbo].[StringTable] READONLY
AS
INSERT INTO [dbo].[MyTable] ([Value])
SELECT [Value] FROM @Paths
GO

那么 C# 代码将类似于以下内容(请记住,我已将其输入到 S/O 编辑器中,因此可能存在拼写错误):

private static IEnumerable<SqlDataRecord> TransformStringList(ICollection<string> source)
{
     if (source == null || source.Count == 0)
     {
         return null;
     }
     return GetRecords(source, 
                       () => new SqlDataRecord(new SqlMetaData("Value", SqlDbType.NVarChar, -1)), 
                       (record, value) => record.SetString(0, value));
}

private static IEnumerable<SqlDataRecord> GetRecords<T>(IEnumerable<T> source, Func<SqlDataRecord> factory, Action<SqlDataRecord, T> hydrator)
{
    SqlDataRecord dataRecord = factory();
    foreach (var value in source)
    {
        hydrator(dataRecord, value);
        yield return dataRecord;
    }
}

private InsertStrings(ICollection<string> strings, SqlConnection connection)
{
    using (var transaction = connection.BeginTransaction())
    {
        using (var cmd = new SqlCommand("dbo.InsertStrings"))
        {
            cmd.Connection = connection;
            cmd.Transaction = transaction;
            cmd.CommandType = CommandType.StoredProcedure;
            cmd.Parameters.Add(new SqlParameter("@Paths", SqlDbType.Structured) { Value = TransformStringList(strings) };
            cmd.ExecuteNonQuery();
        }
    }
}

这种方法的速度可以与 SqlBulkCopy 相媲美,但它还可以通过运行您通过过程更新的内容来实现更好的控制,并且还可以更轻松地处理并发性。

编辑 -> 出于完整性考虑,此方法适用于 SQL Server 2008 及更高版本。鉴于没有 SQL Server 2010 这样的东西,我想我最好提一下。

【讨论】:

  • 您不需要存储过程来使用 TVP。如果每秒的调用计数很低,它甚至不会提高性能。为这个概念 +1。
  • @usr - 不,你没有,但这是我通常在这种情况下给出的例子。虽然它不会提高性能,但它确实可以防止数据逻辑通过各种代码位蔓延,并提供一个简单的点来应用权限。
  • TVP 将是进行此类批量操作的方式。您不需要使用存储过程,但为了维护的好处 - 为什么不呢?
  • 我是一名转行到 DBA 的程序员 - 曾经像这样嵌入 SQL,现在无法忍受程序员这样做。正如 OP 所说,他是 SQL 的初学者。这通常是这种情况——SQL 新手可以获得查询以返回结果,但不知道如何优化该查询。开发人员换工作,表增长,然后 1 秒的查询需要 10 分钟,但它嵌入在无法更改的应用程序中......
  • @Jodrell - 这就是同义词的用途。每天拥有不同的表本身就是一个糟糕的设计选择,但同义词确实为您提供了一个退出条款。
【解决方案2】:

在sql server中,

CREATE TYPE [dbo].[ArchiveData]
AS TABLE (
    [Timestamp] [DateTime] NOT NULL,
    [Data] [VarBinary](MAX) NOT NULL,
    [IntField1] [Int] NOT NULL,
    [...] [Int] NOT NULL,
    [IntField20] NOT NULL)
GO

那么您的代码应该类似于下面的代码。此代码使用Table Value Parameter 一次插入所有待处理数据,是一个事务。

请注意省略了缓慢且不必要的USE DATABASE 并使用逐字字符串 (@"") 以使代码更具可读性。

// The insert sql statement.
string sql =
@"INSERT INTO [dbo].[Archive_Dev02_20131015] (
    [Timestamp],
    [Data],
    [IntField1],
    [...],                         
    [IntField20])
 SELECT * FROM @data;";

using (SqlCommand cmd = new SqlCommand(sql))
{
    using (SqlTransaction transaction = connection.BeginTransaction()
    {
        cmd.Connection = connection;
        cmd.Transaction = transaction;
        cmd.Parameters.Add(new SqlParameter("@data", SqlDbType.Structured)
            {
                Value = TransformSamples(samples);
            });

        int affected = cmd.ExecuteNonQuery();
        transaction.Commit();
    }
}

...

private static IEnumerable<SqlDataRecord> TransformSamples(
        {YourSampleType} samples)
{
    var schema = new[]
    {
        new SqlMetaData("Timestamp", SqlDbType.DateTime),
        new SqlMetaData("Timestamp", SqlDbType.VarBinary, -1),
        new SqlMetaData("IntField1", SqlDbType.Int),
        new SqlMetaData("...", SqlDbType.Int),
        new SqlMetaData("IntField20", SqlDbType.Int)
    };

    foreach (var sample in samples)
    {
        var row = new SqlDataRecord(schema);
        row.SetSqlDate(0, sample.ReceiveDate);
        row.SetSqlBinary(1, sample.Data);
        row.SetSqlInt(2, sample.Data.Length);
        row.SetSqlInt(..., ...);
        row.SetSqlInt(24, sample.IntValue19);
        yield return row;
    }
}

【讨论】:

  • 您实际上不需要为每行实例化一个新的 SqlDataRecord 实例。
【解决方案3】:

按照 juharr 在上述其中一个 cmets 中的建议,我已经设法通过使用 SqlBulkInsert 解决了我的问题。

我主要是根据这篇文章将我的数据转换为可以批量插入数据库的DataTable:

Convert generic List/Enumerable to DataTable?

感谢您的所有回答!

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2019-10-18
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-09-17
    相关资源
    最近更新 更多