1. Соответствует ли триггер принципу реляционной базы данных ACID? Есть ли вероятность того, что вставка может быть зафиксирована, но триггер не работает?
На этот вопрос частично дан ответ в связанном с вами вопросе . Триггерный код выполняется в том же транзакционном контексте, что и оператор DML, вызвавший его срабатывание, сохраняя атомарную часть принципов ACID, о которых вы упомянули. Оператор триггера и код триггера успешно выполняются или не выполняются как единое целое.
Эти свойства ACID также гарантировать всю транзакцию (включая код запуска) оставят базу данных в состоянии , которое не нарушает какие - либо явные ограничения ( Последовательные ) и любые извлекаемые совершенные эффекты сохраняются в случае сбой базы данных ( Durable ).
Если окружающая (возможно, неявная или автоматическая фиксация) транзакция не выполняется на SERIALIZABLE
уровне изоляции , свойство Isolated не гарантируется автоматически. Другая одновременная активность базы данных может помешать правильной работе вашего кода триггера. Например, баланс аккаунта может быть изменен другим сеансом после его прочтения и перед обновлением - классическое условие гонки.
2. Мои заявления IF и UPDATE выглядят странно. Есть ли лучший способ обновить правильную строку [Account]?
Есть очень веские причины, по которым другой вопрос, на который вы ссылаетесь , не предлагает никаких решений на основе триггеров. Код триггера, предназначенный для синхронизации денормализованной структуры, может быть чрезвычайно сложным, чтобы получить правильные результаты и протестировать их должным образом. С этим борются даже очень продвинутые люди SQL Server с многолетним опытом.
Поддержание хорошей производительности в то же время, как сохранение правильности во всех сценариях и избежание проблем, таких как взаимоблокировки, добавляет дополнительные аспекты сложности. Ваш код триггера далеко не является надежным и обновляет баланс каждой учетной записи, даже если изменена только одна транзакция. Решение на основе триггера сопряжено с множеством рисков и проблем, что делает задачу крайне непригодной для тех, кто относительно новичок в этой области технологий.
Чтобы проиллюстрировать некоторые проблемы, ниже приведен пример кода. Это не строго проверенное решение (триггеры трудны!), И я не предлагаю вам использовать его в качестве чего-либо, кроме учебного упражнения. Для реальной системы решения без триггера имеют важные преимущества, поэтому вам следует внимательно изучить ответы на другой вопрос и полностью избежать идеи триггера.
Образцы таблиц
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)
);
предотвращение TRUNCATE TABLE
Триггеры не запускаются TRUNCATE TABLE
. Следующая пустая таблица существует исключительно для предотвращения Transactions
усечения таблицы (ссылка на внешний ключ предотвращает усечение таблицы):
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)
);
Определение триггера
Следующий триггерный код обеспечивает ведение только необходимых записей учетной записи и использует SERIALIZABLE
там семантику. Как желательный побочный эффект, это также позволяет избежать неверных результатов, которые могут возникнуть, если используется уровень изоляции управления версиями строк. Код также избегает выполнения кода триггера, если ни одна строка не была затронута исходной инструкцией. Временная таблица и RECOMPILE
подсказка используются, чтобы избежать проблем плана выполнения триггера, вызванных неточными оценками количества элементов:
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;
тестирование
Следующий код использует таблицу чисел для создания 100 000 учетных записей с нулевым балансом:
INSERT dbo.Accounts
(AccountID, Balance)
SELECT
N.n, $0
FROM dbo.Numbers AS N
WHERE
N.n BETWEEN 1 AND 100000;
Тестовый код ниже вставляет 10000 случайных транзакций:
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;
Используя инструмент SQLQueryStress , я провел этот тест 100 раз на 32 потоках с хорошей производительностью, без блокировок и с правильными результатами. Я до сих пор не рекомендую это как что-либо кроме учебного упражнения.