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

Các nguyên tắc cơ bản về biểu thức bảng, Phần 8 - CTE, tiếp tục xem xét tối ưu hóa

Bài viết này là phần thứ tám trong loạt bài về biểu thức bảng. Cho đến nay, tôi đã cung cấp nền tảng cho biểu thức bảng, bao gồm cả khía cạnh logic và tối ưu hóa của bảng dẫn xuất, khía cạnh logic của CTE và một số khía cạnh tối ưu hóa của CTE. Tháng này, tôi tiếp tục đề cập đến các khía cạnh tối ưu hóa của CTE, đặc biệt giải quyết cách xử lý nhiều tham chiếu CTE.

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

Nhiều tài liệu tham khảo và không xác định

Tháng trước, tôi đã giải thích và chứng minh rằng CTE không được giải quyết, trong khi các bảng tạm thời và các biến bảng thực sự vẫn giữ nguyên dữ liệu. Tôi đã đưa ra các khuyến nghị về thời điểm hợp lý khi sử dụng CTE so với thời điểm hợp lý khi sử dụng các đối tượng tạm thời từ quan điểm hiệu suất truy vấn. Nhưng có một khía cạnh quan trọng khác của việc tối ưu hóa CTE hoặc xử lý vật lý, cần xem xét ngoài hiệu suất của giải pháp — cách xử lý nhiều tham chiếu đến CTE từ một truy vấn bên ngoài. Điều quan trọng là phải nhận ra rằng nếu bạn có một truy vấn bên ngoài có nhiều tham chiếu đến cùng một CTE, thì mỗi truy vấn sẽ được hủy thiết lập riêng biệt. Nếu bạn có các phép tính không xác định trong truy vấn bên trong của CTE, thì các phép tính đó có thể có các kết quả khác nhau trong các tham chiếu khác nhau.

Ví dụ:giả sử bạn gọi hàm SYSDATETIME trong truy vấn bên trong của CTE, tạo một cột kết quả có tên dt. Nói chung, giả sử không có thay đổi trong các đầu vào, một hàm tích hợp được đánh giá một lần cho mỗi truy vấn và tham chiếu, bất kể số hàng liên quan. Nếu bạn chỉ tham chiếu đến CTE một lần từ một truy vấn bên ngoài, nhưng tương tác với cột dt nhiều lần, tất cả các tham chiếu phải đại diện cho cùng một đánh giá chức năng và trả về các giá trị giống nhau. Tuy nhiên, nếu bạn tham chiếu đến CTE nhiều lần trong truy vấn bên ngoài, có thể là với nhiều truy vấn con tham chiếu đến CTE hoặc kết hợp giữa nhiều trường hợp của cùng một CTE (giả sử có bí danh là C1 và C2), các tham chiếu đến C1.dt và C2.dt đại diện cho các đánh giá khác nhau của biểu thức cơ bản và có thể dẫn đến các giá trị khác nhau.

Để chứng minh điều này, hãy xem xét ba lô sau:

 - Batch 1 DECLARE @i AS INT =1; WHILE @@ ROWCOUNT =1 SELECT @i + =1 WHERE SYSDATETIME () =SYSDATETIME (); PRINT @i; GO - Batch 2 DECLARE @i AS INT =1; WHILE @@ ROWCOUNT =1 VỚI C AS (CHỌN SYSDATETIME () NHƯ dt) CHỌN @i + =1 TỪ C WHERE dt =dt; PRINT @i; GO - Batch 3 DECLARE @i AS INT =1; WHILE @@ ROWCOUNT =1 VỚI C AS (CHỌN SYSDATETIME () NHƯ dt) SELECT @i + =1 WHERE (CHỌN dt TỪ C) =(CHỌN dt TỪ C); IN @i; ĐI 

Dựa trên những gì tôi vừa giải thích, bạn có thể xác định lô nào có vòng lặp vô hạn và lô nào sẽ dừng lại tại một số điểm do hai so sánh của vị từ đánh giá thành các giá trị khác nhau không?

