我正在使用电子设备实时数字化波形(每个设备每秒产生大约1000个512字节数组 - 我们有12个设备)。我在C#中为这些设备编写了一个客户端,大部分工作正常并且没有性能问题。
但是,该应用程序的一个要求是归档,并且Microsoft SQL Server 2010被强制作为存储机制(在我的控制范围之外)。数据库布局非常简单:每台设备每天有一个表(“Archive_Dev02_20131015”等)。每个表都有一个Id
列,一个timestamp
列,一个Data
列(varbinary
)和另外20个带有一些元数据的整数列。 Id
和timestamp
上有一个群集主键,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脚本(实际上是我用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
这样做(imo,但我可能错了;))同样的事情,只有这次我使用24500次迭代,一次模拟12个设备。查询大约需要2秒钟。如果我使用与C#版本相同的迭代次数,则查询将在不到一秒的时间内运行。
所以我的第一个问题是:为什么它在SQL服务器上比在C#中运行得更快?这与连接(本地tcp)有什么关系吗?
让事情变得更加混乱(对我来说)这个代码在生产服务器上运行速度慢两倍(IBM刀片中心,32GB内存,光纤连接到SAN,......文件系统操作非常快)。我已经尝试过查看sql活动监视器,写入性能永远不会超过2MB /秒,但这也许是正常的。我是sql server的完全新手(实际上是关于一个称职的DBA的极端对手)。
关于如何使C#代码更高效的任何想法?
答案 0 :(得分: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
那么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
相媲美的速度,但它也可以通过运行您通过过程更新的内容来产生更好的控制,并且还使处理并发变得更加容易。
编辑 - &gt;为了完整起见,这种方法适用于SQL Server 2008及更高版本。看到没有SQL Server 2010这样的东西,我想我最好提一下。
答案 1 :(得分: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;
}
}
答案 2 :(得分:2)
我已经设法通过使用juharr在上面的一条评论中建议的SqlBulkInsert来解决我的问题。
我主要基于这篇文章将我的数据转换为可以批量插入数据库的DataTable:
Convert generic List/Enumerable to DataTable?
感谢您的所有答案!