Desempenho atroz que une tabelas INSERTED e DELETED em um gatilho

12

Eu tenho um gatilho UPDATE em uma tabela que observa uma coluna específica mudar de um valor específico para qualquer outro valor. Quando isso acontece, ele atualiza alguns dados relacionados em outra tabela por meio de uma única instrução UPDATE.

A primeira coisa que o gatilho faz é verificar se alguma linha atualizada teve o valor dessa coluna alterado do valor em questão. Simplesmente associa INSERTED a DELETED e compara o valor nessa coluna. Se nada for qualificado, ele será desativado mais cedo, para que a instrução UPDATE não seja executada.

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

Nesse caso, CUSTNMBR é a chave primária da tabela subjacente. Se eu fizer uma grande atualização nesta tabela (digamos, mais de 5000 linhas), essa declaração levará AGES, mesmo que eu não tenha tocado na coluna CUSTCLAS. Eu posso vê-lo parar nesta declaração por vários minutos no Profiler.

O plano de execução é bizarro. Ele mostra uma verificação inserida com 3.714 execuções e ~ 18,5 milhões de linhas de saída. Isso é executado através de um filtro na coluna CUSTCLAS. Ele une isso (via loop aninhado) a uma Varredura excluída (também filtrada no CUSTCLAS), que é executada apenas uma vez e possui 5000 linhas de saída.

Que coisa idiota que estou fazendo aqui para causar isso? Observe que o gatilho absolutamente deve lidar adequadamente com atualizações de várias linhas.

EDIT :

Eu também tentei escrever assim (no caso EXISTS estava fazendo algo desagradável), mas ainda assim é terrível.

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
fonte
Você pode se livrar do "TOP 1"? Gostaria de pensar que está causando uma sobrecarga que pode não ser necessária se você está apenas verificando para ver se há um único caso ...
JHFB

Respostas:

10

Você pode avaliar o uso explícito INNER MERGE JOINou de INNER HASH JOINdicas, mas, considerando que você provavelmente está usando essas tabelas novamente mais tarde, provavelmente é melhor inserir apenas o conteúdo de insertede deletedtabelas em #temptabelas indexadas e concluir o processo.

Eles não obtêm índices úteis criados para eles automaticamente.

