Bài viết này là phần thứ 7 của loạt bài về các biểu thức bảng được đặt tên. Trong Phần 5 và Phần 6, tôi đã trình bày các khía cạnh khái niệm của các biểu thức bảng thông dụng (CTE). Tháng này và tháng tới, trọng tâm của tôi chuyển sang các cân nhắc tối ưu hóa của CTE.
Tôi sẽ bắt đầu bằng cách nhanh chóng xem lại khái niệm hấp dẫn về các biểu thức bảng đã đặt tên và chứng minh khả năng áp dụng của nó cho các CTE. Sau đó, tôi sẽ chuyển trọng tâm của mình sang các cân nhắc về tính bền bỉ. Tôi sẽ nói về các khía cạnh liên tục của CTE đệ quy và không đệ quy. Tôi sẽ giải thích thời điểm hợp lý khi gắn bó với CTE so với thời điểm thực sự có ý nghĩa hơn khi làm việc với các bảng tạm thời.
Trong các ví dụ của mình, tôi sẽ tiếp tục sử dụng cơ sở dữ liệu mẫu TSQLV5 và PerformanceV5. Bạn có thể tìm thấy tập lệnh tạo và điền TSQLV5 tại đây và sơ đồ ER của nó tại đây. Bạn có thể tìm thấy tập lệnh tạo và điền PerformanceV5 tại đây.
Thay thế / hủy ghi chú
Trong Phần 4 của loạt bài này, tập trung vào việc tối ưu hóa các bảng dẫn xuất, tôi đã mô tả một quá trình hủy ghi / thay thế các biểu thức bảng. Tôi đã giải thích rằng khi SQL Server tối ưu hóa một truy vấn liên quan đến các bảng dẫn xuất, nó áp dụng các quy tắc chuyển đổi cho cây ban đầu của các toán tử logic do trình phân tích cú pháp tạo ra, có thể thay đổi mọi thứ xung quanh những gì ban đầu là ranh giới biểu thức bảng. Điều này xảy ra ở một mức độ mà khi bạn so sánh một kế hoạch cho một truy vấn sử dụng các bảng dẫn xuất với một kế hoạch cho một truy vấn đối chiếu trực tiếp với các bảng cơ sở bên dưới mà bạn đã tự áp dụng logic unnesting, chúng trông giống nhau. Tôi cũng đã mô tả một kỹ thuật để ngăn chặn việc bỏ ghi chú bằng cách sử dụng bộ lọc TOP với một số lượng rất lớn các hàng làm đầu vào. Tôi đã chứng minh một số trường hợp mà kỹ thuật này khá tiện dụng — một trường hợp mục đích là để tránh lỗi và một trường hợp khác vì lý do tối ưu hóa.
Phiên bản TL; DR của việc thay thế / hủy lưu trữ CTE là quy trình giống như quy trình với các bảng dẫn xuất. Nếu bạn hài lòng với tuyên bố này, bạn có thể bỏ qua phần này và chuyển thẳng đến phần tiếp theo về Tính kiên trì. Bạn sẽ không bỏ lỡ bất kỳ điều gì quan trọng mà bạn chưa đọc trước đây. Tuy nhiên, nếu bạn giống tôi, bạn có thể muốn bằng chứng rằng thực sự là như vậy. Sau đó, bạn có thể sẽ muốn tiếp tục đọc phần này và kiểm tra mã mà tôi sử dụng khi truy cập lại các ví dụ về khóa không lưu ý mà tôi đã trình bày trước đó với các bảng dẫn xuất và chuyển đổi chúng để sử dụng CTE.
Trong Phần 4, tôi đã trình bày truy vấn sau (chúng tôi sẽ gọi nó là Truy vấn 1):
USE TSQLV5; SELECT orderid, orderdate FROM ( SELECT * FROM ( SELECT * FROM ( SELECT * FROM Sales.Orders WHERE orderdate >= '20180101' ) AS D1 WHERE orderdate >= '20180201' ) AS D2 WHERE orderdate >= '20180301' ) AS D3 WHERE orderdate >= '20180401';
Truy vấn liên quan đến ba cấp độ lồng nhau của các bảng dẫn xuất, cộng với một truy vấn bên ngoài. Mỗi cấp độ lọc một phạm vi ngày đặt hàng khác nhau. Kế hoạch cho Truy vấn 1 được thể hiện trong Hình 1.
Hình 1:Kế hoạch thực thi cho Truy vấn 1
Kế hoạch trong Hình 1 cho thấy rõ ràng rằng việc bỏ ghi chú các bảng dẫn xuất đã diễn ra vì tất cả các vị từ bộ lọc được hợp nhất thành một vị từ bộ lọc bao trùm duy nhất.
Tôi đã giải thích rằng bạn có thể ngăn quá trình bỏ ghi chú bằng cách sử dụng bộ lọc TOP có ý nghĩa (trái ngược với TOP 100 PERCENT) với số lượng hàng rất lớn làm đầu vào, như truy vấn sau đây hiển thị (chúng tôi sẽ gọi nó là Truy vấn 2):
SELECT orderid, orderdate FROM ( SELECT TOP (9223372036854775807) * FROM ( SELECT TOP (9223372036854775807) * FROM ( SELECT TOP (9223372036854775807) * FROM Sales.Orders WHERE orderdate >= '20180101' ) AS D1 WHERE orderdate >= '20180201' ) AS D2 WHERE orderdate >= '20180301' ) AS D3 WHERE orderdate >= '20180401';
Kế hoạch cho Truy vấn 2 được thể hiện trong Hình 2.
Hình 2:Kế hoạch thực thi cho Truy vấn 2
Kế hoạch cho thấy rõ ràng rằng việc hủy ghi chú đã không diễn ra vì bạn có thể nhìn thấy ranh giới bảng dẫn xuất một cách hiệu quả.
Hãy thử các ví dụ tương tự bằng cách sử dụng CTE. Đây là Truy vấn 1 được chuyển đổi để sử dụng CTE:
WITH C1 AS ( SELECT * FROM Sales.Orders WHERE orderdate >= '20180101' ), C2 AS ( SELECT * FROM C1 WHERE orderdate >= '20180201' ), C3 AS ( SELECT * FROM C2 WHERE orderdate >= '20180301' ) SELECT orderid, orderdate FROM C3 WHERE orderdate >= '20180401';
Bạn nhận được cùng một kế hoạch được hiển thị trước đó trong Hình 1, nơi bạn có thể thấy rằng quá trình hủy lưu trữ đã diễn ra.
Đây là Truy vấn 2 được chuyển đổi để sử dụng CTE:
WITH C1 AS ( SELECT TOP (9223372036854775807) * FROM Sales.Orders WHERE orderdate >= '20180101' ), C2 AS ( SELECT TOP (9223372036854775807) * FROM C1 WHERE orderdate >= '20180201' ), C3 AS ( SELECT TOP (9223372036854775807) * FROM C2 WHERE orderdate >= '20180301' ) SELECT orderid, orderdate FROM C3 WHERE orderdate >= '20180401';
Bạn nhận được cùng một kế hoạch như được hiển thị trước đó trong Hình 2, nơi bạn có thể thấy rằng việc hủy đăng ký đã không diễn ra.
Tiếp theo, hãy xem lại hai ví dụ mà tôi đã sử dụng để chứng minh tính thực tế của kỹ thuật này để ngăn chặn việc bỏ qua — chỉ lần này bằng cách sử dụng CTE.
Hãy bắt đầu với truy vấn sai. Truy vấn sau cố gắng trả về các dòng đặt hàng có chiết khấu lớn hơn chiết khấu tối thiểu và trong đó đối ứng của chiết khấu lớn hơn 10:
SELECT orderid, productid, discount FROM Sales.OrderDetails WHERE discount > (SELECT MIN(discount) FROM Sales.OrderDetails) AND 1.0 / discount > 10.0;
Mức chiết khấu tối thiểu không được là số âm, đúng hơn là bằng 0 hoặc cao hơn. Vì vậy, có thể bạn đang nghĩ rằng nếu một hàng có chiết khấu bằng không, thì vị từ đầu tiên sẽ đánh giá thành false và rằng một đoạn ngắn mạch sẽ ngăn nỗ lực đánh giá vị từ thứ hai, do đó tránh được lỗi. Tuy nhiên, khi bạn chạy mã này, bạn nhận được lỗi chia cho 0:
Msg 8134, Level 16, State 1, Line 99 Divide by zero error encountered.
Vấn đề là mặc dù SQL Server hỗ trợ khái niệm ngắn mạch ở cấp độ xử lý vật lý, không có gì đảm bảo rằng nó sẽ đánh giá các vị từ bộ lọc theo thứ tự viết từ trái sang phải. Một nỗ lực phổ biến để tránh những lỗi như vậy là sử dụng biểu thức bảng được đặt tên để xử lý phần logic lọc mà bạn muốn được đánh giá trước tiên và yêu cầu truy vấn bên ngoài xử lý logic lọc mà bạn muốn được đánh giá thứ hai. Đây là giải pháp đã thử sử dụng CTE:
WITH C AS ( SELECT * FROM Sales.OrderDetails WHERE discount > (SELECT MIN(discount) FROM Sales.OrderDetails) ) SELECT orderid, productid, discount FROM C WHERE 1.0 / discount > 10.0;
Tuy nhiên, thật không may, việc bỏ ghi chú của biểu thức bảng dẫn đến một logic tương đương với truy vấn giải pháp ban đầu và khi bạn cố gắng chạy mã này, bạn lại gặp lỗi chia cho 0:
Msg 8134, Level 16, State 1, Line 108 Divide by zero error encountered.
Sử dụng thủ thuật của chúng tôi với bộ lọc TOP trong truy vấn bên trong, bạn ngăn chặn việc bỏ ghi chú của biểu thức bảng, như vậy:
WITH C AS ( SELECT TOP (9223372036854775807) * FROM Sales.OrderDetails WHERE discount > (SELECT MIN(discount) FROM Sales.OrderDetails) ) SELECT orderid, productid, discount FROM C WHERE 1.0 / discount > 10.0;
Lần này mã chạy thành công mà không có bất kỳ lỗi nào.
Hãy tiếp tục với ví dụ mà bạn sử dụng kỹ thuật này để ngăn chặn việc bỏ qua vì lý do tối ưu hóa. Mã sau chỉ trả lại cho những người gửi hàng có ngày đặt hàng tối đa là vào hoặc sau ngày 1 tháng 1 năm 2018:
USE PerformanceV5; WITH C AS ( SELECT S.shipperid, (SELECT MAX(O.orderdate) FROM dbo.Orders AS O WHERE O.shipperid = S.shipperid) AS maxod FROM dbo.Shippers AS S ) SELECT shipperid, maxod FROM C WHERE maxod >= '20180101';
Nếu bạn đang thắc mắc tại sao không sử dụng một giải pháp đơn giản hơn nhiều với truy vấn được nhóm và bộ lọc HAVING, thì nó liên quan đến mật độ của cột shipperid. Bảng Đơn đặt hàng có 1.000.000 đơn đặt hàng và các lô hàng của những đơn đặt hàng đó được xử lý bởi năm chủ hàng, có nghĩa là trung bình, mỗi chủ hàng xử lý 20% đơn đặt hàng. Kế hoạch cho một truy vấn được nhóm tính toán ngày đặt hàng tối đa cho mỗi người gửi hàng sẽ quét tất cả 1.000.000 hàng, dẫn đến hàng nghìn lần đọc trang. Thật vậy, nếu bạn chỉ đánh dấu truy vấn bên trong của CTE (chúng tôi sẽ gọi nó là Truy vấn 3) tính toán ngày đặt hàng tối đa cho mỗi người gửi hàng và kiểm tra kế hoạch thực hiện của nó, bạn sẽ nhận được kế hoạch được hiển thị trong Hình 3.
Hình 3:Kế hoạch thực thi cho Truy vấn 3
Kế hoạch quét năm hàng trong chỉ mục nhóm trên Người gửi hàng. Đối với mỗi người gửi hàng, kế hoạch áp dụng tìm kiếm chống lại chỉ mục bao gồm trên Đơn đặt hàng, trong đó (shipperid, ngày đặt hàng) là các khóa dẫn đầu chỉ mục, đi thẳng đến hàng cuối cùng trong mỗi phần của người gửi hàng ở cấp độ lá để kéo ngày đặt hàng tối đa cho hiện tại người giao hàng. Vì chúng tôi chỉ có năm người gửi hàng, nên chỉ có năm hoạt động tìm kiếm chỉ mục, dẫn đến một kế hoạch rất hiệu quả. Dưới đây là các thước đo hiệu suất mà tôi nhận được khi thực hiện truy vấn bên trong của CTE:
duration: 0 ms, CPU: 0 ms, reads: 15
Tuy nhiên, khi bạn chạy giải pháp hoàn chỉnh (chúng tôi sẽ gọi nó là Truy vấn 4), bạn sẽ nhận được một kế hoạch hoàn toàn khác, như thể hiện trong Hình 4.
Hình 4:Kế hoạch thực thi cho Truy vấn 4
Điều đã xảy ra là SQL Server đã hủy thiết lập biểu thức bảng, chuyển đổi giải pháp thành tương đương logic của một truy vấn được nhóm lại, dẫn đến việc quét toàn bộ chỉ mục trên Đơn hàng. Dưới đây là các con số hiệu suất mà tôi nhận được cho giải pháp này:
duration: 316 ms, CPU: 281 ms, reads: 3854
Những gì chúng ta cần ở đây là ngăn việc bỏ ghi chú biểu thức bảng diễn ra, để truy vấn bên trong sẽ được tối ưu hóa với các tìm kiếm chống lại chỉ mục trên Đơn hàng và để truy vấn bên ngoài chỉ dẫn đến việc bổ sung toán tử Bộ lọc trong kế hoạch. Bạn đạt được điều này bằng cách sử dụng thủ thuật của chúng tôi bằng cách thêm bộ lọc TOP vào truy vấn bên trong, giống như vậy (chúng tôi sẽ gọi giải pháp này là Truy vấn 5):
WITH C AS ( SELECT TOP (9223372036854775807) S.shipperid, (SELECT MAX(O.orderdate) FROM dbo.Orders AS O WHERE O.shipperid = S.shipperid) AS maxod FROM dbo.Shippers AS S ) SELECT shipperid, maxod FROM C WHERE maxod >= '20180101';
Kế hoạch cho giải pháp này được thể hiện trong Hình 5.
Hình 5:Kế hoạch thực thi cho Truy vấn 5
Kế hoạch cho thấy rằng đã đạt được hiệu quả mong muốn và theo đó, các con số hiệu suất xác nhận điều này:
duration: 0 ms, CPU: 0 ms, reads: 15
Vì vậy, thử nghiệm của chúng tôi xác nhận rằng SQL Server xử lý việc thay thế / hủy ghi chú CTE giống như đối với các bảng dẫn xuất. Điều này có nghĩa là bạn không nên thích cái này hơn cái kia vì lý do tối ưu hóa, thay vào đó là do sự khác biệt về khái niệm quan trọng đối với bạn, như đã thảo luận trong Phần 5.
Tính bền bỉ
Một quan niệm sai lầm phổ biến liên quan đến CTE và các biểu thức bảng được đặt tên nói chung là chúng đóng vai trò như một phương tiện bền bỉ nào đó. Một số người nghĩ rằng SQL Server duy trì tập kết quả của truy vấn bên trong với một bảng làm việc và truy vấn bên ngoài thực sự tương tác với bảng công việc đó. Trong thực tế, các CTE không đệ quy thông thường và các bảng dẫn xuất không tồn tại lâu dài. Tôi đã mô tả logic không lưu ý mà SQL Server áp dụng khi tối ưu hóa một truy vấn liên quan đến các biểu thức bảng, dẫn đến một kế hoạch tương tác thẳng với các bảng cơ sở bên dưới. Lưu ý rằng trình tối ưu hóa có thể chọn sử dụng các bảng làm việc để duy trì các tập kết quả trung gian nếu việc này hợp lý vì lý do hiệu suất hoặc các lý do khác, chẳng hạn như bảo vệ Halloween. Khi nó làm như vậy, bạn sẽ thấy các toán tử Spool hoặc Index Spool trong kế hoạch. Tuy nhiên, những lựa chọn như vậy không liên quan đến việc sử dụng các biểu thức bảng trong truy vấn.
CTE đệ quy
Có một số trường hợp ngoại lệ trong đó SQL Server vẫn giữ nguyên dữ liệu của biểu thức bảng. Một là việc sử dụng các khung nhìn được lập chỉ mục. Nếu bạn tạo chỉ mục nhóm trên một chế độ xem, SQL Server sẽ duy trì tập hợp kết quả của truy vấn bên trong trong chỉ mục nhóm của chế độ xem và giữ nó đồng bộ với bất kỳ thay đổi nào trong các bảng cơ sở bên dưới. Ngoại lệ khác là khi bạn sử dụng truy vấn đệ quy. SQL Server cần duy trì các tập kết quả trung gian của các truy vấn neo và đệ quy trong một ống để nó có thể truy cập tập kết quả của vòng cuối cùng được biểu thị bằng tham chiếu đệ quy đến tên CTE mỗi khi thành viên đệ quy được thực thi.
Để chứng minh điều này, tôi sẽ sử dụng một trong các truy vấn đệ quy từ Phần 6 trong loạt bài này.
Sử dụng mã sau để tạo bảng Nhân viên trong cơ sở dữ liệu tempdb, điền nó với dữ liệu mẫu và tạo chỉ mục hỗ trợ:
SET NOCOUNT ON; USE tempdb; DROP TABLE IF EXISTS dbo.Employees; GO CREATE TABLE dbo.Employees ( empid INT NOT NULL CONSTRAINT PK_Employees PRIMARY KEY, mgrid INT NULL CONSTRAINT FK_Employees_Employees REFERENCES dbo.Employees, empname VARCHAR(25) NOT NULL, salary MONEY NOT NULL, CHECK (empid <> mgrid) ); INSERT INTO dbo.Employees(empid, mgrid, empname, salary) VALUES(1, NULL, 'David' , $10000.00), (2, 1, 'Eitan' , $7000.00), (3, 1, 'Ina' , $7500.00), (4, 2, 'Seraph' , $5000.00), (5, 2, 'Jiru' , $5500.00), (6, 2, 'Steve' , $4500.00), (7, 3, 'Aaron' , $5000.00), (8, 5, 'Lilach' , $3500.00), (9, 7, 'Rita' , $3000.00), (10, 5, 'Sean' , $3000.00), (11, 7, 'Gabriel', $3000.00), (12, 9, 'Emilia' , $2000.00), (13, 9, 'Michael', $2000.00), (14, 9, 'Didi' , $1500.00); CREATE UNIQUE INDEX idx_unc_mgrid_empid ON dbo.Employees(mgrid, empid) INCLUDE(empname, salary); GO
Tôi đã sử dụng CTE đệ quy sau để trả về tất cả cấp dưới của người quản lý gốc cây con đầu vào, sử dụng nhân viên 3 làm người quản lý đầu vào trong ví dụ này:
DECLARE @root AS INT = 3; WITH C AS ( SELECT empid, mgrid, empname FROM dbo.Employees WHERE empid = @root UNION ALL SELECT S.empid, S.mgrid, S.empname FROM C AS M INNER JOIN dbo.Employees AS S ON S.mgrid = M.empid ) SELECT empid, mgrid, empname FROM C;
Kế hoạch cho truy vấn này (chúng tôi sẽ gọi nó là Truy vấn 6) được thể hiện trong Hình 6.
Hình 6:Kế hoạch thực thi cho Truy vấn 6
Hãy quan sát rằng điều đầu tiên xảy ra trong kế hoạch, ở bên phải của nút CHỌN gốc, là việc tạo một bảng làm việc dựa trên cây B được đại diện bởi toán tử Bộ đệm chỉ mục. Phần trên cùng của kế hoạch xử lý logic của thành viên neo. Nó kéo hàng của nhân viên đầu vào từ chỉ mục được nhóm trên Nhân viên và ghi nó vào ống chỉ. Phần dưới cùng của kế hoạch thể hiện logic của thành viên đệ quy. Nó được thực thi lặp đi lặp lại cho đến khi trả về một tập kết quả trống. Đầu vào bên ngoài cho toán tử Vòng lặp lồng nhau nhận được các trình quản lý từ vòng trước từ bộ đệm (toán tử Bộ đệm bảng). Đầu vào bên trong sử dụng toán tử Tìm kiếm chỉ mục dựa trên chỉ mục không phân biệt được tạo trên Nhân viên (mgrid, empid) để có được cấp dưới trực tiếp của người quản lý từ vòng trước. Tập hợp kết quả của mỗi lần thực hiện phần dưới cùng của kế hoạch cũng được ghi vào ống chỉ mục. Lưu ý rằng tất cả có 7 hàng được ghi vào ống chỉ. Một được trả về bởi thành viên neo và 6 khác được trả về bởi tất cả các lần thực thi của thành viên đệ quy.
Ngoài ra, thật thú vị khi để ý cách kế hoạch xử lý giới hạn đệ quy tối đa mặc định, là 100. Hãy quan sát rằng toán tử Vô hướng tính toán dưới cùng tiếp tục tăng bộ đếm bên trong có tên là Expr1011 lên 1 với mỗi lần thực thi thành viên đệ quy. Sau đó, toán tử Assert đặt cờ thành 0 nếu bộ đếm này vượt quá 100. Nếu điều này xảy ra, SQL Server sẽ dừng việc thực thi truy vấn và tạo ra lỗi.
Khi nào thì không nên tiếp tục
Quay lại CTE không đệ quy, thường không tồn tại lâu dài, thay vào đó bạn phải tìm ra từ góc độ tối ưu hóa khi sử dụng chúng so với các công cụ duy trì thực tế như bảng tạm thời và biến bảng. Tôi sẽ xem qua một số ví dụ để chứng minh khi nào mỗi cách tiếp cận là tối ưu hơn.
Hãy bắt đầu với một ví dụ trong đó CTE hoạt động tốt hơn các bảng tạm thời. Điều đó thường xảy ra khi bạn không có nhiều đánh giá cho cùng một CTE, thay vào đó, có lẽ chỉ là một giải pháp mô-đun trong đó mỗi CTE chỉ được đánh giá một lần. Đoạn mã sau (chúng tôi sẽ gọi là Truy vấn 7) truy vấn bảng Đơn hàng trong cơ sở dữ liệu Hiệu suất, có 1.000.000 hàng, để trả về số năm đặt hàng trong đó hơn 70 khách hàng khác nhau đã đặt hàng:
USE PerformanceV5; WITH C1 AS ( SELECT YEAR(orderdate) AS orderyear, custid FROM dbo.Orders ), C2 AS ( SELECT orderyear, COUNT(DISTINCT custid) AS numcusts FROM C1 GROUP BY orderyear ) SELECT orderyear, numcusts FROM C2 WHERE numcusts > 70;
Truy vấn này tạo ra kết quả sau:
orderyear numcusts ----------- ----------- 2015 992 2017 20000 2018 20000 2019 20000 2016 20000
Tôi đã chạy mã này bằng SQL Server 2019 Developer Edition và nhận được kế hoạch được hiển thị trong Hình 7.
Hình 7:Kế hoạch thực thi cho Truy vấn 7
Lưu ý rằng việc hủy ghi chú CTE dẫn đến một kế hoạch lấy dữ liệu từ một chỉ mục trên bảng Đơn hàng và không liên quan đến bất kỳ tập hợp kết quả truy vấn bên trong của CTE. Tôi nhận được các số hiệu suất sau khi thực hiện truy vấn này trên máy của mình:
duration: 265 ms, CPU: 828 ms, reads: 3970, writes: 0
Bây giờ chúng ta hãy thử một giải pháp sử dụng các bảng tạm thời thay vì CTE (chúng tôi sẽ gọi nó là Giải pháp 8), như sau:
SELECT YEAR(orderdate) AS orderyear, custid INTO #T1 FROM dbo.Orders; SELECT orderyear, COUNT(DISTINCT custid) AS numcusts INTO #T2 FROM #T1 GROUP BY orderyear; SELECT orderyear, numcusts FROM #T2 WHERE numcusts > 70; DROP TABLE #T1, #T2;
Các kế hoạch cho giải pháp này được thể hiện trong Hình 8.
Hình 8:Các kế hoạch cho Giải pháp 8
Chú ý các toán tử Chèn bảng ghi các tập hợp kết quả vào các bảng tạm thời # T1 và # T2. Cái đầu tiên đặc biệt đắt vì nó ghi 1.000.000 hàng vào # T1. Đây là số hiệu suất mà tôi nhận được cho lần thực thi này:
duration: 454 ms, CPU: 1517 ms, reads: 14359, writes: 359
Như bạn có thể thấy, giải pháp với CTE tối ưu hơn nhiều.
Khi nào thì tiếp tục
Vì vậy, có phải trường hợp giải pháp mô-đun chỉ liên quan đến một đánh giá duy nhất của mỗi CTE luôn được ưu tiên sử dụng các bảng tạm thời không? Không cần thiết. Trong các giải pháp dựa trên CTE bao gồm nhiều bước và dẫn đến các kế hoạch phức tạp trong đó trình tối ưu hóa cần áp dụng nhiều ước tính cơ bản tại nhiều điểm khác nhau trong kế hoạch, bạn có thể nhận được sự thiếu chính xác tích lũy dẫn đến các lựa chọn không tối ưu. Một trong những kỹ thuật để cố gắng giải quyết các trường hợp như vậy là duy trì một số kết quả trung gian tự đặt bạn thành các bảng tạm thời và thậm chí tạo các chỉ mục trên chúng nếu cần, giúp trình tối ưu hóa khởi đầu mới với số liệu thống kê mới, tăng khả năng cho các ước tính số lượng chất lượng tốt hơn hy vọng sẽ dẫn đến nhiều sự lựa chọn tối ưu hơn. Liệu điều này có tốt hơn giải pháp không sử dụng bảng tạm thời hay không là điều bạn cần phải kiểm tra. Đôi khi, việc đánh đổi thêm chi phí cho các tập hợp kết quả trung gian liên tục vì lợi ích của việc ước tính số lượng bản chất chất lượng tốt hơn sẽ rất đáng giá.
Một trường hợp điển hình khác trong đó sử dụng bảng tạm thời là cách tiếp cận được ưa thích là khi giải pháp dựa trên CTE có nhiều đánh giá của cùng một CTE và truy vấn bên trong của CTE khá tốn kém. Hãy xem xét giải pháp dựa trên CTE sau (chúng tôi sẽ gọi nó là Truy vấn 9), giải pháp này phù hợp với mỗi năm và tháng đơn đặt hàng, một năm và tháng đặt hàng khác có số lượng đơn hàng gần nhất:
WITH OrdCount AS ( SELECT YEAR(orderdate) AS orderyear, MONTH(orderdate) AS ordermonth, COUNT(*) AS numorders FROM dbo.Orders GROUP BY YEAR(orderdate), MONTH(orderdate) ) SELECT O1.orderyear, O1.ordermonth, O1.numorders, O2.orderyear AS orderyear2, O2.ordermonth AS ordermonth2, O2.numorders AS numorders2 FROM OrdCount AS O1 CROSS APPLY ( SELECT TOP (1) O2.orderyear, O2.ordermonth, O2.numorders FROM OrdCount AS O2 WHERE O2.orderyear <> O1.orderyear OR O2.ordermonth <> O1.ordermonth ORDER BY ABS(O1.numorders - O2.numorders), O2.orderyear, O2.ordermonth ) AS O2;
Truy vấn này tạo ra kết quả sau:
orderyear ordermonth numorders orderyear2 ordermonth2 numorders2 ----------- ----------- ----------- ----------- ----------- ----------- 2016 1 21262 2017 3 21267 2019 1 21227 2016 5 21229 2019 2 19145 2018 2 19125 2018 4 20561 2016 9 20554 2018 5 21209 2019 5 21210 2018 6 20515 2016 11 20513 2018 7 21194 2018 10 21197 2017 9 20542 2017 11 20539 2017 10 21234 2019 3 21235 2017 11 20539 2019 4 20537 2017 12 21183 2016 8 21185 2018 1 21241 2019 7 21238 2016 2 19844 2019 12 20184 2018 3 21222 2016 10 21222 2016 4 20526 2019 9 20527 2019 4 20537 2017 11 20539 2017 5 21203 2017 8 21199 2019 6 20531 2019 9 20527 2017 7 21217 2016 7 21218 2018 8 21283 2017 3 21267 2018 10 21197 2017 8 21199 2016 11 20513 2018 6 20515 2019 11 20494 2017 4 20498 2018 2 19125 2019 2 19145 2016 3 21211 2016 12 21212 2019 3 21235 2017 10 21234 2016 5 21229 2019 1 21227 2019 5 21210 2016 3 21211 2017 6 20551 2016 9 20554 2017 8 21199 2018 10 21197 2018 9 20487 2019 11 20494 2016 10 21222 2018 3 21222 2018 11 20575 2016 6 20571 2016 12 21212 2016 3 21211 2019 12 20184 2018 9 20487 2017 1 21223 2016 10 21222 2017 2 19174 2019 2 19145 2017 3 21267 2016 1 21262 2017 4 20498 2019 11 20494 2016 6 20571 2018 11 20575 2016 7 21218 2017 7 21217 2019 7 21238 2018 1 21241 2016 8 21185 2017 12 21183 2019 8 21189 2016 8 21185 2016 9 20554 2017 6 20551 2019 9 20527 2016 4 20526 2019 10 21254 2016 1 21262 2015 12 1018 2018 2 19125 2018 12 21225 2017 1 21223 (49 rows affected)
Kế hoạch cho Truy vấn 9 được thể hiện trong Hình 9.
Hình 9:Kế hoạch thực thi cho Truy vấn 9
Phần trên cùng của kế hoạch tương ứng với phiên bản của OrdCount CTE có bí danh là O1. Tài liệu tham khảo này dẫn đến một đánh giá về Số thứ tự CTE. Phần này của kế hoạch kéo các hàng từ một chỉ mục trên bảng Đơn hàng, nhóm chúng theo năm và tháng và tổng hợp số lượng đơn đặt hàng cho mỗi nhóm, dẫn đến 49 hàng. Phần dưới cùng của kế hoạch tương ứng với bảng dẫn xuất tương quan O2, được áp dụng cho mỗi hàng từ O1, do đó được thực hiện 49 lần. Mỗi lần thực thi sẽ truy vấn CTE của OrdCount và do đó dẫn đến đánh giá riêng về truy vấn bên trong của CTE. Bạn có thể thấy rằng phần dưới cùng của kế hoạch quét tất cả các hàng từ chỉ mục trên Đơn hàng, nhóm và tổng hợp chúng. Về cơ bản, bạn nhận được tổng cộng 50 đánh giá về CTE, dẫn đến 50 lần quét 1.000.000 hàng từ Đơn hàng, nhóm và tổng hợp chúng. Nó có vẻ không phải là một giải pháp rất hiệu quả. Dưới đây là các thước đo hiệu suất mà tôi nhận được khi thực hiện giải pháp này trên máy của mình:
duration: 16 seconds, CPU: 56 seconds, reads: 130404, writes: 0
Cho rằng chỉ có vài chục tháng liên quan, điều sẽ hiệu quả hơn nhiều là sử dụng bảng tạm thời để lưu trữ kết quả của một hoạt động duy nhất nhóm và tổng hợp các hàng từ Đơn đặt hàng, sau đó có cả đầu vào bên ngoài và bên trong của toán tử ÁP DỤNG tương tác với bảng tạm thời. Đây là giải pháp (chúng tôi sẽ gọi là Giải pháp 10) bằng cách sử dụng bảng tạm thời thay vì CTE:
SELECT YEAR(orderdate) AS orderyear, MONTH(orderdate) AS ordermonth, COUNT(*) AS numorders INTO #OrdCount FROM dbo.Orders GROUP BY YEAR(orderdate), MONTH(orderdate); SELECT O1.orderyear, O1.ordermonth, O1.numorders, O2.orderyear AS orderyear2, O2.ordermonth AS ordermonth2, O2.numorders AS numorders2 FROM #OrdCount AS O1 CROSS APPLY ( SELECT TOP (1) O2.orderyear, O2.ordermonth, O2.numorders FROM #OrdCount AS O2 WHERE O2.orderyear <> O1.orderyear OR O2.ordermonth <> O1.ordermonth ORDER BY ABS(O1.numorders - O2.numorders), O2.orderyear, O2.ordermonth ) AS O2; DROP TABLE #OrdCount;
Ở đây, không có nhiều điểm trong việc lập chỉ mục bảng tạm thời, vì bộ lọc TOP dựa trên tính toán trong đặc tả thứ tự của nó và do đó, việc sắp xếp là không thể tránh khỏi. Tuy nhiên, rất có thể trong các trường hợp khác, với các giải pháp khác, bạn cũng nên xem xét lập chỉ mục các bảng tạm thời của mình. Ở bất kỳ mức độ nào, kế hoạch cho giải pháp này được thể hiện trong Hình 10.
Hình 10:Các kế hoạch thực thi cho Giải pháp 10
Hãy quan sát trong sơ đồ hàng đầu về cách thức hoạt động nặng nhọc liên quan đến việc quét 1.000.000 hàng, nhóm và tổng hợp chúng chỉ diễn ra một lần. 49 hàng được ghi vào bảng tạm thời #OrdCount, sau đó sơ đồ dưới cùng tương tác với bảng tạm thời cho cả đầu vào bên ngoài và bên trong của toán tử Vòng lặp lồng nhau, xử lý logic của toán tử APPLICY.
Dưới đây là số hiệu suất mà tôi nhận được để thực hiện giải pháp này:
duration: 0.392 seconds, CPU: 0.5 seconds, reads: 3636, writes: 3
Nó nhanh hơn theo thứ tự về mức độ so với giải pháp dựa trên CTE.
Tiếp theo là gì?
Trong bài viết này, tôi bắt đầu đề cập đến các cân nhắc tối ưu hóa liên quan đến CTE. Tôi đã chỉ ra rằng quá trình hủy đăng ký / thay thế diễn ra với các bảng dẫn xuất hoạt động theo cùng một cách với CTE. Tôi cũng đã thảo luận về thực tế là các CTE không đệ quy không được duy trì và giải thích rằng khi tính bền bỉ là yếu tố quan trọng đối với hiệu suất của giải pháp của bạn, bạn phải tự xử lý nó bằng cách sử dụng các công cụ như bảng tạm thời và biến bảng. Tháng tới, tôi sẽ tiếp tục thảo luận bằng cách đề cập đến các khía cạnh bổ sung của tối ưu hóa CTE.