主頁(yè) > 知識(shí)庫(kù) > 關(guān)于喜憂參半的SQL Server觸發(fā)器詳解

關(guān)于喜憂參半的SQL Server觸發(fā)器詳解

熱門(mén)標(biāo)簽:在電子版地圖標(biāo)注要收費(fèi)嗎 實(shí)體店地圖標(biāo)注怎么標(biāo) 萬(wàn)利達(dá)綜合醫(yī)院地圖標(biāo)注點(diǎn) 電銷(xiāo)機(jī)器人 深圳 武漢AI電銷(xiāo)機(jī)器人 外呼系統(tǒng)會(huì)封嗎 地圖標(biāo)注如何弄全套標(biāo) 股票配資電銷(xiāo)機(jī)器人 南京電銷(xiāo)外呼系統(tǒng)哪家好

前言

SQL Server觸發(fā)器在非常有爭(zhēng)議的主題。它們能以較低的成本提供便利,但經(jīng)常被開(kāi)發(fā)人員、DBA誤用,導(dǎo)致性能瓶頸或維護(hù)性挑戰(zhàn)。

本文簡(jiǎn)要回顧了觸發(fā)器,并深入討論了如何有效地使用觸發(fā)器,以及何時(shí)觸發(fā)器會(huì)使開(kāi)發(fā)人員陷入難以逃脫的困境。

雖然本文中的所有演示都是在SQL Server中進(jìn)行的,但這里提供的建議是大多數(shù)數(shù)據(jù)庫(kù)通用的。觸發(fā)器帶來(lái)的挑戰(zhàn)在MySQL、PostgreSQL、MongoDB和許多其他應(yīng)用中也可以看到。

什么是觸發(fā)器

可以在數(shù)據(jù)庫(kù)或表上定義SQL Server觸發(fā)器,它允許代碼在發(fā)生特定操作時(shí)自動(dòng)執(zhí)行。本文主要關(guān)注表上的DML觸發(fā)器,因?yàn)樗鼈兺贿^(guò)度使用。相反,數(shù)據(jù)庫(kù)的DDL觸發(fā)器通常更集中,對(duì)性能的危害更小。

觸發(fā)器是對(duì)表中數(shù)據(jù)更改時(shí)進(jìn)行計(jì)算的一組代碼。觸發(fā)器可以定義為在插入、更新、刪除或這些操作的任何組合上執(zhí)行。MERGE操作可以觸發(fā)語(yǔ)句中每個(gè)操作的觸發(fā)器。

觸發(fā)器可以定義為INSTEAD OF或AFTER。AFTER觸發(fā)器發(fā)生在數(shù)據(jù)寫(xiě)入表之后,是一組獨(dú)立的操作,和寫(xiě)入表的操作在同一事務(wù)執(zhí)行,但在寫(xiě)入發(fā)生之后執(zhí)行。如果觸發(fā)器失敗,原始操作也會(huì)失敗。INSTEAD OF觸發(fā)器替換調(diào)用的寫(xiě)操作。插入、更新或刪除操作永遠(yuǎn)不會(huì)發(fā)生,而是執(zhí)行觸發(fā)器的內(nèi)容。

觸發(fā)器允許在發(fā)生寫(xiě)操作時(shí)執(zhí)行TSQL,而不管這些寫(xiě)操作的來(lái)源是什么。它們通常用于在希望確保執(zhí)行寫(xiě)操作時(shí)運(yùn)行關(guān)鍵操作,如日志記錄、驗(yàn)證或其他DML。這很方便,寫(xiě)操作可以來(lái)自API、應(yīng)用程序代碼、發(fā)布腳本,或者內(nèi)部流程,觸發(fā)器無(wú)論如何都會(huì)觸發(fā)。

觸發(fā)器是什么樣的

用WideWorldImporters示例數(shù)據(jù)庫(kù)中的Sales.Orders 表舉例,假設(shè)需要記錄該表上的所有更新或刪除操作,以及有關(guān)更改發(fā)生的一些細(xì)節(jié)。這個(gè)操作可以通過(guò)修改代碼來(lái)完成,但是這樣做需要對(duì)表的代碼寫(xiě)入中的每個(gè)位置進(jìn)行更改。通過(guò)觸發(fā)器解決這一問(wèn)題,可以采取以下步驟:

1. 創(chuàng)建一個(gè)日志表來(lái)接受寫(xiě)入的數(shù)據(jù)。下面的TSQL創(chuàng)建了一個(gè)簡(jiǎn)單日志表,以及一些添加的數(shù)據(jù)點(diǎn):

CREATE TABLE Sales.Orders_log
( Orders_log_ID int NOT NULL IDENTITY(1,1) 
 CONSTRAINT PK_Sales_Orders_log PRIMARY KEY CLUSTERED,
 OrderID int NOT NULL,
 CustomerID_Old int NOT NULL,
 CustomerID_New int NOT NULL,
 SalespersonPersonID_Old int NOT NULL,
 SalespersonPersonID_New int NOT NULL,
 PickedByPersonID_Old int NULL,
 PickedByPersonID_New int NULL,
 ContactPersonID_Old int NOT NULL,
 ContactPersonID_New int NOT NULL,
 BackorderOrderID_Old int NULL,
 BackorderOrderID_New int NULL,
 OrderDate_Old date NOT NULL,
 OrderDate_New date NOT NULL,
 ExpectedDeliveryDate_Old date NOT NULL,
 ExpectedDeliveryDate_New date NOT NULL,
 CustomerPurchaseOrderNumber_Old nvarchar(20) NULL,
 CustomerPurchaseOrderNumber_New nvarchar(20) NULL,
 IsUndersupplyBackordered_Old bit NOT NULL,
 IsUndersupplyBackordered_New bit NOT NULL,
 Comments_Old nvarchar(max) NULL,
 Comments_New nvarchar(max) NULL,
 DeliveryInstructions_Old nvarchar(max) NULL,
 DeliveryInstructions_New nvarchar(max) NULL,
 InternalComments_Old nvarchar(max) NULL,
 InternalComments_New nvarchar(max) NULL,
 PickingCompletedWhen_Old datetime2(7) NULL,
 PickingCompletedWhen_New datetime2(7) NULL,
 LastEditedBy_Old int NOT NULL,
 LastEditedBy_New int NOT NULL,
 LastEditedWhen_Old datetime2(7) NOT NULL,
 LastEditedWhen_New datetime2(7) NOT NULL,
 ActionType VARCHAR(6) NOT NULL,
 ActionTime DATETIME2(3) NOT NULL,
UserName VARCHAR(128) NULL);

該表記錄所有列的舊值和新值。這是非常全面的,我們可以簡(jiǎn)單地記錄舊版本的行,并能夠通過(guò)將新版本和舊版本合并在一起來(lái)了解更改的過(guò)程。最后3列是新增的,提供了有關(guān)執(zhí)行的操作類(lèi)型(插入、更新或刪除)、時(shí)間和操作人。

2. 創(chuàng)建一個(gè)觸發(fā)器來(lái)記錄表的更改:

CREATE TRIGGER TR_Sales_Orders_Audit
 ON Sales.Orders
 AFTER INSERT, UPDATE, DELETE
