这个while循环是否需要显式事务?

Jon*_*ica 12 sql-server t-sql transaction sql-server-2014

SQL Server 2014:

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

对于日志运输等,我们显然也希望将其保持为一口大小的交易。

如果我们让下面运行一会儿,然后取消/终止查询,那么到目前为止所做的工作会全部提交,还是需要添加明确的 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
Run Code Online (Sandbox Code Playgroud)

Sol*_*zky 15

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

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

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

不。事实上,鉴于您希望能够停止进程并重新启动,添加显式事务(或启用隐式事务)将是一个坏主意,因为停止进程可能会在执行COMMIT. 在这种情况下,您将需要手动发出COMMIT(如果您在 SSMS 中),或者如果您从 SQL 代理作业运行它,那么您就没有这个机会,最终可能会出现孤立事务。


此外,您可能希望设置@CHUNK_SIZE为较小的数字。锁升级通常发生在单个对象上获得 5000 个锁时。根据行的大小,如果它执行的是行锁与页锁,您可能会超过该限制。如果一行的大小使得每个页面只能容纳 1 或 2 行,那么即使它正在执行页面锁定,您也可能总是遇到此问题。

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

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


与确切问题无关,但与问题中的查询相关,这里可以进行一些改进(或者至少从外观上看是这样):

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

  2. If the deleted field is a BIT datatype, then isn't that value determined by whether or not deletedDate is 2000-01-01? Why do you need both?

  3. If these two fields are new and you added them as NULL so it could be an online / non-blocking operation and are now wanting to update them to their "default" value, then that wasn't necessary. Starting in SQL Server 2012 (Enterprise Edition only), adding NOT NULL columns that have a DEFAULT constraint are non-blocking operations as long as the value of the DEFAULT is a constant. So if you aren't using the fields yet, just drop and re-add as NOT NULL and with a DEFAULT constraint.

  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 代理作业中对此进行调度,然后等待下一个调度时间再次启动。然后,您可以安排每 20 分钟运行一次,以便在 组之间有一些强制的喘息空间n,但它仍然会在无人看管的情况下完成整个过程。然后,当没有其他事情可做时,就让作业自行删除:-)。

    4. 在循环中,执行:
      1. 通过类似的东西填充当前批次 DELETE TOP (4995) FROM #FullSet OUTPUT Deleted.KeyField INTO #CurrentSet (KeyField);
      2. IF (@@ROWCOUNT = 0) BREAK;
      3. 使用以下内容执行 UPDATE: 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. 您可能需要手动 UPDATE STATS 和/或 REBUILD 索引以使其保持最新状态,因为基础数据变化非常频繁
    4. 一定要记住,索引在帮助 的同时SELECT会伤害 ,UPDATE因为它是另一个必须在该操作期间更新的对象,因此需要更多的 I/O。这既可以使用过滤索引(由于与过滤器匹配的行较少,因此在更新行时会缩小),并等待一段时间添加索引(如果它在开始时不会非常有用,那么没有理由招致额外的 I/O)。

更新:请参阅我对与此问题相关的问题的回答,以完整实施上述建议,包括跟踪状态和彻底取消的机制:sql server:以小块更新大表上的字段:如何获取进度/状态?