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

Vấn đề với các chức năng và chế độ xem của cửa sổ

Giới thiệu

Kể từ khi được giới thiệu trong SQL Server 2005, các chức năng cửa sổ như ROW_NUMBERRANK đã được chứng minh là cực kỳ hữu ích trong việc giải quyết nhiều vấn đề T-SQL phổ biến. Trong nỗ lực tổng quát hóa các giải pháp như vậy, các nhà thiết kế cơ sở dữ liệu thường tìm cách kết hợp chúng vào các khung nhìn để thúc đẩy quá trình đóng gói và sử dụng lại mã. Thật không may, một hạn chế trong trình tối ưu hóa truy vấn SQL Server thường có nghĩa là các dạng xem chứa các chức năng cửa sổ không hoạt động tốt như mong đợi. Bài đăng này hoạt động thông qua một ví dụ minh họa về vấn đề, nêu chi tiết lý do và cung cấp một số cách giải quyết.

Sự cố này cũng có thể xảy ra trong các bảng dẫn xuất, biểu thức bảng thông thường và các hàm nội dòng, nhưng tôi thường gặp nhất với các chế độ xem vì chúng được cố ý viết để chung chung hơn.

Các chức năng của cửa sổ

Các chức năng của cửa sổ được phân biệt bằng sự hiện diện của OVER() và có ba loại:

  • Các chức năng của cửa sổ xếp hạng
    • ROW_NUMBER
    • RANK
    • DENSE_RANK
    • NTILE
  • Các chức năng của cửa sổ tổng hợp
    • MIN , MAX , AVG , SUM
    • COUNT , COUNT_BIG
    • CHECKSUM_AGG
    • STDEV , STDEVP , VAR , VARP
  • Các chức năng của cửa sổ phân tích
    • LAG , LEAD
    • FIRST_VALUE , LAST_VALUE
    • PERCENT_RANK , PERCENTILE_CONT , PERCENTILE_DISC , CUME_DIST

Các chức năng cửa sổ xếp hạng và tổng hợp đã được giới thiệu trong SQL Server 2005 và được mở rộng đáng kể trong SQL Server 2012. Các chức năng cửa sổ phân tích là mới cho SQL Server 2012.

Tất cả các chức năng cửa sổ được liệt kê ở trên đều dễ bị giới hạn bởi trình tối ưu hóa được trình bày chi tiết trong bài viết này.

Ví dụ

Sử dụng cơ sở dữ liệu mẫu AdventureWorks, nhiệm vụ trước mắt là viết một truy vấn trả về tất cả các giao dịch sản phẩm số 878 đã xảy ra vào ngày gần đây nhất có sẵn. Có tất cả các cách để thể hiện yêu cầu này trong T-SQL, nhưng chúng tôi sẽ chọn viết một truy vấn sử dụng hàm cửa sổ. Bước đầu tiên là tìm các bản ghi giao dịch cho sản phẩm số 878 và xếp hạng chúng theo thứ tự ngày giảm dần:

 SELECT th.TransactionID, th.ReferenceOrderID, th.TransactionDate, th.Quantity, rnk =RANK () OVER (ORDER BY th.TransactionDate DESC) TỪ Production.TransactionHistory AS thWHERE th.ProductID =878ORDER BY rnk;  

Kết quả của truy vấn đúng như mong đợi, với sáu giao dịch xảy ra vào ngày gần đây nhất có sẵn. Kế hoạch thực thi chứa một tam giác cảnh báo, cảnh báo chúng tôi về chỉ mục bị thiếu:

Như thường lệ đối với các đề xuất chỉ mục bị thiếu, chúng ta cần nhớ rằng đề xuất không phải là kết quả của việc phân tích thông qua truy vấn - đó là dấu hiệu cho thấy chúng ta cần suy nghĩ một chút về cách truy vấn này truy cập vào dữ liệu mà nó cần.