AS
BEGIN
 SET NOCOUNT ON;
 INSERT INTO Sales.Orders_log
 (OrderID, CustomerID_Old, CustomerID_New, 
 SalespersonPersonID_Old, SalespersonPersonID_New, 
 PickedByPersonID_Old, PickedByPersonID_New,
 ContactPersonID_Old, ContactPersonID_New, 
 BackorderOrderID_Old, BackorderOrderID_New, 
 OrderDate_Old, OrderDate_New, ExpectedDeliveryDate_Old,
 ExpectedDeliveryDate_New, 
 CustomerPurchaseOrderNumber_Old, 
 CustomerPurchaseOrderNumber_New, 
 IsUndersupplyBackordered_Old, 
 IsUndersupplyBackordered_New,
 Comments_Old, Comments_New, 
 DeliveryInstructions_Old, DeliveryInstructions_New, 
 InternalComments_Old, InternalComments_New, 
 PickingCompletedWhen_Old,
 PickingCompletedWhen_New, LastEditedBy_Old, 
 LastEditedBy_New, LastEditedWhen_Old, 
 LastEditedWhen_New, ActionType, ActionTime, UserName)
 SELECT
 ISNULL(Inserted.OrderID, Deleted.OrderID) AS OrderID,
 Deleted.CustomerID AS CustomerID_Old,
 Inserted.CustomerID AS CustomerID_New,
 Deleted.SalespersonPersonID AS SalespersonPersonID_Old,
 Inserted.SalespersonPersonID AS SalespersonPersonID_New,
 Deleted.PickedByPersonID AS PickedByPersonID_Old,
 Inserted.PickedByPersonID AS PickedByPersonID_New,
 Deleted.ContactPersonID AS ContactPersonID_Old,
 Inserted.ContactPersonID AS ContactPersonID_New,
 Deleted.BackorderOrderID AS BackorderOrderID_Old,
 Inserted.BackorderOrderID AS BackorderOrderID_New,
 Deleted.OrderDate AS OrderDate_Old,
 Inserted.OrderDate AS OrderDate_New,
 Deleted.ExpectedDeliveryDate 
 AS ExpectedDeliveryDate_Old,
 Inserted.ExpectedDeliveryDate 
 AS ExpectedDeliveryDate_New,
 Deleted.CustomerPurchaseOrderNumber 
 AS CustomerPurchaseOrderNumber_Old,
 Inserted.CustomerPurchaseOrderNumber 
 AS CustomerPurchaseOrderNumber_New,
 Deleted.IsUndersupplyBackordered 
 AS IsUndersupplyBackordered_Old,
 Inserted.IsUndersupplyBackordered 
 AS IsUndersupplyBackordered_New,
 Deleted.Comments AS Comments_Old,
 Inserted.Comments AS Comments_New,
 Deleted.DeliveryInstructions 
 AS DeliveryInstructions_Old,
 Inserted.DeliveryInstructions 
 AS DeliveryInstructions_New,
 Deleted.InternalComments AS InternalComments_Old,
 Inserted.InternalComments AS InternalComments_New,
 Deleted.PickingCompletedWhen 
 AS PickingCompletedWhen_Old,
 Inserted.PickingCompletedWhen 
 AS PickingCompletedWhen_New,
 Deleted.LastEditedBy AS LastEditedBy_Old,
 Inserted.LastEditedBy AS LastEditedBy_New,
 Deleted.LastEditedWhen AS LastEditedWhen_Old,
 Inserted.LastEditedWhen AS LastEditedWhen_New,
 CASE
 WHEN Inserted.OrderID IS NULL THEN 'DELETE'
 WHEN Deleted.OrderID IS NULL THEN 'INSERT'
 ELSE 'UPDATE'
 END AS ActionType,
 SYSUTCDATETIME() ActionTime,
 SUSER_SNAME() AS UserName
 FROM Inserted
 FULL JOIN Deleted
 ON Inserted.OrderID = Deleted.OrderID;
END

該觸發(fā)器的唯一功能是將數(shù)據(jù)插入到日志表中,每行數(shù)據(jù)對(duì)應(yīng)一個(gè)給定的寫(xiě)操作。它很簡(jiǎn)單,隨著時(shí)間的推移易于記錄和維護(hù),表也會(huì)發(fā)生變化。如果需要跟蹤其他詳細(xì)信息,可以添加其他列,如數(shù)據(jù)庫(kù)名稱(chēng)、服務(wù)器名稱(chēng)、受影響列的行數(shù)或調(diào)用的應(yīng)用程序。

3.最后一步是測(cè)試和驗(yàn)證日志表是否正確。

以下是添加觸發(fā)器后對(duì)表進(jìn)行更新的測(cè)試:

UPDATE Orders
 SET InternalComments = 'Item is no longer backordered',
 BackorderOrderID = NULL,
 IsUndersupplyBackordered = 0,
 LastEditedBy = 1,
 LastEditedWhen = SYSUTCDATETIME()
FROM sales.Orders
WHERE Orders.OrderID = 10;

結(jié)果如下:

點(diǎn)擊并拖拽以移動(dòng)​

上面省略了一些列,但是我們可以快速確認(rèn)已經(jīng)觸發(fā)了更改,包括日志表末尾新增的列。

INSERT和DELETE

前面的示例中,進(jìn)行插入和刪除操作后,讀取日志表中使用的數(shù)據(jù)。這種特殊的表可以作為任何相關(guān)寫(xiě)操作的一部分。INSERT將包含被插入操作觸發(fā),DELETE將被刪除操作觸發(fā),UPDATE包含被插入和刪除操作觸發(fā)。

對(duì)于INSERT和UPDATE,將包含表中每個(gè)列新值的快照。對(duì)于DELETE和UPDATE操作,將包含寫(xiě)操作之前表中每個(gè)列舊值的快照。

觸發(fā)器什么時(shí)候最有用

DML觸發(fā)器的最佳使用是簡(jiǎn)短、簡(jiǎn)單且易于維護(hù)的寫(xiě)操作,這些操作在很大程度上獨(dú)立于應(yīng)用程序業(yè)務(wù)邏輯。

  • 觸發(fā)器的一些重要用途包括:
  • 記錄對(duì)歷史表的更改
  • 審計(jì)用戶及其對(duì)敏感表的操作。
  • 向表中添加應(yīng)用程序可能無(wú)法使用的額外值(由于安全限制或其他限制),例如:
    •  登錄/用戶名
    •  操作發(fā)生時(shí)間
    • 服務(wù)器/數(shù)據(jù)庫(kù)名稱(chēng)
  • 簡(jiǎn)單的驗(yàn)證。

關(guān)鍵是讓觸發(fā)器代碼保持足夠的緊湊,從而便于維護(hù)。當(dāng)觸發(fā)器增長(zhǎng)到成千上萬(wàn)行時(shí),它們就成了開(kāi)發(fā)人員不敢去打擾的黑盒。結(jié)果,更多的代碼被添加進(jìn)來(lái),但是舊的代碼很少被檢查。即使有了文檔,這也很難維護(hù)。

為了讓觸發(fā)器有效地發(fā)揮作用,應(yīng)該將它們編寫(xiě)為基于設(shè)置的。如果存儲(chǔ)過(guò)程必須在觸發(fā)器中使用,則確保它們?cè)谛枰獣r(shí)使用表值參數(shù),以便可以基于集的方式移動(dòng)數(shù)據(jù)。下面是一個(gè)觸發(fā)器的示例,該觸發(fā)器遍歷id,以便使用結(jié)果順序id執(zhí)行示例存儲(chǔ)過(guò)程:

