在 MS SQL Server 中检测列更改的最有效方法

Posted

技术标签:

【中文标题】在 MS SQL Server 中检测列更改的最有效方法【英文标题】:Most efficient method to detect column change in MS SQL Server 【发布时间】:2010-10-13 16:26:20 【问题描述】:

我们的系统在 SQL Server 2000 上运行,我们正在为升级到 SQL Server 2008 做准备。我们有很多触发器代码,我们需要在其中检测给定列中的更改,然后对其进行操作列是否已更改。

显然 SQL Server 提供了 UPDATE() 和 COLUMNS_UPDATED() 函数,但这些函数只告诉您 SQL 语句中涉及了哪些列,实际更改了哪些列。

要确定哪些列已更改,您需要类似于以下的代码(对于支持 NULL 的列):

IF UPDATE(Col1)
    SELECT @col1_changed = COUNT(*) 
    FROM Inserted i
        INNER JOIN Deleted d ON i.Table_ID = d.Table_ID
    WHERE ISNULL(i.Col1, '<unique null value>') 
            != ISNULL(i.Col1, '<unique null value>')

需要为您有兴趣测试的每一列重复此代码。然后,您可以检查“更改”值以确定是否执行昂贵的操作。当然,这段代码本身是有问题的,因为它只告诉你列中至少有一个值在所有被修改的行上发生了变化。

您可以使用以下方式测试单个 UPDATE 语句:

UPDATE Table SET Col1 = CASE WHEN i.Col1 = d.Col1 
          THEN Col1 
          ELSE dbo.fnTransform(Col1) END
FROM Inserted i
    INNER JOIN Deleted d ON i.Table_ID = d.Table_ID

...但是当您需要调用存储过程时,这不起作用。在这些情况下,据我所知,您必须依靠其他方法。

我的问题是,是否有人对在触发器中预测数据库操作的问题的最佳/最便宜的方法是什么有洞察力(或者,更好的是,硬数据)在修改的行中的特定列值是否具有实际改变与否。以上两种方法似乎都不理想,我想知道是否存在更好的方法。

【问题讨论】:

我为这个相关的旧问题添加了一个新答案:***.com/questions/1254787/… 这很有趣,谢谢提醒! 【参考方案1】:

我建议使用上面 Todd/arghtype 提到的 EXCEPT 集合运算符。

我添加了这个答案,因为我将“插入”放在“删除”之前,以便检测到插入和更新。所以我通常可以有一个触发器来涵盖插入和更新。也可以通过添加 OR (NOT EXISTS(SELECT * FROM inserted) AND EXISTS(SELECT * FROM deleted)) 来检测删除

它确定一个值是否仅在指定的列中发生了变化。与其他解决方案相比,我没有研究它的性能,但它在我的数据库中运行良好。

它使用 EXCEPT 集合运算符返回左侧查询中未在右侧查询中找到的任何行。此代码可用于 INSERT、UPDATE 和 DELETE 触发器。

“PKID”列是主键。需要启用两个集合之间的匹配。如果您有多个主键列,那么您需要包含所有列才能在插入集和删除集之间进行正确匹配。

-- Only do trigger logic if specific field values change.
IF EXISTS(SELECT  PKID
                ,Column1
                ,Column7
                ,Column10
          FROM inserted
          EXCEPT
          SELECT PKID
                ,Column1
                ,Column7
                ,Column10
          FROM deleted )    -- Tests for modifications to fields that we are interested in
OR (NOT EXISTS(SELECT * FROM inserted) AND EXISTS(SELECT * FROM deleted)) -- Have a deletion
BEGIN
          -- Put code here that does the work in the trigger

END

如果你想在后续的触发逻辑中使用更改后的行,我通常会将 EXCEPT 查询的结果放入一个表变量中,以便以后引用。

我希望这很有趣:-)

【讨论】:

【参考方案2】:

我认为您可能想使用 EXCEPT 运算符进行调查。它是一个基于集合的运算符,可以清除未更改的行。好消息是它认为空值是相等的,因为它在 EXCEPT 运算符之前列出的第一个集合中查找行,而不是在 EXCEPT 之后列出的第二个集合中的行

WITH ChangedData AS (
SELECT d.Table_ID , d.Col1 FROM deleted d
EXCEPT 
SELECT i.Table_ID , i.Col1  FROM inserted i
)
/*Do Something with the ChangedData */

