没有数据更改的 UPDATE 性能

Mar*_*own 36 performance sql-server update query-performance

如果我有一个UPDATE实际上没有改变任何数据的语句(因为数据已经处于更新状态)。在WHERE子句中进行检查以防止更新是否有任何性能优势?

例如,以下 UPDATE 1 和 UPDATE 2 之间的执行速度是否有任何差异:

CREATE TABLE MyTable (ID int PRIMARY KEY, Value int);
INSERT INTO MyTable (ID, Value)
VALUES
    (1, 1),
    (2, 2),
    (3, 3);

-- UPDATE 1
UPDATE MyTable
SET
    Value = 2
WHERE
    ID = 2
    AND Value <> 2;
SELECT @@ROWCOUNT;

-- UPDATE 2
UPDATE MyTable
SET
    Value = 2
WHERE
    ID = 2;
SELECT @@ROWCOUNT;

DROP TABLE MyTable;
Run Code Online (Sandbox Code Playgroud)

我问的原因是我需要行数来包含未更改的行,所以我知道如果 ID 不存在是否进行插入。因此,我使用了 UPDATE 2 表单。如果使用 UPDATE 1 表单有性能优势,是否有可能以某种方式获得我需要的行数?

Sol*_*zky 27

如果我有一个实际上没有更改任何数据的 UPDATE 语句(因为数据已经处于更新状态),那么在 where 子句中进行检查以防止更新是否有任何性能优势?

