【问题标题】:C# & SQL Server - best way to delete multiple Rows in "one go" using a stored procedureC# & SQL Server - 使用存储过程“一次性”删除多行的最佳方法
【发布时间】:2016-03-25 17:46:24
【问题描述】:

我知道这里有很多相同的主题问题,我的问题是如果我想删除大约 1K 行而不是少数,给定RecordIDList<int>,我可以避免使用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) 的长度。

我想如果这不是一个有点骇人听闻的解决方案,它就不会受到长度的限制......

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

【问题讨论】:

  • 如何获取要删除的Id?它们是否与用户相关(用户选择要删除的这些项目)?如果没有,你能在程序里面直接找到它们吗?另外,你用什么来访问数据? ADO.NET?实体框架?
  • 这可以做到在数据库上创建一个接受列表的函数这里是我目前使用 Select 语句的示例,但您将不得不乱搞,因为良好的学习体验使用类似的东西这个Declare @SomeList varchar(8000) SET @SiteList = '1,2,3,4,5,6,7' SELECT * from tblSomeTable WHERE Some_KeyID IN (SELECT Item FROM fnParseInt(@SomeList,','))
  • 另外你只需要说Delete FROM TABLE not Delete * FROM我建议你开始阅读一些基本的Sql教程
  • @jpgrassi 我读了一个包含所有内容+列的表,根据文件存在决定删除哪些记录,如果为 false,则将 id 添加到列表中。取 list ,按 ids 删除。没有 EnTT FW
  • @MethodMan 我对 fn 不太熟悉,到目前为止,我更喜欢 SpMan,您能否在 list fn 上提供更广泛的示例代码?

标签: c# performance sql-server-2012 sql-server-2014


【解决方案1】:

您可以使用表值参数来处理它。应用层看起来像

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 写入本地 temp 并在那里设置索引

Here is a great article on TVP's 有一个关于性能考虑的很好的部分,以及什么时候会发生什么。

因此,如果您担心达到字符串参数的限制,表值参数可能是您的选择。但最后,如果不进一步了解您正在使用的数据集,就很难说。

【讨论】:

  • 感谢您详细描述的答案,我知道昨天的链接,我正在搜索我的问题时的链接,今天通过再次阅读“传递表值参数”部分关于与谷歌匹配的 TVP 类型,如果你愿意的话,我会引导我到你自己的类的自定义实现,[developmentsimplyput.blogspot.co.il/2012/10/…] 所以现在我将对两者进行测试/基准测试。我几周前知道TVP,当没有更好的选择时,我通过DataTable,在那个链接中有其他自定义类型..
  • DECLARE @Results TABLE(id INTEGER) 是如何使用的?那是做什么用的?我的项目中有删除工作,但从 ExecuteNonQuery() 返回 -1
【解决方案2】:

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 个 id 的查询失败。它与字符串最大长度无关,它只是无法处理它。

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

【讨论】:

  • 感谢您在性能上指定的答案和详细信息,我正忙于实现自定义 : ICollection&lt;T&gt; , IEnumerator&lt;T&gt;,内部 T[] 作为内部列表“其中 T 是 SqlDataRecord”所有这些而不是沉重的DataTable 和所有的努力只是为了以后看到@James 在往返中的评论.. 我想知道DataTable 是否真的作为一个对象传递而不是在引擎盖下逐行传递......跨度>
【解决方案3】:

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

我不确定您的 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 一起使用。

【讨论】:

  • 在这种特定情况下,类型是 Int32,我使用 TVP,在一堆记录而不是几个或一个列列表上写入服务器,并将其作为参数传递,我会在这种情况下,尝试使用(Google)“实现 IEnumerable 和 IEnumerator 的自定义编写的类”而不是 DataTable
  • @Robb_2015:在这种情况下编写自定义类可能没有意义。您希望将删除分解为可管理的块有两个原因,1. 减少锁定表的时间过长,2. 减少从服务器来回发送的字节数,以及 3. 减少客户端和服务器。自定义枚举器只能减少客户端的内存使用量,但会以 #3 为代价,这可能更重要。
  • 我想到了#3,但认为往返发生在两者中或两者都不发生......所以我不介意内存使用与往返开销一样多,我想知道什么时候更好如果是这样,则使用自定义类,如果对于每条记录都有针对服务器的单独操作
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2010-09-15
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多