C#和SQL Server – 使用存储过程“一次性”删除多行的最佳方法

我知道在SO中存在许多相同的问题,我的问题是如果我想删除说1K行而不是很少,给定RecordIDList ,我可以避免使用DataTable ,并将列表翻译成一份声明:

 string ParmRecordsToDelete_CsvWhereIN = "(" for(int CurIdx=0; CurIdx < RecIdsToDelete.Count; CurIdx++) { ParmRecordsToDelete_CsvWhereIN += RecIdsToDelete[CurIdx] + ", "; //this method to create passed parameter //logic to remove on last Coma on last Index.. //or use stringJoin and somehow remove the last coma } ParRecordsToDelete_CsvWhereIN +=")"; 

这将创建像“(’1’,’2’,’3’……)”

然后创建一个SqlCommand来调用存储过程:

 Delete * From @TblName WHERE @ColName IN @RecordsToDeleteCsvWhereIN 

这是一种有效的方法吗? 单个参数的长度是否有限制? 我想这是N/VARCHAR(MAX)长度。

我想如果它不是一个有点黑客的解决方案,它不会受到长度的限制……

什么是最好的快速解决方案,还是我在正确的轨道上?

您可以使用表值参数来处理此问题。 应用程序层看起来像

C#

 var tvp = new DataTable(); tvp.Columns.Add("Id", typeof(int)); foreach(var id in RecIdsToDelete) tvp.Rows.Add(new {id}); var connection = new SqlConnection("your connection string"); var delete = new SqlCommand("your stored procedure name", connection) { CommandType = CommandType.StoredProcedure }; delete .Parameters .AddWithValue("@ids", tvp) .SqlDbType = SqlDbType.Structured; delete.ExecuteNonQuery(); 

SQL

 IF NOT EXISTS(SELECT * FROM sys.table_types WHERE name = 'IDList') BEGIN CREATE TYPE IDList AS TABLE(ID INTEGER) END CREATE PROCEDURE School.GroupStudentDelete ( @IDS IDLIST READONLY ) AS SET NOCOUNT ON; BEGIN TRY BEGIN TRANSACTION DECLARE @Results TABLE(id INTEGER) DELETE FROM TblName WHERE Id IN (SELECT ID FROM @IDS) COMMIT TRANSACTION END TRY BEGIN CATCH PRINT ERROR_MESSAGE(); ROLLBACK TRANSACTION THROW; -- Rethrow exception END CATCH GO 

与构建字符串相比,这种方法有许多优点

  • 您可以避免在应用程序层中创建创建查询,从而创建关注点
  • 您可以更轻松地测试执行计划并优化查询
  • 您不太容易受到SQL注入攻击,因为您的给定方法无法使用参数化查询来构建IN子句
  • 代码更具可读性和说明性
  • 你最终不会构建过长的字符串

性能

有关TVP在大型数据集上的性能的一些考虑因素。

因为TVP是变量,所以它们不编译统计数据。 这意味着查询优化器有时可以捏造执行计划。 如果发生这种情况,有两种选择:

  • 在索引是一个问题的任何TVP语句上设置OPTION (RECOMPILE)
  • 将TVP写入本地临时文件并在那里设置索引

这是关于TVP的一篇很棒的文章,其中有关于性能考虑的一个很好的部分,以及什么时候可以期待。

因此,如果您担心对字符串参数进行限制,那么表值参数可能是最佳选择。 但最终,如果不了解更多关于您正在使用的数据集,很难说。

IN @Parameter不是一个选项,这样的东西不起作用。 您可以将ID硬连接到IN (1,2,3,4...) ,但这很糟糕。 我测量了一些30000 ID的轻量级选择:

 TVP: 339ms first run, 319ms second run hard wired: 67728ms first run, 42ms second run 

如您所见,当SQL服务器必须解析大量字符串时,它需要很长时间。 在第二次运行时,查询计划可以从执行计划缓存中获取,不幸的是,具有较大的id范围,这是极不可能的。 它只是浪费执行计划缓存。

TVP可以毫无问题地扩展到数百万个ID,硬连线字符串会导致sql server失败,查询失败的次数少于100000个。 它与字符串最大长度无关,它只是无法处理它。

顺便说一句。 如果您构建这样的字符串,请使用StringBuilder或string.Join,在循环中追加字符串是非常低效的。

更好的方法是使用表值参数。 您必须为参数定义类型,但它比指定字符串中的值更有效,尤其是数值或日期值,因为服务器不必解析字符串以获取单个值。

我不确定你的ID类型是什么,但如果它是’BIGINT’,例如:

 IF NOT EXISTS (SELECT * FROM dbo.systypes WHERE name='IDList') CREATE TYPE IDList AS TABLE (Id BIGINT); GO 

要初始化类型,然后使用它创建存储过程,如下所示:

 IF NOT EXISTS (SELECT * FROM dbo.sysobjects WHERE name='DeleteMultipleRecords') EXECUTE sp_executesql N'CREATE PROCEDURE DeleteMultipleRecords AS BEGIN SET NOCOUNT ON; END' GO ALTER PROCEDURE [dbo].[DeleteMultipleRecords] @IDs IDList READONLY AS BEGIN SET NOCOUNT ON DELETE FROM [Table] WHERE Id IN (SELECT Id FROM @IDs) END 

您也可以将它与C#中的动态SQL一起使用。