Зверская производительность, объединяющая INSERTED и DELETED таблицы в триггере

12

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

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

IF NOT EXISTS (
    SELECT TOP 1 i.CUSTNMBR
    FROM INSERTED i
        INNER JOIN DELETED d
            ON i.CUSTNMBR = d.CUSTNMBR
    WHERE d.CUSTCLAS = 'Misc'
        AND i.CUSTCLAS != 'Misc'
)
    RETURN

В этом случае CUSTNMBR является первичным ключом базовой таблицы. Если я сделаю большое обновление этой таблицы (скажем, 5000+ строк), этот оператор займет AGES, даже если я не коснулся столбца CUSTCLAS. Я могу наблюдать за тем, как это заявление в течение нескольких минут отображается в Profiler.

План исполнения причудливый. Он показывает вставленное сканирование с 3714 выполнениями и ~ 18,5 миллионами выходных строк. Это проходит через фильтр в столбце CUSTCLAS. Он соединяет это (через вложенный цикл) с удаленным сканированием (также фильтруемым по CUSTCLAS), которое выполняется только один раз и имеет 5000 выходных строк.

Какую идиотскую вещь я делаю здесь, чтобы вызвать это? Обратите внимание, что триггер обязательно должен правильно обрабатывать многострочные обновления.

РЕДАКТИРОВАТЬ :

Я также попытался написать это так (на случай, если EXISTS делал что-то неприятное), но это все так же ужасно.

DECLARE @CUSTNMBR varchar(31)
SELECT TOP 1 @CUSTNMBR = i.CUSTNMBR
FROM INSERTED i
    INNER JOIN DELETED d
        ON i.CUSTNMBR = d.CUSTNMBR
WHERE d.CUSTCLAS = 'Misc'
    AND i.CUSTCLAS != 'Misc'

IF @CUSTNMBR IS NULL
    RETURN
db2
источник
Можете ли вы избавиться от "ТОП 1"? Я думаю, что это вызывает некоторые накладные расходы, которые могут не потребоваться, если вы просто проверяете, есть ли один случай ...
JHFB

Ответы:

10

Вы можете оценить использование явных INNER MERGE JOINили INNER HASH JOINподсказок, но, учитывая, что вы, вероятно, будете снова использовать эти таблицы позже в триггере, вам, вероятно, лучше просто вставить содержимое insertedи deletedтаблицы в индексированные #tempтаблицы и покончить с этим.

Они не получают полезные индексы, созданные для них автоматически.

Мартин Смит
источник
Ладно, это значительно ускоряет, однако есть потенциал для каскадного запуска триггера. Если я использую одинаковые имена временных таблиц (#i, #d) в каждом триггере, они конфликтуют. Есть ли лучшее / безопасное решение, чем просто использовать разные имена временных таблиц в каждом триггере?
db2
Можно оценить с помощью табличных переменных (с первичным ключом, определенным CUSTNMBRдля создания уникального кластеризованного индекса) и использовать OPTION (RECOMPILE)подсказку, чтобы он учитывал количество строк, или, возможно, просто использовать конкретное соглашение об именах, такое как#i_dbo_YourTable
Martin Smith,
Я думаю, что я согласен называть их как #trigger_name_i. Если я пойду с табличными переменными, мне придется еще больше загромождать код с помощью явных CREATE TABLE. У нас есть каскадные триггеры, но не рекурсивные, поэтому я думаю, что буду в безопасности ...
db2
Для этой цели я рекомендую переменную таблицы вместо временной таблицы; Табличные переменные могут по-прежнему иметь первичные и вторичные (уникальные) индексы, они автоматически очищаются при выходе из триггера, а переменные таблицы ограничиваются только выполнением этого триггера (это не будет конфликтовать с другими переменными таблицы с тем же именем выше или ниже) стек вызовов). Чтобы сэкономить на накладных расходах кода определения таблицы, определите тип таблицы для каждого и используйте имя типа для объявления переменных таблицы.
Крис Смит
@ChrisSmith вам также часто понадобится, OPTION (RECOMPILE)чтобы учитывалось количество элементов .
Мартин Смит
10

Я знаю, что на этот вопрос уже получен ответ, но он только что появился как недавно активный, и я столкнулся с этим и для таблиц с миллионами строк. Не сбрасывая со счетов принятый ответ, я могу, по крайней мере, добавить, что мой опыт показывает, что ключевым фактором в производительности триггера при выполнении аналогичных тестов (проверка того, действительно ли один или несколько столбцов были изменены значения) является ли столбец (столбцы) тестирование было на самом деле частью UPDATEзаявления. Я обнаружил , что сравнение столбцов между insertedи deletedтаблицы , которые фактически были не часть UPDATEзаявления поставить огромное сопротивление на производительности , которая была в противном случае не была , если эти поля были частьюUPDATEзаявление (независимо от того, что их значение действительно изменяется). Почему все это работает (то есть, запрос для сравнения N полей в X строках), чтобы определить, изменилось ли что-нибудь, если вы можете логически исключить возможность изменения любого из этих столбцов, что, очевидно, невозможно, если бы они не присутствовали в SETпункте UPDATEзаявления.

Решением, которое я использовал, было использование функции UPDATE (), которая работает только внутри триггеров. Эта встроенная функция сообщает, был ли указан столбец в UPDATEоператоре, и может использоваться для выхода из триггера, если интересующие вас столбцы не являются частью UPDATE. Это можно использовать вместе с a, SELECTчтобы определить, действительно ли эти столбцы, если они присутствуют в UPDATE, имеют реальные изменения. У меня есть код в верхней части нескольких триггеров аудита, который выглядит следующим образом:

-- exit on updates that do not update the only 3 columns we ETL
IF (
     EXISTS(SELECT 1 FROM DELETED) -- this is an UPDATE (Trigger is AFTER INSERT, UPDATE)
     AND (
            NOT (UPDATE(Column3) OR UPDATE(Column7)
                 OR UPDATE(Column11)) -- the columns we care about are not being updated
            OR NOT EXISTS(
                        SELECT 1
                        FROM INSERTED ins
                        INNER JOIN DELETED del
                                ON del.KeyField1 = ins.KeyField1
                                AND del.KeyField2 = ins.KeyField2
                        WHERE ins.Column3 <> del.Column3
                                 COLLATE Latin1_General_100_CS_AS -- case-sensitive compare
                        OR    ISNULL(ins.Column7, -99) <> 
                                 ISNULL(del.Column7, -99) -- NULLable INT field
                        OR    ins.[Column11] <> del.[Column11] -- NOT NULL INT field
                      )
          )
    )
BEGIN
    RETURN;
END;

Эта логика перейдет к остальной части триггера, если:

  1. Операция является INSERT
  2. По крайней мере одно из соответствующих полей находится в SETпредложении, UPDATE и хотя бы один из этих столбцов в одной строке изменился

NOT (UPDATE...) OR NOT EXISTS()Может выглядеть странным или назад, но он предназначен , чтобы избежать делает SELECTна insertedи deletedтаблиц , если ни один из соответствующих столбцов не являются частью UPDATE.

В зависимости от ваших потребностей, функция COLUMNS_UPDATED () - это еще одна опция для определения того, какие столбцы являются частью UPDATEоператора.

Соломон Руцкий
источник
1
Хороший момент, что они должны проверить UPDATE(CUSTCLAS)и просто пропустить все это, если ложь (+1). Я не думаю, что вы правы, что не обновленные столбцы не так легко доступны в версиях строк, как обновленные.
Мартин Смит
@MartinSmith, как мы можем доказать это так или иначе? Хотя, это может не иметь значения, если поведение предсказуемо так, как я обнаружил. Я просто знаю, что это резкое различие в производительности при выполнении одного и того же SELECT, объединении между INSERTED и DELETED, проверке полей на фактические различия, в зависимости от того, были ли поля в WHERE в наборе UPDATE или нет. Поведение, которое я видел, является последовательным, отсюда моя теория, но было бы хорошо / интересно узнать настоящую причину. Я подозревал, что поля, не входящие в SET, должны были вернуться к базовой таблице для их значения.
Соломон Руцкий
Я посмотрел на структуру этого раньше. Я не могу вспомнить, нашел ли я хороший способ сделать это, или я просто использовал легко найти способную строку и исчерпывающий поиск tempdbс помощьюDBCC PAGE
Martin Smith
ОК. На экземпляре с одним файлом минимального размера tempdbя только что попробовал этот скрипт , вставил вывод в блокнот и искал "EEEEEE". Я вижу вывод на скриншоте здесь . Обратите внимание до и после версий обоих столбцов в обеих строках. Там могут быть гораздо более простые способы, но достаточно для моих целей здесь!
Мартин Смит
Хотя на самом деле есть другие длинные строки EEEEEE на tempdbстраницах рядом с BBBBBBили DDDDDD. Возможно, придется провести еще какое-то расследование! Хотя, возможно, это связано с REPLICATEзвонком.
Мартин Смит
2

Я мог бы попытаться переписать, используя, если существует

IF EXISTS (SELECT TOP 1 i.CUSTNMBR     
            FROM INSERTED i         
            INNER JOIN DELETED d             
            ON i.CUSTNMBR = d.CUSTNMBR and d.custclass = 'Misc'  
            WHERE d.CUSTCLAS <>i.CUSTCLAS)    
BEGIN

--do your triggerstuff here
END
HLGEM
источник
1

http://dave.brittens.org/blog/writing-well-behaved-triggers.html

Согласно Дэйву, вы должны использовать временные таблицы или переменные таблиц с индексами, потому что у виртуальных таблиц INSERTED / DELETED их нет. Если у вас есть возможность рекурсивных триггеров, вам следует использовать табличные переменные, чтобы избежать конфликтов имен.

Надеюсь, кто-то найдет это полезным, так как оригинальный пост был довольно давно ...

Кит
источник
-1

Следующий код может повысить производительность этого триггера. Я не знал правильный тип данных столбца [custclass], поэтому вам нужно настроить его.

DECLARE @i AS TABLE (CUSTNMBR VARCHAR(31) NOT NULL PRIMARY KEY, custclass VARCHAR(10) NOT NULL)
DECLARE @d AS TABLE (CUSTNMBR VARCHAR(31) NOT NULL PRIMARY KEY, custclass VARCHAR(10) NOT NULL)
INSERT INTO @i SELECT CUSTNMBR, custclass FROM inserted
INSERT INTO @d SELECT CUSTNMBR, custclass FROM deleted
IF NOT EXISTS
  (SELECT * FROM @i AS i INNER JOIN @d AS d ON d.CUSTNMBR = i.CUSTNMBR
   WHERE i.custclass <> d.custclass) RETURN

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

Доний
источник