Kế hoạch thực thi cung cấp một nguồn thông tin phong phú có thể giúp chúng tôi xác định các cách để cải thiện hiệu suất của các truy vấn quan trọng. Mọi người thường tìm kiếm những thứ như quét và tra cứu lớn như một cách để xác định tối ưu hóa đường dẫn truy cập dữ liệu tiềm năng. Những vấn đề này thường có thể được giải quyết nhanh chóng bằng cách tạo chỉ mục mới hoặc mở rộng chỉ mục hiện có với nhiều cột được bao gồm hơn.
Chúng tôi cũng có thể sử dụng các kế hoạch sau khi thực hiện để so sánh thực tế với số lượng hàng dự kiến giữa các nhà khai thác kế hoạch. Khi chúng được phát hiện có sự chênh lệch đáng kể, chúng tôi có thể cố gắng cung cấp thông tin thống kê tốt hơn cho trình tối ưu hóa bằng cách cập nhật thống kê hiện có, tạo đối tượng thống kê mới, sử dụng thống kê trên các cột được tính toán hoặc có thể bằng cách chia một truy vấn phức tạp thành thành phần ít phức tạp hơn các bộ phận.
Ngoài ra, chúng ta cũng có thể xem xét các hoạt động tốn kém trong kế hoạch, đặc biệt là những hoạt động tiêu tốn bộ nhớ như sắp xếp và băm. Đôi khi có thể tránh được việc sắp xếp thông qua các thay đổi lập chỉ mục. Đôi khi, chúng tôi có thể phải cấu trúc lại truy vấn bằng cách sử dụng cú pháp ủng hộ một kế hoạch duy trì thứ tự mong muốn cụ thể.
Đôi khi, hiệu suất vẫn không đủ tốt ngay cả khi đã áp dụng tất cả các kỹ thuật điều chỉnh hiệu suất này. Bước tiếp theo có thể thực hiện là suy nghĩ kỹ hơn một chút về kế hoạch nói chung . Điều này có nghĩa là lùi lại một bước, cố gắng hiểu chiến lược tổng thể được chọn bởi trình tối ưu hóa truy vấn, để xem liệu chúng tôi có thể xác định một cải tiến thuật toán hay không.
Bài viết này khám phá loại phân tích thứ hai này, sử dụng một bài toán ví dụ đơn giản về việc tìm các giá trị cột duy nhất trong một tập dữ liệu lớn vừa phải. Như thường thấy trong các bài toán tương tự trong thế giới thực, cột quan tâm sẽ có tương đối ít giá trị duy nhất, so với số hàng trong bảng. Phân tích này có hai phần:tạo dữ liệu mẫu và viết chính truy vấn giá trị khác biệt.
Tạo dữ liệu mẫu
Để cung cấp ví dụ đơn giản nhất có thể, bảng thử nghiệm của chúng tôi chỉ có một cột duy nhất với chỉ mục được phân nhóm (cột này sẽ chứa các giá trị trùng lặp nên chỉ mục không thể được khai báo là duy nhất):
CREATE TABLE dbo.Test ( data integer NOT NULL, ); GO CREATE CLUSTERED INDEX cx ON dbo.Test (data);
Để chọn ra một số số, chúng tôi sẽ chọn tải mười triệu hàng tổng cộng, với phân phối đồng đều trên nghìn giá trị khác biệt . Một kỹ thuật phổ biến để tạo dữ liệu như thế này là kết hợp chéo một số bảng hệ thống và áp dụng ROW_NUMBER
hàm số. Chúng tôi cũng sẽ sử dụng toán tử mô-đun để giới hạn các số được tạo thành các giá trị khác biệt mong muốn:
INSERT dbo.Test WITH (TABLOCK) (data) SELECT TOP (10000000) (ROW_NUMBER() OVER (ORDER BY (SELECT 0)) % 1000) + 1 FROM master.sys.columns AS C1 WITH (READUNCOMMITTED) CROSS JOIN master.sys.columns AS C2 WITH (READUNCOMMITTED) CROSS JOIN master.sys.columns C3 WITH (READUNCOMMITTED);
Kế hoạch thực thi ước tính cho truy vấn đó như sau (nhấp vào hình ảnh để phóng to nếu cần):
Quá trình này mất khoảng 30 giây để tạo dữ liệu mẫu trên máy tính xách tay của tôi. Đó không phải là một khoảng thời gian quá dài, nhưng vẫn rất thú vị khi xem xét những gì chúng ta có thể làm để làm cho quá trình này hiệu quả hơn…
Phân tích kế hoạch
Chúng ta sẽ bắt đầu bằng cách tìm hiểu từng hoạt động trong kế hoạch nhằm mục đích gì.
Phần kế hoạch thực thi ở bên phải của Người điều hành phân đoạn liên quan đến việc sản xuất các hàng bằng cách ghép chéo các bảng hệ thống:
Toán tử Phân đoạn ở đó trong trường hợp hàm cửa sổ có PARTITION BY
mệnh đề. Đó không phải là trường hợp ở đây, nhưng dù sao thì nó cũng có trong kế hoạch truy vấn. Toán tử Dự án trình tự tạo số hàng và Hàng đầu giới hạn đầu ra kế hoạch ở mười triệu hàng:
Tính toán Scalar xác định biểu thức áp dụng hàm modulo và thêm một biểu thức vào kết quả:
Chúng ta có thể xem các nhãn của biểu thức Dự án Trình tự và Tính toán vô hướng có liên quan như thế nào bằng cách sử dụng tab Biểu thức của Plan Explorer:
Điều này cho chúng ta cảm nhận đầy đủ hơn về quy trình của kế hoạch này:Dự án trình tự đánh số các hàng và gắn nhãn cho biểu thức Expr1050
; Compute Scalar gắn nhãn kết quả của phép tính modulo và cộng một là Expr1052
. Cũng chú ý đến chuyển đổi ngầm trong biểu thức Tính toán vô hướng. Cột của bảng đích thuộc loại số nguyên, trong khi ROW_NUMBER
hàm tạo ra một bigint, do đó, chuyển đổi thu hẹp là cần thiết.
Toán tử tiếp theo trong kế hoạch là Sắp xếp. Theo ước tính chi phí của trình tối ưu hóa truy vấn, đây dự kiến là hoạt động tốn kém nhất (88,1% ước tính ):
Có thể không rõ ràng ngay lập tức tại sao kế hoạch này có tính năng sắp xếp, vì không có yêu cầu sắp xếp thứ tự rõ ràng trong truy vấn. Sắp xếp được thêm vào kế hoạch để đảm bảo các hàng đến toán tử Chèn chỉ mục theo cụm theo thứ tự chỉ mục được phân cụm. Điều này thúc đẩy quá trình viết tuần tự, tránh chia nhỏ trang và là một trong những điều kiện tiên quyết đối với INSERT
được ghi nhật ký tối thiểu hoạt động.
Đây đều là những thứ tốt tiềm ẩn, nhưng bản thân Sort là khá đắt. Thật vậy, việc kiểm tra kế hoạch thực thi sau thực thi ("thực tế") cho thấy Sắp xếp cũng hết bộ nhớ tại thời điểm thực thi và phải tràn sang tempdb vật lý đĩa:
Tràn Sắp xếp xảy ra mặc dù số lượng hàng ước tính là chính xác và mặc dù thực tế là truy vấn đã được cấp tất cả bộ nhớ mà nó yêu cầu (như đã thấy trong thuộc tính kế hoạch cho INSERT
gốc nút):
Tràn sắp xếp cũng được biểu thị bằng sự hiện diện của IO_COMPLETION
đợi trong tab thống kê chờ của Plan Explorer PRO:
Cuối cùng đối với phần phân tích kế hoạch này, hãy chú ý đến DML Request Sort
thuộc tính của toán tử Chèn chỉ mục theo cụm được đặt thành true:
Cờ này chỉ ra rằng trình tối ưu hóa yêu cầu cây con bên dưới Chèn cung cấp các hàng theo thứ tự sắp xếp khóa chỉ mục (do đó cần có toán tử Sắp xếp có vấn đề).
Tránh sắp xếp
Bây giờ chúng ta đã biết tại sao sắp xếp xuất hiện, chúng tôi có thể kiểm tra xem điều gì sẽ xảy ra nếu chúng tôi loại bỏ nó. Có nhiều cách chúng tôi có thể viết lại truy vấn để "đánh lừa" trình tối ưu hóa nghĩ rằng sẽ có ít hàng được chèn hơn (vì vậy việc sắp xếp sẽ không đáng giá) nhưng một cách nhanh chóng để tránh việc sắp xếp trực tiếp (chỉ dành cho mục đích thử nghiệm) là sử dụng cờ theo dõi không có tài liệu 8795. Điều này đặt DML Request Sort
thuộc tính thành false, vì vậy các hàng không còn bắt buộc phải đến Chèn chỉ mục theo cụm theo thứ tự khóa được phân nhóm:
TRUNCATE TABLE dbo.Test; GO INSERT dbo.Test WITH (TABLOCK) (data) SELECT TOP (10000000) ROW_NUMBER() OVER (ORDER BY (SELECT 0)) % 1000 FROM master.sys.columns AS C1 WITH (READUNCOMMITTED) CROSS JOIN master.sys.columns AS C2 WITH (READUNCOMMITTED) CROSS JOIN master.sys.columns C3 WITH (READUNCOMMITTED) OPTION (QUERYTRACEON 8795);
Kế hoạch truy vấn sau khi thực hiện mới như sau (nhấp vào hình ảnh để phóng to):
Toán tử Sắp xếp đã biến mất, nhưng truy vấn mới chạy trong hơn 50 giây (so với 30 giây trước). Có một vài lý do cho việc này. Đầu tiên, chúng tôi không có khả năng xảy ra các chèn được ghi nhật ký tối thiểu vì chúng yêu cầu dữ liệu được sắp xếp (DML Request Sort =true). Thứ hai, một số lượng lớn các chia tách trang "xấu" sẽ xảy ra trong quá trình chèn. Trong trường hợp điều đó có vẻ phản trực quan, hãy nhớ rằng mặc dù ROW_NUMBER
hàm đánh số các hàng một cách tuần tự, tác dụng của toán tử modulo là trình bày một chuỗi lặp lại các số 1… 1000 vào Chèn chỉ mục theo cụm.
Vấn đề cơ bản tương tự cũng xảy ra nếu chúng tôi sử dụng thủ thuật T-SQL để giảm số lượng hàng dự kiến để tránh sắp xếp thay vì sử dụng cờ theo dõi không được hỗ trợ.
Tránh Phân loại II
Nhìn vào kế hoạch một cách tổng thể, có vẻ như rõ ràng chúng tôi muốn tạo các hàng theo cách tránh sắp xếp rõ ràng, nhưng vẫn thu được lợi ích của việc ghi nhật ký tối thiểu và tránh tách trang xấu. Nói một cách đơn giản:chúng tôi muốn một kế hoạch trình bày các hàng theo thứ tự khóa được nhóm lại, nhưng không cần sắp xếp.
Được trang bị thông tin chi tiết mới này, chúng tôi có thể thể hiện truy vấn của mình theo một cách khác. Truy vấn sau đây tạo từng số từ 1 đến 1000 và các phép nối chéo được đặt với 10.000 hàng để tạo ra mức độ trùng lặp cần thiết. Ý tưởng là tạo một bộ chèn trình bày 10.000 hàng được đánh số '1' rồi 10.000 hàng được đánh số '2'… và v.v.
TRUNCATE TABLE dbo.Test; GO INSERT dbo.Test WITH (TABLOCK) (data) SELECT N.number FROM ( SELECT SV.number FROM master.dbo.spt_values AS SV WITH (READUNCOMMITTED) WHERE SV.[type] = N'P' AND SV.number >= 1 AND SV.number <= 1000 ) AS N CROSS JOIN ( SELECT TOP (10000) Dummy = NULL FROM master.sys.columns AS C1 WITH (READUNCOMMITTED) CROSS JOIN master.sys.columns AS C2 WITH (READUNCOMMITTED) CROSS JOIN master.sys.columns C3 WITH (READUNCOMMITTED) ) AS C;
Thật không may, trình tối ưu hóa vẫn tạo ra một kế hoạch có loại:
Không có nhiều điều để nói về bảo vệ của trình tối ưu hóa ở đây, đây chỉ là một kế hoạch sơ sài. Nó đã chọn tạo 10.000 hàng sau đó nối chéo những hàng có số từ 1 đến 1000. Điều này không cho phép giữ nguyên thứ tự tự nhiên của các số, vì vậy không thể tránh được việc sắp xếp.
Tránh sắp xếp - Cuối cùng!
Chiến lược mà trình tối ưu hóa đã bỏ lỡ là lấy các con số 1… 1000 đầu tiên và nối chéo từng số với 10.000 hàng (tạo 10.000 bản sao của mỗi số theo thứ tự). Kế hoạch dự kiến sẽ tránh sắp xếp bằng cách sử dụng kết nối chéo các vòng lặp lồng nhau giữ nguyên thứ tự của các hàng ở đầu vào bên ngoài.
Chúng tôi có thể đạt được kết quả này bằng cách buộc trình tối ưu hóa truy cập các bảng dẫn xuất theo thứ tự được chỉ định trong truy vấn, sử dụng FORCE ORDER
gợi ý truy vấn:
TRUNCATE TABLE dbo.Test; GO INSERT dbo.Test WITH (TABLOCK) (data) SELECT N.number FROM ( SELECT SV.number FROM master.dbo.spt_values AS SV WITH (READUNCOMMITTED) WHERE SV.[type] = N'P' AND SV.number >= 1 AND SV.number <= 1000 ) AS N CROSS JOIN ( SELECT TOP (10000) Dummy = NULL FROM master.sys.columns AS C1 WITH (READUNCOMMITTED) CROSS JOIN master.sys.columns AS C2 WITH (READUNCOMMITTED) CROSS JOIN master.sys.columns C3 WITH (READUNCOMMITTED) ) AS C OPTION (FORCE ORDER);
Cuối cùng, chúng tôi có được kế hoạch như sau:
Kế hoạch này tránh một loại sắp xếp rõ ràng trong khi vẫn tránh chia tách trang "xấu" và cho phép chèn được ghi nhật ký tối thiểu vào chỉ mục nhóm (giả sử cơ sở dữ liệu không sử dụng FULL
mô hình phục hồi). Nó tải tất cả mười triệu hàng trong khoảng 9 giây trên máy tính xách tay của tôi (với một đĩa quay SATA 7200 vòng / phút). Điều này thể hiện mức tăng hiệu quả rõ rệt trong 30-50 giây thời gian trôi qua được xem trước khi viết lại.
Tìm các giá trị riêng biệt
Bây giờ chúng ta đã tạo dữ liệu mẫu, chúng ta có thể chuyển sự chú ý sang việc viết một truy vấn để tìm các giá trị riêng biệt trong bảng. Một cách tự nhiên để thể hiện yêu cầu này trong T-SQL như sau:
SELECT DISTINCT data FROM dbo.Test WITH (TABLOCK) OPTION (MAXDOP 1);
Kế hoạch thực hiện rất đơn giản, như bạn mong đợi:
Quá trình này mất khoảng 2900 mili giây để chạy trên máy của tôi và yêu cầu 43,406 đọc logic:
Xóa MAXDOP (1)
gợi ý truy vấn tạo ra một kế hoạch song song:
Quá trình này hoàn thành sau khoảng 1500 mili giây (nhưng với 8,764 ms thời gian CPU tiêu thụ) và 43,804 đọc logic:
Các kế hoạch và kết quả hoạt động giống nhau nếu chúng ta sử dụng GROUP BY
thay vì DISTINCT
.
Một thuật toán tốt hơn
Các kế hoạch truy vấn hiển thị ở trên đọc tất cả các giá trị từ bảng cơ sở và xử lý chúng thông qua Tổng hợp luồng. Suy nghĩ về toàn bộ nhiệm vụ, việc quét tất cả 10 triệu hàng dường như không hiệu quả khi chúng ta biết rằng có tương đối ít giá trị khác biệt.
Một chiến lược tốt hơn có thể là tìm một giá trị thấp nhất trong bảng, sau đó tìm giá trị cao nhất tiếp theo, v.v. cho đến khi chúng tôi dùng hết giá trị. Điều quan trọng, phương pháp này cho phép tìm kiếm đơn lẻ vào chỉ mục hơn là quét mọi hàng.
Chúng tôi có thể triển khai ý tưởng này trong một truy vấn duy nhất bằng cách sử dụng CTE đệ quy, trong đó phần liên kết tìm thấy thấp nhất giá trị khác biệt, sau đó phần đệ quy tìm giá trị khác biệt tiếp theo, v.v. Nỗ lực đầu tiên để viết truy vấn này là:
WITH RecursiveCTE AS ( -- Anchor SELECT data = MIN(T.data) FROM dbo.Test AS T UNION ALL -- Recursive SELECT MIN(T.data) FROM dbo.Test AS T JOIN RecursiveCTE AS R ON R.data < T.data ) SELECT data FROM RecursiveCTE OPTION (MAXRECURSION 0);
Rất tiếc, cú pháp đó không biên dịch:
Ok, vì vậy các hàm tổng hợp không được phép. Thay vì sử dụng MIN
, chúng ta có thể viết cùng một logic bằng cách sử dụng TOP (1)
với ORDER BY
:
WITH RecursiveCTE AS ( -- Anchor SELECT TOP (1) T.data FROM dbo.Test AS T ORDER BY T.data UNION ALL -- Recursive SELECT TOP (1) T.data FROM dbo.Test AS T JOIN RecursiveCTE AS R ON R.data < T.data ORDER BY T.data ) SELECT data FROM RecursiveCTE OPTION (MAXRECURSION 0);
Vẫn không có niềm vui.
Hóa ra là chúng ta có thể giải quyết những hạn chế này bằng cách viết lại phần đệ quy để đánh số các hàng ứng viên theo thứ tự bắt buộc, sau đó lọc cho hàng được đánh số là 'một'. Điều này có vẻ hơi mạch lạc, nhưng logic hoàn toàn giống nhau:
WITH RecursiveCTE AS ( -- Anchor SELECT TOP (1) data FROM dbo.Test AS T ORDER BY T.data UNION ALL -- Recursive SELECT R.data FROM ( -- Number the rows SELECT T.data, rn = ROW_NUMBER() OVER ( ORDER BY T.data) FROM dbo.Test AS T JOIN RecursiveCTE AS R ON R.data < T.data ) AS R WHERE -- Only the row that sorts lowest R.rn = 1 ) SELECT data FROM RecursiveCTE OPTION (MAXRECURSION 0);
Truy vấn này không biên dịch và tạo ra kế hoạch sau thực thi sau:
Lưu ý toán tử Top trong phần đệ quy của kế hoạch thực thi (được đánh dấu). Chúng tôi không thể viết T-SQL TOP
trong phần đệ quy của một biểu thức bảng chung đệ quy, nhưng điều đó không có nghĩa là trình tối ưu hóa không thể sử dụng một biểu thức! Trình tối ưu hóa giới thiệu Top dựa trên suy luận về số lượng hàng mà nó sẽ cần kiểm tra để tìm một hàng được đánh số '1'.
Hiệu suất của kế hoạch (không song song) này tốt hơn nhiều so với cách tiếp cận Tổng hợp theo dòng. Quá trình hoàn thành trong khoảng 50 mili giây , với 3007 số đọc lôgic so với bảng nguồn (và 6001 hàng được đọc từ bảng làm việc cuộn), so với giá trị tốt nhất trước đó là 1500ms (Thời gian CPU 8764 ms ở DOP 8) và 43,804 đọc logic:
Kết luận
Không phải lúc nào bạn cũng có thể đạt được đột phá về hiệu suất truy vấn bằng cách tự mình xem xét các phần tử kế hoạch truy vấn riêng lẻ. Đôi khi, chúng ta cần phân tích chiến lược đằng sau toàn bộ kế hoạch thực hiện, sau đó suy nghĩ kỹ để tìm ra một thuật toán và cách triển khai hiệu quả hơn.