Sql-Server

大批量時觸發性能慢

  • May 6, 2020

我有一個插入審計表的更新觸發器。在有人決定更新超過 100 萬條記錄之前,我們沒有問題……(那是我的錯。我認為開發時不會有問題)。現在面對現實,我需要找到解決方案…

我一直在做許多測試和研究,試圖找出如何解決我的觸發器性能不佳的問題……我得出的結論是,要盡量減少執行中“表格插入”的不良性能計劃,我需要小批量插入。

問題是:由於我不確定所有不同的更新都來自哪裡,我試圖弄清楚如何在觸發器中批量插入審計記錄?

例如,100 萬條記錄的主表更新將發生並呼叫觸發器,該觸發器將在某種類型的循環中一次插入 10 萬條記錄。

這可能嗎?如果是這樣,您如何建議?如果沒有,我還能如何改進執行計劃的表插入?

添加測試腳本以重現:

這是實物的簡化版

-- drop trigger PriceHist_trig_U 
-- drop table MyPriceTable
-- drop table price_history
Create Table MyPriceTable (SKU varchar(13), PriceGroup varchar(5), PriceLevel int, Price float, Qty float, ManyOtherColumns Varchar(100)
CONSTRAINT [PRICE_TAB_P01] PRIMARY KEY CLUSTERED 
(
   SKU ASC,
   PriceGroup ASC,
   PriceLevel ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY]

Declare @Id int
Set @Id = 1

While @Id <= 1000000
Begin 
  insert into MyPriceTable values (right('000000000000' + CAST(@Id as nvarchar(10)),13),'Grp ' + CAST(@Id%10 as nvarchar(10)), @id%3, RAND()*(25-10)+10, 1, 'there are many other columns')
  Print @Id
  Set @Id = @Id + 1
End

-- Drop table   price_history 
create table price_history (SKU varchar(13), PriceGroup varchar(5), PriceLevel int, Price float, Qty float, ManyOtherColumns Varchar(100), historyDate datetime, ChangedColumns varchar(Max))
CREATE NONCLUSTERED INDEX price_history_nc1 ON price_history
(
   HistoryDate ASC,
   SKU ASC,
   PriceGroup ASC,
   PriceLevel ASC
)

go
Create TRIGGER PriceHist_trig_U ON MyPriceTable FOR UPDATE 
AS 
INSERT INTO price_history (SKU, PriceGroup, PriceLevel, price, Qty, ManyOtherColumns, HistoryDate, ChangedColumns) 
           SELECT INS.SKU,INS.PriceGroup,INS.PriceLevel,INS.Price,INS.Qty,INS.ManyOtherColumns, getdate(),  
CASE WHEN update(Price) and INS.Price<>DEL.Price THEN 'Price-' ELSE '' END +
CASE WHEN update(Qty) and INS.Qty<>DEL.Qty THEN 'Qty-' ELSE '' END +
CASE WHEN update(ManyOtherColumns) and INS.ManyOtherColumns<>DEL.ManyOtherColumns THEN 'other-' ELSE '' END 
FROM INSERTED INS 
JOIN DELETED DEL ON DEL.sku=INS.sku AND DEL.PriceGroup=INS.PriceGroup AND DEL.PriceLevel=INS.PriceLevel 
WHERE  (update(Price) and INS.Price<>DEL.Price) 
   OR (update(Qty) and INS.Qty<>DEL.Qty) 
   OR (update(ManyOtherColumns) and INS.ManyOtherColumns<>DEL.ManyOtherColumns)

/* tests */ 
update MyPriceTable set price = price-1

當我在禁用觸發器的情況下執行它時,它會在 2 秒內執行。啟用觸發器後,需要 32 秒才能完成。執行計劃在“表格插入”上顯示 98%

我一直在試圖弄清楚如何改進表格插入,但找不到任何具體的東西……

我嘗試過使用聚集索引,但性能更差。

任何幫助,將不勝感激

我把它放在這裡是因為它有點長,但我認為它不應該作為答案。這裡沒有答案,只有觀察和建議。

短版,沒有任何辦法可以使查詢更快並獲得相同的結果。如果要通過批處理來解決問題,則需要更改將數據輸入主表的過程。否則,您必須更改歷史程序。

首先,讓您放慢速度的不是表插入,而是將 INSERTED 和 DELETED 表拉在一起的查詢。

為什麼不?

INSERTED 和 DELETED 表是沒有索引的堆。將它們連接在一起需要兩次表掃描和一次排序。操作越大,成本越高。

在觸發器內進行批處理

這在這裡對您沒有幫助,因為源表是堆。如果不創建某種可以使用的密鑰,您就無法行走它們,並且添加任何東西只會增加(在最壞的情況下)或只會增加複雜性(最好的情況)而不會改進任何東西。

在觸發器外進行批處理

如果您可以重新排列,以便在觸發器外部執行較小的更新語句,則 INSERTED/DELETED 表將更小,從而使操作更快且阻塞更少,儘管總成本將相同。

解決方案?

任何解決此問題的解決方案都需要以某種方式進行更改。您沒有提及您的 SQL 版本,但如果您使用的是 2016 年或更高版本,您可以查看臨時表。https://docs.microsoft.com/en-us/sql/relational-databases/tables/temporal-tables?view=sql-server-ver15

或者,對於這種類型的歷史表,您只想擷取 UPDATES,我會直接插入 DELETED 表內容。沒有與 INSERTED 表的其他比較或連接。您的成本應該與插入大致相同,因此增加最少(我的意思是,I/O 增加一倍,但這是您可以獲得的最低限度)。

然後查看它,您只需獲取所有歷史記錄+實時記錄,您就可以看到發生了什麼變化以及何時發生的變化。它沒有您目前版本所具有的“ChangedColumns”列表,但如果您願意,您可以將類似的內容放在一起。

祝你好運。

不幸的是,沒有足夠的資訊。有用的資訊是:

  • 涉及的表的結構,包括索引
  • 執行計劃
  • 觸發器的作用和它的樣子(也許它的程式碼可以改進)
  • 審計表實際包含的內容

小批量分解該過程可能會有所幫助。請記住,無論您是僅插入一行還是插入 1000 行,每次批處理操作都會觸發一次觸發器。很多人中了這個陷阱,認為每個 insert = 1 觸發呼叫,但事實並非如此。因此,您的觸發器應該知道要添加的行數並採取相應措施。

缺少適當的索引可能是插入速度慢的一個因素。同樣,我們無法知道。您必須查看您的表結構、執行計劃和触發器程式碼。

就其本身而言,插入或更新超過 100 萬條記錄並不是一項小操作。該操作可能包含在隱式或顯式事務中,這會增加更多成本(通過寫入事務日誌文件)。

與其重新發明輪子,不如利用SQL Server 中已有的**審計功能更好。**例如,請參閱:了解 SQL Server 審計。很多時候,觸發器不是解決方案,而是問題。

事實上,這可能不是慢的插入,可能是觸發器進行一些計算或獲取數據來進行插入。

您可以做的一件事是創建目標表的副本,而不使用觸發器並模擬大規模更新以測量時間差。換句話說,驗證觸發器確實是導致性能不佳的原因並對其進行量化。執行執行計劃並進行比較。

引用自:https://dba.stackexchange.com/questions/264971