開發者常在「何時用約束、何時用觸發器」上猶豫不決,甚至混用導致邏輯衝突、效能倒退。本文從執行時機、效能特性、適用場景三個維度,建立清晰的決策框架,並以實戰案例對比外鍵約束與觸發器實現參考完整性的差異。
| 維度 | 約束 | DML 觸發器 (AFTER) |
|---|---|---|
| 執行階段 | 資料寫入引擎前 (編譯期即可驗證元數據) | 資料寫入完成後 (已產生日誌、持有鎖) |
| 回滾成本 | 極低 (未產生實際資料頁修改) | 高 (已寫入資料頁、日誌,需補償性刪除/回滾) |
| 效能優化 | 優化器原生感知,可簡化執行計畫、推導統計資訊 | 黑盒,優化器無法洞察內部邏輯 |
| 維護成本 | 宣告式,零程式碼 | 程序式,需編寫/除錯/版控 T-SQL |
| 適用邏輯 | 單列/跨列/跨表「靜態規則」 (值域、唯一性、參考完整性) | 「動態規則」 (歷史比較、跨表聚合、稽核、複雜業務流程) |
✅ 外鍵約束 (推薦):
ALTER TABLE SalesHistory
ADD CONSTRAINT FK_SalesHistory_Product
FOREIGN KEY (Product) REFERENCES I_SalesProducts(Product);
❌ 觸發器實現 (反模式):
CREATE TRIGGER TR_SalesHistory_FK ON SalesHistory
FOR INSERT, UPDATE
AS
BEGIN
IF UPDATE(Product) AND @@ROWCOUNT <> (
SELECT COUNT(*) FROM inserted i
JOIN I_SalesProducts s ON i.Product = s.Product
)
BEGIN
ROLLBACK TRANSACTION;
THROW 50001, 'Product 不存在於主檔', 1;
END
END
案例: 「BigScreen 產品單日銷售額 < $500 時,筆數不得超過 5 筆」
此規則需:
觸發器實現:
CREATE TRIGGER TR_MaxDailySales ON SalesHistory
FOR INSERT, UPDATE
AS
BEGIN
SET NOCOUNT ON;
DECLARE @Today DATE = CAST(GETDATE() AS DATE);
DECLARE @Product VARCHAR(20) = 'BigScreen';
DECLARE @MaxCount TINYINT = 5;
DECLARE @PriceThreshold MONEY = 500;
DECLARE @InsertedCount INT = (
SELECT COUNT(*) FROM inserted
WHERE Product = @Product AND SalePrice < @PriceThreshold AND SaleDate = @Today
);
DECLARE @ExistingCount INT = (
SELECT COUNT(*) FROM SalesHistory
WHERE Product = @Product AND SalePrice < @PriceThreshold AND SaleDate = @Today
);
IF @InsertedCount + @ExistingCount > @MaxCount
BEGIN
ROLLBACK TRANSACTION;
THROW 50002, '單日特價銷售超額', 1;
END
END
nested triggers 伺服器選項,或在觸發器開頭 IF TRIGGER_NESTLEVEL() > 1 RETURN。約束是資料庫的「免疫系統」,觸發器是「特種部隊」。讓免疫系統處理日常病毒,特種部隊處理特種作戰,資料庫才能長治久安。