Chỉ mục được đề xuất chắc chắn sẽ hiệu quả hơn so với việc quét toàn bộ bảng, vì nó sẽ cho phép một chỉ mục tìm kiếm sản phẩm cụ thể mà chúng ta quan tâm. Chỉ mục cũng sẽ bao gồm tất cả các cột cần thiết, nhưng nó sẽ không tránh được việc sắp xếp (bởi TransactionDate giảm dần). Chỉ mục lý tưởng cho truy vấn này sẽ cho phép tìm kiếm trên ProductID , trả lại các bản ghi đã chọn trong TransactionDate ngược lại đặt hàng và che các cột trả lại khác:

 TẠO CHỈ SỐ KHÔNG CHỈNH SỬA ixON Production.TransactionHistory (ProductID, TransactionDate DESC) BAO GỒM (ReferenceOrderID, Quantity); 

Với chỉ mục đó, kế hoạch thực thi sẽ hiệu quả hơn nhiều. Quá trình quét chỉ mục theo nhóm đã được thay thế bằng tìm kiếm phạm vi và sắp xếp rõ ràng không còn cần thiết nữa:

Bước cuối cùng cho truy vấn này là giới hạn kết quả chỉ ở những hàng xếp hạng # 1. Chúng tôi không thể lọc trực tiếp trong WHERE mệnh đề của truy vấn của chúng tôi vì các hàm cửa sổ chỉ có thể xuất hiện trong SELECTORDER BY mệnh đề.

Chúng ta có thể giải quyết hạn chế này bằng cách sử dụng bảng dẫn xuất, biểu thức bảng chung, hàm hoặc dạng xem. Nhân dịp này, chúng tôi sẽ sử dụng một biểu thức bảng chung (hay còn gọi là chế độ xem trong dòng):

 VỚI các giao dịch được xếp hạng AS (SELECT th.TransactionID, th.ReferenceOrderID, th.TransactionDate, th.Quantity, rnk =RANK () OVER (ORDER BY th.TransactionDate DESC) TỪ Production.TransactionHistory AS th WHERE th.ProductID =878 ) CHỌN TransactionID, ReferenceOrderID, TransactionDate, QuantityFROM Xếp hạng Giao dịchWHERE rnk =1; 

Kế hoạch thực thi giống như trước đây, với một Bộ lọc bổ sung để chỉ trả về các hàng được xếp hạng # 1:

Truy vấn trả về sáu hàng được xếp hạng bằng nhau mà chúng tôi mong đợi:

Tổng quát hóa truy vấn

Nó chỉ ra rằng truy vấn của chúng tôi rất hữu ích, vì vậy quyết định được thực hiện để tổng quát hóa nó và lưu trữ định nghĩa trong một chế độ xem. Để điều này hoạt động đối với bất kỳ sản phẩm nào, chúng ta cần thực hiện hai việc:trả lại ProductID từ chế độ xem và phân vùng chức năng xếp hạng theo sản phẩm:

 TẠO CHẾ ĐỘ XEM dbo.MostRecentTransactionsPerProductWITH SCHEMABINDINGASSELECT sq1.ProductID, sq1.TransactionID, sq1.ReferenceOrderID, sq1.TransactionDate, sq1.QuantityFROM (SELECT th.ProductID, th.TransactionID, th. rnk =RANK () HẾT (PHẦN THEO LỆNH CỦA THỨ.ProductID THEO THỨ.TransactionDate DESC) TỪ Production.TransactionHistory AS th) AS sq1WHERE sq1.rnk =1; 

Việc chọn tất cả các hàng từ chế độ xem dẫn đến kế hoạch thực thi sau và kết quả chính xác:

Giờ đây, chúng tôi có thể tìm thấy các giao dịch gần đây nhất cho sản phẩm 878 với một truy vấn đơn giản hơn nhiều trên chế độ xem:

 SELECT mrt.ProductID, mrt.TransactionID, mrt.ReferenceOrderID, mrt.TransactionDate, mrt.QuantityFROM dbo.MostRecentTransactionsPerProduct AS mrt WHERE mrt.ProductID =878; 

