【问题标题】:Most Efficient (Fast) T-SQL DELETE For Many Rows?多行最高效(快速)的 T-SQL 删除?
【发布时间】:2009-04-03 16:03:16
【问题描述】:

我们的服务器应用程序全天以每秒 1000-2000 行的速度接收有关要添加到数据库的行的信息。表中有两个互斥列唯一标识一行:一个是数字标识符,称为“tag”,另一个是 50 个字符的字符串,称为“longTag”。一行可以有一个标签或一个longTag;不是两者兼而有之。

从套接字进入的每一行可能已经存在也可能不存在于表中。如果存在,则必须使用新信息更新该行。如果它不存在,则必须添加它。我们使用的是 SQL 2005,在某些情况下甚至使用 SQL 2000,因此我们不能使用新的 MERGE 关键字。

我现在这样做的方式是构建一个巨大的 DELETE 语句,如下所示:

DELETE from MyRecords
WHERE tag = 1
OR tag = 2
OR longTag = 'LongTag1'
OR tag = 555

...每个传入的行都有自己的 'OR tag = n' 或 'OR longTag = 'x'' 子句。

然后我使用 ISQLXMLBulkLoad 执行 XML 批量加载以一次加载所有新记录。

巨大的 DELETE 语句有时会超时,需要 30 秒或更长时间。我不知道为什么。

当记录从套接字进来时,它们必须要么被插入,要么必须替换现有的行。我的做法是不是最好的做法?

编辑:新行与替换行的比率将非常倾向于新行。在我看到的来自生产的数据中,每次更正通常会有 100-1000 行新行。

编辑 2:插入和删除都必须作为单个事务处理。如果插入或删除失败,它们都必须回滚,使表保持在插入和删除开始之前的相同状态。

编辑 3:关于 NULL 标签。我需要先简单地描述一下这个系统。这是一个交易系统的数据库。 MyTable 是一个包含两种交易的交易表:所谓的“日内交易”和所谓的“开仓头寸”。日间交易只是简单的交易——如果您是期权交易员并且您进行了交易,那么该交易将是该系统中的日间交易。开放头寸基本上是您迄今为止的投资组合的摘要。开仓头寸和日间交易都存储在同一个表中。日内交易有标签(longTags 或数字标签),而开仓头寸没有。开仓可以有重复的行——这是正常的。但是日间交易不能有重复的行。如果当日交易与数据库中已有记录的标签相同,则表中的数据将替换为新数据。

所以tag和longTag中的值有4种可能:

1) 标签非零且 longTag 为空:这是一个带有数字标识符的日间交易。 2) tag 为零,longTag 有一个非空字符值。这是带有字母数字标识符的日间交易。 3) tag 为零,longTag 为空:这是一个开仓。 4) tag 非零且longTag 有一个非空字符值。我们的服务器软件阻止了这种情况的发生,但如果发生这种情况,longTag 将被忽略,并且将被视为与案例 #1 相同。同样,这不会发生。

【问题讨论】:

  • 只是出于好奇,你在做什么以如此快的速度生成数据?
  • 您能否详细说明您的 tag 或 longTag 是否可以变为 NULL?此信息对于索引问题可能很有趣。而且 - 如果它们是互斥的,它们不能被折叠成一列吗?
  • 这是一个交易服务器。每条记录都是股票或期权交易。
  • 我会详细说明——我要去开会。 :) 大约 30 分钟后回来。
  • @Tomalak:我添加了另一个关于 NULL 标签或 longTags 的编辑。它们也不能被折叠,因为其他系统是围绕它构建的——一些我们无法控制的系统。基本上,我致力于在我之前做出的许多非常糟糕的决定。我希望我能改变很多事情。

标签: sql-server performance tsql


【解决方案1】:

我认为将巨大的 DELETE 语句拆分为 2 个 DELETE 可能会有所帮助。

1个DELETE处理tag和一个单独的DELETE处理longTag。 这将有助于 SQL Server 选择有效地使用索引。

当然,您仍然可以在 1 个 DB 往返中触发 2 个 DELETE 语句。

希望对你有帮助