CREATE TRIGGER TR_Sales_Orders_Process
 ON Sales.Orders
 AFTER INSERT
AS
BEGIN
 SET NOCOUNT ON;
 DECLARE @count INT;
 SELECT @count = COUNT(*) FROM inserted;
 DECLARE @min_id INT;
 SELECT @min_id = MIN(OrderID) FROM inserted;
 DECLARE @current_id INT = @min_id;
 WHILE @current_id  @current_id + @count
 BEGIN
 EXEC dbo.process_order_fulfillment 
 @OrderID = @current_id;
 SELECT @current_id = @current_id + 1;
 END
END

雖然相對(duì)簡(jiǎn)單,但當(dāng)一次插入多行時(shí)對(duì) Sales.Orders的INSERT操作的性能將受到影響,因?yàn)镾QL Server在執(zhí)行process_order_fulfillment存儲(chǔ)過(guò)程時(shí)將被迫逐個(gè)執(zhí)行。一個(gè)簡(jiǎn)單的修復(fù)方法是重寫(xiě)存儲(chǔ)過(guò)程,并將一組Order id傳遞到存儲(chǔ)過(guò)程中,而不是一次一個(gè)地這樣做:

CREATE TYPE dbo.udt_OrderID_List AS TABLE(
 OrderID INT NOT NULL,
 PRIMARY KEY CLUSTERED 
( OrderID ASC));
GO
CREATE TRIGGER TR_Sales_Orders_Process
 ON Sales.Orders
 AFTER INSERT
AS
BEGIN
 SET NOCOUNT ON;
 DECLARE @OrderID_List dbo.udt_OrderID_List;
 EXEC dbo.process_order_fulfillment @OrderIDs = @OrderID_List;
END

更改的結(jié)果是將完整的id集合從觸發(fā)器傳遞到存儲(chǔ)過(guò)程并進(jìn)行處理。只要存儲(chǔ)過(guò)程以基于集合的方式管理這些數(shù)據(jù),就可以避免重復(fù)執(zhí)行,也就是說(shuō),避免在觸發(fā)器內(nèi)使用存儲(chǔ)過(guò)程有很大的價(jià)值,因?yàn)樗鼈兲砑恿祟~外的封裝層,進(jìn)一步隱藏了在數(shù)據(jù)寫(xiě)入表時(shí)執(zhí)行的TSQL。它們應(yīng)該被認(rèn)為是最后的手段,只有當(dāng)可以在應(yīng)用程序的許多地方多次重寫(xiě)TSQL時(shí)才使用。

什么時(shí)候觸發(fā)器是危險(xiǎn)的

架構(gòu)師和開(kāi)發(fā)人員面臨的最大挑戰(zhàn)之一是確保觸發(fā)器只在需要時(shí)使用,而不允許它們成為一刀切的解決方案。向觸發(fā)器添加TSQL通常被認(rèn)為比向應(yīng)用程序添加代碼更快、更容易,但隨著時(shí)間的推移,這樣做的成本會(huì)隨著每添加一行代碼而增加。

觸發(fā)器在以下情況下會(huì)變得危險(xiǎn):

  • 保持盡可能少的觸發(fā)以減少?gòu)?fù)雜性。
  • 觸發(fā)代碼變得復(fù)雜。如果更新表中的一行導(dǎo)致要執(zhí)行數(shù)千行添加的觸發(fā)器代碼,那么開(kāi)發(fā)人員就很難完全理解數(shù)據(jù)寫(xiě)入表時(shí)會(huì)發(fā)生什么。更糟糕的是,當(dāng)出現(xiàn)問(wèn)題時(shí),故障排除非常具有挑戰(zhàn)性。
  • 觸發(fā)器跨服務(wù)器。這將網(wǎng)絡(luò)操作引入到觸發(fā)器中,可能導(dǎo)致在出現(xiàn)連接問(wèn)題時(shí)寫(xiě)入速度變慢或失敗。如果目標(biāo)數(shù)據(jù)庫(kù)是要維護(hù)的對(duì)象,那么即使是跨數(shù)據(jù)庫(kù)觸發(fā)器也會(huì)有問(wèn)題。
  • 觸發(fā)器調(diào)用觸發(fā)器。觸發(fā)器中最令人痛苦的是,當(dāng)插入一行時(shí),寫(xiě)操作會(huì)導(dǎo)致75個(gè)表中有100個(gè)觸發(fā)器要執(zhí)行。在編寫(xiě)觸發(fā)器代碼時(shí),確保觸發(fā)器可以執(zhí)行所有必要的邏輯,而不會(huì)觸發(fā)更多觸發(fā)器。額外的觸發(fā)通常是不必要的。
  • 遞歸觸發(fā)器被設(shè)置為ON。這是一個(gè)默認(rèn)設(shè)置為off的數(shù)據(jù)庫(kù)級(jí)別設(shè)置。打開(kāi)時(shí),它允許觸發(fā)器的內(nèi)容調(diào)用相同的觸發(fā)器。遞歸觸發(fā)器會(huì)極大地?fù)p害性能,調(diào)試時(shí)也會(huì)非?;靵y。通常,當(dāng)一個(gè)觸發(fā)器中的DML作為操作的一部分觸發(fā)其他觸發(fā)器時(shí),使用遞歸觸發(fā)器。
  • 函數(shù)、存儲(chǔ)過(guò)程或視圖都在觸發(fā)器中。在觸發(fā)器中封裝更多的業(yè)務(wù)邏輯會(huì)使它們變得更復(fù)雜,并給人一種觸發(fā)器代碼短小簡(jiǎn)單的錯(cuò)誤印象,而實(shí)際上并非如此。盡可能避免在觸發(fā)器中使用存儲(chǔ)過(guò)程和函數(shù)。
  • 迭代發(fā)生。循環(huán)和游標(biāo)本質(zhì)上是逐行操作的,可能會(huì)導(dǎo)致對(duì)1000行的操作一次觸發(fā)1000次,這極大地?fù)p害了查詢性能。

這是一個(gè)很長(zhǎng)的列表,但通??梢钥偨Y(jié)為短而簡(jiǎn)單的觸發(fā)器會(huì)表現(xiàn)得更好,并避免上面的大多數(shù)陷阱。如果使用觸發(fā)器來(lái)維護(hù)復(fù)雜的業(yè)務(wù)邏輯,那么隨著時(shí)間的推移,越來(lái)越多的業(yè)務(wù)邏輯將被添加進(jìn)來(lái),并且不可避免地將違反上述最佳實(shí)踐。

重要的是要注意,為了維護(hù)原子的、事務(wù),受觸發(fā)器影響的任何對(duì)象都將保持事務(wù)處于打開(kāi)狀態(tài),直到該觸發(fā)器完成。這意味著長(zhǎng)觸發(fā)器不僅會(huì)使事務(wù)持續(xù)時(shí)間更長(zhǎng),而且還會(huì)持有鎖并導(dǎo)致持續(xù)時(shí)間更長(zhǎng)。因此,在測(cè)試觸發(fā)器時(shí),在為現(xiàn)有觸發(fā)器創(chuàng)建或添加額外邏輯時(shí),應(yīng)該了解它們對(duì)鎖、阻塞和等待的影響。

