如果我有一个UPDATE语句,该语句实际上并未更改任何数据(因为数据已经处于更新状态),那么将检查放在where子句中以防止更新是否对性能有好处?
当然可能由于UPDATE 1而导致性能略有不同:
- 实际上没有更新任何行(因此无需写入磁盘,甚至没有最少的日志活动),并且
- 获取的限制锁少于进行实际更新所需的限制(因此并发性更好)(请参阅最后的“更新”部分)
但是,您需要根据系统上的架构,数据和系统负载来衡量相差多少。有几个因素会影响非更新UPDATE的影响:
- 正在更新的表上的争用量
- 被更新的行数
- 如果要更新的表上有UPDATE触发器(如Mark在问题注释中所指出)。如果执行
UPDATE TableName SET Field1 = Field1
,那么更新触发条件会并指示该字段被更新(如果你使用查询要么UPDATE()或COLUMNS_UPDATED功能),以及在这两个领域INSERTED
,并DELETED
表是相同的值。
另外,以下摘要部分可在Paul White的文章“非更新更新的影响”(如@spaghettidba在对他的回答的评论中指出)中找到:
SQL Server包含许多优化措施,以避免在处理UPDATE操作时避免不必要的日志记录或页面刷新,这些操作不会导致对持久数据库的任何更改。
- 对集群表的非更新更新通常避免额外的日志记录和页面刷新,除非构成集群键(一部分)的列受到更新操作的影响。
- 如果群集密钥的任何部分被“更新”为相同的值,则记录该操作,就像数据已更改一样,并且在缓冲池中将受影响的页面标记为脏页。这是UPDATE转换为Delete-then-insert操作的结果。
- 堆表的行为与集群表相同,不同之处在于,堆表没有集群键来引起任何额外的日志记录或页面刷新。即使堆上存在非集群主键,情况依然如此。因此,对堆的非更新更新通常避免了额外的日志记录和刷新(但请参见下文)。
- 对于使用“ SET column_name = column_name”以外的任何语法将包含8000字节以上数据的LOB列更新为相同值的任何行,堆表和群集表都将遭受额外的日志记录和刷新。
- 简单地在数据库上启用任一类型的行版本控制隔离级别总是会导致额外的日志记录和刷新。无论更新事务有效的隔离级别如何,都会发生这种情况。
请记住以下两点(特别是如果您不按链接查看Paul的全文):
非更新更新仍具有某些日志活动,表明事务正在开始和结束。只是没有数据修改发生(这仍然是一个不错的节省)。
如上所述,您需要在系统上进行测试。使用与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;
对于多行,您可以使用该OUTPUT
子句获取做出决策所需的信息。通过准确捕获要更新的行,您可以缩小项目范围以查找不更新不存在的行与不更新存在但不需要更新的行之间的区别。
我在以下答案中展示了基本的实现:
使用xml参数更新多个数据时,如何避免使用合并查询?
该答案中显示的方法不会过滤掉已存在但仍不需要更新的行。可以添加该部分,但首先需要确切显示要合并到的数据集的位置MyTable
。他们来自临时餐桌吗?一个表值参数(TVP)?
更新1:
我终于能够进行一些测试,这是我发现的有关事务日志和锁定的内容。首先,表的架构:
CREATE TABLE [dbo].[Test]
(
[ID] [int] NOT NULL CONSTRAINT [PK_Test] PRIMARY KEY CLUSTERED,
[StringField] [varchar](500) NULL
);
接下来,测试将字段更新为它已经具有的值:
UPDATE rt
SET rt.StringField = '04CF508B-B78E-4264-B9EE-E87DC4AD237A'
FROM dbo.Test rt
WHERE rt.ID = 4082117
结果:
-- 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
最后,由于值未更改而筛选出更新的测试:
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';
结果:
-- 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
如您所见,过滤出该行时,没有任何内容写入事务日志,这与标记事务开始和结束的两个条目相对。虽然这两个条目几乎是空的,但它们仍然是东西。
同样,在筛选出未更改的行时,PAGE和KEY资源的锁定限制较少。如果没有其他进程与该表进行交互,那么它可能不是问题(但是,实际可能性有多大?)。请记住,任何链接的博客中显示的测试(甚至是我的测试)都隐含地假定表上没有争用,因为它从来都不是测试的一部分。说非更新的更新很轻,以至于不需要进行过滤就需要花费一小撮盐,因为测试或多或少是在真空中完成的。但是在生产中,此表很可能不是孤立的。当然,很可能一点点的日志记录和更多限制性的锁并不会转化为效率降低。那么,最可靠的信息来源可以回答这个问题吗?SQL Server。特别:您的 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;
结果:
-- Transaction Log (0 entries):
Operation
----------------------------
-- SQL Profiler (2 Lock:Acquired events):
Mode Type
--------------------------------------
6 - IS 5 - OBJECT
6 - IS 6 - PAGE
因此,仅获取了2个锁,而不是3个,并且这两个锁都是共享的,不是Intent eXclusive或Intent Update(Lock兼容性)。请记住,获得的每个锁也会被释放,每个锁实际上是2个操作,因此此新方法总共有4个操作,而不是最初提出的方法中的6个操作。考虑到此操作每15毫秒(如OP所述,大约运行一次)运行一次,即每秒约66次。因此,最初的建议是每秒396次锁定/解锁操作,而即使是较轻型的锁定,这种新方法也仅相当于每秒264次锁定/解锁操作。这不能保证性能出色,但肯定值得测试:-)。