Đây là phần thứ ba trong loạt bài về các giải pháp cho thử thách máy phát chuỗi số. Trong Phần 1, tôi đã đề cập đến các giải pháp tạo các hàng một cách nhanh chóng. Trong Phần 2, tôi đã đề cập đến các giải pháp truy vấn bảng cơ sở vật lý mà bạn điền trước với các hàng. Tháng này, tôi sẽ tập trung vào một kỹ thuật hấp dẫn có thể được sử dụng để xử lý thử thách của chúng tôi, nhưng điều đó cũng có những ứng dụng thú vị ngoài nó. Tôi không biết tên chính thức của kỹ thuật này, nhưng khái niệm này hơi giống với khái niệm loại bỏ phân vùng ngang, vì vậy, tôi sẽ gọi nó một cách không chính thức là loại bỏ đơn vị theo chiều ngang kỹ thuật. Kỹ thuật này có thể có những lợi ích hiệu suất tích cực thú vị, nhưng cũng có những lưu ý mà bạn cần lưu ý, trong những điều kiện nhất định, nó có thể phải chịu một hình phạt về hiệu suất.
Một lần nữa, xin cảm ơn Alan Burstein, Joe Obbish, Adam Machanic, Christopher Ford, Jeff Moden, Charlie, NoamGr, Kamil Kosno, Dave Mason, John Nelson # 2, Ed Wagner, Michael Burbea và Paul White vì đã chia sẻ ý kiến và nhận xét của bạn.
Tôi sẽ thực hiện thử nghiệm của mình trong tempdb, cho phép thống kê thời gian:
SET NOCOUNT ON; USE tempdb; SET STATISTICS TIME ON;
Ý tưởng trước đây
Kỹ thuật loại bỏ đơn vị ngang có thể được sử dụng thay thế cho logic loại bỏ cột hoặc loại bỏ đơn vị dọc kỹ thuật mà tôi đã dựa vào trong một số giải pháp mà tôi đã đề cập trước đó. Bạn có thể đọc về các nguyên tắc cơ bản của logic loại bỏ cột với biểu thức bảng trong Nguyên tắc cơ bản về biểu thức bảng, Phần 3 - Các bảng có nguồn gốc, các cân nhắc tối ưu hóa trong “Phép chiếu cột và một từ trên SELECT *.”
Ý tưởng cơ bản của kỹ thuật loại bỏ đơn vị dọc là nếu bạn có một biểu thức bảng lồng nhau trả về cột x và y và truy vấn bên ngoài của bạn chỉ tham chiếu đến cột x, thì quá trình biên dịch truy vấn sẽ loại bỏ y khỏi cây truy vấn ban đầu và do đó kế hoạch không cần phải đánh giá nó. Điều này có một số ý nghĩa tích cực liên quan đến tối ưu hóa, chẳng hạn như đạt được mức độ phù hợp của chỉ mục với riêng x và nếu y là kết quả của một phép tính, thì không cần phải đánh giá biểu thức cơ bản của y. Ý tưởng này là trọng tâm của giải pháp của Alan Burstein. Tôi cũng đã dựa vào nó trong một số giải pháp khác mà tôi đã đề cập, chẳng hạn như với hàm dbo.GetNumsAlanCharlieItzikBatch (từ Phần 1), các hàm dbo.GetNumsJohn2DaveObbishAlanCharlieItzik và dbo.GetNumsJohn2DaveObbishAlank2 và những người khác từ Phần 2. Ví dụ:tôi sẽ sử dụng dbo.GetNumsAlanCharlieItzikBatch làm giải pháp cơ bản với logic loại bỏ dọc.
Xin nhắc lại, giải pháp này sử dụng một phép nối với một bảng giả có chỉ mục columnstore để xử lý hàng loạt. Đây là mã để tạo bảng giả:
DROP TABLE IF EXISTS dbo.BatchMe; GO CREATE TABLE dbo.BatchMe(col1 INT NOT NULL, INDEX idx_cs CLUSTERED COLUMNSTORE);
Và đây là mã với định nghĩa của hàm dbo.GetNumsAlanCharlieItzikBatch:
CREATE OR ALTER FUNCTION dbo.GetNumsAlanCharlieItzikBatch(@low AS BIGINT = 1, @high AS BIGINT) RETURNS TABLE AS RETURN WITH L0 AS ( SELECT 1 AS c FROM (VALUES(1),(1),(1),(1),(1),(1),(1),(1), (1),(1),(1),(1),(1),(1),(1),(1)) AS D(c) ), L1 AS ( SELECT 1 AS c FROM L0 AS A CROSS JOIN L0 AS B ), L2 AS ( SELECT 1 AS c FROM L1 AS A CROSS JOIN L1 AS B ), L3 AS ( SELECT 1 AS c FROM L2 AS A CROSS JOIN L2 AS B ), Nums AS ( SELECT ROW_NUMBER() OVER(ORDER BY (SELECT NULL)) AS rownum FROM L3 ) SELECT TOP(@high - @low + 1) rownum AS rn, @high + 1 - rownum AS op, @low - 1 + rownum AS n FROM Nums LEFT OUTER JOIN dbo.BatchMe ON 1 = 0 ORDER BY rownum; GO
Tôi đã sử dụng mã sau để kiểm tra hiệu suất của hàm với 100 triệu hàng, trả về cột kết quả được tính toán n (thao tác đối với kết quả của hàm ROW_NUMBER), được sắp xếp theo thứ tự n:
DECLARE @n AS BIGINT; SELECT @n = n FROM dbo.GetNumsAlanCharlieItzikBatch(1, 100000000) ORDER BY n OPTION(MAXDOP 1);
Đây là thống kê thời gian mà tôi nhận được cho bài kiểm tra này:
Thời gian CPU =9328 ms, thời gian đã trôi qua =9330 ms.Tôi đã sử dụng mã sau để kiểm tra hiệu suất của hàm với 100 triệu hàng, trả về cột rn (trực tiếp, không điều chỉnh, kết quả của hàm ROW_NUMBER), được sắp xếp theo rn:
DECLARE @n AS BIGINT; SELECT @n = rn FROM dbo.GetNumsAlanCharlieItzikBatch(1, 100000000) ORDER BY rn OPTION(MAXDOP 1);
Đây là thống kê thời gian mà tôi nhận được cho bài kiểm tra này:
Thời gian CPU =7296 ms, thời gian đã trôi qua =7291 ms.Hãy xem lại các ý tưởng quan trọng được đưa vào giải pháp này.
Dựa trên logic loại bỏ cột, alan nảy ra ý tưởng trả về không chỉ một cột với chuỗi số, mà trả về ba cột:
- Cột rn đại diện cho một kết quả không thể điều chỉnh của hàm ROW_NUMBER, bắt đầu bằng 1. Nó rất rẻ để tính toán. Thứ tự bảo toàn cả khi bạn cung cấp hằng số và khi bạn cung cấp các hằng số không phải là biến (biến, cột) làm đầu vào cho hàm. Điều này có nghĩa là khi truy vấn bên ngoài của bạn sử dụng ORDER BY rn, bạn sẽ không nhận được toán tử Sắp xếp trong kế hoạch.
- Cột n đại diện cho một phép tính dựa trên @low, một hằng số và rownum (kết quả của hàm ROW_NUMBER). Nó là bảo toàn trật tự đối với rownum khi bạn cung cấp các hằng số làm đầu vào cho hàm. Đó là nhờ sự hiểu biết sâu sắc của Charlie về việc gấp liên tục (xem Phần 1 để biết chi tiết). Tuy nhiên, nó không phải là thứ tự bảo toàn khi bạn cung cấp các yếu tố không phải là đầu vào, vì bạn không bị gấp liên tục. Tôi sẽ chứng minh điều này ở phần sau trong phần lưu ý.
- Cột op biểu diễn n theo thứ tự ngược lại. Nó là kết quả của một sự tính toán và nó không phải là thứ tự bảo toàn.
Dựa trên logic loại bỏ cột, nếu bạn cần trả về một chuỗi số bắt đầu bằng 1, bạn truy vấn cột rn, rẻ hơn truy vấn n. Nếu bạn cần một chuỗi số bắt đầu bằng giá trị khác 1, bạn truy vấn n và trả thêm chi phí. Nếu bạn cần kết quả được sắp xếp theo cột số, với các hằng số làm đầu vào, bạn có thể sử dụng ORDER BY rn hoặc ORDER BY n. Nhưng với không phải là đầu vào, bạn muốn đảm bảo sử dụng ORDER BY rn. Có thể là một ý kiến hay nếu bạn luôn sử dụng ORDER BY rn khi cần kết quả được sắp xếp ở mức an toàn.
Ý tưởng loại bỏ đơn vị theo chiều ngang tương tự như ý tưởng loại bỏ đơn vị dọc, chỉ áp dụng cho các tập hợp hàng thay vì tập hợp các cột. Trên thực tế, Joe Obbish đã dựa trên ý tưởng này trong hàm dbo.GetNumsObbish (từ Phần 2), và chúng ta sẽ tiến thêm một bước nữa. Trong giải pháp của mình, Joe đã hợp nhất nhiều truy vấn đại diện cho các dãy số con rời nhau, sử dụng bộ lọc trong mệnh đề WHERE của mỗi truy vấn để xác định khả năng áp dụng của dải con. Khi bạn gọi hàm và chuyển các đầu vào không đổi đại diện cho các dấu phân cách trong phạm vi mong muốn của bạn, SQL Server sẽ loại bỏ các truy vấn không thể áp dụng tại thời điểm biên dịch, do đó, kế hoạch thậm chí không phản ánh chúng.
Loại bỏ đơn vị ngang, thời gian biên dịch so với thời gian chạy
Có lẽ nên bắt đầu bằng cách trình bày khái niệm loại bỏ đơn vị ngang trong một trường hợp tổng quát hơn, đồng thời thảo luận về sự khác biệt quan trọng giữa loại bỏ thời gian biên dịch và thời gian chạy. Sau đó, chúng ta có thể thảo luận về cách áp dụng ý tưởng vào thử thách chuỗi số của chúng ta.
Tôi sẽ sử dụng ba bảng có tên dbo.T1, dbo.T2 và dbo.T3 trong ví dụ của mình. Sử dụng mã DDL và DML sau để tạo và điền các bảng này:
DROP TABLE IF EXISTS dbo.T1, dbo.T2, dbo.T3; GO CREATE TABLE dbo.T1(col1 INT); INSERT INTO dbo.T1(col1) VALUES(1); CREATE TABLE dbo.T2(col1 INT); INSERT INTO dbo.T2(col1) VALUES(2); CREATE TABLE dbo.T3(col1 INT); INSERT INTO dbo.T3(col1) VALUES(3);
Giả sử rằng bạn muốn triển khai một TVF nội tuyến được gọi là dbo.OneTable chấp nhận một trong ba tên bảng trên làm đầu vào và trả về dữ liệu từ bảng được yêu cầu. Dựa trên khái niệm loại bỏ đơn vị theo chiều ngang, bạn có thể triển khai chức năng như sau:
CREATE OR ALTER FUNCTION dbo.OneTable(@WhichTable AS NVARCHAR(257)) RETURNS TABLE AS RETURN SELECT col1 FROM dbo.T1 WHERE @WhichTable = N'dbo.T1' UNION ALL SELECT col1 FROM dbo.T2 WHERE @WhichTable = N'dbo.T2' UNION ALL SELECT col1 FROM dbo.T3 WHERE @WhichTable = N'dbo.T3'; GO
Hãy nhớ rằng TVF nội tuyến áp dụng nhúng tham số. Điều này có nghĩa là khi bạn chuyển một hằng số chẳng hạn như N'dbo.T2 'làm đầu vào, quy trình nội tuyến sẽ thay thế tất cả các tham chiếu đến @WhichTable bằng hằng số trước khi tối ưu hóa . Sau đó, quá trình loại bỏ có thể loại bỏ các tham chiếu đến T1 và T3 khỏi cây truy vấn ban đầu và do đó việc tối ưu hóa truy vấn dẫn đến một kế hoạch chỉ tham chiếu đến T2. Hãy kiểm tra ý tưởng này với truy vấn sau:
SELECT * FROM dbo.OneTable(N'dbo.T2');
Kế hoạch cho truy vấn này được thể hiện trong Hình 1.
Hình 1:Kế hoạch cho dbo.OneTable với đầu vào không đổi
Như bạn có thể thấy, chỉ có bảng T2 xuất hiện trong kế hoạch.
Mọi thứ phức tạp hơn một chút khi bạn chuyển một người không ủng hộ làm đầu vào. Đây có thể là trường hợp khi sử dụng một biến, một tham số thủ tục hoặc truyền một cột qua APPLICY. Giá trị đầu vào là không xác định tại thời điểm biên dịch hoặc khả năng tái sử dụng kế hoạch được tham số hóa cần được tính đến.
Trình tối ưu hóa không thể loại bỏ bất kỳ bảng nào khỏi kế hoạch, nhưng nó vẫn có một mẹo. Nó có thể sử dụng toán tử Bộ lọc khởi động phía trên các cây con truy cập các bảng và chỉ thực thi cây con có liên quan dựa trên giá trị thời gian chạy của @WhichTable. Sử dụng mã sau để kiểm tra chiến lược này:
DECLARE @T AS NVARCHAR(257) = N'dbo.T2'; SELECT * FROM dbo.OneTable(@T);
Kế hoạch cho việc thực hiện này được thể hiện trong Hình 2:
Hình 2:Kế hoạch cho dbo.OneTable với đầu vào không cần thiết
Plan Explorer làm cho nó rõ ràng một cách tuyệt vời khi thấy rằng chỉ cây con thích hợp được thực thi (Executions =1) và tô màu xám cho các cây con không được thực thi (Executions =0). Ngoài ra, STATISTICS IO chỉ hiển thị thông tin I / O cho bảng đã được truy cập:
Bảng 'T2'. Quét đếm 1, đọc lôgic 1, đọc vật lý 0, máy chủ trang đọc 0, đọc trước đọc 0, máy chủ trang đọc trước đọc 0, lôgic đọc 0, đọc vật lý lob 0, máy chủ trang lob đọc 0, đọc lob- đọc trước 0, máy chủ trang lob đọc trước đọc 0.Áp dụng logic loại bỏ đơn vị ngang cho thử thách chuỗi số
Như đã đề cập, bạn có thể áp dụng khái niệm loại bỏ đơn vị ngang bằng cách sửa đổi bất kỳ giải pháp nào trước đó hiện đang sử dụng logic loại bỏ dọc. Tôi sẽ sử dụng hàm dbo.GetNumsAlanCharlieItzikBatch làm điểm bắt đầu cho ví dụ của mình.
Nhớ lại rằng Joe Obbish đã sử dụng phép loại bỏ đơn vị theo chiều ngang để trích xuất các biên con rời rạc có liên quan của dãy số. Chúng tôi sẽ sử dụng khái niệm này để phân tách theo chiều ngang của phép tính ít tốn kém hơn (rn) trong đó @low =1 với phép tính đắt hơn (n) trong đó @low <> 1.
Trong khi thực hiện, chúng ta có thể thử nghiệm bằng cách thêm ý tưởng của Jeff Moden vào hàm fnTally của anh ấy, trong đó anh ấy sử dụng hàng sentinel có giá trị 0 cho các trường hợp phạm vi bắt đầu bằng @low =0.
Vì vậy, chúng tôi có bốn đơn vị ngang:
- Hàng dấu gạch chéo với 0 trong đó @low =0, với n =0
- Các hàng ĐẦU (@high) trong đó @low =0, với n =rownum rẻ và op =@high - rownum
- Các hàng ĐẦU (@high) trong đó @low =1, với n =rownum rẻ và op =@high + 1 - rownum
- TOP (@high - @low + 1) các hàng trong đó @low <> 0 VÀ @low <> 1, với n =@low - 1 + rownum đắt hơn và op =@high + 1 - rownum
Giải pháp này kết hợp các id từ Alan, Charlie, Joe, Jeff và tôi, vì vậy chúng tôi sẽ gọi phiên bản chế độ hàng loạt của hàm là dbo.GetNumsAlanCharlieJoeJeffItzikBatch.
Trước tiên, hãy nhớ đảm bảo rằng bạn vẫn có dbo bảng giả.BatchMe hiện diện để xử lý hàng loạt trong giải pháp của chúng tôi hoặc sử dụng mã sau nếu bạn không có:
DROP TABLE IF EXISTS dbo.BatchMe; GO CREATE TABLE dbo.BatchMe(col1 INT NOT NULL, INDEX idx_cs CLUSTERED COLUMNSTORE);
Đây là mã với định nghĩa của hàm dbo.GetNumsAlanCharlieJoeJeffItzikBatch:
CREATE OR ALTER FUNCTION dbo.GetNumsAlanCharlieJoeJeffItzikBatch(@low AS BIGINT = 1, @high AS BIGINT) RETURNS TABLE AS RETURN WITH L0 AS ( SELECT 1 AS c FROM (VALUES(1),(1),(1),(1),(1),(1),(1),(1), (1),(1),(1),(1),(1),(1),(1),(1)) AS D(c) ), L1 AS ( SELECT 1 AS c FROM L0 AS A CROSS JOIN L0 AS B ), L2 AS ( SELECT 1 AS c FROM L1 AS A CROSS JOIN L1 AS B ), L3 AS ( SELECT 1 AS c FROM L2 AS A CROSS JOIN L2 AS B ), Nums AS ( SELECT ROW_NUMBER() OVER(ORDER BY (SELECT NULL)) AS rownum FROM L3 ) SELECT @low AS n, @high AS op WHERE @low = 0 AND @high > @low UNION ALL SELECT TOP(@high) rownum AS n, @high - rownum AS op FROM Nums LEFT OUTER JOIN dbo.BatchMe ON 1 = 0 WHERE @low = 0 ORDER BY rownum UNION ALL SELECT TOP(@high) rownum AS n, @high + 1 - rownum AS op FROM Nums LEFT OUTER JOIN dbo.BatchMe ON 1 = 0 WHERE @low = 1 ORDER BY rownum UNION ALL SELECT TOP(@high - @low + 1) @low - 1 + rownum AS n, @high + 1 - rownum AS op FROM Nums LEFT OUTER JOIN dbo.BatchMe ON 1 = 0 WHERE @low <> 0 AND @low <> 1 ORDER BY rownum; GO
Quan trọng:Khái niệm loại bỏ đơn vị theo chiều ngang chắc chắn sẽ phức tạp hơn để thực hiện so với khái niệm dọc, vậy tại sao phải bận tâm? Bởi vì nó loại bỏ trách nhiệm chọn đúng cột từ người dùng. Người dùng chỉ cần lo lắng về việc truy vấn cột có tên n, trái ngược với việc nhớ sử dụng rn khi phạm vi bắt đầu bằng 1 và n nếu không.
Hãy bắt đầu bằng cách thử nghiệm giải pháp với đầu vào không đổi 1 và 100.000.000, yêu cầu kết quả được sắp xếp theo thứ tự:
DECLARE @n AS BIGINT; SELECT @n = n FROM dbo.GetNumsAlanCharlieJoeJeffItzikBatch(1, 100000000) ORDER BY n OPTION(MAXDOP 1);
Kế hoạch cho việc thực hiện này được thể hiện trong Hình 3.
Hình 3:Kế hoạch cho dbo.GetNumsAlanCharlieJoeJeffItzikBatch (1, 100M)
Quan sát rằng cột được trả về duy nhất dựa trên biểu thức ROW_NUMBER trực tiếp, không được điều chỉnh, (Expr1313). Cũng lưu ý rằng không cần phải sắp xếp trong kế hoạch.
Tôi nhận được thống kê thời gian sau cho lần thực hiện này:
Thời gian CPU =7359 ms, thời gian đã trôi qua =7354 ms.Thời gian chạy phản ánh đầy đủ thực tế là kế hoạch sử dụng chế độ hàng loạt, biểu thức ROW_NUMBER không được điều chỉnh và không có sắp xếp.
Tiếp theo, kiểm tra chức năng với dải không đổi 0 đến 99,999.999:
DECLARE @n AS BIGINT; SELECT @n = n FROM dbo.GetNumsAlanCharlieJoeJeffItzikBatch(0, 99999999) ORDER BY n OPTION(MAXDOP 1);
Kế hoạch cho việc thực hiện này được thể hiện trong Hình 4.
Hình 4:Kế hoạch cho dbo.GetNumsAlanCharlieJoeJeffItzikBatch (0, 99999999)
Kế hoạch sử dụng toán tử Kết hợp Kết hợp (Concatenation) để hợp nhất hàng sentinel có giá trị 0 và phần còn lại. Mặc dù phần thứ hai vẫn hoạt động hiệu quả như trước, nhưng logic hợp nhất chiếm một khoản phí khá lớn khoảng 26% trong thời gian chạy, dẫn đến các số liệu thống kê về thời gian sau:
Thời gian CPU =9265 ms, thời gian đã trôi qua =9298 ms.Hãy kiểm tra hàm với phạm vi không đổi từ 2 đến 100.000.001:
DECLARE @n AS BIGINT; SELECT @n = n FROM dbo.GetNumsAlanCharlieJoeJeffItzikBatch(2, 100000001) ORDER BY n OPTION(MAXDOP 1);
Kế hoạch cho việc thực hiện này được thể hiện trong Hình 5.
Hình 5:Kế hoạch cho dbo.GetNumsAlanCharlieJoeJeffItzikBatch (2, 100000001)
Lần này không có logic hợp nhất đắt tiền nào vì phần hàng sentinel không liên quan. Tuy nhiên, hãy quan sát rằng cột được trả về là biểu thức được thao tác @low - 1 + rownum, sau khi nhúng / nội tuyến và liên tục gấp tham số đã trở thành 1 + rownum.
Đây là thống kê thời gian mà tôi nhận được cho việc thực hiện này:
Thời gian CPU =9000 ms, thời gian đã trôi qua =9015 ms.Đúng như dự đoán, tốc độ này không nhanh như với phạm vi bắt đầu bằng 1, nhưng thú vị là, nhanh hơn với phạm vi bắt đầu bằng 0.
Xóa hàng 0 canh gác
Do kỹ thuật với hàng sentinel với giá trị 0 có vẻ chậm hơn so với việc áp dụng thao tác với rownum, nên bạn chỉ cần tránh nó. Điều này đưa chúng tôi đến một giải pháp dựa trên loại bỏ ngang được đơn giản hóa kết hợp các ý tưởng từ Alan, Charlie, Joe và chính tôi. Tôi sẽ gọi hàm với giải pháp này là dbo.GetNumsAlanCharlieJoeItzikBatch. Đây là định nghĩa của hàm:
CREATE OR ALTER FUNCTION dbo.GetNumsAlanCharlieJoeItzikBatch(@low AS BIGINT = 1, @high AS BIGINT) RETURNS TABLE AS RETURN WITH L0 AS ( SELECT 1 AS c FROM (VALUES(1),(1),(1),(1),(1),(1),(1),(1), (1),(1),(1),(1),(1),(1),(1),(1)) AS D(c) ), L1 AS ( SELECT 1 AS c FROM L0 AS A CROSS JOIN L0 AS B ), L2 AS ( SELECT 1 AS c FROM L1 AS A CROSS JOIN L1 AS B ), L3 AS ( SELECT 1 AS c FROM L2 AS A CROSS JOIN L2 AS B ), Nums AS ( SELECT ROW_NUMBER() OVER(ORDER BY (SELECT NULL)) AS rownum FROM L3 ) SELECT TOP(@high) rownum AS n, @high + 1 - rownum AS op FROM Nums LEFT OUTER JOIN dbo.BatchMe ON 1 = 0 WHERE @low = 1 ORDER BY rownum UNION ALL SELECT TOP(@high - @low + 1) @low - 1 + rownum AS n, @high + 1 - rownum AS op FROM Nums LEFT OUTER JOIN dbo.BatchMe ON 1 = 0 WHERE @low <> 1 ORDER BY rownum; GO
Hãy kiểm tra nó với phạm vi từ 1 đến 100 triệu:
DECLARE @n AS BIGINT; SELECT @n = n FROM dbo.GetNumsAlanCharlieJoeItzikBatch(1, 100000000) ORDER BY n OPTION(MAXDOP 1);
Kế hoạch giống như kế hoạch được hiển thị trước đó trong Hình 3, như dự kiến.
Theo đó, tôi nhận được số liệu thống kê thời gian sau:
Thời gian CPU =7219 ms, thời gian đã trôi qua =7243 ms.Kiểm tra nó với phạm vi 0 đến 99,999.999:
DECLARE @n AS BIGINT; SELECT @n = n FROM dbo.GetNumsAlanCharlieJoeItzikBatch(0, 99999999) ORDER BY n OPTION(MAXDOP 1);
Lần này, bạn nhận được cùng một kế hoạch như kế hoạch được hiển thị trước đó trong Hình 5 - không phải Hình 4.
Đây là số liệu thống kê về thời gian mà tôi nhận được cho việc thực thi này:
Thời gian CPU =9313 ms, thời gian đã trôi qua =9334 ms.Kiểm tra nó với phạm vi 2 đến 100.000.001:
DECLARE @n AS BIGINT; SELECT @n = n FROM dbo.GetNumsAlanCharlieJoeItzikBatch(2, 100000001) ORDER BY n OPTION(MAXDOP 1);
Một lần nữa, bạn sẽ có được kế hoạch tương tự như kế hoạch được hiển thị trước đó trong Hình 5.
Tôi nhận được thống kê thời gian sau cho lần thực hiện này:
Thời gian CPU =9125 ms, thời gian đã trôi qua =9148 ms.Lưu ý khi sử dụng đầu vào không thoải mái
Với cả kỹ thuật loại bỏ đơn vị dọc và ngang, mọi thứ hoạt động lý tưởng miễn là bạn chuyển các hằng số làm đầu vào. Tuy nhiên, bạn cần lưu ý những cảnh báo có thể dẫn đến các hình phạt về hiệu suất khi bạn vượt qua các đầu vào không kiên quyết. Kỹ thuật loại bỏ đơn vị dọc có ít vấn đề hơn và các vấn đề tồn tại cũng dễ giải quyết hơn, vì vậy hãy bắt đầu với nó.
Hãy nhớ rằng trong bài viết này, chúng tôi đã sử dụng hàm dbo.GetNumsAlanCharlieItzikBatch làm ví dụ dựa trên khái niệm loại bỏ đơn vị dọc. Hãy chạy một loạt các bài kiểm tra với các đầu vào không quan trọng, chẳng hạn như các biến.
Như thử nghiệm đầu tiên của chúng tôi, chúng tôi sẽ trả về rn và yêu cầu dữ liệu được sắp xếp theo thứ tự của rn:
DECLARE @mylow AS BIGINT = 1, @myhigh AS BIGINT = 100000000; DECLARE @n AS BIGINT; SELECT @n = rn FROM dbo.GetNumsAlanCharlieItzikBatch(@mylow, @myhigh) ORDER BY rn OPTION(MAXDOP 1);
Hãy nhớ rằng rn đại diện cho biểu thức ROW_NUMBER không được điều chỉnh, vì vậy việc chúng tôi sử dụng đầu vào không tùy chỉnh không có ý nghĩa đặc biệt trong trường hợp này. Không cần phải sắp xếp rõ ràng trong kế hoạch.
Tôi nhận được số liệu thống kê về thời gian sau cho quá trình thực thi này:
Thời gian CPU =7390 ms, thời gian đã trôi qua =7386 ms.Những con số này đại diện cho trường hợp lý tưởng.
Trong thử nghiệm tiếp theo, sắp xếp các hàng kết quả theo n:
DECLARE @mylow AS BIGINT = 1, @myhigh AS BIGINT = 100000000; DECLARE @n AS BIGINT; SELECT @n = n FROM dbo.GetNumsAlanCharlieItzikBatch(@mylow, @myhigh) ORDER BY n OPTION(MAXDOP 1);
Kế hoạch cho việc thực hiện này được thể hiện trong Hình 6.
Hình 6:Kế hoạch cho dbo.GetNumsAlanCharlieItzikBatch (@mylow, @myhigh) đặt hàng bởi n
Thấy vấn đề? Sau khi nội dòng, @low được thay thế bằng @ mylow — không phải bằng giá trị trong @mylow, là 1. Do đó, việc gấp liên tục không diễn ra và do đó n không phải là thứ tự bảo toàn đối với rownum. Điều này dẫn đến việc sắp xếp rõ ràng trong kế hoạch.
Đây là thống kê thời gian mà tôi nhận được cho việc thực hiện này:
Thời gian CPU =25141 ms, thời gian đã trôi qua =25628 ms.Thời gian thực thi gần như tăng gấp ba lần so với khi không cần phân loại rõ ràng.
Một giải pháp đơn giản là sử dụng ý tưởng ban đầu của Alan Burstein để luôn sắp xếp thứ tự theo rn khi bạn cần kết quả được sắp xếp theo thứ tự, cả khi trả về rn và khi trả về n, như sau:
DECLARE @mylow AS BIGINT = 1, @myhigh AS BIGINT = 100000000; DECLARE @n AS BIGINT; SELECT @n = n FROM dbo.GetNumsAlanCharlieItzikBatch(@mylow, @myhigh) ORDER BY rn OPTION(MAXDOP 1);
Lần này không có sự sắp xếp rõ ràng nào trong kế hoạch.
Tôi nhận được thống kê thời gian sau cho lần thực hiện này:
Thời gian CPU =9156 ms, thời gian đã trôi qua =9184 ms.Các con số phản ánh đầy đủ thực tế là bạn đang trả về biểu thức đã thao tác, nhưng không phải sắp xếp rõ ràng.
Với các giải pháp dựa trên kỹ thuật loại bỏ đơn vị theo chiều ngang, chẳng hạn như hàm dbo.GetNumsAlanCharlieJoeItzikBatch của chúng tôi, tình hình sẽ phức tạp hơn khi sử dụng đầu vào không liên quan.
Đầu tiên chúng ta hãy kiểm tra hàm với một phạm vi rất nhỏ gồm 10 con số:
DECLARE @mylow AS BIGINT = 1, @myhigh AS BIGINT = 10; DECLARE @n AS BIGINT; SELECT @n = n FROM dbo.GetNumsAlanCharlieJoeItzikBatch(@mylow, @myhigh) ORDER BY n OPTION(MAXDOP 1);
Kế hoạch cho việc thực hiện này được thể hiện trong Hình 7.
Hình 7:Kế hoạch cho dbo.GetNumsAlanCharlieJoeItzikBatch (@mylow, @myhigh)
Có một mặt rất đáng báo động trong kế hoạch này. Quan sát rằng các toán tử bộ lọc xuất hiện bên dưới các nhà khai thác hàng đầu! Trong bất kỳ lệnh gọi hàm nào có đầu vào không cố định, đương nhiên một trong các nhánh bên dưới toán tử Kết nối sẽ luôn có điều kiện lọc sai. Tuy nhiên, cả hai toán tử Hàng đầu đều yêu cầu số hàng khác không. Vì vậy, toán tử Top phía trên toán tử với điều kiện lọc sai sẽ yêu cầu các hàng và sẽ không bao giờ được thỏa mãn vì toán tử bộ lọc sẽ tiếp tục loại bỏ tất cả các hàng mà nó sẽ nhận được từ nút con của nó. Công việc trong cây con bên dưới toán tử Bộ lọc sẽ phải chạy đến khi hoàn thành. Trong trường hợp của chúng ta, điều này có nghĩa là cây con sẽ thực hiện công việc tạo ra các hàng 4B, mà toán tử Bộ lọc sẽ loại bỏ. Bạn thắc mắc tại sao toán tử bộ lọc lại làm phiền việc yêu cầu các hàng từ nút con của nó, nhưng có vẻ như đó là cách nó hiện đang hoạt động. Thật khó để thấy điều này với một kế hoạch tĩnh. Ví dụ:xem trực tiếp điều này sẽ dễ dàng hơn với tùy chọn thực thi truy vấn trực tiếp trong SentryOne Plan Explorer, như thể hiện trong Hình 8. Hãy thử.
Hình 8:Thống kê truy vấn trực tiếp cho dbo.GetNumsAlanCharlieJoeItzikBatch (@mylow, @myhigh)
Quá trình kiểm tra này mất 9:15 phút để hoàn thành trên máy của tôi và hãy nhớ rằng yêu cầu là trả về một dãy 10 số.
Hãy nghĩ xem có cách nào để tránh kích hoạt toàn bộ cây con không liên quan hay không. Để đạt được điều này, bạn sẽ muốn các toán tử Bộ lọc khởi động xuất hiện ở trên các toán tử hàng đầu thay vì bên dưới chúng. Nếu bạn đọc Nguyên tắc cơ bản về biểu thức bảng, Phần 4 - Bảng có nguồn gốc, cân nhắc tối ưu hóa, tiếp tục, bạn biết rằng bộ lọc TOP ngăn chặn việc bỏ ghi chú các biểu thức bảng. Vì vậy, tất cả những gì bạn cần làm là đặt truy vấn TOP trong bảng dẫn xuất và áp dụng bộ lọc trong truy vấn bên ngoài so với bảng dẫn xuất.
Đây là chức năng đã sửa đổi của chúng tôi triển khai thủ thuật này:
CREATE OR ALTER FUNCTION dbo.GetNumsAlanCharlieJoeItzikBatch(@low AS BIGINT = 1, @high AS BIGINT) RETURNS TABLE AS RETURN WITH L0 AS ( SELECT 1 AS c FROM (VALUES(1),(1),(1),(1),(1),(1),(1),(1), (1),(1),(1),(1),(1),(1),(1),(1)) AS D(c) ), L1 AS ( SELECT 1 AS c FROM L0 AS A CROSS JOIN L0 AS B ), L2 AS ( SELECT 1 AS c FROM L1 AS A CROSS JOIN L1 AS B ), L3 AS ( SELECT 1 AS c FROM L2 AS A CROSS JOIN L2 AS B ), Nums AS ( SELECT ROW_NUMBER() OVER(ORDER BY (SELECT NULL)) AS rownum FROM L3 ) SELECT * FROM ( SELECT TOP(@high) rownum AS n, @high + 1 - rownum AS op FROM Nums LEFT OUTER JOIN dbo.BatchMe ON 1 = 0 ORDER BY rownum ) AS D1 WHERE @low = 1 UNION ALL SELECT * FROM ( SELECT TOP(@high - @low + 1) @low - 1 + rownum AS n, @high + 1 - rownum AS op FROM Nums LEFT OUTER JOIN dbo.BatchMe ON 1 = 0 ORDER BY rownum ) AS D2 WHERE @low <> 1; GO
Như mong đợi, các thực thi với hằng số tiếp tục hoạt động và hoạt động giống như khi không có thủ thuật.
Đối với đầu vào không cố định, hiện nay với phạm vi nhỏ, nó rất nhanh. Đây là một bài kiểm tra với phạm vi 10 số:
DECLARE @mylow AS BIGINT = 1, @myhigh AS BIGINT = 10; DECLARE @n AS BIGINT; SELECT @n = n FROM dbo.GetNumsAlanCharlieJoeItzikBatch(@mylow, @myhigh) ORDER BY n OPTION(MAXDOP 1);
Kế hoạch cho việc thực hiện này được thể hiện trong Hình 9.
Hình 9:Kế hoạch cải tiến dbo.GetNumsAlanCharlieJoeItzikBatch (@mylow, @myhigh)
Quan sát rằng hiệu quả mong muốn của việc đặt các toán tử Bộ lọc phía trên các toán tử Top đã đạt được. Tuy nhiên, cột thứ tự n được coi là kết quả của thao tác, và do đó không được coi là cột bảo toàn thứ tự đối với rownum. Do đó, có sự sắp xếp rõ ràng trong kế hoạch.
Kiểm tra chức năng với phạm vi lớn 100 triệu con số:
DECLARE @mylow AS BIGINT = 1, @myhigh AS BIGINT = 100000000; DECLARE @n AS BIGINT; SELECT @n = n FROM dbo.GetNumsAlanCharlieJoeItzikBatch(@mylow, @myhigh) ORDER BY n OPTION(MAXDOP 1);
Tôi nhận được số liệu thống kê thời gian sau:
Thời gian CPU =29907 ms, thời gian đã trôi qua =29909 ms.Thật là một kẻ vô tích sự; nó gần như hoàn hảo!
Tóm tắt và thông tin chi tiết về hiệu suất
Hình 10 tóm tắt thống kê thời gian cho các giải pháp khác nhau.
Hình 10:Tóm tắt hiệu suất theo thời gian của các giải pháp
Vậy chúng ta đã học được gì từ tất cả những điều này? Tôi đoán không phải làm điều đó một lần nữa! Chỉ đùa thôi. Chúng tôi biết rằng sẽ an toàn hơn khi sử dụng khái niệm loại bỏ theo chiều dọc như trong dbo.GetNumsAlanCharlieItzikBatch, hiển thị cả kết quả ROW_NUMBER không được điều khiển (rn) và kết quả bị thao túng (n). Chỉ cần đảm bảo rằng khi cần trả về kết quả được sắp xếp theo thứ tự, hãy luôn đặt hàng theo rn, cho dù trả về rn hay n.
Nếu bạn hoàn toàn chắc chắn rằng giải pháp của bạn sẽ luôn được sử dụng với các hằng số làm đầu vào, bạn có thể sử dụng khái niệm loại bỏ đơn vị ngang. Điều này sẽ dẫn đến một giải pháp trực quan hơn cho người dùng, vì họ sẽ tương tác với một cột cho các giá trị tăng dần. Tôi vẫn khuyên bạn nên sử dụng thủ thuật với các bảng dẫn xuất để ngăn chặn việc bỏ ghi chú và đặt toán tử Bộ lọc phía trên các toán tử Hàng đầu nếu hàm đã từng được sử dụng với các đầu vào không cần thiết, chỉ để an toàn.
Chúng tôi vẫn chưa hoàn thành. Tháng tới, tôi sẽ tiếp tục khám phá các giải pháp bổ sung.