Kỳ vọng của chúng tôi là kế hoạch thực thi cho truy vấn mới này sẽ giống hệt như trước khi chúng tôi tạo chế độ xem. Trình tối ưu hóa truy vấn sẽ có thể đẩy bộ lọc được chỉ định trong WHERE xuống chế độ xem, dẫn đến tìm kiếm chỉ mục.

Tuy nhiên, chúng ta cần dừng lại và suy nghĩ một chút vào thời điểm này. Trình tối ưu hóa truy vấn chỉ có thể tạo ra các kế hoạch thực thi được đảm bảo tạo ra kết quả giống như đặc tả truy vấn logic - có an toàn không khi đẩy WHERE của chúng tôi mệnh đề vào chế độ xem? PARTITION BY mệnh đề của chức năng cửa sổ trong khung nhìn. Lý do là việc loại bỏ các nhóm hoàn chỉnh (phân vùng) khỏi hàm cửa sổ sẽ không ảnh hưởng đến xếp hạng của các hàng được trả về bởi truy vấn. Câu hỏi đặt ra là trình tối ưu hóa truy vấn SQL Server có biết điều này không? Câu trả lời phụ thuộc vào phiên bản SQL Server chúng tôi đang chạy.

Kế hoạch thực thi SQL Server 2005

Xem xét các thuộc tính Bộ lọc trong kế hoạch này cho thấy nó áp dụng hai vị từ:

ProductID = 878 vị từ chưa được đẩy xuống chế độ xem, dẫn đến kế hoạch quét chỉ mục của chúng tôi, xếp hạng mọi hàng trong bảng trước khi lọc cho sản phẩm # 878 và các hàng được xếp hạng # 1.

Trình tối ưu hóa truy vấn SQL Server 2005 không thể đẩy các vị từ phù hợp qua một hàm cửa sổ trong phạm vi truy vấn thấp hơn (dạng xem, biểu thức bảng chung, hàm trong dòng hoặc bảng dẫn xuất). Giới hạn này áp dụng cho tất cả các bản dựng SQL Server 2005.

Kế hoạch thực thi SQL Server 2008+

Đây là kế hoạch thực thi cho cùng một truy vấn trên SQL Server 2008 trở lên:

ProductID vị từ đã được đẩy thành công qua các toán tử xếp hạng, thay thế quá trình quét chỉ mục bằng tìm kiếm chỉ mục hiệu quả.

Trình tối ưu hóa truy vấn năm 2008 bao gồm quy tắc đơn giản hóa mới SelOnSeqPrj (chọn trên dự án trình tự) có thể đẩy các vị từ phạm vi bên ngoài an toàn vào các chức năng cửa sổ trong quá khứ. Để tạo ra kế hoạch kém hiệu quả hơn cho truy vấn này trong SQL Server 2008 trở lên, chúng tôi phải tạm thời vô hiệu hóa tính năng tối ưu hóa truy vấn này:

 SELECT mrt.ProductID, mrt.TransactionID, mrt.ReferenceOrderID, mrt.TransactionDate, mrt.QuantityFROM dbo.MostRecentTransactionsPerProduct AS mrt WHERE mrt.ProductID =878OPTION (QUERYRULEOFF SelOnSepreqPrj); 
  

Thật không may, SelOnSeqPrj quy tắc đơn giản hóa chỉ hoạt động khi vị từ thực hiện so sánh với một hằng số . Vì lý do đó, truy vấn sau tạo ra phương án tối ưu phụ trên SQL Server 2008 trở lên:

 DECLARE @ProductID INT =878; CHỌN mrt.ProductID, mrt.TransactionID, mrt.ReferenceOrderID, mrt.TransactionDate, mrt.QuantityFROM dbo.MostRecentTransactionsPerProduct AS mrt WHERE mrt.ProductID =@ProductID; 

