真正可怕的方法是将每个INSERT
语句作为自己的批处理执行:
第 1 批:
INSERT INTO Entries (id, name) VALUES (1, 'Ian Boyd);
第 2 批:
INSERT INTO Entries (id, name) VALUES (2, 'Bottlenecked);
第 3 批:
INSERT INTO Entries (id, name) VALUES (3, 'Marek Grzenkowicz);
第 4 批:
INSERT INTO Entries (id, name) VALUES (4, 'Giorgi);
第 5 批:
INSERT INTO Entries (id, name) VALUES (5, 'AMissico);
注意:出于说明的目的,参数化、错误检查和任何其他挑剔的内容都被省略了。
这是真正的,可怕的,可怕的做事方式。它提供了非常糟糕的性能,因为您每次都会遭受网络往返时间的困扰。
一个更好的解决方案是将所有INSERT
语句批处理为一个批处理:
第 1 批:
INSERT INTO Entries (id, name) VALUES (1, 'Ian Boyd');
INSERT INTO Entries (id, name) VALUES (2, 'Bottlenecked');
INSERT INTO Entries (id, name) VALUES (3, 'Marek Grzenkowicz');
INSERT INTO Entries (id, name) VALUES (4, 'Giorgi');
INSERT INTO Entries (id, name) VALUES (5, 'AMissico');
这样,您只需要往返一次。此版本具有巨大的性能优势;大约快 5 倍。
更好的是使用该VALUES
子句:
INSERT INTO Entries (id, name)
VALUES
(1, 'Ian Boyd'),
(2, 'Bottlenecked'),
(3, 'Marek Grzenkowicz'),
(4, 'Giorgi'),
(5, 'AMissico');
INSERT
与 5 个单独的 s 版本相比,这为您提供了一些性能改进;它让服务器做它擅长的事情:在集合上操作:
- 每个触发器只需操作一次
- 外键检查一次
- 唯一约束检查一次
SQL Sever 喜欢对数据集进行操作;这是维京人的地方!
参数限制
为清楚起见,上述 T-SQL 示例删除了所有参数化内容。但实际上你想参数化查询
- 节省服务器不必编译每个 T-SQL 批处理的性能奖励并没有那么多(尽管在高速批量导入期间,节省解析时间确实可以加起来。)
- 与其说是避免 SQL 注入;因为你已经是一个优秀的开发者了
QuotedString(firstName)
- 但要避免在数千兆字节的临时查询计划中淹没服务器的查询计划缓存。(我见过 SQL Server 的工作集,即 RAM,是 2 GB 的未参数化 SQL 查询计划)
但布鲁诺有一个重要的观点。SQL Server 的驱动程序只允许您在批处理中包含 2,100 个参数。上面的查询有两个值:
@id,@名称
如果您在一个批次中导入 1,051 行,即 2,102 个参数 - 您将收到错误消息:
此 RPC 请求中提供的参数过多
这就是为什么我通常一次插入 5 或 10 行。每批添加更多行并不能提高性能 - 收益递减。
它使参数数量保持在较低水平,不会接近 T-SQL 批处理大小限制。还有一个事实是,一个VALUES
子句无论如何都限制为 1000 个元组。
实施它
您的第一种方法很好,但您确实有以下问题:
- 参数名称冲突
- 无限制的行数(可能达到 2100 参数限制)
所以目标是生成一个字符串,例如:
INSERT INTO Entries (id, name) VALUES
(@p1, @p2),
(@p3, @p4),
(@p5, @p6),
(@p7, @p8),
(@p9, @p10)
我会在我的裤子座位上更改你的代码
IEnumerable<Entry> entries = GetStuffToInsert();
SqlCommand cmd = new SqlCommand();
StringBuilder sql = new StringBuilder();
Int32 batchSize = 0; //how many rows we have build up so far
Int32 p = 1; //the current paramter name (i.e. "@p1") we're going to use
foreach(var entry in entries)
{
//Build the names of the parameters
String pId = String.Format("@p{0}", p); //the "Id" parameter name (i.e. "p1")
String pName = String.Format("@p{0}", p+1); //the "Name" parameter name (i.e. "p2")
p += 2;
//Build a single "(p1, p2)" row
String row = String.Format("({0}, {1})", pId, pName); //a single values tuple
//Add the row to our running SQL batch
if (batchSize > 0)
sb.AppendLine(",");
sb.Append(row);
batchSize += 1;
//Add the parameter values for this row
cmd.Parameters.Add(pID, System.Data.SqlDbType.Int ).Value = entry.Id;
cmd.Parameters.Add(pName, System.Data.SqlDbType.String).Value = entry.Name;
if (batchSize >= 5)
{
String sql = "INSERT INTO Entries (id, name) VALUES"+"\r\n"+
sb.ToString();
cmd.CommandText = sql;
cmd.ExecuteNonQuery();
cmd.Parameters.Clear();
sb.Clear();
batchSize = 0;
p = 1;
}
}
//handle the last few stragglers
if (batchSize > 0)
{
String sql = "INSERT INTO Entries (id, name) VALUES"+"\r\n"+
sb.ToString();
cmd.CommandText = sql;
cmd.ExecuteNonQuery();
}