Đó là ngày thứ Ba của tháng - bạn biết đấy, ngày diễn ra bữa tiệc khối blogger được gọi là T-SQL Tuesday. Tháng này, nó được tổ chức bởi Russ Thomas (@SQLJudo) và chủ đề là, "Gọi tất cả các bộ điều chỉnh và đầu bánh răng." Tôi sẽ xử lý một vấn đề liên quan đến hiệu suất ở đây, mặc dù tôi xin lỗi rằng nó có thể không hoàn toàn phù hợp với các nguyên tắc mà Russ đưa ra trong lời mời của anh ấy (Tôi sẽ không sử dụng gợi ý, cờ theo dõi hoặc hướng dẫn kế hoạch) .
Tại SQLBits tuần trước, tôi đã thuyết trình về các trình kích hoạt, và người bạn tốt của tôi và cũng là đồng nghiệp MVP Erland Sommarskog tình cờ tham dự. Tại một thời điểm, tôi đã đề xuất rằng trước khi tạo trình kích hoạt mới trên bảng, bạn nên kiểm tra xem có bất kỳ trình kích hoạt nào đã tồn tại hay không và cân nhắc kết hợp logic thay vì thêm một trình kích hoạt bổ sung. Lý do của tôi chủ yếu là vì khả năng bảo trì mã, mà còn vì hiệu suất. Erland hỏi tôi đã bao giờ thử nghiệm để xem liệu có thêm bất kỳ chi phí nào khi có nhiều trình kích hoạt kích hoạt cho cùng một hành động hay không, và tôi phải thừa nhận rằng, không, tôi đã không làm bất cứ điều gì sâu rộng. Vì vậy, tôi sẽ làm điều đó ngay bây giờ.
Trong AdventureWorks2014, tôi đã tạo một tập hợp các bảng đơn giản đại diện cho sys.all_objects
(~ 2.700 hàng) và sys.all_columns
(~ 9.500 hàng). Tôi muốn đo lường tác động lên khối lượng công việc của nhiều cách tiếp cận khác nhau đối với việc cập nhật cả hai bảng - về cơ bản, bạn có người dùng cập nhật bảng cột và bạn sử dụng trình kích hoạt để cập nhật một cột khác trong cùng một bảng và một vài cột trong bảng đối tượng.
- T1:Đường cơ sở :Giả sử rằng bạn có thể kiểm soát tất cả các truy cập dữ liệu thông qua một thủ tục được lưu trữ; trong trường hợp này, các cập nhật đối với cả hai bảng có thể được thực hiện trực tiếp mà không cần trình kích hoạt. (Điều này không thực tế trong thế giới thực, vì bạn không thể cấm truy cập trực tiếp vào các bảng một cách đáng tin cậy.)
- T2:Một lần kích hoạt so với bảng khác :Giả sử rằng bạn có thể kiểm soát câu lệnh cập nhật đối với bảng bị ảnh hưởng và thêm các cột khác, nhưng các cập nhật cho bảng phụ cần được triển khai bằng trình kích hoạt. Chúng tôi sẽ cập nhật tất cả ba cột bằng một câu lệnh.
- T3:Một lần kích hoạt chống lại cả hai bảng :Trong trường hợp này, chúng tôi có một trình kích hoạt với hai câu lệnh, một câu lệnh cập nhật cột khác trong bảng bị ảnh hưởng và một câu lệnh cập nhật tất cả ba cột trong bảng phụ.
- T4:Một lần kích hoạt chống lại cả hai bảng :Giống như T3, nhưng lần này, chúng ta có một trình kích hoạt với bốn câu lệnh, một câu lệnh cập nhật cột khác trong bảng bị ảnh hưởng và một câu lệnh cho mỗi cột được cập nhật trong bảng phụ. Đây có thể là cách nó được xử lý nếu các yêu cầu được thêm vào theo thời gian và một tuyên bố riêng được coi là an toàn hơn về mặt kiểm tra hồi quy.
- T5:Hai lần kích hoạt :Một trình kích hoạt chỉ cập nhật bảng bị ảnh hưởng; còn lại sử dụng một câu lệnh duy nhất để cập nhật ba cột trong bảng phụ. Đây có thể là cách được thực hiện nếu các trình kích hoạt khác không được nhận thấy hoặc nếu việc sửa đổi chúng bị cấm.
- T6:Bốn lần kích hoạt :Một trình kích hoạt chỉ cập nhật bảng bị ảnh hưởng; ba cột còn lại cập nhật từng cột trong bảng phụ. Một lần nữa, đây có thể là cách được thực hiện nếu bạn không biết các trình kích hoạt khác tồn tại hoặc nếu bạn sợ chạm vào các trình kích hoạt khác do lo ngại về hồi quy.
Đây là dữ liệu nguồn mà chúng tôi đang xử lý:
-- sys.all_objects: SELECT * INTO dbo.src FROM sys.all_objects; CREATE UNIQUE CLUSTERED INDEX x ON dbo.src([object_id]); GO -- sys.all_columns: SELECT * INTO dbo.tr1 FROM sys.all_columns; CREATE UNIQUE CLUSTERED INDEX x ON dbo.tr1([object_id], column_id); -- repeat 5 times: tr2, tr3, tr4, tr5, tr6
Bây giờ, đối với mỗi 6 bài kiểm tra, chúng tôi sẽ chạy các bản cập nhật của mình 1.000 lần và đo khoảng thời gian
T1:Đường cơ sở
Đây là kịch bản mà chúng tôi đủ may mắn để tránh các tác nhân (một lần nữa, không thực tế lắm). Trong trường hợp này, chúng tôi sẽ đo số lần đọc và thời lượng của lô này. Tôi đặt /*real*/
vào văn bản truy vấn để tôi có thể dễ dàng lấy số liệu thống kê chỉ cho những câu lệnh này chứ không phải bất kỳ câu lệnh nào từ bên trong trình kích hoạt, vì cuối cùng các chỉ số sẽ cuộn lên thành các câu lệnh gọi ra trình kích hoạt. Cũng xin lưu ý rằng các bản cập nhật thực tế mà tôi đang thực hiện không thực sự có ý nghĩa gì, vì vậy hãy bỏ qua việc tôi đang đặt đối chiếu với tên máy chủ / phiên bản và principal_id
của đối tượng đến session_id
của phiên hiện tại .
UPDATE /*real*/ dbo.tr1 SET name += N'', collation_name = @@SERVERNAME WHERE name LIKE '%s%'; UPDATE /*real*/ s SET modify_date = GETDATE(), is_ms_shipped = 0, principal_id = @@SPID FROM dbo.src AS s INNER JOIN dbo.tr1 AS t ON s.[object_id] = t.[object_id] WHERE t.name LIKE '%s%'; GO 1000
T2:Kích hoạt đơn
Đối với điều này, chúng tôi cần trình kích hoạt đơn giản sau, chỉ cập nhật dbo.src
:
CREATE TRIGGER dbo.tr_tr2 ON dbo.tr2 AFTER UPDATE AS BEGIN SET NOCOUNT ON; UPDATE s SET modify_date = GETDATE(), is_ms_shipped = 0, principal_id = SUSER_ID() FROM dbo.src AS s INNER JOIN inserted AS i ON s.[object_id] = i.[object_id]; END GO
Sau đó, lô của chúng tôi chỉ cần cập nhật hai cột trong bảng chính:
UPDATE /*real*/ dbo.tr2 SET name += N'', collation_name = @@SERVERNAME WHERE name LIKE '%s%'; GO 1000
T3:Một lần kích hoạt chống lại cả hai bảng
Đối với thử nghiệm này, trình kích hoạt của chúng tôi trông giống như sau:
CREATE TRIGGER dbo.tr_tr3 ON dbo.tr3 AFTER UPDATE AS BEGIN SET NOCOUNT ON; UPDATE t SET collation_name = @@SERVERNAME FROM dbo.tr3 AS t INNER JOIN inserted AS i ON t.[object_id] = i.[object_id]; UPDATE s SET modify_date = GETDATE(), is_ms_shipped = 0, principal_id = @@SPID FROM dbo.src AS s INNER JOIN inserted AS i ON s.[object_id] = i.[object_id]; END GO
Và bây giờ lô chúng tôi đang thử nghiệm chỉ phải cập nhật cột gốc trong bảng chính; cái còn lại do trình kích hoạt xử lý:
UPDATE /*real*/ dbo.tr3 SET name += N'' WHERE name LIKE '%s%'; GO 1000
T4:Một lần kích hoạt chống lại cả hai bảng
Điều này giống như T3, nhưng bây giờ trình kích hoạt có bốn câu lệnh:
CREATE TRIGGER dbo.tr_tr4 ON dbo.tr4 AFTER UPDATE AS BEGIN SET NOCOUNT ON; UPDATE t SET collation_name = @@SERVERNAME FROM dbo.tr4 AS t INNER JOIN inserted AS i ON t.[object_id] = i.[object_id]; UPDATE s SET modify_date = GETDATE() FROM dbo.src AS s INNER JOIN inserted AS i ON s.[object_id] = i.[object_id]; UPDATE s SET is_ms_shipped = 0 FROM dbo.src AS s INNER JOIN inserted AS i ON s.[object_id] = i.[object_id]; UPDATE s SET principal_id = @@SPID FROM dbo.src AS s INNER JOIN inserted AS i ON s.[object_id] = i.[object_id]; END GO
Lô thử nghiệm không thay đổi:
UPDATE /*real*/ dbo.tr4 SET name += N'' WHERE name LIKE '%s%'; GO 1000
T5:Hai lần kích hoạt
Ở đây chúng tôi có một trình kích hoạt để cập nhật bảng chính và một trình kích hoạt để cập nhật bảng phụ:
CREATE TRIGGER dbo.tr_tr5_1 ON dbo.tr5 AFTER UPDATE AS BEGIN SET NOCOUNT ON; UPDATE t SET collation_name = @@SERVERNAME FROM dbo.tr5 AS t INNER JOIN inserted AS i ON t.[object_id] = i.[object_id]; END GO CREATE TRIGGER dbo.tr_tr5_2 ON dbo.tr5 AFTER UPDATE AS BEGIN SET NOCOUNT ON; UPDATE s SET modify_date = GETDATE(), is_ms_shipped = 0, principal_id = @@SPID FROM dbo.src AS s INNER JOIN inserted AS i ON s.[object_id] = i.[object_id]; END GO
Lô thử nghiệm lại rất cơ bản:
UPDATE /*real*/ dbo.tr5 SET name += N'' WHERE name LIKE '%s%'; GO 1000
T6:Bốn kích hoạt
Lần này chúng tôi có một trình kích hoạt cho mỗi cột bị ảnh hưởng; một trong bảng chính và ba trong bảng phụ.
CREATE TRIGGER dbo.tr_tr6_1 ON dbo.tr6 AFTER UPDATE AS BEGIN SET NOCOUNT ON; UPDATE t SET collation_name = @@SERVERNAME FROM dbo.tr6 AS t INNER JOIN inserted AS i ON t.[object_id] = i.[object_id]; END GO CREATE TRIGGER dbo.tr_tr6_2 ON dbo.tr6 AFTER UPDATE AS BEGIN SET NOCOUNT ON; UPDATE s SET modify_date = GETDATE() FROM dbo.src AS s INNER JOIN inserted AS i ON s.[object_id] = i.[object_id]; END GO CREATE TRIGGER dbo.tr_tr6_3 ON dbo.tr6 AFTER UPDATE AS BEGIN SET NOCOUNT ON; UPDATE s SET is_ms_shipped = 0 FROM dbo.src AS s INNER JOIN inserted AS i ON s.[object_id] = i.[object_id]; END GO CREATE TRIGGER dbo.tr_tr6_4 ON dbo.tr6 AFTER UPDATE AS BEGIN SET NOCOUNT ON; UPDATE s SET principal_id = @@SPID FROM dbo.src AS s INNER JOIN inserted AS i ON s.[object_id] = i.[object_id]; END GO
Và lô thử nghiệm:
UPDATE /*real*/ dbo.tr6 SET name += N'' WHERE name LIKE '%s%'; GO 1000
Đo lường tác động của khối lượng công việc
Cuối cùng, tôi đã viết một truy vấn đơn giản đối với sys.dm_exec_query_stats
để đo số lần đọc và thời lượng cho mỗi bài kiểm tra:
SELECT [cmd] = SUBSTRING(t.text, CHARINDEX(N'U', t.text), 23), avg_elapsed_time = total_elapsed_time / execution_count * 1.0, total_logical_reads FROM sys.dm_exec_query_stats AS s CROSS APPLY sys.dm_exec_sql_text(s.sql_handle) AS t WHERE t.text LIKE N'%UPDATE /*real*/%' ORDER BY cmd;
Kết quả
Tôi đã chạy các bài kiểm tra 10 lần, thu thập kết quả và tính trung bình mọi thứ. Đây là cách nó bị hỏng:
Test / Batch | Thời lượng trung bình (micro giây) | Tổng số lần đọc (8 nghìn trang) |
---|---|---|
T1 :UPDATE / * real * / dbo.tr1… | 22.608 | 205.134 |
T2 :UPDATE / * real * / dbo.tr2… | 32.749 | 11.331.628 |
T3 :UPDATE / * real * / dbo.tr3… | 72.899 | 22.838.308 |
T4 :UPDATE / * real * / dbo.tr4… | 78.372 | 44.463.275 |
T5 :UPDATE / * real * / dbo.tr5… | 88.563 | 41.514.778 |
T6 :UPDATE / * real * / dbo.tr6… | 127.079 | 100.330.753 |
Và đây là biểu diễn đồ họa của khoảng thời gian:
Kết luận
Rõ ràng rằng, trong trường hợp này, có một số chi phí đáng kể cho mỗi trình kích hoạt được gọi - tất cả các lô này cuối cùng đều ảnh hưởng đến cùng một số hàng, nhưng trong một số trường hợp, các hàng giống nhau đã được chạm nhiều lần. Tôi có thể sẽ thực hiện thêm thử nghiệm tiếp theo để đo lường sự khác biệt khi cùng một hàng không bao giờ được chạm nhiều hơn một lần - một giản đồ phức tạp hơn, có lẽ, trong đó 5 hoặc 10 bảng khác phải được chạm vào mỗi lần và các câu lệnh khác nhau này có thể là trong một trình kích hoạt duy nhất hoặc trong nhiều trình kích hoạt. Tôi đoán rằng sự khác biệt về tổng chi phí sẽ bị thúc đẩy nhiều hơn bởi những thứ như đồng thời và số lượng hàng bị ảnh hưởng hơn là bởi chi phí của chính trình kích hoạt - nhưng chúng ta sẽ thấy.
Bạn muốn tự mình thử bản demo? Tải xuống tập lệnh tại đây.