Database
 sql >> Cơ Sở Dữ Liệu >  >> RDS >> Database

T-SQL Thứ ba # 64:Một kích hoạt hay nhiều?

Đó 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.


  1. Database
  2.   
  3. Mysql
  4.   
  5. Oracle
  6.   
  7. Sqlserver
  8.   
  9. PostgreSQL
  10.   
  11. Access
  12.   
  13. SQLite
  14.   
  15. MariaDB
  1. Tạo bảng tổng hợp động với chức năng QUOTENAME

  2. Cải thiện Giải pháp Đánh số Trung vị Hàng

  3. Vấn đề Halloween - Phần 4

  4. Giới thiệu về TimescaleDB

  5. Cách viết câu lệnh CASE trong SQL