这处理了允许 Null 的列的问题,而无需在触发器中使用 ISNULL(),并且只返回更改为 col1 的行的 ID,以提供一种基于良好集合的方法来检测更改。我还没有测试过这种方法,但它可能值得你花时间。我认为 EXCEPT 是在 SQL Server 2005 中引入的。

【讨论】:

我在我的数据库中经常使用这种方法,虽然我没有测量过性能,但它似乎很快。我没有测量,因为我没有看到性能受到影响。顺便说一句,我的逻辑将 INSERT 放在 EXCEPT 之前,并处理 INSERT 和 UPDATE 更改的检测。顺便说一句,我没有使用“WITH”语句,但这看起来很有趣。请参阅下面我略有不同的答案。 这适用于列的* 表示法,非常适合动态使用【参考方案3】:

SQL Server 2008 中还有另一种用于更改跟踪的技术:

Comparing Change Data Capture and Change Tracking

【讨论】:

【参考方案4】:

虽然 HLGEM 在上面给出了一些很好的建议,但这并不是我所需要的。在过去的几天里我做了很多测试,我想我至少会在这里分享结果,因为看起来不会有更多信息了。

我设置了一个表,它实际上是我们系统的一个主表的一个较窄的子集(9 列),并用生产数据填充它,使其与我们的生产版本表一样深。

然后我复制了该表,并在第一个表上编写了一个触发器,该触发器试图检测每个单独的列更改,然后根据该列中的数据是否实际更改来预测每个列的更新。

对于第二个表,我编写了一个触发器,该触发器使用广泛的条件 CASE 逻辑对单个语句中的所有列进行所有更新。

然后我进行了 4 次测试:

    对单行的单列更新 单列更新到 10000 行 对单行的九列更新 9 列更新到 10000 行

我对索引和非索引版本的表重复了这个测试,然后在 SQL 2000 和 SQL 2008 服务器上重复了整个测试。

我得到的结果相当有趣:

第二种方法(在 SET 子句中有一个带有毛茸茸的 CASE 逻辑的单个更新语句)比单个更改检测(或多或少取决于测试)具有一致的性能更好,唯一的例外是单个-在 SQL 2000 上运行的列被索引的许多行的列更改。在我们的特定情况下,我们不会像这样进行很多狭窄、深度的更新,因此对于我的目的而言,单语句方法绝对是要走的路。


我很想听听其他人对类似类型测试的结果,看看我的结论是否像我怀疑的那样普遍,或者它们是否特定于我们的特定配置。

为了让你开始,这是我使用的测试脚本——你显然需要想出其他数据来填充它:

create table test1
( 
    t_id int NOT NULL PRIMARY KEY,
    i1 int NULL,
    i2 int NULL,
    i3 int NULL,
    v1 varchar(500) NULL,
    v2 varchar(500) NULL,
    v3 varchar(500) NULL,
    d1 datetime NULL,
    d2 datetime NULL,
    d3 datetime NULL
)

create table test2
( 
    t_id int NOT NULL PRIMARY KEY,
    i1 int NULL,
    i2 int NULL,
    i3 int NULL,
    v1 varchar(500) NULL,
    v2 varchar(500) NULL,
    v3 varchar(500) NULL,
    d1 datetime NULL,
    d2 datetime NULL,
    d3 datetime NULL
)

-- optional indexing here, test with it on and off...
CREATE INDEX [IX_test1_i1] ON [dbo].[test1] ([i1])
CREATE INDEX [IX_test1_i2] ON [dbo].[test1] ([i2])
CREATE INDEX [IX_test1_i3] ON [dbo].[test1] ([i3])
CREATE INDEX [IX_test1_v1] ON [dbo].[test1] ([v1])
CREATE INDEX [IX_test1_v2] ON [dbo].[test1] ([v2])
CREATE INDEX [IX_test1_v3] ON [dbo].[test1] ([v3])
CREATE INDEX [IX_test1_d1] ON [dbo].[test1] ([d1])
CREATE INDEX [IX_test1_d2] ON [dbo].[test1] ([d2])
CREATE INDEX [IX_test1_d3] ON [dbo].[test1] ([d3])

