大量数据的插入性能下降(SQL Server/C#)

wou*_*ter 5 c# sql-server

我正在使用电子设备实时数字化波形(每个设备每秒产生大约1000个512字节数组 - 我们有12个设备).我在C#中为这些设备编写了一个客户端,大部分工作正常并且没有性能问题.

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

基本机制如下所示:

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();                
}       
Run Code Online (Sandbox Code Playgroud)

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

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

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

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
Run Code Online (Sandbox Code Playgroud)

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

所以我的第一个问题是:为什么SQL服务器上的运行速度比C#快?这与连接(本地tcp)有什么关系吗?

为了让事情变得更加混乱(对我而言),这个代码在生产服务器上的运行速度是原来的两倍(IBM刀片中心,32GB内存,与SAN的光纤连接,......文件系统操作非常快).我已经尝试过查看sql活动监视器,写入性能永远不会超过2MB /秒,但这可能也是正常的.我是一个完全新手的SQL服务器(事实上,关于一个称职的DBA的极端对立面).

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

Mat*_*eld 12

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

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
Run Code Online (Sandbox Code Playgroud)

那么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();
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

这种方法具有可SqlBulkCopy与之竞争的速度,但它通过运行您通过过程更新的内容的能力也可以产生更好的控制,并且还使得处理并发变得更加容易.

编辑 - >为了完整起见,此方法适用于SQL Server 2008及更高版本.看到没有SQL Server 2010这样的东西,我想我最好提一下.

  • @Jodrell - 这就是同义词的用途.每天使用不同的表本身就是一个糟糕的设计选择,但同义词确实为您提供了一个get-out子句. (2认同)