如何改善觸發(fā)器

有很多方法可以使觸發(fā)器更易于維護(hù)、更容易理解和性能更高。以下是一些關(guān)于如何有效管理觸發(fā)器和避免落入陷阱的建議。

觸發(fā)器本身應(yīng)該有良好的文檔記錄:

  • 這個(gè)觸發(fā)器為什么存在?
  • 它能做什么?
  • 它是如何工作的?
  • 對(duì)于觸發(fā)器的工作方式是否有任何例外或警告?

此外,如果觸發(fā)器中的TSQL難以理解,那么可以添加內(nèi)聯(lián)注釋?zhuān)詭椭谝淮尾榭此拈_(kāi)發(fā)人員。

下面是觸發(fā)器文檔的樣例:

/* 12/29/2020 EHP
 This trigger logs all changes to the table to the Orders_log
 table that occur for non-internal customers.
 CustomerID = -1 signifies an internal/test customer and 
 these are not audited.
*/
CREATE TRIGGER TR_Sales_Orders_Audit
 ON Sales.Orders
 FOR INSERT, UPDATE, DELETE
AS
BEGIN
 SET NOCOUNT ON;
 INSERT INTO Sales.Orders_log
 (OrderID, CustomerID_Old, CustomerID_New, 
 SalespersonPersonID_Old, SalespersonPersonID_New,
 PickedByPersonID_Old, PickedByPersonID_New,
 ContactPersonID_Old, ContactPersonID_New, 
 BackorderOrderID_Old, BackorderOrderID_New, 
 OrderDate_Old, OrderDate_New, 
 ExpectedDeliveryDate_Old,
 ExpectedDeliveryDate_New, 
 CustomerPurchaseOrderNumber_Old, 
 CustomerPurchaseOrderNumber_New, 
 IsUndersupplyBackordered_Old, 
 IsUndersupplyBackordered_New,
 Comments_Old, Comments_New, 
 DeliveryInstructions_Old, DeliveryInstructions_New, 
 nternalComments_Old, InternalComments_New, 
 PickingCompletedWhen_Old, PickingCompletedWhen_New, 
 LastEditedBy_Old, LastEditedBy_New, 
 LastEditedWhen_Old, LastEditedWhen_New, 
 ActionType, ActionTime, UserName)
 SELECT
 ISNULL(Inserted.OrderID, Deleted.OrderID) AS OrderID, 
 -- The OrderID can never change. 
 --This ensures we get the ID correctly, 
 --regardless of operation type.
 Deleted.CustomerID AS CustomerID_Old,
 Inserted.CustomerID AS CustomerID_New,
 Deleted.SalespersonPersonID AS SalespersonPersonID_Old,
 Inserted.SalespersonPersonID AS SalespersonPersonID_New,
 Deleted.PickedByPersonID AS PickedByPersonID_Old,
 Inserted.PickedByPersonID AS PickedByPersonID_New,
 Deleted.ContactPersonID AS ContactPersonID_Old,
 Inserted.ContactPersonID AS ContactPersonID_New,
 Deleted.BackorderOrderID AS BackorderOrderID_Old,
 Inserted.BackorderOrderID AS BackorderOrderID_New,
 Deleted.OrderDate AS OrderDate_Old,
 Inserted.OrderDate AS OrderDate_New,
 Deleted.ExpectedDeliveryDate AS ExpectedDeliveryDate_Old,
 Inserted.ExpectedDeliveryDate AS ExpectedDeliveryDate_New,
 Deleted.CustomerPurchaseOrderNumber 
 AS CustomerPurchaseOrderNumber_Old,
 Inserted.CustomerPurchaseOrderNumber 
 AS CustomerPurchaseOrderNumber_New,
 Deleted.IsUndersupplyBackordered 
 AS IsUndersupplyBackordered_Old,
 Inserted.IsUndersupplyBackordered 
 AS IsUndersupplyBackordered_New,
 Deleted.Comments AS Comments_Old,
 Inserted.Comments AS Comments_New,
 Deleted.DeliveryInstructions 
 AS DeliveryInstructions_Old,
 Inserted.DeliveryInstructions 
 AS DeliveryInstructions_New,
 Deleted.InternalComments AS InternalComments_Old,
 Inserted.InternalComments AS InternalComments_New,
 Deleted.PickingCompletedWhen AS PickingCompletedWhen_Old,
 Inserted.PickingCompletedWhen 
 AS PickingCompletedWhen_New,
 Deleted.LastEditedBy AS LastEditedBy_Old,
 Inserted.LastEditedBy AS LastEditedBy_New,
 Deleted.LastEditedWhen AS LastEditedWhen_Old,
 Inserted.LastEditedWhen AS LastEditedWhen_New,
 CASE -- Determine the operation type based on whether 
 --Inserted exists, Deleted exists, or both exist.
 WHEN Inserted.OrderID IS NULL THEN 'DELETE'
 WHEN Deleted.OrderID IS NULL THEN 'INSERT'
 ELSE 'UPDATE'
 END AS ActionType,
 SYSUTCDATETIME() ActionTime,
 SUSER_SNAME() AS UserName
 FROM Inserted
 FULL JOIN Deleted
 ON Inserted.OrderID = Deleted.OrderID
 WHERE Inserted.CustomerID > -1 
 -- -1 indicates an internal/non-production 
 --customer that should not be audited.
 OR Deleted.CustomerID > -1; 
 -- -1 indicates an internal/non-production 
 --customer that should not be audited.
END

請(qǐng)注意,該文檔并不全面,但包含了一個(gè)簡(jiǎn)短的頭,并解釋了觸發(fā)器內(nèi)的一些TSQL關(guān)鍵部分:

  • 排除CustomerID = -1的情況。這一點(diǎn)對(duì)于不知道的人來(lái)說(shuō)是不明顯的,所以這是一個(gè)很好的注釋。
  • ActionType的CASE語(yǔ)句用于什么。
  • 為什么在插入和刪除之間的OrderID列上使用ISNULL。

使用IF UPDATE

在觸發(fā)器中,UPDATE提供了判斷是否將數(shù)據(jù)寫(xiě)入給定列的能力。這可以允許觸發(fā)器檢查列在執(zhí)行操作之前是否發(fā)生了更改。下面是該語(yǔ)法的示例:

CREATE TRIGGER TR_Sales_Orders_Log_BackorderID_Change
 ON Sales.Orders
 AFTER UPDATE
AS
BEGIN
 SET NOCOUNT ON;
 IF UPDATE(BackorderOrderID)
 BEGIN
 UPDATE OrderBackorderLog
 SET BackorderOrderID = Inserted.BackorderOrderID,
 PreviousBackorderOrderID = Deleted.BackorderOrderID
 FROM dbo.OrderBackorderLog
 INNER JOIN Inserted
 ON Inserted.OrderID = OrderBackorderLog.OrderID
 END
END

通過(guò)首先檢查BackorderID是否被更新,觸發(fā)器可以在不需要時(shí)繞過(guò)后續(xù)操作。這是一種提高性能的好方法,它允許觸發(fā)器根據(jù)所需列的更新值完全跳過(guò)代碼。

