Atroz rendimiento que une las tablas INSERTED y DELETED en un disparador

12

Tengo un activador ACTUALIZAR en una tabla que vigila una columna específica que cambia de un valor específico a cualquier otro valor. Cuando esto sucede, actualiza algunos datos relacionados en otra tabla a través de una sola instrucción UPDATE.

Lo primero que hace el disparador es verificar si alguna fila actualizada tuvo el valor de esta columna cambiado del valor en cuestión. Simplemente se une INSERTED a DELETED y compara el valor en esa columna. Si nada califica, se rescata temprano para que la instrucción UPDATE no se ejecute.

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

En este caso, CUSTNMBR es la clave principal de la tabla subyacente. Si hago una gran actualización en esta tabla (por ejemplo, más de 5000 filas), esta declaración toma EDADES, incluso si no he tocado la columna CUSTCLAS. Puedo ver cómo se detiene en esta declaración durante varios minutos en Profiler.

El plan de ejecución es extraño. Muestra una exploración insertada con 3.714 ejecuciones y ~ 18,5 millones de filas de salida. Eso se ejecuta a través de un filtro en la columna CUSTCLAS. Lo une (a través de un bucle anidado) a un Análisis eliminado (también filtrado en CUSTCLAS), que se ejecuta solo una vez y tiene 5000 filas de salida.

¿Qué idiota estoy haciendo aquí para causar esto? Tenga en cuenta que el desencadenante absolutamente debe manejar correctamente las actualizaciones de varias filas.

EDITAR :

También intenté escribirlo así (en caso de que EXISTS estuviera haciendo algo desagradable), pero sigue siendo igual de terrible.

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
fuente
¿Puedes deshacerte del "TOP 1"? Creo que está causando algunos gastos generales que pueden no ser necesarios si solo está verificando si hay un solo caso ...
JHFB

Respuestas:

10

Podría evaluar el uso de indicaciones explícitas INNER MERGE JOINo INNER HASH JOINsugerencias, pero dado que presumiblemente está utilizando estas tablas nuevamente más adelante en el desencadenante, probablemente sea mejor simplemente insertando el contenido insertedy las deletedtablas en las #temptablas indexadas y listo.

No obtienen índices útiles creados para ellos automáticamente.