【讨论】:

  • 当你说“in 1 DB round-trip”时,我认为关键是你可以将它包装在一个事务中,这样你就有了完整的回滚能力。
  • 我的意思是 1 DB 往返。正如 John 提到的 DELETE 的高频率和高容量,减少 50% 的往返应该会有所帮助。
  • /* 2 次往返 / string sql1 = "DELETE FROM MyRecords WHERE tag IN (1,2,55)"; string sql2 = "从 MyRecords 中删除 longTag IN ('LongTag1')"; / 1 次往返 */ string sql = "DELETE FROM MyRecords WHERE tag IN (1,2,55); DELETE FROM MyRecords WHERE longTag IN ('LongTag1')";
【解决方案2】:

一个 OR(或一个 in)几乎就像每个 OR 操作数是一个不同的查询一样工作。也就是说,它变成了一次表扫描,对于每一行,数据库都必须将每个 OR 操作数作为谓词进行测试,直到找到匹配项或操作数用完为止。

将其打包的唯一原因是使其成为一个逻辑工作单元。你也可以在一个事务中包装一堆删除,并且只有在全部成功完成后才提交。

Quassnoi 提出了一个有趣的建议——使用表格——但由于他随后使用了 IN 和 OR,因此结果相同。

但是试试这个。

创建一个镜像您的真实表的新表。称它为 u_real_table。在 tag 和 longTag 上对其进行索引。

将所有传入的数据放入 u_real_table。

现在,当您准备好进行批量处理时,改为加入镜像表或标签上的真实表。从真实表中,删除 u_real_table 中所有标记的行:

delete real_table from real_table a 
   join u_real_table b on (a.tag = b.tag);
insert into real_table select * 
   from u_real_table where tag is not null;

看看我们在这里做了什么?由于我们只在标签上加入,因此标签索引被使用的可能性更大。

首先我们删除了所有新内容,然后我们插入了新的替换内容。我们也可以在这里进行更新。哪个更快取决于您的表结构及其索引。

我们不必编写脚本来执行此操作,只需将记录插入到 u_real_table 中即可。

现在我们对 longTags 做同样的事情:

delete real_table from real_table a 
   join u_real_table b on (a.longTag = b.longTag);
insert into real_table select * 
   from u_real_table where longTag is not null;

最后,我们清除 u_real_table:

delete from u_real_table;

显然,我们将每个删除/插入对包装在一个事务中,这样删除只有在后续插入成功时才变为真实,然后我们将整个东西包装在另一个事务中。因为它是一个逻辑工作单元。

这种方法减少了您的手动工作,减少了人为错误的可能性,并且有一定的机会加快删除速度。

请注意,这依赖于缺少的标签和 longTags 是否正确为空,而不是零或空字符串。

【讨论】:

  • +1,我认为是最好的建议。您还可以使用此表执行 INSERT / WHERE NOT EXISTS + UPDATE,而不是 DELETE + INSERT
  • 您知道,我确实更新了我的服务器代码以实现该线程中概述的 DELETE,它的运行速度比我的原始代码快 4 倍!非常感谢您的建议。
【解决方案3】:

观看此视频,该视频展示了如何进行“蚕食”删除。该过程运行良好,绝对可以减少您看到的锁定/碰撞问题:

http://www.sqlservervideos.com/video/nibbling-deletes

