Sincronización usando disparadores

11

Tengo un requisito similar a las discusiones anteriores en:

Tengo dos mesas [Account].[Balance]y [Transaction].[Amount]:

CREATE TABLE Account (
      AccountID    INT
    , Balance      MONEY
);

CREATE TABLE Transaction (
      TransactionID INT
     , AccountID    INT
    , Amount      MONEY
);

Cuando hay una inserción, actualización o eliminación en la [Transaction]tabla, se [Account].[Balance]debe actualizar según el [Amount].

Actualmente tengo un disparador para hacer este trabajo:

ALTER TRIGGER [dbo].[TransactionChanged] 
ON  [dbo].[Transaction]
AFTER INSERT, UPDATE, DELETE
AS 
BEGIN
IF  EXISTS (select 1 from [Deleted]) OR EXISTS (select 1 from [Inserted])
    UPDATE [dbo].[Account]
    SET
    [Account].[Balance] = [Account].[Balance] + 
        (
            Select ISNULL(Sum([Inserted].[Amount]),0)
            From [Inserted] 
            Where [Account].[AccountID] = [Inserted].[AccountID]
        )
        -
        (
            Select ISNULL(Sum([Deleted].[Amount]),0)
            From [Deleted] 
            Where [Account].[AccountID] = [Deleted].[AccountID]
        )
END

Aunque esto parece estar funcionando, tengo preguntas:

  1. ¿El disparador sigue el principio ACID de la base de datos relacional? ¿Hay alguna posibilidad de que se inserte un inserto pero el gatillo falla?
  2. Mis declaraciones IFy UPDATEparecen extrañas. ¿Hay alguna forma mejor de actualizar la [Account]fila correcta ?
Yiping
fuente

Respuestas:

13

1. ¿El disparador sigue el principio ACID de la base de datos relacional? ¿Hay alguna posibilidad de que se inserte un inserto pero el gatillo falla?

Esta pregunta se responde parcialmente en una pregunta relacionada con la que se vinculó. El código de activación se ejecuta en el mismo contexto transaccional que la instrucción DML que provocó su activación, preservando la parte atómica de los principios de ACID que usted menciona. La declaración de activación y el código de activación tienen éxito o fallan como una unidad.

Las propiedades de ACID también garantizan que toda la transacción (incluido el código de activación) dejará la base de datos en un estado que no viola ninguna restricción explícita ( coherente ) y cualquier efecto comprometido recuperable sobrevivirá a un bloqueo de la base de datos ( duradero ).

A menos que la transacción circundante (quizás implícita o de confirmación automática) se ejecute en el SERIALIZABLEnivel de aislamiento , la propiedad Aislada no se garantiza automáticamente. Otra actividad concurrente de la base de datos podría interferir con la operación correcta de su código de activación. Por ejemplo, el saldo de la cuenta podría cambiarse en otra sesión después de leerlo y antes de actualizarlo, una condición clásica de carrera.

2. Mis declaraciones IF y UPDATE se ven extrañas. ¿Hay alguna forma mejor de actualizar la fila correcta [Cuenta]?

Hay muy buenas razones por las que la otra pregunta a la que se vinculó no ofrece soluciones basadas en disparadores. El código de activación diseñado para mantener sincronizada una estructura desnormalizada puede ser extremadamente difícil de corregir y probar correctamente. Incluso las personas muy avanzadas de SQL Server con muchos años de experiencia luchan con esto.

Mantener un buen rendimiento al mismo tiempo que preservar la corrección en todos los escenarios y evitar problemas como puntos muertos agrega dimensiones adicionales de dificultad. Su código de activación no está cerca de ser robusto y actualiza el saldo de cada cuenta, incluso si solo se modifica una transacción. Hay todo tipo de riesgos y desafíos con una solución basada en disparadores, lo que hace que la tarea sea totalmente inadecuada para alguien relativamente nuevo en esta área de tecnología.

Para ilustrar algunos de los problemas, muestro un código de muestra a continuación. Esta no es una solución rigurosamente probada (¡los desencadenantes son difíciles!) Y no te sugiero que la uses como algo más que un ejercicio de aprendizaje. Para un sistema real, las soluciones sin disparador tienen beneficios importantes, por lo que debe revisar cuidadosamente las respuestas a la otra pregunta y evitar la idea del disparador por completo.