Hãy nhớ rằng tôi đã nói rằng một lệnh gọi đến một hàm không xác định được tích hợp sẵn như SYSDATETIME được đánh giá một lần cho mỗi truy vấn và tham chiếu. Điều này có nghĩa là trong Batch 1, bạn có hai đánh giá khác nhau và sau khi lặp lại đủ vòng lặp, chúng sẽ dẫn đến các giá trị khác nhau. Thử nó. Có bao nhiêu lần lặp lại mã đã báo cáo?

Đối với Batch 2, mã có hai tham chiếu đến cột dt từ cùng một phiên bản CTE, có nghĩa là cả hai đều đại diện cho cùng một đánh giá chức năng và phải đại diện cho cùng một giá trị. Do đó, Batch 2 có một vòng lặp vô hạn. Chạy nó trong bất kỳ khoảng thời gian nào bạn muốn, nhưng cuối cùng bạn sẽ cần phải dừng việc thực thi mã.

Đối với Batch 3, truy vấn bên ngoài có hai truy vấn con khác nhau tương tác với CTE C, mỗi truy vấn đại diện cho một phiên bản khác nhau trải qua một quá trình unnesting riêng biệt. Mã không chỉ định rõ ràng các bí danh khác nhau cho các trường hợp khác nhau của CTE vì hai truy vấn con xuất hiện trong các phạm vi độc lập, nhưng để dễ hiểu hơn, bạn có thể nghĩ cả hai như sử dụng các bí danh khác nhau chẳng hạn như C1 trong một truy vấn con và C2 trong khác. Vì vậy, cứ như thể một truy vấn con tương tác với C1.dt và truy vấn kia với C2.dt. Các tham chiếu khác nhau đại diện cho các đánh giá khác nhau về biểu thức cơ bản và do đó có thể dẫn đến các giá trị khác nhau. Hãy thử chạy mã và thấy rằng nó dừng lại ở một số điểm. Phải mất bao nhiêu lần lặp lại cho đến khi dừng lại?

Thật thú vị khi thử và xác định các trường hợp bạn có một đánh giá duy nhất so với nhiều đánh giá của biểu thức cơ bản trong kế hoạch thực thi truy vấn. Hình 1 có các kế hoạch thực thi đồ họa cho ba lô (nhấp để phóng to).

Hình 1:Kế hoạch thực thi đồ họa cho Lô 1, Lô 2 và Lô 3

Thật không may, không có niềm vui nào từ các kế hoạch thực hiện đồ họa; chúng đều có vẻ giống hệt nhau mặc dù về mặt ngữ nghĩa, ba lô không có nghĩa giống nhau. Cảm ơn @CodeRecce và Forrest (@tsqladdict), với tư cách là một cộng đồng, chúng tôi đã cố gắng đi đến tận cùng vấn đề này bằng các phương tiện khác.

Như @CodeRecce đã phát hiện ra, các kế hoạch XML nắm giữ câu trả lời. Dưới đây là các phần liên quan của XML cho ba lô:

−− Đợt 1



−− Đợt 2


<Định danh>

<Định danh>


−− Đợt 3


<Định danh>

<Định danh>

Bạn có thể thấy rõ ràng trong kế hoạch XML cho Batch 1 rằng vị từ bộ lọc so sánh kết quả của hai lệnh gọi trực tiếp riêng biệt của hàm SYSDATETIME nội tại.

Trong kế hoạch XML cho Batch 2, vị từ bộ lọc so sánh biểu thức hằng ConstExpr1002 đại diện cho một lệnh gọi hàm SYSDATETIME với chính nó.

Trong kế hoạch XML cho Batch 3, vị từ bộ lọc so sánh hai biểu thức hằng số khác nhau được gọi là ConstExpr1005 và ConstExpr1006, mỗi biểu thức đại diện cho một lệnh gọi riêng của hàm SYSDATETIME.

