Самый эффективный метод обнаружения изменений столбцов в MS SQL Server - PullRequest
23 голосов
/ 16 марта 2009

Наша система работает на 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

... но это не работает, когда вам нужно вызвать хранимую процедуру. Насколько я могу судить, в этих случаях вы должны прибегнуть к другим подходам.

У меня вопрос, есть ли у кого-нибудь понимание (или, что еще лучше, достоверные данные) относительно того, какой лучший / самый дешевый подход к проблеме предиката операции базы данных в триггере, имеет ли конкретное значение столбца в измененной строке на самом деле изменился или нет. Ни один из вышеперечисленных методов не кажется идеальным, и мне было интересно, существует ли лучший метод.

Ответы [ 5 ]

18 голосов
/ 17 марта 2009

Давайте начнем с того, что я никогда не буду, и я имею в виду никогда не вызывать хранимый процесс в триггере. Чтобы учесть многострочную вставку, вам нужно навести курсор на процедуру. Это означает, что 200 000 строк, которые вы только что загрузили, хотя запрос на основе набора (скажем, обновление всех цен на 10%) вполне может заблокировать таблицу на несколько часов, поскольку триггер доблестно пытается справиться с нагрузкой. Плюс, если что-то изменится в процедуре, вы можете вообще сломать любые вставки в таблицу или даже полностью повесить таблицу. Я твердо уверен, что код триггера не должен вызывать ничего, кроме триггера.

Лично я предпочитаю просто выполнять свою задачу. Если в триггере я написал действия, которые хочу сделать правильно, он будет обновлять, удалять или вставлять только те столбцы, которые изменились.

Пример: предположим, что вы хотите обновить поле last_name, которое вы храните в двух местах из-за денормализации, помещенной туда по соображениям производительности.

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

Как вы можете видеть, он будет обновлять только имена, которые отличаются от тех, которые в настоящее время находятся в таблице, которую я обновляю.

Если вы хотите проводить аудит и записывать только те строки, которые изменились, тогда сделайте сравнение, используя все поля, например где i.field1 <> d.field1 или i.field2 <> d.field3 (и т. д. во всех полях)

10 голосов
/ 02 сентября 2014

Я думаю, вы, возможно, захотите исследовать, используя оператор 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 для хорошего подхода на основе набора для обнаружения изменений. Я не проверял подход, но он может стоить вашего времени. Я думаю, КРОМЕ было введено с SQL Server 2005.

7 голосов
/ 27 марта 2009

Хотя HLGEM дал несколько хороших советов выше, это было не совсем то, что мне было нужно. За последние несколько дней я провел немало испытаний и решил, что по крайней мере поделюсь здесь результатами, учитывая, что, похоже, больше информации не будет.

Я создал таблицу, которая была фактически более узким подмножеством (9 столбцов) одной из первичных таблиц нашей системы, и наполнил ее производственными данными, чтобы она была такой же глубокой, как и наша производственная версия таблицы.

Затем я продублировал эту таблицу и в первом из них написал триггер, который пытался обнаружить каждое отдельное изменение столбца, а затем предвосхитил каждое обновление столбца, изменились ли данные в этом столбце на самом деле.

Для второй таблицы я написал триггер, который использовал расширенную условную логику CASE для выполнения всех обновлений всех столбцов в одном операторе.

Затем я провел 4 теста:

  1. Обновление в одну строку для одной строки
  2. Обновление в один столбец до 10000 строк
  3. Обновление из одной колонки в одну строку
  4. Обновление из девяти столбцов до 10000 строк

Я повторил этот тест для индексированных и неиндексированных версий таблиц, а затем повторил все это на серверах SQL 2000 и SQL 2008.

Результаты, которые я получил, были довольно интересны:

Второй метод (один единственный оператор обновления с логикой «CASE» в предложении SET) был равномерно более эффективным, чем обнаружение отдельного изменения (в большей или меньшей степени в зависимости от теста) с единственным исключением одного изменение столбца, затрагивающее многие строки, где столбец был проиндексирован, работает на 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
6 голосов
/ 14 августа 2015

Я рекомендую использовать оператор установки EXCEPT, как указано выше в Todd / arghtype.

Я добавил этот ответ, потому что я поставил «вставлено» перед «удалено», чтобы были обнаружены ВСТАВКИ, а также ОБНОВЛЕНИЯ. Поэтому у меня обычно может быть один триггер, чтобы охватить как вставки, так и обновления. Может также обнаруживать удаления путем добавления ИЛИ (НЕ СУЩЕСТВУЕТ (ВЫБРАТЬ * ИЗ ВСТАВЛЕНО) И СУЩЕСТВУЕТ (ВЫБРАТЬ * ИЗ ИЗМЕНЕНО))

Определяет, изменилось ли значение только в указанных столбцах. Я не исследовал его производительность по сравнению с другими решениями, но он хорошо работает в моей базе данных.

Он использует оператор набора 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 в табличную переменную, на которую можно ссылаться позже.

Надеюсь, это интересно: -)

4 голосов
/ 30 ноября 2011

В SQL Server 2008 есть еще один метод отслеживания изменений:

Сравнение сбора данных изменений и отслеживания изменений

...