Tablas de muestra

CREATE TABLE dbo.Accounts
(
    AccountID integer NOT NULL,
    Balance money NOT NULL,

    CONSTRAINT PK_Accounts_ID
    PRIMARY KEY CLUSTERED (AccountID)
);

CREATE TABLE dbo.Transactions
(
    TransactionID integer IDENTITY NOT NULL,
    AccountID integer NOT NULL,
    Amount money NOT NULL,

    CONSTRAINT PK_Transactions_ID
    PRIMARY KEY CLUSTERED (TransactionID),

    CONSTRAINT FK_Accounts
    FOREIGN KEY (AccountID)
    REFERENCES dbo.Accounts (AccountID)
);

Previniendo TRUNCATE TABLE

Los disparadores no son disparados por TRUNCATE TABLE. La siguiente tabla vacía existe únicamente para evitar que la Transactionstabla se trunca (una referencia de una clave externa evita el truncamiento de la tabla):

CREATE TABLE dbo.PreventTransactionsTruncation
(
    Dummy integer NULL,

    CONSTRAINT FK_Transactions
    FOREIGN KEY (Dummy)
    REFERENCES dbo.Transactions (TransactionID),

    CONSTRAINT CHK_NoRows
    CHECK (Dummy IS NULL AND Dummy IS NOT NULL)
);

Definición de disparador

El siguiente código de activación garantiza que solo se mantengan las entradas de cuenta necesarias, y utiliza SERIALIZABLEsemántica allí. Como un efecto secundario deseable, esto también evita los resultados incorrectos que podrían resultar si se usa un nivel de aislamiento de versiones de fila. El código también evita ejecutar el código desencadenante si la instrucción de origen no afectó a ninguna fila. La tabla temporal y la RECOMPILEsugerencia se utilizan para evitar problemas del plan de ejecución del disparador causados ​​por estimaciones de cardinalidad inexactas:

CREATE TRIGGER dbo.TransactionChange ON dbo.Transactions 
AFTER INSERT, UPDATE, DELETE 
AS
BEGIN
IF @@ROWCOUNT = 0 OR
    TRIGGER_NESTLEVEL
    (
        OBJECT_ID(N'dbo.TransactionChange', N'TR'),
        'AFTER', 
        'DML'
    ) > 1 
    RETURN;

    SET NOCOUNT, XACT_ABORT ON;

    CREATE TABLE #Delta
    (
        AccountID integer PRIMARY KEY,
        Amount money NOT NULL
    );

    INSERT #Delta
        (AccountID, Amount)
    SELECT 
        InsDel.AccountID,
        Amount = SUM(InsDel.Amount)
    FROM 
    (
        SELECT AccountID, Amount
        FROM Inserted
        UNION ALL
        SELECT AccountID, $0 - Amount
        FROM Deleted
    ) AS InsDel
    GROUP BY
        InsDel.AccountID;

    UPDATE A
    SET Balance += D.Amount
    FROM #Delta AS D
    JOIN dbo.Accounts AS A WITH (SERIALIZABLE)
        ON A.AccountID = D.AccountID
    OPTION (RECOMPILE);
END;

Pruebas

El siguiente código usa una tabla de números para crear 100,000 cuentas con un saldo cero:

INSERT dbo.Accounts
    (AccountID, Balance)
SELECT
    N.n, $0
FROM dbo.Numbers AS N
WHERE
    N.n BETWEEN 1 AND 100000;

El siguiente código de prueba inserta 10,000 transacciones aleatorias:

INSERT dbo.Transactions
    (AccountID, Amount)
SELECT 
    CONVERT(integer, RAND(CHECKSUM(NEWID())) * 100000 + 1),
    CONVERT(money, RAND(CHECKSUM(NEWID())) * 500 - 250)
FROM dbo.Numbers AS N
WHERE 
    N.n BETWEEN 1 AND 10000;

Utilizando la herramienta SQLQueryStress , ejecuté esta prueba 100 veces en 32 subprocesos con buen rendimiento, sin puntos muertos y resultados correctos. Todavía no lo recomiendo como algo más que un ejercicio de aprendizaje.

Paul White 9
fuente