问题:在 SQL Server 2016 中,将列更新为相同的值(例如,将列从更新'john'
为'john'
)是否会产生与将列更新为不同值时相同数量的事务日志记录?阅读下文了解更多详情。
我们有几个按计划运行的 SQL 代理作业。这些作业从源表(复制数据、链接服务器)中选择数据,对其进行转换,然后相应地插入/更新/删除本地目标表的行。
在尝试找到实现这一目标的最佳方法时,我们已经通过各种策略。我们试过了
- 使用 MERGE 从源更新目标
- 使用 UPDATE 从源更新目标以更新所有列
- 使用每个目标列的单个 UPDATE 语句从源更新目标
现在,我只是一个初级 DBD,我对事务日志如何工作的理解非常有限。话虽如此,我的前辈们得出的结论是,我们不能使用 MERGE 或 UPDATE 语句,其中所有列都在同一语句中处理,因为它会产生过多的日志记录。这样做的理由是,当您UPDATE
在 SQL Server 中执行语句时,当您设置一个列值并且新值等于旧值时,它仍然在事务日志中标记为更新。当您执行大量无意义的 SET 操作时,这显然会变得代价高昂。
在以下示例中,我们使用源表中的值更新目标表的first_name
和,并由 连接。last_name
id
-- create source- and target-table
CREATE TABLE [#tgt] (
[id] Int PRIMARY KEY,
[first_name] NVarchar(10),
[last_name] NVarchar(10)
)
CREATE TABLE [#src] (
[id] Int PRIMARY KEY,
[first_name] NVarchar(10),
[last_name] NVarchar(10)
)
-- fill some dummy-data
INSERT INTO [#tgt]([id], [first_name], [last_name])VALUES(1, 'john', 'lennon')
INSERT INTO [#src]([id], [first_name], [last_name])VALUES(1, 'john', 'cena')
-- update target-table with values from source-table
UPDATE
[T]
SET
[T].[first_name] = [S].[first_name],
[T].[last_name] = [S].[last_name]
FROM
[#tgt] AS [T]
JOIN [#src] AS [S] ON [S].[id] = [T].[id]
DROP TABLE [#tgt]
DROP TABLE [#src]
此示例不检查是否有任何值实际更改。如果我们暂时忽略NULL
-checking 和 sane fallbacks,这可以通过以下方式之一进行检查:
-- Example #1: updates all rows where first-name or last-name has changed
UPDATE ..
SET ..
FROM ..
WHERE [T].[first_name] <> [S].[first_name] OR [T].[last_name] <> [S].[last_name]
-- Example #2: updates all rows, sets target-value to source-value if value has changed
UPDATE ..
SET
ISNULL(NULLIF([S].[first_name], [T].[first_name]), [T].[first_name]),
ISNULL(NULLIF([S].[last_name], [T].[last_name]), [T].[last_name])
FROM ..
在示例 #1 中,SET 操作将更新所有列,即使只有 1 列已更改。
在示例 #2 中,SET 操作将更新所有行的所有列,如果值未更改则回退到旧值。
在这两个示例中,所有列都被 SET 操作击中,并且根据我的前辈的说法,这会在频繁执行时创建不必要/有问题的事务日志记录量。
这同样适用于MERGE
- 语句。即使您检查匹配行的更改,所有列都会被更新命中。
MERGE [#tgt] AS tgt
USING [#src] AS src
ON (tgt.id = src.id)
WHEN MATCHED AND ([tgt].[first_name] <> [src].[first_name] OR [tgt].[last_name] <> [src].[last_name])
THEN UPDATE SET
[tgt].[first_name] = [src].[first_name],
[tgt].[last_name] = [src].[last_name];
那么我们该怎么办?对我们希望更新的每一列使用单个 UPDATE 语句。在这种情况下:
-- first_name
UPDATE [T]
SET [T].[first_name] = [S].[first_name]
FROM
[#tgt] AS [T]
JOIN [#src] AS [S] ON [S].[id] = [T].[id]
WHERE [T].[first_name] <> [S].[first_name]
-- last_name
UPDATE [T]
SET [T].[last_name] = [S].[last_name]
FROM
[#tgt] AS [T]
JOIN [#src] AS [S] ON [S].[id] = [T].[id]
WHERE [T].[last_name] <> [S].[last_name]
现在,这种方法有几个缺点:
- 所有更新语句必须在同一个事务中执行,以确保一行不会被半更新。
- 编写所有代码真的很糟糕(想象一下有 50 多列的表)
感觉必须有更聪明的方法来解决这个问题,我将不胜感激对这篇文章中所做陈述的任何澄清和更正。如前所述,我只是尽力理解为什么必须这样。
为冗长的帖子道歉,并提前致谢。
好吧,他们能得出这样的结论真是太好了。但是,他们是否提供了任何证据或他们的测试脚本来显示这种行为?我有兴趣看到这样的测试 ;-)
这是其中的一个案例,一点点知识就会产生误导。是的,将列更新为完全相同的值被视为更新,就像测试通过
UPDATE()
函数更新的列1
一样,只要该列在 SET 语句中,就会返回,无论值是否更改。但是,缺少的部分是:
如果没有任何列的值发生变化,则该行实际上并未更新。如果根本没有更新任何行,那么唯一的事务日志活动就是两条记录:一条
LOP_BEGIN_XACT
标记事务的开始,一条LOP_COMMIT_XACT
标记事务的结束。但是没有实际的数据页或索引页被修改。这假设“受影响的行”> 0,但实际上没有任何改变。如果所有行都被过滤掉,以至于没有行被更新(即“受影响的行”= 0),则没有Tran Log 活动。
如果任何列的值发生变化,则将其他列设置为其现有值在事务日志中看起来与未指定值未发生变化的列相同。
每个查询(除非在显式事务中与其他查询分组)都是它自己的事务,事务日志中的每个事务至少有 2 个条目:一个用于 BEGIN,一个用于 COMMIT 或 ABORT。
因此:
就 Tran Log 而言,您的“示例 1”和“示例 2”这两个选项几乎相同。如果至少有一行要更新,那么它们应该是相同的。但是,如果没有任何列的值发生变化的行,那么“示例 1”(带有
WHERE
子句)将导致较少的 Tran Log 活动,因为没有条目,而在“示例 2”中(所有行“已更新”)将有 BEGIN 和 COMMIT 条目。因此,我建议使用该WHERE
子句,因为它在您的意图中是明确的,并且会导致稍微减少 Tran Log 活动。遵循“前辈”的建议肯定会导致更多的Tran Log 活动,更不用说性能下降了。为什么?因为:
UPDATE
语句包装到一个显式事务中以减少不一致以及额外的 BEGIN / END 日志条目,在某些情况下您仍会多次更新该行,并且每次修改都会被记录下来。UPDATE
语句重新扫描它们仍然需要更多时间。确切地知道并亲眼所见总是比依靠猜测或别人声称的要好。为此,您应该测试您的各种选项,包括您的前辈建议的两个单独的更新,并在每次测试后通过以下方式检查:
PS 我在 SQL Server 2012 (SP3) Developer Edition 上进行了初步测试。然后,我在 SQL Server 2016 (RTM) Express Edition 上再次测试,结果是一样的。
PPS 逻辑上和
[T].[first_name] = ISNULL(NULLIF([S].[first_name], [T].[first_name]), [T].[first_name])
没什么区别[T].[first_name] = [S].[first_name]
,只是包裹了更多的功能。但是,如果两列都是'A'
,那么使用'A'
来自同一个表的an 更新它与'A'
来自另一个表的 an 更新是完全相同的操作。PPPS 在检查字符串字段中的任何差异时,您确实需要使用二进制排序规则,否则可能会更改仅大小写(或宽度或组合字符等),以便列的排序规则将比较值相同. 我确实知道你提到的那些是简化的例子,但我只是确保这方面没有被忽视 :-)。因此:
变成:
和:
变成:
您的日志记录级别是多少?你知道简单会清除语句之间的事务日志吗?
如果您想最小化日志记录,则进行多次更新
然后对 last_name 做同样的事情
我知道它看起来很长,但它会最大限度地减少日志记录
多次更新通常比 OR 更有效
呃,哦,我刚刚读了同样的交易
一个或另一个会有很大的不同
视图呢
只需在更新期间使用视图 - 这个视图应该非常有效