Như một tùy chọn khác, Forrest (@tsqladdict) đề xuất sử dụng cờ theo dõi 8605, hiển thị biểu diễn cây truy vấn ban đầu được tạo bởi SQL Server, sau khi bật cờ theo dõi 3604 khiến đầu ra của TF 8605 được hướng đến máy khách SSMS. Sử dụng mã sau để bật cả hai cờ theo dõi:

 DBCC TRACEON (3604); - đầu ra trực tiếp tới máy kháchGO DBCC TRACEON (8605); - hiển thị cây truy vấn ban đầu 

Tiếp theo, bạn chạy mã mà bạn muốn lấy cây truy vấn. Dưới đây là các phần liên quan của đầu ra mà tôi nhận được từ TF 8605 cho ba lô:

−− Đợt 1

Cây chuyển đổi
***:***

LogOp_Project COL:Expr1000

LogOp_Select

LogOp_ConstTableGet (1) [trống]

ScaOp_Comp x_cmpEq

ScaOp_Intricular sysdatetime

ScaOp_Intricular sysdatetime

AncOp_PrjList

AncOp_PrjEl COL:Expr1000

ScaOp_Arithmetic x_aopAdd

ScaOp_Identifier COL:@i

ScaOp_Const TI (int, ML =4) XVAR (int, Not Owned, Value =1)

−− Đợt 2

Cây chuyển đổi
***:***

LogOp_Project COL:Expr1001

LogOp_Select

LogOp_ViewAnchor

LogOp_Project

LogOp_ConstTableGet (1) [trống]

AncOp_PrjList

AncOp_PrjEl COL:Expr1000

ScaOp_Intricular sysdatetime

ScaOp_Comp x_cmpEq

ScaOp_Identifier COL:Expr1000

ScaOp_Identifier COL:Expr1000

AncOp_PrjList

AncOp_PrjEl COL:Expr1001

ScaOp_Arithmetic x_aopAdd

ScaOp_Identifier COL:@i

ScaOp_Const TI (int, ML =4) XVAR (int, Not Owned, Value =1)

−− Đợt 3

Cây chuyển đổi
***:***

LogOp_Project COL:Expr1004

LogOp_Select

LogOp_ConstTableGet (1) [trống]

ScaOp_Comp x_cmpEq

ScaOp_Subquery COL:Expr1001

LogOp_Project

LogOp_ViewAnchor

LogOp_Project

LogOp_ConstTableGet (1) [trống]

AncOp_PrjList

AncOp_PrjEl COL:Expr1000

ScaOp_Intricular sysdatetime

AncOp_PrjList

AncOp_PrjEl COL:Expr1001

ScaOp_Identifier COL:Expr1000

ScaOp_Subquery COL:Expr1003

LogOp_Project

LogOp_ViewAnchor

LogOp_Project

LogOp_ConstTableGet (1) [trống]

AncOp_PrjList

AncOp_PrjEl COL:Expr1002

ScaOp_Intricular sysdatetime

AncOp_PrjList

AncOp_PrjEl COL:Expr1003

ScaOp_Identifier COL:Expr1002

AncOp_PrjList

AncOp_PrjEl COL:Expr1004

ScaOp_Arithmetic x_aopAdd

ScaOp_Identifier COL:@i

ScaOp_Const TI (int, ML =4) XVAR (int, Not Owned, Value =1)

Trong Batch 1, bạn có thể xem so sánh giữa kết quả của hai đánh giá riêng biệt của hàm nội tại SYSDATETIME.

Trong Batch 2, bạn sẽ thấy một đánh giá của hàm dẫn đến cột có tên Expr1000, sau đó là so sánh giữa cột này với chính nó.

Trong Batch 3, bạn thấy hai đánh giá riêng biệt của hàm. Một trong cột được gọi là Expr1000 (sau đó được chiếu bởi cột truy vấn con có tên là Expr1001). Một cột khác trong cột được gọi là Expr1002 (sau đó được chiếu bởi cột truy vấn con được gọi là Expr1003). Sau đó, bạn có so sánh giữa Expr1001 và Expr1003.