CREATE INDEX [IX_test2_i1] ON [dbo].[test2] ([i1])
CREATE INDEX [IX_test2_i2] ON [dbo].[test2] ([i2])
CREATE INDEX [IX_test2_i3] ON [dbo].[test2] ([i3])
CREATE INDEX [IX_test2_v1] ON [dbo].[test2] ([v1])
CREATE INDEX [IX_test2_v2] ON [dbo].[test2] ([v2])
CREATE INDEX [IX_test2_v3] ON [dbo].[test2] ([v3])
CREATE INDEX [IX_test2_d1] ON [dbo].[test2] ([d1])
CREATE INDEX [IX_test2_d2] ON [dbo].[test2] ([d2])
CREATE INDEX [IX_test2_d3] ON [dbo].[test2] ([d3])

insert into test1 (t_id, i1, i2, i3, v1, v2, v3, d1, d2, d3)
-- add data population here...

insert into test2 (t_id, i1, i2, i3, v1, v2, v3, d1, d2, d3)
select t_id, i1, i2, i3, v1, v2, v3, d1, d2, d3 from test1

go

create trigger test1_update on test1 for update
as
begin

declare @i1_changed int,
    @i2_changed int,
    @i3_changed int,
    @v1_changed int,
    @v2_changed int,
    @v3_changed int,
    @d1_changed int,
    @d2_changed int,
    @d3_changed int

IF UPDATE(i1)
    SELECT @i1_changed = COUNT(*) FROM Inserted i INNER JOIN Deleted d
        ON i.t_id = d.t_id WHERE ISNULL(i.i1,0) != ISNULL(d.i1,0)
IF UPDATE(i2)
    SELECT @i2_changed = COUNT(*) FROM Inserted i INNER JOIN Deleted d
        ON i.t_id = d.t_id WHERE ISNULL(i.i2,0) != ISNULL(d.i2,0)
IF UPDATE(i3)
    SELECT @i3_changed = COUNT(*) FROM Inserted i INNER JOIN Deleted d
        ON i.t_id = d.t_id WHERE ISNULL(i.i3,0) != ISNULL(d.i3,0)
IF UPDATE(v1)
    SELECT @v1_changed = COUNT(*) FROM Inserted i INNER JOIN Deleted d
        ON i.t_id = d.t_id WHERE ISNULL(i.v1,'') != ISNULL(d.v1,'')
IF UPDATE(v2)
    SELECT @v2_changed = COUNT(*) FROM Inserted i INNER JOIN Deleted d
        ON i.t_id = d.t_id WHERE ISNULL(i.v2,'') != ISNULL(d.v2,'')
IF UPDATE(v3)
    SELECT @v3_changed = COUNT(*) FROM Inserted i INNER JOIN Deleted d
        ON i.t_id = d.t_id WHERE ISNULL(i.v3,'') != ISNULL(d.v3,'')
IF UPDATE(d1)
    SELECT @d1_changed = COUNT(*) FROM Inserted i INNER JOIN Deleted d
        ON i.t_id = d.t_id WHERE ISNULL(i.d1,'1/1/1980') != ISNULL(d.d1,'1/1/1980')
IF UPDATE(d2)
    SELECT @d2_changed = COUNT(*) FROM Inserted i INNER JOIN Deleted d
        ON i.t_id = d.t_id WHERE ISNULL(i.d2,'1/1/1980') != ISNULL(d.d2,'1/1/1980')
IF UPDATE(d3)
    SELECT @d3_changed = COUNT(*) FROM Inserted i INNER JOIN Deleted d
        ON i.t_id = d.t_id WHERE ISNULL(i.d3,'1/1/1980') != ISNULL(d.d3,'1/1/1980')

if (@i1_changed > 0)
begin
    UPDATE test1 SET i1 = CASE WHEN i.i1 > d.i1 THEN i.i1 ELSE d.i1 END
    FROM test1
        INNER JOIN inserted i ON test1.t_id = i.t_id
        INNER JOIN deleted d ON i.t_id = d.t_id
    WHERE i.i1 != d.i1
end

if (@i2_changed > 0)
begin
    UPDATE test1 SET i2 = CASE WHEN i.i2 > d.i2 THEN POWER(i.i2, 1.1) ELSE POWER(d.i2, 1.1) END
    FROM test1
        INNER JOIN inserted i ON test1.t_id = i.t_id
        INNER JOIN deleted d ON i.t_id = d.t_id
    WHERE i.i2 != d.i2
end

if (@i3_changed > 0)
begin
    UPDATE test1 SET i3 = i.i3 ^ d.i3
    FROM test1
        INNER JOIN inserted i ON test1.t_id = i.t_id
        INNER JOIN deleted d ON i.t_id = d.t_id
    WHERE i.i3 != d.i3
