Жорстока продуктивність, що приєднується до 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.

План виконання є химерним. Він показує вкладене сканування з 3 714 виконанням та ~ 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

Чи можете ви позбутися "ТОП-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, ПРИЄДНАЄТЬСЯ між ВСТАНОВЛЕНОМ і ВІДКРИТИм, перевіряючи поля на фактичні відмінності, залежно від того, поля в ДІ були в НАСТАВКІ ОНОВЛЕННЯ чи ні. Поведінка, яку я бачив, є послідовною, отже, моя теорія, але було б добре / цікаво дізнатися справжню причину. Я підозрював, що поля, які не знаходяться в 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

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

Зауважте, що ви можете включити до них додаткові стовпці в копії пам'яті вставлених та видалених таблиць, якщо вони потрібні у вашому тригерному коді. Первинні клавіші цих таблиць значно збільшать продуктивність приєднання при оновленні більше кількох рядків одночасно. Удачі!

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.