Vì vậy, với việc đào sâu hơn một chút về những gì kế hoạch thực thi đồ họa cho thấy, bạn thực sự có thể tìm ra khi nào một biểu thức cơ bản được đánh giá chỉ một lần so với nhiều lần. Giờ bạn đã hiểu các trường hợp khác nhau, bạn có thể phát triển các giải pháp của mình dựa trên hành vi mong muốn mà bạn đang theo đuổi.

Các chức năng của cửa sổ với thứ tự không xác định

Có một lớp tính toán khác có thể khiến bạn gặp rắc rối khi được sử dụng trong các giải pháp có nhiều tham chiếu đến cùng một CTE. Đó là các hàm cửa sổ dựa trên thứ tự không xác định. Lấy chức năng cửa sổ ROW_NUMBER làm ví dụ. Khi được sử dụng với đặt hàng từng phần (sắp xếp thứ tự theo các phần tử không xác định duy nhất hàng), mỗi đánh giá của truy vấn cơ bản có thể dẫn đến việc gán số hàng khác nhau ngay cả khi dữ liệu cơ bản không thay đổi. Với nhiều tham chiếu CTE, hãy nhớ rằng mỗi tham chiếu được hủy thiết lập riêng biệt và bạn có thể nhận được các tập kết quả khác nhau. Tùy thuộc vào những gì truy vấn bên ngoài thực hiện với mỗi tham chiếu, ví dụ:những cột nào từ mỗi tham chiếu mà nó tương tác và cách thức, trình tối ưu hóa có thể quyết định truy cập dữ liệu cho từng trường hợp bằng cách sử dụng các chỉ mục khác nhau với các yêu cầu sắp xếp khác nhau.

Hãy coi đoạn mã sau làm ví dụ:

 SỬ DỤNG TSQLV5; VỚI C AS (SELECT *, ROW_NUMBER () OVER (ORDER BY orderdate) AS rownum FROM Sales.Orders) SELECT C1.orderid, C1.shipcountry, C2.orderidFROM C AS C1 INNER JOIN C AS C2 ON C1.rownum =C2. rownumWHERE C1.orderid <> C2.orderid; 

Truy vấn này có bao giờ trả về tập kết quả trống không? Có lẽ phản ứng ban đầu của bạn là không thể. Nhưng hãy nghĩ về những gì tôi vừa giải thích kỹ hơn một chút và bạn sẽ nhận ra rằng, ít nhất là về mặt lý thuyết, do hai quá trình gỡ bỏ CTE riêng biệt sẽ diễn ra ở đây — một trong số C1 và một quy trình khác của C2 — có thể xảy ra. Tuy nhiên, lý thuyết là một điều gì đó có thể xảy ra và một điều khác là chứng minh điều đó. Ví dụ:khi tôi chạy mã này mà không tạo bất kỳ chỉ mục mới nào, tôi tiếp tục nhận được tập kết quả trống:

 orderid shipcountry orderid --------------- ----------- (0 hàng bị ảnh hưởng)  

Tôi nhận được kế hoạch được hiển thị trong Hình 23 cho truy vấn này.

Hình 2:Kế hoạch đầu tiên cho truy vấn với hai tham chiếu CTE

Điều thú vị cần lưu ý ở đây là trình tối ưu hóa đã chọn sử dụng các chỉ mục khác nhau để xử lý các tham chiếu CTE khác nhau vì đó là những gì nó cho là tối ưu. Rốt cuộc, mỗi tham chiếu trong truy vấn bên ngoài liên quan đến một tập hợp con khác nhau của các cột CTE. Một tham chiếu dẫn đến quá trình quét chuyển tiếp có thứ tự của chỉ mục idx_nc_orderedate và tham chiếu còn lại trong quá trình quét không có thứ tự của chỉ mục được nhóm theo sau bởi thao tác sắp xếp theo thứ tự tăng dần. Mặc dù chỉ mục idx_nc_orderedate chỉ được xác định rõ ràng chỉ trên cột orderdate làm khóa, nhưng trên thực tế, nó được định nghĩa trên (orderdate, orderid) làm khóa của nó vì orderid là khóa chỉ mục được phân nhóm và được bao gồm làm khóa cuối cùng trong tất cả các chỉ mục không được phân loại. Vì vậy, quá trình quét theo thứ tự của chỉ mục thực sự phát ra các hàng được sắp xếp theo orderdate, orderid. Đối với quá trình quét không theo thứ tự của chỉ mục được phân nhóm, ở cấp công cụ lưu trữ, dữ liệu được quét theo thứ tự khóa chỉ mục (dựa trên orderid) để giải quyết các kỳ vọng về tính nhất quán tối thiểu của mức cách ly mặc định được đọc cam kết. Do đó, toán tử Sắp xếp nhập dữ liệu được sắp xếp theo thứ tự, sắp xếp các hàng theo ngày thứ tự và trong thực tế, kết thúc bằng cách đưa ra các hàng được sắp xếp theo thứ tự, orderid.