Martin Smith
fuente
De acuerdo, esto lo acelera tremendamente, sin embargo, existe el potencial para la ejecución de disparadores en cascada. Si uso los mismos nombres de tabla temporal (#i, #d) en cada desencadenador, entran en conflicto. ¿Existe una solución mejor / más segura que simplemente usar un nombre de tabla temporal diferente en cada activador?
db2
Podría evaluar el uso de variables de tabla (con una clave principal definida CUSTNMBRpara crear el índice agrupado único) y usar la OPTION (RECOMPILE)sugerencia para que tenga en cuenta el número de filas o tal vez simplemente usar una convención de nomenclatura particular como#i_dbo_YourTable
Martin Smith
Creo que me conformaré con nombrarlos así #trigger_name_i. Si voy con variables de tabla, tendré que abarrotar el código aún más con CREATE TABLE explícitas. Tenemos disparadores en cascada, pero no disparadores recursivos, así que creo que estaré a salvo ...
db2
Recomiendo una variable de tabla en lugar de una tabla temporal para este propósito; Las variables de tabla aún pueden tener índices primarios y secundarios (únicos), se limpian automáticamente cuando el desencadenante sale y las variables de tabla se enfocan solo a esa ejecución de desencadenante (no entrará en conflicto con otras variables de tabla del mismo nombre más arriba o más bajo en la pila de llamadas). Para ahorrar en la sobrecarga del código de definición de la tabla, defina un tipo de tabla para cada uno y use el nombre del tipo para declarar las variables de la tabla.
Chris Smith el
@ChrisSmith que a menudo también necesitarías OPTION (RECOMPILE)para tener en cuenta la cardinalidad.
Martin Smith el
10

Sé que esto se ha respondido, pero apareció como activo recientemente y también me he encontrado con esto para tablas con muchos millones de filas. Si bien no descarto la respuesta aceptada, al menos puedo agregar que mi experiencia muestra que un factor clave en el rendimiento del disparador al hacer pruebas similares (ver si una o más columnas han cambiado sus valores) es si las columnas ser probado fueron en realidad parte de la UPDATEdeclaración. Descubrí que comparar columnas entre las tablas insertedy deletedque, de hecho, no formaban parte de la UPDATEdeclaración, suponía un gran lastre para el rendimiento que, de lo contrario, no existiría si esos campos fueran parte deUPDATEdeclaración (independientemente de que su valor se cambie realmente). ¿Por qué hacer todo ese trabajo (es decir, una consulta para comparar N campos en X filas) para determinar si algo ha cambiado si puede descartar lógicamente la posibilidad de que alguna de esas columnas se cambie, lo que obviamente no es posible si no estuvieran presentes? en la SETcláusula de la UPDATEdeclaración.

La solución que empleé fue usar la función UPDATE () que solo funciona dentro de Triggers. Esta función incorporada le indica si se especificó una columna en la UPDATEinstrucción y se puede usar para salir del activador si las columnas que le preocupan no son parte de UPDATE. Esto se puede usar junto con a SELECTpara determinar si esas columnas, suponiendo que estén presentes en el UPDATE, tienen cambios reales. Tengo un código en la parte superior de varios disparadores de auditoría que se ve así:

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

Esta lógica procederá al resto del disparador si:

  1. La operación es un INSERT
  2. Al menos uno de los campos relevantes está en la SETcláusula de una UPDATE y al menos una de esas columnas en una fila ha cambiado

El NOT (UPDATE...) OR NOT EXISTS()podría parecer extraño o hacia atrás, pero está diseñado para evitar hacer el SELECTde los insertedy deletedlas tablas si ninguna de las columnas relevantes son parte de la UPDATE.

Dependiendo de sus necesidades, la función COLUMNS_UPDATED () es otra opción para determinar qué columnas son parte de la UPDATEdeclaración.

Solomon Rutzky
fuente
1
Buen punto que deberían verificar UPDATE(CUSTCLAS)y simplemente omitir todo si es falso (+1). No creo que tenga razón en que las columnas no actualizadas no están tan disponibles en las versiones de fila como las actualizadas.
Martin Smith
@ MartininSmith, ¿cómo hacemos para demostrarlo de una forma u otra? Aunque, podría no importar si el comportamiento es predecible de la manera que he encontrado. Solo sé que es una diferencia de rendimiento drástica hacer lo mismo SELECCIONAR, UNIRSE entre INSERTED y DELETED, verificar los campos en busca de diferencias reales, dependiendo de si los campos en WHERE estaban en el SET de la ACTUALIZACIÓN o no. El comportamiento que he visto es consistente, de ahí mi teoría, pero sería bueno / interesante saber la verdadera razón. Sospeché que los campos que no estaban en el SET tenían que volver a la tabla base por su valor.
Solomon Rutzky
He visto la estructura de esto antes. No recuerdo si encontré una buena manera de hacerlo o si simplemente utilicé una cadena fácil de encontrar y una búsqueda exhaustiva tempdbconDBCC PAGE
Martin Smith
OKAY. En una instancia con un archivo único de tamaño mínimo tempdb, acabo de probar este script , pegué el resultado en el bloc de notas y busqué "EEEEEE". Veo el resultado en la captura de pantalla aquí . Tenga en cuenta versiones anteriores y posteriores de ambas columnas en ambas filas. ¡Puede haber formas mucho más fáciles pero suficientes para mis propósitos aquí!
Martin Smith
Aunque en realidad hay otras cadenas EEEEEE largas en las tempdbpáginas que no están al lado de BBBBBBo DDDDDD. ¡Podría tener que investigar un poco más! Aunque tal vez esto se deba a la REPLICATEllamada.
Martin Smith
2

Podría intentar reescribir usando si existe

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

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

Según Dave, debe usar tablas temporales o variables de tabla con índices, porque las tablas virtuales INSERTED / DELETED no tienen ninguna. Si tiene la posibilidad de disparadores recursivos, entonces debe usar variables de tabla para evitar colisiones de nombres.

Espero que alguien encuentre esto útil ya que la publicación original fue hace bastante tiempo ...

Keith
fuente
-1

El siguiente código puede aumentar el rendimiento de este activador. No conocía el tipo de datos correcto de la columna [custclass] , por lo que debe ajustarlo.

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

Tenga en cuenta que puede incluir columnas adicionales en estas en copias de memoria de las tablas insertadas y eliminadas si las necesita en su código de activación. Las claves principales en estas tablas aumentarán en gran medida el rendimiento de la unión al actualizar más de unas pocas filas a la vez. ¡Buena suerte!

Dony
fuente