由于UPDATE 1存在轻微的性能差异,因此肯定会存在:

  • 实际上没有更新任何行(因此没有什么可以写入磁盘,甚至没有最小的日志活动),以及
  • 采取比进行实际更新所需的限制更少的锁(因此更适合并发)(请参阅最后的更新部分

但是,您需要根据自己的架构、数据和系统负载在系统上测量差异有多大。有几个因素会影响非更新 UPDATE 的影响程度:

  • 正在更新的表上的争用数量
  • 正在更新的行数
  • 如果正在更新的表上有 UPDATE 触发器(如 Mark 在对问题的评论中所述)。如果您执行UPDATE TableName SET Field1 = Field1,则更新触发器将触发并指示该字段已更新(如果您使用UPDATE()COLUMNS_UPDATED函数进行检查),并且INSERTEDDELETED表中的字段是相同的值。

此外,在 Paul White 的文章The Impact of Non-Updating Updates 中还可以找到以下摘要部分(@spaghettidba 在对他的回答的评论中指出):

SQL Server 包含许多优化,以避免在处理不会对持久性数据库造成任何更改的 UPDATE 操作时进行不必要的日志记录或页面刷新。

  • 对集群表的非更新更新通常会避免额外的日志记录和页面刷新,除非形成(部分)集群键的列受到更新操作的影响。
  • 如果集群键的任何部分被“更新”为相同的值,则操作将被记录为数据已更改,并且受影响的页面在缓冲池中被标记为脏。这是将 UPDATE 转换为先删除后插入操作的结果。
  • 堆表的行为与集群表相同,只是它们没有集群键来导致任何额外的日志记录或页面刷新。即使堆上存在非聚集主键,情况仍然如此。因此,对堆的非更新更新通常避免额外的日志记录和刷新(但见下文)。
  • 对于包含超过 8000 字节数据的 LOB 列使用除“SET column_name = column_name”之外的任何语法更新为相同值的任何行,堆和集群表都将遭受额外的日志记录和刷新。
  • 简单地在数据库上启用任一类型的行版本控制隔离级别总是会导致额外的日志记录和刷新。无论更新事务的隔离级别如何,都会发生这种情况。

请记住(特别是如果您没有点击链接查看 Paul 的全文),以下两项:

  1. 非更新更新仍然有一些日志活动,表明事务正在开始和结束。只是没有发生数据修改(这仍然是一个很好的节省)。

  2. 如上所述,您需要在您的系统上进行测试。使用 Paul 正在使用的相同研究查询,看看您是否得到相同的结果。我在我的系统上看到的结果与文章中显示的结果略有不同。仍然没有要写入的脏页,但会增加一点日志活动。


...我需要行数来包含未更改的行,因此我知道如果 ID 不存在是否进行插入。...是否有可能以某种方式获得我需要的行数?

简单地说,如果您只是处理单行,则可以执行以下操作:

UPDATE MyTable
SET    Value = 2
WHERE  ID = 2
AND Value <> 2;

IF (@@ROWCOUNT = 0)
BEGIN
  IF (NOT EXISTS(
                 SELECT *
                 FROM   MyTable
                 WHERE  ID = 2 -- or Value = 2 depending on the scenario
                )
     )
  BEGIN
     INSERT INTO MyTable (ID, Value) -- or leave out ID if it is an IDENTITY
     VALUES (2, 2);
  END;
END;
Run Code Online (Sandbox Code Playgroud)

对于多行,您可以使用OUTPUT子句获取做出该决定所需的信息。通过准确捕获更​​新的行,您可以缩小要查找的项目的范围,以了解不更新不存在的行与不更新存在但不需要更新的行之间的区别。

我在以下答案中展示了基本实现:

使用xml参数插入多个数据时如何避免使用Merge查询?

该答案中显示的方法不会过滤掉存在但不需要更新的行。可以添加该部分,但您首先需要准确显示要合并到的数据集的位置MyTable。它们来自临时表吗?表值参数 (TVP)?


更新1:

我终于能够进行一些测试,这是我发现的有关事务日志和锁定的内容。首先,表的架构:

CREATE TABLE [dbo].[Test]
(
  [ID] [int] NOT NULL CONSTRAINT [PK_Test] PRIMARY KEY CLUSTERED,
  [StringField] [varchar](500) NULL
);
Run Code Online (Sandbox Code Playgroud)

接下来,测试将字段更新为它已有的值:

UPDATE rt
SET    rt.StringField = '04CF508B-B78E-4264-B9EE-E87DC4AD237A'
FROM   dbo.Test rt
WHERE  rt.ID = 4082117
Run Code Online (Sandbox Code Playgroud)

结果:

-- Transaction Log (2 entries):
Operation
----------------------------
LOP_BEGIN_XACT
LOP_COMMIT_XACT


-- SQL Profiler (3 Lock:Acquired events):
Mode            Type
--------------------------------------
8 - IX          5 - OBJECT
8 - IX          6 - PAGE
5 - X           7 - KEY
Run Code Online (Sandbox Code Playgroud)

最后,由于值不变而过滤掉更新的测试:

UPDATE rt
SET    rt.StringField = '04CF508B-B78E-4264-B9EE-E87DC4AD237A'
FROM   dbo.Test rt
WHERE  rt.ID = 4082117
AND    rt.StringField <> '04CF508B-B78E-4264-B9EE-E87DC4AD237A';
Run Code Online (Sandbox Code Playgroud)

结果:

-- Transaction Log (0 entries):
Operation
----------------------------


-- SQL Profiler (3 Lock:Acquired events):
Mode            Type
--------------------------------------
8 - IX          5 - OBJECT
7 - IU          6 - PAGE
4 - U           7 - KEY
Run Code Online (Sandbox Code Playgroud)

如您所见,与标记事务开始和结束的两个条目相反,过滤掉行时,事务日志中没有写入任何内容。虽然这两个条目确实几乎没有,但它们仍然是一些东西。

此外,在过滤掉未更改的行时,锁定 PAGE 和 KEY 资源的限制较少。如果没有其他进程与此表交互,那么这可能不是问题(但真的有多大可能?)。请记住,任何链接的博客(甚至我的测试)中显示的测试都隐含地假设表上没有争用,因为它从来不是测试的一部分。说非更新更新是如此轻量级,以至于需要采取一些措施进行过滤是不值得的,因为测试或多或少是在真空中完成的。但在生产中,这张表很可能不是孤立的。当然,很可能一点点的日志记录和更多限制性的锁不会转化为较低的效率。那么回答这个问题最可靠的信息来源是什么?SQL 服务器。具体来说:您的SQL Server。它会告诉你哪种方法更适合你的系统:-)。