Một lần nữa, về lý thuyết, không có gì đảm bảo rằng hai tham chiếu sẽ luôn đại diện cho cùng một tập hợp kết quả ngay cả khi dữ liệu cơ bản không thay đổi. Một cách đơn giản để chứng minh điều này là sắp xếp hai chỉ mục tối ưu khác nhau cho hai tham chiếu, nhưng có một thứ tự dữ liệu theo orderdate ASC, orderid ASC và thứ tự dữ liệu khác theo orderdate DESC, orderid ASC (hoặc hoàn toàn ngược lại). Chúng tôi đã có chỉ mục cũ tại chỗ. Đây là mã để tạo cái sau:

 TẠO CHỈ SỐ idx_nc_odD_oid_I_sc TRÊN Bán hàng. Đơn đặt hàng (ngày đặt hàng DESC, orderid) BAO GỒM (shipcountry); 

Chạy mã lần thứ hai sau khi tạo chỉ mục:

 WITH C AS (SELECT *, ROW_NUMBER () OVER (ORDER BY orderdate) AS rownum FROM Sales.Orders) SELECT C1.orderid, C1.shipcountry, C2.orderidFROM C AS C1 INNER JOIN C AS C2 ON C1.rownum =C2.rownumWHERE C1.orderid <> C2.orderid; 

Tôi nhận được kết quả sau khi chạy mã này sau khi tạo chỉ mục mới:

 orderid shipcountry orderid --------------------------------- 10251 Pháp 1025010250 Braxin 1025110261 Braxin 1026010260 Đức 1026110271 Mỹ 10270 ... 11070 Đức 1107311077 Mỹ 1107411076 Pháp 1107511075 Thụy Sĩ 1107611074 Đan Mạch 11077 (546 hàng bị ảnh hưởng) 

Rất tiếc.

Kiểm tra kế hoạch truy vấn cho việc thực thi này như thể hiện trong Hình 3:

Hình 3:Kế hoạch thứ hai cho truy vấn với hai tham chiếu CTE

Lưu ý rằng nhánh trên cùng của kế hoạch quét chỉ mục idx_nc_orderdate theo kiểu chuyển tiếp có thứ tự, khiến toán tử Dự án trình tự tính toán các số hàng để nhập dữ liệu trong thực tế được sắp xếp theo thứ tự ASC, orderid ASC. Nhánh dưới cùng của kế hoạch quét chỉ mục mới idx_nc_odD_oid_I_sc theo kiểu lùi có thứ tự, khiến nhà điều hành Dự án trình tự nhập dữ liệu trong thực tế được sắp xếp theo thứ tự ASC, orderid DESC. Điều này dẫn đến sự sắp xếp số hàng khác nhau cho hai tham chiếu CTE bất cứ khi nào có nhiều hơn một lần xuất hiện của cùng một giá trị ngày đặt hàng. Do đó, truy vấn tạo ra một tập kết quả khác.