Sự cố vẫn có thể xảy ra ngay cả khi vị từ sử dụng một giá trị không đổi. SQL Server có thể quyết định tự động tham số hóa các truy vấn tầm thường (một truy vấn mà kế hoạch tốt nhất hiển nhiên tồn tại). Nếu tham số tự động thành công, trình tối ưu hóa sẽ thấy một tham số thay vì một hằng số và SelOnSeqPrj quy tắc không được áp dụng.

Đối với các truy vấn không cố gắng tham số hóa tự động (hoặc ở nơi nó được xác định là không an toàn), việc tối ưu hóa vẫn có thể không thành công, nếu tùy chọn cơ sở dữ liệu cho FORCED PARAMETERIZATION đang bật. Truy vấn thử nghiệm của chúng tôi (với giá trị không đổi 878) không an toàn cho tham số hóa tự động, nhưng cài đặt tham số bắt buộc sẽ ghi đè điều này, dẫn đến kế hoạch không hiệu quả:

 ALTER DATABASE AdventureWorksSET PARAMETERIZATION FORCED; GOSELECT mrt.ProductID, mrt.TransactionID, mrt.ReferenceOrderID, mrt.TransactionDate, mrt.QuantityFROM dbo.MostRecentTransariesPerProductID, mrt.TransactionID, mrt.ReferenceOrderID, mrt.TransactionDate, mrt.> 

SQL Server 2008+ Giải pháp thay thế

Để cho phép trình tối ưu hóa 'thấy' một giá trị không đổi cho truy vấn tham chiếu đến biến cục bộ hoặc tham số, chúng tôi có thể thêm OPTION (RECOMPILE) gợi ý truy vấn:

 DECLARE @ProductID INT =878; CHỌN mrt.ProductID, mrt.TransactionID, mrt.ReferenceOrderID, mrt.TransactionDate, mrt.QuantityFROM dbo.MostRecentTransactionsPerProduct AS mrt WHERE mrt.ProductID =@ProductIDOPTION (RECOMPILE); 

Lưu ý: Kế hoạch thực thi trước khi thực thi (‘ước tính’) vẫn hiển thị quét chỉ mục vì giá trị của biến thực sự chưa được đặt. Khi truy vấn được thực thi tuy nhiên, kế hoạch thực thi cho thấy kế hoạch tìm kiếm chỉ mục mong muốn:

SelOnSeqPrj quy tắc không tồn tại trong SQL Server 2005, vì vậy OPTION (RECOMPILE) không thể giúp đỡ ở đó. Trong trường hợp bạn đang thắc mắc, OPTION (RECOMPILE) giải pháp thay thế dẫn đến một tìm kiếm ngay cả khi tùy chọn cơ sở dữ liệu cho tham số bắt buộc được bật.

Giải pháp cho tất cả các phiên bản # 1

Trong một số trường hợp, có thể thay thế dạng xem có vấn đề, biểu thức bảng thông thường hoặc bảng dẫn xuất bằng một hàm giá trị bảng nội dòng được tham số hóa:

 TẠO CHỨC NĂNG dbo.MostRecentTransactionsForProduct (@ProductID integer) BẢNG TRẢ LẠI BẢNG ĐIỀU KHIỂN LẬP TRÌNH CHỌN Sq1.ProductID, sq1.TransactionID, sq1.ReferenceOrderID, sq1.TransactionDate, sq1.Quantity FROM (SELECT th. ReferenceOrderID, th.TransactionDate, th.Quantity, rnk =RANK () OVER (PARTITION BY th.ProductID ORDER BY th.TransactionDate DESC) TỪ Production.TransactionHistory AS th WHERE th.ProductID =@ProductID) AS sq1 WHERE sq1.rnk =1; 

Hàm này đặt ProductID một cách rõ ràng vị ngữ trong cùng phạm vi với hàm cửa sổ, tránh giới hạn trình tối ưu hóa. Được viết để sử dụng hàm trong dòng, truy vấn mẫu của chúng tôi sẽ trở thành:

 SELECT mrt.ProductID, mrt.TransactionID, mrt.ReferenceOrderID, mrt.TransactionDate, mrt.QuantityFROM dbo.MostRecentTransactionsForProduct (878) AS mrt; 

