在while循环中是否需要显式事务?


11

SQL Server 2014:

我们有一个非常大的表(一亿行),我们需要在表上更新几个字段。

对于日志传送等,显然,我们也希望将其保持在很小的规模上。

如果让下面的代码运行一段时间,然后取消/终止查询,那么到目前为止已完成的工作将全部提交吗,还是我们需要添加显式的BEGIN TRANSACTION / END TRANSACTION语句以便我们可以随时取消?

DECLARE @CHUNK_SIZE int
SET @CHUNK_SIZE = 10000

UPDATE TOP(@CHUNK_SIZE) [huge-table] set deleted = 0, deletedDate = '2000-01-01'
where deleted is null or deletedDate is null

WHILE @@ROWCOUNT > 0
BEGIN
    UPDATE TOP(@CHUNK_SIZE) [huge-table] set deleted = 0, deletedDate = '2000-01-01'
    where deleted is null or deletedDate is null
END

Answers:


13

单个语句(DML,DDL等)本身就是事务。因此,是的,在循环的每次迭代之后(技术上:在每个语句之后),该UPDATE语句更改的任何内容都已自动提交。

当然总会有例外,对吧?可以通过SET IMPLICIT_TRANSACTIONS启用隐式事务,在这种情况下,第一条UPDATE语句将启动您必须进行COMMITROLLBACK结束的事务。这是一个会话级别设置,在大多数情况下默认情况下为OFF。

我们是否需要添加显式的BEGIN TRANSACTION / END TRANSACTION语句,以便我们可以随时取消?

不会。事实上,鉴于您希望能够停止该进程并重新启动,因此添加一个显式事务(或启用隐式事务)将是一个坏主意,因为在执行之前可能会停止该进程COMMIT。在那种情况下,您将需要手动发出COMMIT(如果您在SSMS中),或者如果您是通过SQL Agent作业运行的,则您将没有机会,并且最终可能会成为孤立事务。


另外,您可能需要设置@CHUNK_SIZE较小的数字。锁升级通常发生在单个对象上获得5000个锁的情况下。根据行的大小以及是否执行行锁与页锁,您可能会超过该限制。如果一行的大小使得每个页面只能容纳1或2行,那么即使它正在执行页面锁定,也可能总是会遇到这种情况。

如果表已分区,则可以选择将表的LOCK_ESCALATION选项(在SQL Server 2008中引入)设置为,AUTO以便在升级时仅锁定分区,而不锁定整个表。或者,对于任何表,您都可以将相同的选项设置为DISABLE,尽管您对此必须非常小心。有关详细信息,请参见ALTER TABLE

以下是一些有关锁升级和阈值的文档:锁升级(它适用于“ SQL Server 2008 R2和更高版本”)。这是一篇有关检测和修复锁升级的博客文章:Microsoft SQL Server中的锁定(第12部分-锁升级)


与确切的问题无关,但与问题中的查询有关,可以在此处进行一些改进(或者至少从查看的角度来看,似乎是这样):

  1. 对于您的循环,这样做WHILE (@@ROWCOUNT = @CHUNK_SIZE)会稍微好一点,因为如果在上一次迭代中更新的行数少于请求UPDATE的数量,那么就没有工作要做。

  2. 如果该deleted字段是一个BIT数据类型,那么是不是该值被确定是否deletedDate2000-01-01?为什么同时需要两者?

  3. 如果这两个字段是新字段,并且您按原样添加了它们,NULL那么它可能是联机/非阻塞操作,并且现在想要将它们更新为“默认”值,那么就没有必要了。从SQL Server 2012(仅企业版)开始,添加NOT NULL具有DEFAULT约束的列是非阻塞操作,只要DEFAULT的值是常数即可。因此,如果您尚未使用这些字段,只需NOT NULL使用DEFAULT约束删除并重新添加。

  4. 如果在执行此UPDATE时没有其他进程在更新这些字段,则如果将要更新的记录排队,然后仅处理该队列,则速度会更快。当前方法会影响性能,因为您每次必须重新查询表以获取需要更改的集合。相反,您可以执行以下操作,该操作仅在这两个字段上扫描表一次,然后仅发出针对性很强的UPDATE语句。随时停止该进程并稍后再启动它也不会有任何损失,因为队列的初始填充只会找到剩下的要更新的记录。

    1. 创建一个临时表(#FullSet),该表中只包含来自聚集索引的键字段。
    2. 创建具有相同结构的第二个临时表(#CurrentSet)。
    3. 通过插入#FullSet SELECT TOP(n) KeyField1, KeyField2 FROM [huge-table] where deleted is null or deletedDate is null;

      TOP(n)由于表的大小是在那里。由于表中有1亿行,您实际上并不需要用整个键集填充队列表,尤其是如果您计划如此频繁地停止该过程并稍后重新启动它时。因此,也许将其设置n为100万,然后让其运行到完成。您始终可以在运行100万个(甚至更少)的SQL Agent作业中安排此时间,然后等待下一个安排的时间再次开始。然后,您可以安排每20分钟运行一次,以便在的各组之间有一定的强制呼吸空间n,但仍会无人值守地完成整个过程。然后,当无事可做时,只需删除作业即可:-)。

    4. 循环执行:
      1. 通过类似的方法填充当前批次 DELETE TOP (4995) FROM #FullSet OUTPUT Deleted.KeyField INTO #CurrentSet (KeyField);
      2. IF (@@ROWCOUNT = 0) BREAK;
      3. 使用类似的方法进行更新: UPDATE ht SET ht.deleted = 0, ht.deletedDate='2000-01-01' FROM [huge-table] ht INNER JOIN #CurrentSet cs ON cs.KeyField = ht.KeyField;
      4. 清除当前集: TRUNCATE TABLE #CurrentSet;
  5. 在某些情况下,添加过滤索引有助于将索引SELECT馈入#FullSet临时表。以下是与添加此类索引有关的一些注意事项:
    1. WHERE条件应与查询的WHERE条件匹配,因此 WHERE deleted is null or deletedDate is null
    2. 在该过程的开始,大多数行都将符合您的WHERE条件,因此索引并不是那么有用。您可能要等到50%左右的某个位置才能添加。当然,由于几个因素的不同,它的帮助程度和最佳添加索引的时间也有所不同,因此这是反复试验的结果。
    3. 您可能必须手动更新STATS和/或重建索引以使其保持最新,因为基础数据的更改非常频繁
    4. 请务必记住,索引在帮助的同时SELECT会伤害,UPDATE因为索引是该操作期间必须更新的另一个对象,因此会有更多的I / O。这既可以使用过滤索引(由于与过滤器匹配的行越少,更新的索引就会缩小),又需要等待一会儿才能添加索引(如果一开始它并没有多大帮助,那么就没有理由额外的I / O)。

更新:请参阅我对与上述问题有关的问题的回答,以完整地实现上面建议的内容,包括跟踪状态并彻底取消的机制:sql server:以小块的形式更新大表上的字段:如何获取进度/状态?


在某些情况下,您在#4中提出的建议可能会更快,但这似乎增加了相当大的代码复杂性。我希望从简单开始,然后如果这不能满足您的需求,请考虑选择其他方法。
培根片,2015年

@BaconBits同意从简单开始。公平地说,这些建议并不意味着适用于所有情况。问题在于如何处理非常大的表(超过1亿行)。
所罗门·鲁兹基
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.