COLUMNS_UPDATED指示表中的哪些列作為寫(xiě)操作的一部分進(jìn)行了更新,可以在觸發(fā)器中使用它來(lái)快速確定指定的列是否受到插入或更新操作的影響。雖然有文檔記錄,但它使用起來(lái)很復(fù)雜,很難進(jìn)行文檔記錄。我通常不建議使用它,因?yàn)樗鼛缀蹩隙〞?huì)使不熟悉它的開(kāi)發(fā)人員感到困惑。

請(qǐng)注意,對(duì)于UPDATE或COLUMNS_UPDATED,列是否更改并不重要。對(duì)列進(jìn)行寫(xiě)操作,即使值沒(méi)有改變,對(duì)于UPDATE操作仍然返回1,對(duì)于COLUMNS_UPDATED操作仍然返回1。它們只跟蹤指定的列是否是寫(xiě)操作的目標(biāo),而不跟蹤值本身是否改變。

每個(gè)操作一個(gè)觸發(fā)器

讓觸發(fā)代碼盡可能的簡(jiǎn)單。數(shù)據(jù)庫(kù)表的觸發(fā)器數(shù)量增長(zhǎng)會(huì)大大增加表的復(fù)雜性,理解其操作變得更加困難。。

例如,考慮以下表觸發(fā)器定義方式:

CREATE TRIGGER TR_Sales_Orders_I
 ON Sales.Orders
 AFTER INSERT
CREATE TRIGGER TR_Sales_Orders_IU
 ON Sales.Orders
 AFTER INSERT, UPDATE
CREATE TRIGGER TR_Sales_Orders_UD
 ON Sales.Orders
 AFTER UPDATE, DELETE
CREATE TRIGGER TR_Sales_Orders_UID
 ON Sales.Orders
 AFTER UPDATE, INSERT, DELETE
CREATE TRIGGER TR_Sales_Orders_ID
 ON Sales.Orders
 AFTER INSERT, DELETE

當(dāng)插入一行時(shí)會(huì)發(fā)生什么?觸發(fā)器的觸發(fā)順序是什么?這些問(wèn)題的答案需要研究。維護(hù)更少的觸發(fā)器是一個(gè)簡(jiǎn)單的解決方案,并且消除了對(duì)給定表中如何發(fā)生寫(xiě)操作的猜測(cè)。作為參考,可以使用系統(tǒng)存儲(chǔ)過(guò)程sp_settriggerorder修改觸發(fā)器順序,不過(guò)這只適用于AFTER觸發(fā)器。

再簡(jiǎn)單一點(diǎn)

觸發(fā)器的最佳實(shí)踐是操作簡(jiǎn)單,執(zhí)行迅速,并且不會(huì)因?yàn)樗鼈兊膱?zhí)行而觸發(fā)更多的觸發(fā)器。觸發(fā)器的復(fù)雜程度并沒(méi)有明確的規(guī)則,但有一條簡(jiǎn)單的指導(dǎo)原則是,理想的觸發(fā)器應(yīng)該足夠簡(jiǎn)單,如果必須將觸發(fā)器中包含的邏輯移到其他地方,那么遷移的代價(jià)不會(huì)高得令人望而卻步。也就是說(shuō),如果觸發(fā)器中的業(yè)務(wù)邏輯非常復(fù)雜,以至于移動(dòng)它的成本太高而無(wú)法考慮,那么這些觸發(fā)器很可能變得過(guò)于復(fù)雜。

使用我們前面的示例,考慮一下更改審計(jì)的觸發(fā)器。這可以很容易地從觸發(fā)器轉(zhuǎn)移到存儲(chǔ)過(guò)程或代碼中,而這樣做的工作量并不大。觸發(fā)器中記錄日志的方便性使它值得一做,但與此同時(shí),我們應(yīng)該知道開(kāi)發(fā)人員將TSQL從觸發(fā)器遷移到另一個(gè)位置需要多少小時(shí)。

時(shí)間的計(jì)算可以看作是觸發(fā)器的可維護(hù)性成本的一部分。也就是說(shuō),如果有必要,為擺脫觸發(fā)機(jī)制而必須付出的代價(jià)。這聽(tīng)起來(lái)可能很抽象,但平臺(tái)之間的數(shù)據(jù)庫(kù)遷移是很常見(jiàn)的。在SQL Server中執(zhí)行良好的一組觸發(fā)器在Oracle或PostgreSQL中可能并不有效。

優(yōu)化表變量

有時(shí),一個(gè)觸發(fā)器中需要臨時(shí)表,以允許對(duì)數(shù)據(jù)進(jìn)行多次更新。臨時(shí)表存儲(chǔ)在tempdb中,并且受到tempdb數(shù)據(jù)庫(kù)大小、速度和性能約束的影響。

對(duì)于經(jīng)常訪問(wèn)的臨時(shí)表,優(yōu)化表變量是在內(nèi)存中(而不是在tempdb中)維護(hù)臨時(shí)數(shù)據(jù)的好方法。

下面的TSQL為內(nèi)存優(yōu)化數(shù)據(jù)配置了一個(gè)數(shù)據(jù)庫(kù)(如果需要):

ALTER DATABASE WideWorldImporters 
SET MEMORY_OPTIMIZED_ELEVATE_TO_SNAPSHOT = ON;
ALTER DATABASE WideWorldImporters ADD FILEGROUP WWI_InMemory_Data 
 CONTAINS MEMORY_OPTIMIZED_DATA;
ALTER DATABASE WideWorldImporters ADD FILE 
 (NAME='WideWorldImporters_IMOLTP_File_1', 
 FILENAME='C:\SQLData\WideWorldImporters_IMOLTP_File_1.mem') 
 TO FILEGROUP WWI_InMemory_Data;

一旦配置完成,就可以創(chuàng)建一個(gè)內(nèi)存優(yōu)化的表類(lèi)型:

CREATE TYPE dbo.SalesOrderMetadata
AS TABLE
( OrderID INT NOT NULL PRIMARY KEY NONCLUSTERED,
 CustomerID INT NOT NULL,
 SalespersonPersonID INT NOT NULL,
 ContactPersonID INT NOT NULL,
 INDEX IX_SalesOrderMetadata_CustomerID NONCLUSTERED HASH 
 (CustomerID) WITH (BUCKET_COUNT = 1000))
WITH (MEMORY_OPTIMIZED = ON);

這個(gè)TSQL創(chuàng)建了演示的觸發(fā)器所需要的表:

CREATE TABLE dbo.OrderAdjustmentLog
( OrderAdjustmentLog_ID int NOT NULL IDENTITY(1,1) 
 CONSTRAINT PK_OrderAdjustmentLog PRIMARY KEY CLUSTERED,
 OrderID INT NOT NULL,
 CustomerID INT NOT NULL,
 SalespersonPersonID INT NOT NULL,
 ContactPersonID INT NOT NULL,
CreateTimeUTC DATETIME2(3) NOT NULL);

下面是一個(gè)使用內(nèi)存優(yōu)化表的觸發(fā)器演示:

CREATE TRIGGER TR_Sales_Orders_Mem_Test
 ON Sales.Orders
 AFTER UPDATE