Điều này tạo ra kế hoạch tìm kiếm chỉ mục mong muốn trên tất cả các phiên bản SQL Server hỗ trợ các chức năng cửa sổ. Cách giải quyết này tạo ra một tìm kiếm ngay cả khi vị từ tham chiếu đến một tham số hoặc biến cục bộ - OPTION (RECOMPILE) là không bắt buộc. PARTITION BY hiện đã dư thừa và để không trả lại ProductID nữa cột. Tôi để định nghĩa giống như chế độ xem mà nó thay thế để minh họa rõ ràng hơn nguyên nhân của sự khác biệt về kế hoạch thực thi.

Giải pháp cho tất cả các phiên bản # 2

Cách giải quyết thứ hai chỉ áp dụng cho các hàm cửa sổ xếp hạng được lọc để trả về các hàng được đánh số hoặc xếp hạng # 1 (sử dụng ROW_NUMBER , RANK hoặc DENSE_RANK ). Tuy nhiên, đây là một cách sử dụng rất phổ biến, vì vậy cần phải nhắc lại.

Một lợi ích bổ sung là cách giải quyết này có thể tạo ra các kế hoạch thậm chí còn hiệu quả hơn so với các kế hoạch tìm kiếm chỉ mục đã thấy trước đây. Xin nhắc lại, kế hoạch tốt nhất trước đó trông như thế này:

Kế hoạch thực thi đó xếp hạng 1,918 mặc dù cuối cùng nó chỉ trả về 6 . Chúng tôi có thể cải thiện kế hoạch thực thi này bằng cách sử dụng hàm cửa sổ trong ORDER BY thay vì xếp hạng các hàng và sau đó lọc cho xếp hạng # 1:

 CHỌN ĐẦU (1) VỚI TIES th.TransactionID, th.ReferenceOrderID, th.TransactionDate, th.QuantityFROM Production.TransactionHistory AS thWHERE th.ProductID =878ORDER BY RANK () OVER (ORDER BY th.TransactionDate DESC); 

Truy vấn đó minh họa một cách độc đáo việc sử dụng hàm cửa sổ trong ORDER BY nhưng chúng tôi có thể làm tốt hơn nữa, loại bỏ hoàn toàn chức năng cửa sổ:

 CHỌN HÀNG ĐẦU (1) VỚI TIES th.TransactionID, th.ReferenceOrderID, th.TransactionDate, th.QuantityFROM Production.TransactionHistory AS thWHERE th.ProductID =878ORDER BY th.TransactionDate DESC; 

Kế hoạch này chỉ đọc 7 hàng từ bảng để trả về cùng một tập hợp kết quả 6 hàng. Tại sao lại có 7 hàng? Toán tử hàng đầu đang chạy trong WITH TIES chế độ:

Nó tiếp tục yêu cầu từng hàng một từ cây con của nó cho đến khi Ngày giao dịch thay đổi. Hàng thứ bảy là bắt buộc đối với Trên cùng để đảm bảo rằng không có hàng nào có giá trị ràng buộc nữa sẽ đủ điều kiện.

Chúng tôi có thể mở rộng logic của truy vấn ở trên để thay thế định nghĩa chế độ xem có vấn đề:

 ALTER VIEW dbo.MostRecentTransactionsPerProductWITH SCHEMABINDINGASSELECT p.ProductID, Xếp hạng1.TransactionID, Xếp hạng1 .ReferenceOrderID, Xếp hạng1, Ngày giao dịch, Xếp hạng1. # 1 kết quả cho mỗi ID sản phẩm CHỌN HÀNG ĐẦU (1) VỚI TIES th.TransactionID, th.ReferenceOrderID, th.TransactionDate, th.Quantity from Production.TransactionHistory AS th WHERE th.ProductID =p.ProductID ORDER BY th.TransactionDate DESC) AS được xếp hạng 1; 

