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