Nếu bạn muốn tránh những lỗi như vậy, một tùy chọn rõ ràng là duy trì kết quả truy vấn bên trong trong một đối tượng tạm thời như bảng tạm thời hoặc biến bảng. Tuy nhiên, nếu bạn gặp phải trường hợp muốn sử dụng CTE, một giải pháp đơn giản là sử dụng tổng thứ tự trong hàm cửa sổ bằng cách thêm dấu ngắt. Nói cách khác, hãy đảm bảo rằng bạn sắp xếp theo tổ hợp các biểu thức xác định duy nhất một hàng. Trong trường hợp của chúng tôi, bạn có thể chỉ cần thêm orderid một cách rõ ràng làm dấu buộc, như sau:

 VỚI C AS (SELECT *, ROW_NUMBER () OVER (ORDER BY orderdate, orderid) AS rownum FROM Sales.Orders) CHỌN C1.orderid, C1.shipcountry, C2.orderidFROM C AS C1 INNER JOIN C AS C2 ON C1 .rownum =C2.rownumWHERE C1.orderid <> C2.orderid; 

Bạn nhận được một tập hợp kết quả trống như mong đợi:

 orderid shipcountry orderid --------------- ----------- (0 hàng bị ảnh hưởng)  

Không cần thêm bất kỳ chỉ mục nào nữa, bạn sẽ có được kế hoạch được hiển thị trong Hình 4:

Hình 4:Kế hoạch thứ ba cho truy vấn với hai tham chiếu CTE

Nhánh trên cùng của kế hoạch giống như đối với kế hoạch trước đó được thể hiện trong Hình 3. Nhánh dưới cùng có một chút khác biệt. Chỉ mục mới được tạo trước đó không thực sự lý tưởng cho truy vấn mới theo nghĩa là nó không có dữ liệu được sắp xếp theo thứ tự như nhu cầu của hàm ROW_NUMBER (orderdate, orderid). Nó vẫn là chỉ mục bao trùm hẹp nhất mà trình tối ưu hóa có thể tìm thấy cho tham chiếu CTE tương ứng của nó, vì vậy nó đã được chọn; tuy nhiên, nó được quét theo kiểu Có thứ tự:Sai. Sau đó, toán tử Sắp xếp rõ ràng sẽ sắp xếp dữ liệu theo ngày thứ tự, thứ tự như nhu cầu tính toán ROW_NUMBER. Tất nhiên, bạn có thể thay đổi định nghĩa chỉ mục để cả orderdate và orderid sử dụng cùng một hướng và theo cách này, việc sắp xếp rõ ràng sẽ bị loại bỏ khỏi kế hoạch. Tuy nhiên, điểm chính là bằng cách sử dụng đặt hàng toàn bộ, bạn tránh gặp rắc rối do lỗi cụ thể này.

Khi bạn hoàn tất, hãy chạy mã sau để dọn dẹp:

 DROP INDEX NẾU CÓ idx_nc_odD_oid_I_sc BẬT Sales.Orders; 

Kết luận

Điều quan trọng là phải hiểu rằng nhiều tham chiếu đến cùng một CTE từ một truy vấn bên ngoài dẫn đến các đánh giá riêng biệt về truy vấn bên trong của CTE. Đặc biệt cẩn thận với các tính toán không xác định, vì các đánh giá khác nhau có thể dẫn đến các giá trị khác nhau.

Khi sử dụng các hàm cửa sổ như ROW_NUMBER và tổng hợp với một khung, hãy đảm bảo sử dụng tổng thứ tự để tránh nhận được các kết quả khác nhau cho cùng một hàng trong các tham chiếu CTE khác nhau.


  1. Database
  2.   
  3. Mysql
  4.   
  5. Oracle
  6.   
  7. Sqlserver
  8.   
  9. PostgreSQL
  10.   
  11. Access
  12.   
  13. SQLite
  14.   
  15. MariaDB
  1. Sử dụng Salesforce SOQL từ Linux

  2. SQL SELECT DISTINCT:Các phương pháp hay nhất về hiệu suất

  3. Điều chỉnh Hiệu suất Toàn bộ Kế hoạch Truy vấn

  4. Phần 1 - Cách cài đặt cơ sở dữ liệu của SuiteCRM và Reverse Engineer

  5. Đang theo dõi CLR_MANUAL_EVENT ở mức cao