AS
BEGIN
 SET NOCOUNT ON;
 DECLARE @OrderData dbo.SalesOrderMetadata;
 INSERT INTO @OrderData
 (OrderID, CustomerID, SalespersonPersonID, 
 ContactPersonID)
 SELECT
 OrderID,
 CustomerID,
 SalespersonPersonID,
 ContactPersonID
 FROM Inserted;
 
 DELETE OrderData
 FROM @OrderData OrderData
 INNER JOIN sales.Customers
 ON Customers.CustomerID = OrderData.CustomerID
 WHERE Customers.IsOnCreditHold = 0;
 UPDATE OrderData
 SET ContactPersonID = 1
 FROM @OrderData OrderData
 WHERE OrderData.ContactPersonID IS NULL;
 
 INSERT INTO dbo.OrderAdjustmentLog
 (OrderID, CustomerID, SalespersonPersonID, 
 ContactPersonID, CreateTimeUTC)
 SELECT
 OrderData.OrderID,
 OrderData.CustomerID,
 OrderData.SalespersonPersonID,
 OrderData.ContactPersonID,
 SYSUTCDATETIME()
 FROM @OrderData OrderData;
END

觸發(fā)器內(nèi)需要的操作越多,節(jié)省的時(shí)間就越多,因?yàn)閮?nèi)存優(yōu)化的表變量不需要IO來(lái)讀/寫(xiě)。

一旦讀取了來(lái)自所插入表的初始數(shù)據(jù),觸發(fā)器的其余部分就可以不處理tempdb,從而減少使用標(biāo)準(zhǔn)表變量或臨時(shí)表的開(kāi)銷(xiāo)。

下面的代碼設(shè)置了一些測(cè)試數(shù)據(jù),并運(yùn)行一個(gè)更新來(lái)演示上述代碼的結(jié)果:

UPDATE Customers
 SET IsOnCreditHold = 1
FROM Sales.Customers
WHERE Customers.CustomerID = 832;
UPDATE Orders
 SET SalespersonPersonID = 2
FROM sales.Orders
WHERE CustomerID = 832;

一旦執(zhí)行,OrderAdjustmentLog表的內(nèi)容可以被驗(yàn)證:

結(jié)果是意料之中的。通過(guò)減少對(duì)標(biāo)準(zhǔn)存儲(chǔ)的依賴(lài)并將中間表移動(dòng)到內(nèi)存中,內(nèi)存優(yōu)化表提供了一種大大提高觸發(fā)速度的方法。這僅限于對(duì)臨時(shí)對(duì)象有大量調(diào)用的場(chǎng)景,但在存儲(chǔ)過(guò)程或其他過(guò)程性TSQL中也很有用。

替代觸發(fā)器

像所有的工具一樣,觸發(fā)器也可能被濫用,并成為混亂、性能瓶頸和可維護(hù)性噩夢(mèng)的根源。有許多比觸發(fā)器更可取的替代方案,在實(shí)現(xiàn)(或添加到現(xiàn)有的)觸發(fā)器之前應(yīng)該考慮它們。

Temporal tables

Temporal tables是在SQL Server 2016中引入的,它提供了一種向表添加版本控制的簡(jiǎn)單方法,無(wú)需構(gòu)建自己的數(shù)據(jù)結(jié)構(gòu)和ETL。這種記錄對(duì)應(yīng)用程序是不可見(jiàn)的,并提供了符合ANSI標(biāo)準(zhǔn)的完整版本支持,使之成為一種簡(jiǎn)單的方法來(lái)解決保存舊版本數(shù)據(jù)的問(wèn)題。

Check約束

對(duì)于簡(jiǎn)單的數(shù)據(jù)驗(yàn)證,Check約束可以提供所需的內(nèi)容,而不需要函數(shù)、存儲(chǔ)過(guò)程或觸發(fā)器。在列上定義Check約束,并在創(chuàng)建數(shù)據(jù)時(shí)自動(dòng)驗(yàn)證數(shù)據(jù)。

下面是一個(gè)Check約束的示例:

ALTER TABLE Sales.Invoices WITH CHECK ADD CONSTRAINT
CK_Sales_Invoices_ReturnedDeliveryData_Must_Be_Valid_JSON
CHECK ([ReturnedDeliveryData] IS NULL OR 
ISJSON([ReturnedDeliveryData])>(0))

這段代碼檢查一個(gè)列是否是有效的JSON。如果是,則執(zhí)行正常進(jìn)行。如果不是,那么SQL Server將拋出一個(gè)錯(cuò)誤,寫(xiě)操作將失敗。Check約束可以檢查列和值的任何組合,因此可以管理簡(jiǎn)單或復(fù)雜的驗(yàn)證任務(wù)。

創(chuàng)建Check約束的成本不高,而且易于維護(hù)。它們也更容易記錄和理解,因?yàn)镃heck約束的范圍僅限于驗(yàn)證傳入數(shù)據(jù)和確保數(shù)據(jù)完整性,而觸發(fā)器實(shí)際上可以做任何可以想象的事情!

唯一約束

如果一個(gè)列需要唯一的值,并且不是表上的主鍵,那么唯一約束是完成該任務(wù)的一種簡(jiǎn)單而有效的方法。唯一約束是索引和唯一性的組合。為了有效地驗(yàn)證唯一性,索引是必需的。

下面是一個(gè)唯一約束的例子:

ALTER TABLE Warehouse.Colors ADD CONSTRAINT 
UQ_Warehouse_Colors_ColorName UNIQUE NONCLUSTERED (ColorName ASC);

每當(dāng)一行被插入到 Warehouse.Colors表中,將檢查ColorName的唯一性。如果寫(xiě)操作碰巧導(dǎo)致了重復(fù)的顏色,那么語(yǔ)句將失敗,數(shù)據(jù)將不會(huì)被更改。為此目的構(gòu)建了唯一約束,這是在列上強(qiáng)制唯一性的最簡(jiǎn)單方法。

內(nèi)置的解決方案將更高效、更容易維護(hù)和更容易記錄。任何看到唯一約束的開(kāi)發(fā)人員都將立即理解它的作用,而不需要深入挖掘TSQL來(lái)弄清事情是如何工作的,這種簡(jiǎn)單性使其成為理想的解決方案。

外鍵約束

與Check約束和唯一約束一樣,外鍵約束是在寫(xiě)入數(shù)據(jù)之前驗(yàn)證數(shù)據(jù)完整性的另一種方式。外鍵將一一表中的列鏈接到另一張表。當(dāng)數(shù)據(jù)插入到目標(biāo)表時(shí),它的值將根據(jù)引用的表進(jìn)行檢查。如果該值存在,則寫(xiě)操作正常進(jìn)行。如果不是,則拋出錯(cuò)誤,語(yǔ)句失敗。

這是一個(gè)簡(jiǎn)單的外鍵例子:

ALTER TABLE Sales.Orders WITH CHECK ADD CONSTRAINT
FK_Sales_Orders_CustomerID_Sales_Customers FOREIGN KEY (CustomerID)
REFERENCES Sales.Customers (CustomerID);

當(dāng)數(shù)據(jù)寫(xiě)入Sales.Orders時(shí),CustomerID列將根據(jù)Sales.Customers中的CustomerID列進(jìn)行檢查。

與唯一約束類(lèi)似,外鍵只有一個(gè)目的:驗(yàn)證寫(xiě)入一個(gè)表的數(shù)據(jù)是否存在于另一個(gè)表中。它易于文檔化,易于理解,實(shí)現(xiàn)效率高。

觸發(fā)器不是執(zhí)行這些驗(yàn)證檢查的正確位置,與使用外鍵相比,它是效率較低的解決方案。