更新 2:

如果新值与当前值相同的操作(即没有更新)超过了新值不同且需要更新的操作,那么以下模式可能会被证明更好,特别是如果桌子上有很多争论。这个想法是先做一个简单的事情SELECT来获取当前值。如果你没有得到一个值,那么你就有了关于INSERT. 如果你确实有一个价值,你可以做一个简单的事情,IF并且UPDATE 在需要时发出。

DECLARE @CurrentValue VARCHAR(500) = NULL,
        @NewValue VARCHAR(500) = '04CF508B-B78E-4264-B9EE-E87DC4AD237A',
        @ID INT = 4082117;

SELECT @CurrentValue = rt.StringField
FROM   dbo.Test rt
WHERE  rt.ID = @ID;

IF (@CurrentValue IS NULL) -- if NULL is valid, use @@ROWCOUNT = 0
BEGIN
  -- row does not exist
  INSERT INTO dbo.Test (ID, StringField)
  VALUES (@ID, @NewValue);
END;
ELSE
BEGIN
  -- row exists, so check value to see if it is different
  IF (@CurrentValue <> @NewValue)
  BEGIN
    -- value is different, so do the update
    UPDATE rt
    SET    rt.StringField = @NewValue
    FROM   dbo.Test rt
    WHERE  rt.ID = @ID;
  END;
END;
Run Code Online (Sandbox Code Playgroud)

结果:

-- Transaction Log (0 entries):
Operation
----------------------------


-- SQL Profiler (2 Lock:Acquired events):
Mode            Type
--------------------------------------
6 - IS          5 - OBJECT
6 - IS          6 - PAGE
Run Code Online (Sandbox Code Playgroud)

所以只获取了 2 个锁而不是 3 个,并且这两个锁都是 Intent Shared,而不是 Intent eXclusive 或 Intent Update(锁兼容性)。请记住,每个获取的锁也会被释放,每个锁实际上是 2 个操作,因此这个新方法总共有 4 个操作,而不是最初提出的方法中的 6 个操作。考虑到此操作每 15 毫秒运行一次(大约,如 OP 所述),即大约每秒 66 次。所以最初的提议相当于每秒 396 次锁定/解锁操作,而这种新方法即使是更轻量级的锁,也只能达到每秒 264 次锁定/解锁操作。这并不能保证出色的性能,但确实值得测试:-)。


Bre*_*zar 14

缩小一点并考虑更大的图景。在现实世界中,您的更新语句真的会像这样:

UPDATE MyTable
  SET Value = 2
WHERE
     ID = 2
     AND Value <> 2;
Run Code Online (Sandbox Code Playgroud)

或者它看起来更像这样:

UPDATE Customers
  SET AddressLine1 = '123 Main St',
      AddressLine2 = 'Apt 24',
      City = 'Chicago',
      State = 'IL',
      (and a couple dozen more fields)
WHERE
     ID = 2
     AND (AddressLine1 <> '123 Main St'
     OR AddressLine2 <> 'Apt 24'
     OR City <> 'Chicago'
     OR State <> 'IL'
      (and a couple dozen more fields))
Run Code Online (Sandbox Code Playgroud)

因为在现实世界中,表有很多列。这意味着您将不得不生成大量复杂的动态应用程序逻辑来构建动态字符串,或者您必须每次都指定每个字段的前后内容。

如果您为每个表动态构建这些更新语句,只传递正在更新的字段,您可能会很快遇到类似于几年前NHibernate 参数大小问题的计划缓存污染问题。更糟糕的是,如果您在 SQL Server 中构建更新语句(如在存储过程中),那么您将消耗宝贵的 CPU 周期,因为 SQL Server 在将字符串串联在一起时效率并不高。

由于这些复杂性,在进行更新时进行这种逐行、逐字段的比较通常没有意义。相反,请考虑基于集合的操作。