end

if (@v1_changed > 0)
begin
    UPDATE test1 SET v1 = i.v1 + 'a'
    FROM test1
        INNER JOIN inserted i ON test1.t_id = i.t_id
        INNER JOIN deleted d ON i.t_id = d.t_id
    WHERE i.v1 != d.v1
end

UPDATE test1 SET v2 = LEFT(i.v2, 5) + '|' + RIGHT(d.v2, 5)
FROM test1
    INNER JOIN inserted i ON test1.t_id = i.t_id
    INNER JOIN deleted d ON i.t_id = d.t_id

if (@v3_changed > 0)
begin
    UPDATE test1 SET v3 = LEFT(i.v3, 5) + '|' + LEFT(i.v2, 5) + '|' + LEFT(i.v1, 5)
    FROM test1
        INNER JOIN inserted i ON test1.t_id = i.t_id
        INNER JOIN deleted d ON i.t_id = d.t_id
    WHERE i.v3 != d.v3
end

if (@d1_changed > 0)
begin
    UPDATE test1 SET d1 = DATEADD(dd, 1, i.d1)
    FROM test1
        INNER JOIN inserted i ON test1.t_id = i.t_id
        INNER JOIN deleted d ON i.t_id = d.t_id
    WHERE i.d1 != d.d1
end

if (@d2_changed > 0)
begin
    UPDATE test1 SET d2 = DATEADD(dd, DATEDIFF(dd, i.d2, d.d2), d.d2)
    FROM test1
        INNER JOIN inserted i ON test1.t_id = i.t_id
        INNER JOIN deleted d ON i.t_id = d.t_id
    WHERE i.d2 != d.d2
end

UPDATE test1 SET d3 = DATEADD(dd, 15, i.d3)
FROM test1
    INNER JOIN inserted i ON test1.t_id = i.t_id
    INNER JOIN deleted d ON i.t_id = d.t_id

end

go

create trigger test2_update on test2 for update
as
begin

    UPDATE test2 SET
        i1 = 
            CASE
            WHEN ISNULL(i.i1, 0) != ISNULL(d.i1, 0)
            THEN CASE WHEN i.i1 > d.i1 THEN i.i1 ELSE d.i1 END
            ELSE test2.i1 END,
        i2 = 
            CASE
            WHEN ISNULL(i.i2, 0) != ISNULL(d.i2, 0)
            THEN CASE WHEN i.i2 > d.i2 THEN POWER(i.i2, 1.1) ELSE POWER(d.i2, 1.1) END
            ELSE test2.i2 END,
        i3 = 
            CASE
            WHEN ISNULL(i.i3, 0) != ISNULL(d.i3, 0)
            THEN i.i3 ^ d.i3
            ELSE test2.i3 END,
        v1 = 
            CASE
            WHEN ISNULL(i.v1, '') != ISNULL(d.v1, '')
            THEN i.v1 + 'a'
            ELSE test2.v1 END,
        v2 = LEFT(i.v2, 5) + '|' + RIGHT(d.v2, 5),
        v3 = 
            CASE
            WHEN ISNULL(i.v3, '') != ISNULL(d.v3, '')
            THEN LEFT(i.v3, 5) + '|' + LEFT(i.v2, 5) + '|' + LEFT(i.v1, 5)
            ELSE test2.v3 END,
        d1 = 
            CASE
            WHEN ISNULL(i.d1, '1/1/1980') != ISNULL(d.d1, '1/1/1980')
            THEN DATEADD(dd, 1, i.d1)
            ELSE test2.d1 END,
        d2 = 
            CASE
            WHEN ISNULL(i.d2, '1/1/1980') != ISNULL(d.d2, '1/1/1980')
            THEN DATEADD(dd, DATEDIFF(dd, i.d2, d.d2), d.d2)
            ELSE test2.d2 END,
        d3 = DATEADD(dd, 15, i.d3)
    FROM test2
        INNER JOIN inserted i ON test2.t_id = i.t_id
        INNER JOIN deleted d ON test2.t_id = d.t_id

end

go