存儲(chǔ)過(guò)程

在觸發(fā)器中實(shí)現(xiàn)的邏輯通??梢院苋菀椎匾苿?dòng)到存儲(chǔ)過(guò)程中。這消除了大量觸發(fā)代碼可能導(dǎo)致的復(fù)雜性,同時(shí)允許開(kāi)發(fā)人員更好的維護(hù)。存儲(chǔ)過(guò)程可以自由地構(gòu)造操作,以確保盡可能多的原子性。

實(shí)現(xiàn)觸發(fā)器的基本原則之一是確保一組操作與寫(xiě)操作一致。所有成功或失敗都是作為原子事務(wù)的一部分。應(yīng)用程序并不總是需要這種級(jí)別的原子性。如果有必要,可以在存儲(chǔ)過(guò)程中使用適當(dāng)?shù)母綦x級(jí)別或表鎖定來(lái)保證事務(wù)的完整性。

雖然SQL Server(和大多數(shù)RDBMS)提供了ACID保證事務(wù)將是原子的、一致的、隔離的和持久的,但我們自己代碼中的事務(wù)可能需要也可能不需要遵循相同的規(guī)則?,F(xiàn)實(shí)世界的應(yīng)用程序?qū)?shù)據(jù)完整性的需求各不相同。

存儲(chǔ)過(guò)程允許自定義代碼,以實(shí)現(xiàn)應(yīng)用程序所需的數(shù)據(jù)完整性,確保性能和計(jì)算資源不會(huì)浪費(fèi)在不需要的數(shù)據(jù)完整性上。

例如,一個(gè)允許用戶發(fā)布照片的社交媒體應(yīng)用程序不太可能需要它的事務(wù)完全原子化和一致。如果我的照片出現(xiàn)在你之前或之后一秒,沒(méi)人會(huì)在意。同樣,如果你在我編輯照片的時(shí)候評(píng)論我的照片,時(shí)間對(duì)使用這些數(shù)據(jù)的人來(lái)說(shuō)可能并不重要。另一方面,一個(gè)管理貨幣交易的銀行應(yīng)用程序需要確保交易是謹(jǐn)慎執(zhí)行的,這樣就不會(huì)出現(xiàn)資金丟失或數(shù)字報(bào)告錯(cuò)誤的情況。如果我有一個(gè)銀行賬戶,里面有20美元,我取出20美元的同時(shí),其他人也取出了20美元,我們不可能都成功。我們中的一個(gè)先得到20美元,另一個(gè)遇到關(guān)于0美元余額的適當(dāng)錯(cuò)誤消息。

函數(shù)

函數(shù)提供了一種簡(jiǎn)單的方法,可以將重要的邏輯封裝到一個(gè)單獨(dú)的位置。在50個(gè)表插入中重用的單個(gè)函數(shù)比50個(gè)觸發(fā)器(每個(gè)表一個(gè)觸發(fā)器)執(zhí)行相同邏輯要容易得多。

考慮以下函數(shù):

CREATE FUNCTION Website.CalculateCustomerPrice
 (@CustomerID INT, @StockItemID INT, @PricingDate DATE)
RETURNS DECIMAL(18,2)
WITH EXECUTE AS OWNER
AS
BEGIN
 DECLARE @CalculatedPrice decimal(18,2);
 DECLARE @UnitPrice decimal(18,2);
 DECLARE @LowestUnitPrice decimal(18,2);
 DECLARE @HighestDiscountAmount decimal(18,2);
 DECLARE @HighestDiscountPercentage decimal(18,3);
 DECLARE @BuyingGroupID int;
 DECLARE @CustomerCategoryID int;
 DECLARE @DiscountedUnitPrice decimal(18,2);
 SELECT @BuyingGroupID = BuyingGroupID,
 @CustomerCategoryID = CustomerCategoryID
 FROM Sales.Customers
 WHERE CustomerID = @CustomerID;
 SELECT @UnitPrice = si.UnitPrice
 FROM Warehouse.StockItems AS si
 WHERE si.StockItemID = @StockItemID;
 SET @CalculatedPrice = @UnitPrice;
 SET @LowestUnitPrice = (
 SELECT MIN(sd.UnitPrice)
 FROM Sales.SpecialDeals AS sd
 WHERE ((sd.StockItemID = @StockItemID) 
 OR (sd.StockItemID IS NULL))
 AND ((sd.CustomerID = @CustomerID) 
 OR (sd.CustomerID IS NULL))
 AND ((sd.BuyingGroupID = @BuyingGroupID) 
 OR (sd.BuyingGroupID IS NULL))
 AND ((sd.CustomerCategoryID = @CustomerCategoryID) 
 OR (sd.CustomerCategoryID IS NULL))
 AND ((sd.StockGroupID IS NULL) OR EXISTS (SELECT 1 
 FROM Warehouse.StockItemStockGroups AS sisg
 WHERE sisg.StockItemID = @StockItemID
 AND sisg.StockGroupID = sd.StockGroupID))
 AND sd.UnitPrice IS NOT NULL
 AND @PricingDate BETWEEN sd.StartDate AND sd.EndDate);
 IF @LowestUnitPrice IS NOT NULL AND @LowestUnitPrice  @UnitPrice
 BEGIN
 SET @CalculatedPrice = @LowestUnitPrice;
 END;
 SET @HighestDiscountAmount = (
 SELECT MAX(sd.DiscountAmount)
 FROM Sales.SpecialDeals AS sd
 WHERE ((sd.StockItemID = @StockItemID) 
 OR (sd.StockItemID IS NULL))
 AND ((sd.CustomerID = @CustomerID) 
 OR (sd.CustomerID IS NULL))
 AND ((sd.BuyingGroupID = @BuyingGroupID) 
 OR (sd.BuyingGroupID IS NULL))
 AND ((sd.CustomerCategoryID = @CustomerCategoryID) 
 OR (sd.CustomerCategoryID IS NULL))
 AND ((sd.StockGroupID IS NULL) OR EXISTS 
 (SELECT 1 FROM Warehouse.StockItemStockGroups AS sisg 
 WHERE sisg.StockItemID = @StockItemID
 AND sisg.StockGroupID = sd.StockGroupID))
 AND sd.DiscountAmount IS NOT NULL
 AND @PricingDate BETWEEN sd.StartDate AND sd.EndDate);
 IF @HighestDiscountAmount IS NOT NULL AND (
 @UnitPrice - @HighestDiscountAmount)  @CalculatedPrice
 BEGIN
 SET @CalculatedPrice = @UnitPrice - @HighestDiscountAmount;
 END;
 SET @HighestDiscountPercentage = (
 SELECT MAX(sd.DiscountPercentage)
 FROM Sales.SpecialDeals AS sd
 WHERE ((sd.StockItemID = @StockItemID)
 OR (sd.StockItemID IS NULL))
 AND ((sd.CustomerID = @CustomerID) 
 OR (sd.CustomerID IS NULL))
 AND ((sd.BuyingGroupID = @BuyingGroupID) 
 OR (sd.BuyingGroupID IS NULL))
 AND ((sd.CustomerCategoryID = @CustomerCategoryID) 
 OR (sd.CustomerCategoryID IS NULL))
 AND ((sd.StockGroupID IS NULL) OR EXISTS 
 (SELECT 1 FROM Warehouse.StockItemStockGroups AS sisg
 WHERE sisg.StockItemID = @StockItemID
 AND sisg.StockGroupID = sd.StockGroupID))
 AND sd.DiscountPercentage IS NOT NULL
 AND @PricingDate BETWEEN sd.StartDate AND sd.EndDate);
 IF @HighestDiscountPercentage IS NOT NULL
 BEGIN
 SET @DiscountedUnitPrice = ROUND(@UnitPrice * 
 @HighestDiscountPercentage / 100.0, 2);
 IF @DiscountedUnitPrice  @CalculatedPrice 
 SET @CalculatedPrice = @DiscountedUnitPrice;
 END;
 RETURN @CalculatedPrice;