【讨论】:

    【解决方案4】:

    这样的事情可以简化流程(您只需插入行,无论它们是否已经存在 - 不需要预先的 DELETE 语句):

    CREATE TRIGGER dbo.TR_MyTable_Merge 
       ON  dbo.MyTable 
       INSTEAD OF INSERT
    AS 
    BEGIN
      SET NOCOUNT ON;
    
      BEGIN TRANSACTION
    
      DELETE MyTable 
      FROM   MyTable t INNER JOIN inserted i ON t.tag = i.tag 
    
      DELETE MyTable 
      FROM   MyTable t INNER JOIN inserted i ON t.longTag = i.longTag
    
      INSERT MyTable 
      SELECT * FROM inserted
    
      COMMIT TRANSACTION
    
      SET NOCOUNT OFF;
    END
    

    编辑:以前合并的 DELETE 语句分为两个单独的语句,以实现最佳索引使用。

    根本不使用 DELETE,而是更新受影响/重复的行在索引上会更容易。

    【讨论】:

    • 我喜欢这样,但是如果使用 try/catch 而不是删除,而是在更新失败时使用 catch 块中发生的插入进行更新(对于新记录)。跨度>
    • 嗯...可能。我会考虑写一个替代方案。
    • 我刚刚向我的 OP 添加了一条评论,说明我的系统中的正常情况。正常情况是行将不存在,因此在绝大多数情况下 UPDATE 将失败。这对优化设计有何影响?
    • 我不确定我是否得到你的问题。上述触发器将使您无需预先进行任何手动删除。您只需向表中插入行,如果它们已经存在,触发器就会处理它们。
    【解决方案5】:

    也许:

    DELETE FROM MyRecords
    WHERE  tag IN (1, 2, 555) -- build a list
    OR longTag IN ('LongTag1')
    

    我怀疑索引会帮助您删除,但会大大减慢您的插入,所以我不会玩太多。但是我的直觉还不是很完美,你也许可以调整 FillFactor 或其他项目来解决这个问题,而我确实知道的一件事是你真的想对两者进行分析。

    另一种选择是将新插入加载到临时表(命名为InputQueue),然后加入 MyRecords 上的临时表以处理过滤更新。这也使得分两步进行更新变得容易:您可以删除标签和 longTags 作为单独的操作,这样可能会更有效。

    【讨论】:

    • 针对临时表存在将比在非平凡列表中快得多
    • @Joel:这比我做的快吗?
    • @annakata:什么临时表?能详细点吗?
    • 我无法知道它是否比你正在做的更快。我没有你的数据或系统。我只能推测。正如 annakata 指出的那样,第二个想法可能会有所改进,但我不能保证。到目前为止,对于所有答案,我最喜欢 Tomalak 的想法。
    • 是的,我只是假设性地说,因为我们确实没有足够的数据 - 临时表将是您自己创建的,只是 IN 是一个昂贵的东西来遍历存在的地方temp 很快,因为它一旦找到“true”就会返回
    【解决方案6】:

    使用 OR 可能会导致表扫描 - 你能把它分成四个语句吗?将每个包裹在事务中也可以加快处理速度。

    DELETE from MyRecords
    WHERE tag = 1
    
    DELETE from MyRecords
    WHERE tag = 2
    
    DELETE from MyRecords
    WHERE tag = 555
    
    DELETE from MyRecords
    WHERE longTag = 'LongTag1'
    

    【讨论】:

    • 实际数据中实际上可能有 1000-2000 次删除。如果我发送 2000 个 DELETE 是最优的吗?
    • 你的意思是你有1000-2000个不同的标签?
    • 是的,1000-2000 个不同的标签。
    【解决方案7】:

    您的表似乎没有在(tag)(longTag) 上建立索引

    建立两个索引:一个在(tag),一个在(longTag)

    如果您打算删除大量记录,则声明两个表变量,用值填充它们并像这样删除:

    DECLARE @tag TABLE (id INT);
    DECLARE @longTag TABLE (id VARCHAR(50));
    
    INSERT
    INTO  @tag
    VALUES (`tag1`)
    
    INSERT
    INTO  @tag
    VALUES (`tag2`)
    
    /* ... */
    
    INSERT INTO @longTag
    VALUES ('LongTag1')
    
    /* ... */
    
    
    DELETE
    FROM    MyRecords r
    WHERE   r.tag IN (SELECT * FROM @tag)
            OR r.longTag IN (SELECT * FROM @longTag)
    

    您也可以尝试执行两遍DELETE

    DELETE
    FROM    MyRecords r
    WHERE   r.tag IN (SELECT * FROM @tag)
    
    DELETE
    FROM    MyRecords r
    WHERE   r.longTag IN (SELECT * FROM @longTag)
    

    看看哪些语句运行时间更长,看看索引是否有问题。

    【讨论】:

    • 我确实有这两列的索引;尽管不可否认,它们的设计可能不是最优的。
    【解决方案8】:

    索引:

    考虑为 longTag 使用索引的持久计算列,该列存储 longTag 的校验和。您不是索引“LongTag1”,而是索引一个 4 字节的 int 值 (86939596)。

    那么您的查找 [希望*] 更快,您只需在查询/删除中包含 longTag 值。您的代码会稍微复杂一些,但索引可能会更有效率。

    * 需要测试

    【讨论】:

    猜你喜欢
    • 2014-06-22
    • 2017-12-25
    • 2018-09-30
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2014-09-24
    相关资源
    最近更新 更多