-----
-- the below code can be used to confirm that the triggers operated identically over both tables after a test
select top 10 test1.i1, test2.i1, test1.i2, test2.i2, test1.i3, test2.i3, test1.v1, test2.v1, test1.v2, test2.v2, test1.v3, test2.v3, test1.d1, test1.d1, test1.d2, test2.d2, test1.d3, test2.d3
from test1 inner join test2 on test1.t_id = test2.t_id
where 
    test1.i1 != test2.i1 or 
    test1.i2 != test2.i2 or
    test1.i3 != test2.i3 or
    test1.v1 != test2.v1 or 
    test1.v2 != test2.v2 or
    test1.v3 != test2.v3 or
    test1.d1 != test2.d1 or 
    test1.d2 != test2.d2 or
    test1.d3 != test2.d3

-- test 1 -- one column, one row
update test1 set i3 = 64 where t_id = 1000
go
update test2 set i3 = 64 where t_id = 1000
go

update test1 set i3 = 64 where t_id = 1001
go
update test2 set i3 = 64 where t_id = 1001
go

-- test 2 -- one column, 10000 rows
update test1 set v3 = LEFT(v3, 50) where t_id between 10000 and 20000
go
update test2 set v3 = LEFT(v3, 50) where t_id between 10000 and 20000
go

-- test 3 -- all columns, 1 row, non-self-referential
update test1 set i1 = 1000, i2 = 2000, i3 = 3000, v1 = 'R12345123', v2 = 'Happy!', v3 = 'I am v3!!!', d1 = '1/1/1985', d2 = '1/1/1988', d3 = NULL
where t_id = 3000
go
update test2 set i1 = 1000, i2 = 2000, i3 = 3000, v1 = 'R12345123', v2 = 'Happy!', v3 = 'I am v3!!!', d1 = '1/1/1985', d2 = '1/1/1988', d3 = NULL
where t_id = 3000
go

-- test 4 -- all columns, 10000 rows, non-self-referential
update test1 set i1 = 1000, i2 = 2000, i3 = 3000, v1 = 'R12345123', v2 = 'Happy!', v3 = 'I am v3!!!', d1 = '1/1/1985', d2 = '1/1/1988', d3 = NULL
where t_id between 30000 and 40000
go
update test2 set i1 = 1000, i2 = 2000, i3 = 3000, v1 = 'R12345123', v2 = 'Happy!', v3 = 'I am v3!!!', d1 = '1/1/1985', d2 = '1/1/1988', d3 = NULL
where t_id between 30000 and 40000
go

-----

drop table test1
drop table test2

【讨论】:

【参考方案5】:

让我们从我永远不会开始,我的意思是永远不会在触发器中调用存储过程。要考虑多行插入,您必须通过 proc 游标。这意味着您刚刚通过基于集合的查询(例如将所有价格更新 10%)加载的 200,000 行可能会将表锁定几个小时,因为触发器会勇敢地尝试处理负载。另外,如果 proc 发生变化,您可以完全破坏表中的任何插入,甚至完全挂起表。我坚信触发器代码不应调用触发器之外的任何其他内容。

就我个人而言,我更喜欢简单地完成我的任务。如果我在触发器中编写了我想要正确执行的操作,它只会更新、删除或插入列已更改的位置。

示例:假设您想更新您存储在两个地方的 last_name 字段,因为出于性能原因在此处放置了非规范化。

update t
set lname = i.lname
from table2 t 
join inserted i on t.fkfield = i.pkfield
where t.lname <>i.lname

如您所见,它只会更新与我正在更新的表中当前不同的 lname。

如果您想进行审核并仅记录那些更改的行,则使用所有字段进行比较,例如 其中 i.field1 d.field1 或 i.field2 d.field3 (等通过所有字段)

【讨论】:

在您提出的情况下,您最终会锁定 table2 以更新您对原始表所做的每一次修改,即使您根本没有修改过 lname。这是我试图避免的一部分。不过感谢您的建议! 我投了赞成票,因为我发现了不从触发器调用 SP 的艰难方法......再也不会!

以上是关于在 MS SQL Server 中检测列更改的最有效方法的主要内容,如果未能解决你的问题,请参考以下文章

MS Sql Server 对象创建/更改脚本

将 MS SQL SERVER 数据库导出到 MySql 数据库的最简单方法

varchar(max) MS SQL Server 2000,有问题吗?

需要在 MS Access 中使用链接表更改 sql server 数据库名称

在 SQL Server Management Studio 中更改 SQL Server 数据库中的列属性

在这种情况下,在 SQL Server 中对三个 nchar 列进行索引的最有效方法是啥?