END;

就復(fù)雜性而言,這絕對(duì)是一頭猛獸。雖然它接受標(biāo)量參數(shù)來(lái)確定計(jì)算價(jià)格,但它執(zhí)行的操作非常大,甚至包括對(duì)Warehouse.StockItemStockGroups, Warehouse.StockItems和Sales.Customers的額外讀取。如果這是一個(gè)經(jīng)常針對(duì)單行數(shù)據(jù)使用的關(guān)鍵計(jì)算,那么將其封裝在一個(gè)函數(shù)中是獲得所需計(jì)算的一種簡(jiǎn)單方法,而不會(huì)增加觸發(fā)器的復(fù)雜性。小心使用函數(shù),并確保使用大型數(shù)據(jù)集進(jìn)行測(cè)試。簡(jiǎn)單的標(biāo)量函數(shù)通??梢院芎玫厣炜s性較大的數(shù)據(jù),但更復(fù)雜的函數(shù)可能性能較差。

編碼

當(dāng)從應(yīng)用程序修改表中的數(shù)據(jù)時(shí),還可以在寫(xiě)入數(shù)據(jù)之前執(zhí)行額外的數(shù)據(jù)操作或驗(yàn)證。這通常代價(jià)低廉,性能很好,并有助于減少失控觸發(fā)器對(duì)數(shù)據(jù)庫(kù)的負(fù)面影響。

將代碼放入觸發(fā)器的常見(jiàn)理由是,這樣做可以避免修改代碼、推送構(gòu)建,否則會(huì)導(dǎo)致更改應(yīng)用程序。這與在數(shù)據(jù)庫(kù)中進(jìn)行更改相關(guān)的任何風(fēng)險(xiǎn)直接相反。這通常是應(yīng)用程序開(kāi)發(fā)人員和數(shù)據(jù)庫(kù)開(kāi)發(fā)人員之間關(guān)于誰(shuí)將負(fù)責(zé)新代碼的討論。

這是一個(gè)粗略的指導(dǎo)方針,但有助于在代碼添加到應(yīng)用程序或觸發(fā)器之后測(cè)量可維護(hù)性和風(fēng)險(xiǎn)。

計(jì)算列

其他列發(fā)生更改時(shí),計(jì)算列可以包括通過(guò)各種各樣的算術(shù)運(yùn)算和函數(shù)進(jìn)行計(jì)算,得到結(jié)果。它們可以包含在索引中,也可以包含在唯一的約束中,甚至主鍵中。

當(dāng)任何底層值發(fā)生變化時(shí),SQL Server會(huì)自動(dòng)維護(hù)計(jì)算的列。注意,每個(gè)計(jì)算出來(lái)的列最終都是由表中其他列的值決定的。

這是使用觸發(fā)器來(lái)維護(hù)指定列值的一種很好的替代方法。計(jì)算列是高效的、自動(dòng)的,并且不需要維護(hù)。它們只是簡(jiǎn)單地工作,甚至允許將復(fù)雜的計(jì)算直接集成到一個(gè)表中,而在應(yīng)用程序或SQL Server中不需要額外的代碼。

使用SQL Server觸發(fā)器

觸發(fā)器在SQL Server中是一個(gè)有用的特性,但像所有工具一樣,它也可能被誤用或?yàn)E用。在決定是否使用觸發(fā)器時(shí),一定要考慮觸發(fā)器的目的。

如果一個(gè)觸發(fā)器被用來(lái)將簡(jiǎn)短的事務(wù)數(shù)據(jù)寫(xiě)入日志表,那么它很可能是一個(gè)很好的觸發(fā)器。如果觸發(fā)器被用來(lái)強(qiáng)制執(zhí)行復(fù)雜的業(yè)務(wù)規(guī)則,那么很可能需要重新考慮處理這類(lèi)操作的最佳方式。

有很多工具可以作為觸發(fā)器的可行替代品,比如檢查約束、計(jì)算列等,解決問(wèn)題的方法并不短缺。數(shù)據(jù)庫(kù)體系結(jié)構(gòu)的成功在于為工作選擇正確的工具。

原文鏈接:https://www.red-gate.com/simple-talk/sql/database-administration/sql-server-triggers-good-scary/

總結(jié)

到此這篇關(guān)于關(guān)于SQL Server觸發(fā)器的文章就介紹到這了,更多相關(guān)SQL Server觸發(fā)器內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!

您可能感興趣的文章:
  • 利用SQL Server觸發(fā)器實(shí)現(xiàn)表的歷史修改痕跡記錄
  • SQLSERVER對(duì)加密的存儲(chǔ)過(guò)程、視圖、觸發(fā)器進(jìn)行解密(推薦)
  • SQL Server 使用觸發(fā)器(trigger)發(fā)送電子郵件步驟詳解
  • SQL Server實(shí)現(xiàn)用觸發(fā)器捕獲DML操作的會(huì)話信息【實(shí)例】
  • SQL Server:觸發(fā)器實(shí)例詳解
  • SqlServer觸發(fā)器詳解
  • SqlServer實(shí)現(xiàn)類(lèi)似Oracle的before觸發(fā)器示例
  • SQL SERVER中各類(lèi)觸發(fā)器的完整語(yǔ)法及參數(shù)說(shuō)明
  • SQL SERVER 觸發(fā)器介紹
  • SQL Server誤區(qū)30日談 第4天 DDL觸發(fā)器就是INSTEAD OF觸發(fā)器
  • SQL Server 觸發(fā)器詳情

標(biāo)簽:泰安 武威 廣東 安徽 濟(jì)寧 臺(tái)州 濟(jì)源 汕頭

巨人網(wǎng)絡(luò)通訊聲明:本文標(biāo)題《關(guān)于喜憂參半的SQL Server觸發(fā)器詳解》,本文關(guān)鍵詞  關(guān)于,喜憂參半,的,SQL,Server,;如發(fā)現(xiàn)本文內(nèi)容存在版權(quán)問(wèn)題,煩請(qǐng)?zhí)峁┫嚓P(guān)信息告之我們,我們將及時(shí)溝通與處理。本站內(nèi)容系統(tǒng)采集于網(wǎng)絡(luò),涉及言論、版權(quán)與本站無(wú)關(guān)。
  • 相關(guān)文章
  • 下面列出與本文章《關(guān)于喜憂參半的SQL Server觸發(fā)器詳解》相關(guān)的同類(lèi)信息!
  • 本頁(yè)收集關(guān)于關(guān)于喜憂參半的SQL Server觸發(fā)器詳解的相關(guān)信息資訊供網(wǎng)民參考!
  • 推薦文章