Chế độ xem hiện sử dụng CROSS APPLY để kết hợp các kết quả của ORDER BY được tối ưu hóa của chúng tôi truy vấn cho từng sản phẩm. Truy vấn thử nghiệm của chúng tôi không thay đổi:

 DECLARE @ProductID số nguyên; SET @ProductID =878; CHỌN mrt.ProductID, mrt.TransactionID, mrt.ReferenceOrderID, mrt.TransactionDate, mrt.QuantityFROM dbo.MostRecentTransactionsPerProduct AS mrt WHERE mrt.ProductID =@ProductID; 

Cả kế hoạch trước và sau khi thực hiện đều hiển thị tìm kiếm chỉ mục mà không cần OPTION (RECOMPILE) gợi ý truy vấn. Sau đây là kế hoạch sau khi thực hiện ('thực tế'):

Nếu chế độ xem đã sử dụng ROW_NUMBER thay vì RANK , chế độ xem thay thế sẽ đơn giản là bỏ qua WITH TIES mệnh đề trên TOP (1) . Tất nhiên, dạng xem mới cũng có thể được viết dưới dạng một hàm giá trị bảng nội dòng được tham số hóa.

Người ta có thể tranh luận rằng kế hoạch tìm kiếm chỉ mục ban đầu với rnk = 1 vị từ cũng có thể được tối ưu hóa để chỉ kiểm tra 7 hàng. Rốt cuộc, trình tối ưu hóa phải biết rằng xếp hạng được tạo ra bởi nhà điều hành Dự án trình tự theo thứ tự tăng dần nghiêm ngặt, vì vậy việc thực thi có thể kết thúc ngay khi nhìn thấy một hàng có xếp hạng lớn hơn một. Tuy nhiên, hôm nay trình tối ưu hóa không chứa logic này.

Lời kết

Mọi người thường thất vọng về hiệu suất của các khung nhìn kết hợp các chức năng cửa sổ. Lý do thường có thể bắt nguồn từ giới hạn của trình tối ưu hóa được mô tả trong bài đăng này (hoặc có lẽ vì nhà thiết kế chế độ xem không đánh giá cao việc các vị từ được áp dụng cho chế độ xem phải xuất hiện trong PARTITION BY điều khoản được đẩy xuống một cách an toàn).

Tôi muốn nhấn mạnh rằng giới hạn này không chỉ áp dụng cho các chế độ xem và nó cũng không giới hạn cho ROW_NUMBER , RANKDENSE_RANK . Bạn nên biết giới hạn này khi sử dụng bất kỳ chức năng nào có OVER mệnh đề trong một dạng xem, biểu thức bảng chung, bảng dẫn xuất hoặc hàm giá trị bảng nội dòng.

Người dùng SQL Server 2005 gặp phải sự cố này phải đối mặt với lựa chọn viết lại dạng xem dưới dạng một hàm giá trị bảng nội dòng được tham số hóa hoặc sử dụng APPLY kỹ thuật (nếu có).

Người dùng SQL Server 2008 có thêm tùy chọn sử dụng OPTION (RECOMPILE) gợi ý truy vấn nếu vấn đề có thể được giải quyết bằng cách cho phép trình tối ưu hóa xem một hằng số thay vì tham chiếu biến hoặc tham số. Tuy nhiên, hãy nhớ kiểm tra kế hoạch sau khi thực hiện khi sử dụng gợi ý này:kế hoạch trước khi thực hiện nói chung không thể hiển thị kế hoạch tối ưu.


  1. Database
  2.   
  3. Mysql
  4.   
  5. Oracle
  6.   
  7. Sqlserver
  8.   
  9. PostgreSQL
  10.   
  11. Access
  12.   
  13. SQLite
  14.   
  15. MariaDB
  1. Kết nối Microsoft Excel với Xero

  2. Mẹo nhanh - Tăng tốc độ khôi phục chậm từ Nhật ký giao dịch

  3. Tham chiếu SQL cho người mới bắt đầu

  4. Câu lệnh SQL WHERE

  5. Cách thêm ngày vào ngày trong T-SQL