Martin Smith
fonte
Ok, isso acelera tremendamente, no entanto, existe o potencial de execução em cascata do gatilho. Se eu usar os mesmos nomes de tabela temporária (#i, #d) em cada gatilho, eles entrarão em conflito. Existe uma solução melhor / mais segura do que usar um nome de tabela temporária diferente em cada gatilho?
DB2
Poderia avaliar o uso de variáveis ​​de tabela (com uma chave primária definida CUSTNMBRpara criar o índice clusterizado exclusivo) e usar a OPTION (RECOMPILE)dica para levá-lo em consideração o número de linhas ou talvez apenas usar uma convenção de nomenclatura específica como#i_dbo_YourTable
Martin Smith
Eu acho que vou concordar em nomeá-los como #trigger_name_i. Se eu for com variáveis ​​de tabela, terei que desorganizar ainda mais o código com CREATE TABLEs explícitas. Temos gatilhos em cascata, mas não disparadores recursiva, então eu acho que vou ser seguro ...
DB2
Eu recomendo uma variável de tabela em vez de uma tabela temporária para esse fim; As variáveis ​​de tabela ainda podem ter índices primário e secundário (exclusivo), elas são automaticamente limpas quando o gatilho sai e as variáveis ​​de tabela têm o escopo definido apenas para a execução do gatilho (não entra em conflito com outras variáveis ​​de tabela com o mesmo nome, maior ou menor em pilha de chamadas). Para economizar na sobrecarga do código de definição de tabela, defina um tipo de tabela para cada um e use o nome do tipo para declarar as variáveis ​​da tabela.
Chris Smith
@ChrisSmith também é necessário OPTION (RECOMPILE)para que a cardinalidade seja levada em consideração.
Martin Smith
10

Eu sei que isso foi respondido, mas ele apareceu recentemente como ativo e eu também encontrei isso em tabelas com muitos milhões de linhas. Embora não desconsidere a resposta aceita, posso pelo menos acrescentar que minha experiência mostra que um fator-chave no desempenho do gatilho ao realizar testes semelhantes (ver se uma ou mais colunas realmente tiveram seus valores alterados) é se as colunas são ou não sendo testado eram na verdade parte da UPDATEdeclaração. Descobri que a comparação de colunas entre as tabelas insertede deletedque, de fato, não faziam parte da UPDATEdeclaração, causou um enorme impacto no desempenho que, caso contrário, não existia se esses campos fizessem parte doUPDATE(independentemente do valor realmente ser alterado). Por que todo esse trabalho (ou seja, uma consulta para comparar N campos nas linhas X) para determinar se alguma coisa mudou, se você pode logicamente descartar a possibilidade de alguma dessas colunas ser alterada, o que obviamente não é possível se elas não estiverem presentes na SETcláusula da UPDATEdeclaração.

A solução que empreguei foi usar a função UPDATE () , que só funciona dentro dos Triggers. Esta função interna informa se uma coluna foi especificada na UPDATEinstrução e pode ser usada para sair do Acionador se as colunas que lhe interessam não fizerem parte da UPDATE. Isso pode ser usado em conjunto com a SELECTpara determinar se essas colunas, supondo que elas estejam presentes na UPDATE, tenham alterações reais. Eu tenho um código no topo de vários gatilhos de auditoria que se parecem com:

-- 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;

Essa lógica continuará para o restante do gatilho se:

  1. A operação é uma INSERT
  2. Pelo menos um dos campos relevantes está na SETcláusula de UPDATE e pelo menos uma dessas colunas em uma linha foi alterada

O NOT (UPDATE...) OR NOT EXISTS()pode parecer estranho ou para trás, mas ele é projetado para evitar fazer a SELECTnos insertede deletedmesas se nenhuma das colunas relevantes fazem parte do UPDATE.

Dependendo das suas necessidades, a função COLUMNS_UPDATED () é outra opção para determinar quais colunas fazem parte da UPDATEinstrução.

Solomon Rutzky
fonte
1
É bom que eles devam verificar UPDATE(CUSTCLAS)e pular a coisa toda se falso (+1). Eu não acho que você esteja correto, pois as colunas não atualizadas não estão tão prontamente disponíveis nas versões de linha quanto as versões atualizadas.
Martin Smith
@ MartinSmith, como é que vamos provar isso de uma maneira ou de outra? Embora, talvez não importe se o comportamento é previsível da maneira que encontrei. Eu apenas sei que é uma diferença drástica de desempenho fazendo o mesmo SELECT, JOINing entre INSERTED e DELETED, verificando os campos quanto a diferenças reais, dependendo se os campos no WHERE estavam no SET of the UPDATE ou não. O comportamento que eu vi é consistente, daí a minha teoria, mas seria bom / interessante saber o verdadeiro motivo. Suspeitei que os campos que não estavam no SET precisassem voltar para a tabela base por seu valor.
Solomon Rutzky
Eu já olhei para a estrutura disso antes. Não me lembro se eu encontrar uma boa maneira de fazê-lo ou eu usei apenas uma fácil verificar corda capaz e uma busca exaustiva através de tempdbcomDBCC PAGE
Martin Smith
ESTÁ BEM. Em uma instância com um arquivo único de tamanho mínimo, tempdbtentei esse script , colei a saída no bloco de notas e procurei "EEEEEE". Eu vejo a saída na captura de tela aqui . Observe as versões anteriores e posteriores das duas colunas nas duas linhas. Pode haver maneiras muito mais fáceis, mas suficientes para meus propósitos aqui!
Martin Smith
Embora na verdade existam outras seqüências longas de EEEEEE nas tempdbpáginas próximas a BBBBBBou DDDDDD. Pode ter que fazer mais algumas investigações! Embora talvez isso seja devido à REPLICATEligação.
Martin Smith
2

Eu poderia tentar reescrever usando se existir

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
fonte
1

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

De acordo com Dave, você deve usar tabelas temporárias ou variáveis ​​de tabela com índices, porque as tabelas virtuais INSERTED / DELETED não possuem nenhuma. Se você tiver a possibilidade de disparadores recursivos, use variáveis ​​de tabela para evitar colisões de nomes.

Espero que alguém ache isso útil, pois o post original foi há algum tempo ...

Keith
fonte
-1

O código a seguir pode aumentar o desempenho desse gatilho. Eu não sabia o tipo de dados correto da coluna [custclass] , então você precisa ajustá-lo.

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

Observe que você pode incluir colunas adicionais nessas cópias de memória das tabelas inseridas e excluídas , se precisar delas no seu código de acionamento. As chaves primárias nessas tabelas aumentarão bastante o desempenho da junção ao atualizar mais de algumas linhas de uma vez. Boa sorte!

Dony
fonte