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

Lỗi T-SQL, cạm bẫy và các phương pháp hay nhất - hàm cửa sổ

Bài viết này là phần thứ tư trong loạt bài về các lỗi T-SQL, các cạm bẫy và các phương pháp hay nhất. Trước đây tôi đã đề cập đến thuyết tất định, truy vấn con và phép nối. Trọng tâm của bài viết tháng này là các lỗi, cạm bẫy và các phương pháp hay nhất liên quan đến chức năng cửa sổ. Cảm ơn Erland Sommarskog, Aaron Bertrand, Alejandro Mesa, Umachandar Jayachandran (UC), Fabiano Neves Amorim, Milos Radivojevic, Simon Sabin, Adam Machanic, Thomas Grohser, Chan Ming Man và Paul White đã đưa ra ý tưởng của bạn!

Trong các ví dụ của mình, tôi sẽ sử dụng cơ sở dữ liệu mẫu có tên là TSQLV5. Bạn có thể tìm thấy tập lệnh tạo và điền cơ sở dữ liệu này tại đây và sơ đồ ER của nó tại đây.

Có hai cạm bẫy phổ biến liên quan đến các hàm cửa sổ, cả hai đều là kết quả của các mặc định ngầm phản trực giác được áp đặt bởi tiêu chuẩn SQL. Một cạm bẫy liên quan đến các phép tính tổng số đang chạy trong đó bạn nhận được một khung cửa sổ với tùy chọn RANGE ngầm định. Một cạm bẫy khác có phần liên quan, nhưng có hậu quả nghiêm trọng hơn, liên quan đến định nghĩa khung ngầm định cho các hàm FIRST_VALUE và LAST_VALUE.

Khung cửa sổ có tùy chọn RANGE ngầm định

Cạm bẫy đầu tiên của chúng tôi liên quan đến việc tính toán tổng số đang chạy bằng hàm cửa sổ tổng hợp, trong đó bạn chỉ định rõ ràng mệnh đề thứ tự cửa sổ, nhưng bạn không chỉ định rõ ràng đơn vị khung cửa sổ (ROWS hoặc RANGE) và phạm vi khung cửa sổ liên quan của nó, ví dụ:ROWS CHUẨN BỊ CHƯA BAO GỒM. Mặc định ngầm là phản trực giác và hậu quả của nó có thể gây bất ngờ và đau đớn.

Để chứng minh cạm bẫy này, tôi sẽ sử dụng một bảng có tên là Giao dịch chứa hai triệu giao dịch tài khoản ngân hàng với tín dụng (giá trị dương) và ghi nợ (giá trị âm). Chạy mã sau để tạo bảng Giao dịch và điền vào bảng đó với dữ liệu mẫu:

 ĐẶT SỐ TÀI KHOẢN BẬT; SỬ DỤNG TSQLV5; - http://tsql.solidq.com/SampleDatabases/TSQLV5.zip BẢNG DROP NẾU TỒN TẠI dbo.Transactions; CREATE TABLE dbo.Transactions (actid INT NOT NULL, tranid INT NOT NULL, val MONEY NOT NULL, CONSTRAINT PK_Transactions PRIMARY KEY (actid, tranid) - tạo chỉ mục POC); DECLARE @num_partitions AS INT =100, @rows_per_partition AS INT =20000; CHÈN VÀO dbo. Giao dịch VỚI (TABLOCK) (actid, tranid, val) CHỌN NP.n, RPP.n, (ABS (CHECKSUM (NEWID ())% 2) * 2-1) * (1 + ABS (CHECKSUM ( NEWID ())% 5)) TỪ dbo.GetNums (1, @num_partitions) AS NP CROSS JOIN dbo.GetNums (1, @rows_per_partition) AS RPP; 

Cạm bẫy của chúng tôi có cả mặt hợp lý với một lỗi lôgic tiềm ẩn cũng như khía cạnh hiệu suất với hình phạt hiệu suất. Hình phạt hiệu suất chỉ có liên quan khi chức năng cửa sổ được tối ưu hóa với các toán tử xử lý chế độ hàng. SQL Server 2016 giới thiệu toán tử Tổng hợp cửa sổ chế độ hàng loạt, loại bỏ phần phạt hiệu suất của lỗi, nhưng trước SQL Server 2019, toán tử này chỉ được sử dụng nếu bạn có chỉ mục cột lưu trữ trên dữ liệu. SQL Server 2019 giới thiệu chế độ hàng loạt khi hỗ trợ cửa hàng hàng loạt, vì vậy bạn có thể xử lý chế độ hàng loạt ngay cả khi không có chỉ mục cột cửa hàng nào hiển thị trên dữ liệu. Để chứng minh hiệu suất phạt với xử lý chế độ hàng, nếu bạn đang chạy các mẫu mã trong bài viết này trên SQL Server 2019 trở lên hoặc trên Cơ sở dữ liệu Azure SQL, hãy sử dụng mã sau để đặt mức tương thích cơ sở dữ liệu thành 140 sao cho chưa bật chế độ hàng loạt trên cửa hàng hàng:

 ALTER DATABASE TSQLV5 SET COMPATIBILITY_LEVEL =140; 

Sử dụng mã sau để bật thống kê thời gian và I / O trong phiên:

 ĐẶT THỜI GIAN THỐNG KÊ, IO ON; 

Để tránh phải đợi hai triệu hàng được in trong SSMS, tôi khuyên bạn nên chạy các mẫu mã trong phần này với tùy chọn Loại bỏ kết quả sau khi thực thi được bật (đi tới Tùy chọn truy vấn, Kết quả, Lưới và chọn Loại bỏ kết quả sau khi thực thi).

Trước khi chúng ta gặp khó khăn, hãy xem xét truy vấn sau (gọi là Truy vấn 1) tính toán số dư tài khoản ngân hàng sau mỗi giao dịch bằng cách áp dụng tổng số đang chạy bằng cách sử dụng hàm tổng hợp cửa sổ với đặc tả khung rõ ràng:

 CHỌN actid, tranid, val, SUM (val) OVER (PHẦN BẰNG LỆNH CỦA HOẠT ĐỘNG THEO LỆNH CỦA tranid ROWS UNBOUNDED PRECEDING) NHƯ số dư TỪ dbo.Transactions; 

Kế hoạch cho truy vấn này, sử dụng xử lý chế độ hàng, được hiển thị trong Hình 1.

Hình 1:Kế hoạch cho Truy vấn 1, xử lý chế độ hàng

Kế hoạch lấy dữ liệu được sắp xếp trước từ chỉ mục nhóm của bảng. Sau đó, nó sử dụng các toán tử Phân đoạn và Dự án trình tự để tính số hàng nhằm tìm ra hàng nào thuộc khung của hàng hiện tại. Sau đó, nó sử dụng các toán tử Segment, Window Spool và Stream Aggregate để tính toán hàm tổng hợp cửa sổ. Toán tử Bộ đệm cửa sổ được sử dụng để cuộn các hàng khung sau đó cần được tổng hợp. Nếu không có bất kỳ tối ưu hóa đặc biệt nào, kế hoạch sẽ phải ghi trên mỗi hàng tất cả các hàng khung áp dụng của nó vào ống chỉ, và sau đó tổng hợp chúng. Điều này sẽ dẫn đến độ phức tạp bậc hai, hoặc N. Tin tốt là khi khung bắt đầu với UNBOUNDED PRECEDING, SQL Server xác định trường hợp này là một đường dẫn nhanh trường hợp, trong đó nó chỉ cần lấy tổng số đang chạy của hàng trước đó và thêm giá trị của hàng hiện tại để tính tổng số đang chạy của hàng hiện tại, dẫn đến chia tỷ lệ tuyến tính. Trong chế độ theo dõi nhanh này, kế hoạch chỉ ghi hai hàng vào ống chỉ cho mỗi hàng đầu vào — một hàng có tổng hợp và một hàng có chi tiết.

Window Spool có thể được thực hiện vật lý theo một trong hai cách. Có thể là bộ đệm trong bộ nhớ nhanh được thiết kế đặc biệt cho các chức năng cửa sổ hoặc bộ đệm chậm trên đĩa, về cơ bản là một bảng tạm thời trong tempdb. Nếu số hàng cần được ghi vào ống chỉ mỗi hàng bên dưới có thể vượt quá 10.000 hoặc nếu SQL Server không thể dự đoán số lượng, nó sẽ sử dụng bộ đệm trên đĩa chậm hơn. Trong kế hoạch truy vấn của chúng tôi, chúng tôi có chính xác hai hàng được ghi vào bộ đệm cho mỗi hàng bên dưới, vì vậy SQL Server sử dụng bộ đệm trong bộ nhớ. Thật không may, không có cách nào để biết bạn đang nhận được loại ống chỉ nào từ kế hoạch. Có hai cách để tìm ra điều này. Một là sử dụng một sự kiện mở rộng được gọi là window_spool_ondisk_warning. Một tùy chọn khác là bật STATISTICS IO và kiểm tra số lần đọc logic được báo cáo cho một bảng được gọi là Worktable. Một số lớn hơn 0 có nghĩa là bạn có bộ đệm trên đĩa. Không có nghĩa là bạn có cuộn trong bộ nhớ. Đây là thống kê I / O cho truy vấn của chúng tôi:

Số lần đọc lôgic của Bảng 'Bàn làm việc':0. Số lần đọc lôgic của Bảng 'Giao dịch':6208.

Như bạn có thể thấy, chúng tôi đã sử dụng ống đệm trong bộ nhớ. Đó thường là trường hợp khi bạn sử dụng đơn vị khung cửa sổ ROWS với UNBOUNDED PRECEDING làm dấu phân cách đầu tiên.

Dưới đây là thống kê thời gian cho truy vấn của chúng tôi:

Thời gian CPU:4297 ms, thời gian đã trôi qua:4441 ms.

Truy vấn này mất khoảng 4,5 giây để hoàn thành trên máy của tôi với kết quả bị hủy.

Bây giờ để bắt. Nếu bạn sử dụng tùy chọn RANGE thay vì ROWS, với các dấu phân cách giống nhau, có thể có sự khác biệt nhỏ về ý nghĩa, nhưng lại có sự khác biệt lớn về hiệu suất trong chế độ hàng. Sự khác biệt về ý nghĩa chỉ có liên quan nếu bạn không có tổng số đơn hàng, tức là nếu bạn đang đặt hàng theo thứ gì đó không phải là duy nhất. Tùy chọn ROWS UNBOUNDED PRECEDING dừng với hàng hiện tại, vì vậy trong trường hợp ràng buộc, phép tính là không xác định. Ngược lại, tùy chọn RANGE UNBOUNDED PRECEDING nhìn trước hàng hiện tại và bao gồm các mối quan hệ nếu có. Nó sử dụng logic tương tự như tùy chọn TOP WITH TIES. Khi bạn có tổng số thứ tự, tức là bạn đang đặt hàng theo thứ gì đó độc nhất, không có ràng buộc nào để bao gồm và do đó ROWS và RANGE trở nên tương đương về mặt logic trong trường hợp như vậy. Vấn đề là khi bạn sử dụng RANGE, SQL Server luôn sử dụng bộ đệm trên đĩa trong xử lý chế độ hàng vì khi xử lý một hàng nhất định, nó không thể dự đoán bao nhiêu hàng nữa sẽ được đưa vào. Điều này có thể bị phạt nặng về hiệu suất.

Hãy xem xét truy vấn sau (gọi là Truy vấn 2), giống như Truy vấn 1, chỉ sử dụng tùy chọn RANGE thay vì ROWS:

 CHỌN actid, tranid, val, SUM (val) OVER (PHẦN THEO LỆNH CỦA HOẠT ĐỘNG THEO LỆNH CỦA tranid RANGE UNBOUNDED PRECEDING) NHƯ số dư TỪ dbo.Transators; 

Kế hoạch cho truy vấn này được thể hiện trong Hình 2.

Hình 2:Kế hoạch cho Truy vấn 2, xử lý chế độ hàng

Truy vấn 2 về mặt logic tương đương với Truy vấn 1 vì chúng ta có tổng thứ tự; tuy nhiên, vì nó sử dụng RANGE, nó được tối ưu hóa với bộ đệm trên đĩa. Hãy quan sát rằng trong kế hoạch cho Truy vấn 2, Bộ đệm cửa sổ trông giống như trong kế hoạch cho Truy vấn 1 và chi phí ước tính giống nhau.

Dưới đây là thời gian và số liệu thống kê I / O để thực hiện Truy vấn 2:

Thời gian CPU:19515 ms, thời gian đã trôi qua:20201 ms.
Số lần đọc logic của bảng 'Bảng làm việc':12044701. Số lần đọc logic của bảng 'Giao dịch':6208.

Lưu ý số lượng lớn các lần đọc logic đối với Worktable, cho thấy rằng bạn đã có bộ đệm trên đĩa. Thời gian chạy lâu hơn bốn lần so với Truy vấn 1.

Nếu bạn đang nghĩ rằng nếu đúng như vậy, bạn sẽ chỉ cần tránh sử dụng tùy chọn RANGE, trừ khi bạn thực sự cần bao gồm các mối quan hệ, đó là suy nghĩ tốt. Vấn đề là nếu bạn sử dụng hàm cửa sổ hỗ trợ khung (tổng hợp, FIRST_VALUE, LAST_VALUE) với mệnh đề thứ tự cửa sổ rõ ràng, nhưng không đề cập đến đơn vị khung cửa sổ và phạm vi liên quan của nó, bạn nhận được RANGE UNBOUNDED PRECEDING theo mặc định . Mặc định này được quy định bởi tiêu chuẩn SQL và tiêu chuẩn đã chọn nó vì nó thường thích các tùy chọn xác định hơn làm giá trị mặc định. Truy vấn sau (gọi là Truy vấn 3) là một ví dụ rơi vào bẫy này:

 CHỌN actid, tranid, val, SUM (val) OVER (PHẦN THEO LỆNH CỦA actid THEO tranid) NHƯ số dư TỪ dbo.Transactions; 

Thông thường, mọi người viết như thế này với giả định rằng họ đang nhận được ROWS UNBOUNDED PRECEDING theo mặc định, mà không nhận ra rằng họ đang thực sự nhận được RANGE UNBOUNDED PRECEDING. Vấn đề là vì hàm sử dụng tổng thứ tự, bạn sẽ nhận được kết quả tương tự như với ROWS, vì vậy bạn không thể nói rằng có vấn đề từ kết quả. Nhưng những con số về hiệu suất mà bạn sẽ nhận được giống như Truy vấn 2. Tôi thấy mọi người luôn rơi vào cái bẫy này.

Phương pháp tốt nhất để tránh vấn đề này là trong trường hợp bạn sử dụng chức năng cửa sổ có khung, hãy trình bày rõ ràng về đơn vị khung cửa sổ và phạm vi của nó, và thường thích ROWS hơn. Chỉ dành riêng việc sử dụng RANGE cho các trường hợp đặt hàng không phải là duy nhất và bạn cần bao gồm các mối quan hệ.

Hãy xem xét truy vấn sau minh họa một trường hợp khi có sự khác biệt về khái niệm giữa ROWS và RANGE:

 SELECT orderdate, orderid, val, SUM (val) OVER (ORDER BY orderdate / pre> 

Truy vấn này tạo ra kết quả sau:

 orderdate orderid val sumrange sumrange ---------- -------- -------- -------- -------- - 2017-07-04 10248 440.00 440.00 440.00 2017-07-05 10249 1863.40 2303.40 2303.40 2017-07-08 10250 1552.60 3856.00 4510.06 2017-07-08 10251 654.06 4510.06 4510.06 2017-07-09 10252 3597.90 8107.96 8107.96 ... 

Quan sát sự khác biệt trong kết quả cho các hàng trong đó cùng một ngày đặt hàng xuất hiện nhiều lần, như trường hợp của ngày 8 tháng 7 năm 2017. Lưu ý cách tùy chọn ROWS không bao gồm các mối quan hệ và do đó không xác định và tùy chọn RANGE hoạt động như thế nào bao gồm các mối quan hệ và do đó luôn mang tính xác định.

Tuy nhiên, sẽ có vấn đề nếu trong thực tế, bạn gặp những trường hợp bạn đặt hàng theo thứ gì đó không phải là duy nhất và bạn thực sự cần bao gồm các mối quan hệ để làm cho phép tính xác định. Điều có lẽ phổ biến hơn nhiều trong thực tế là thực hiện một trong hai điều. Một là phá vỡ mối quan hệ bằng cách thêm thứ gì đó vào thứ tự cửa sổ để làm cho nó trở nên độc nhất và theo cách này dẫn đến một phép tính xác định, như sau:

 CHỌN ngày order, orderid, val, SUM (val) OVER (ORDER BY orderdate, orderid ROWS UNBOUNDED PRECEDING) AS runningsum TỪ Sales.OrderValues ​​ORDER BY orderdate; 

Truy vấn này tạo ra kết quả sau:

 orderdate orderid val runningsum ------------- ---------------- ----------- 2017-07-04 10248 440.00 440.00 2017-07-05 10249 1863.40 2303.40 2017-07-08 10250 1552.60 3856.00 2017-07-08 10251 654.06 4510.06 2017-07-09 10252 3597.90 8107.96 ... 

Một tùy chọn khác là áp dụng phân nhóm sơ bộ, trong trường hợp của chúng tôi, theo ngày đặt hàng, như vậy:

 CHỌN ngày đặt hàng, SUM (val) AS tổng ngày, SUM (SUM (val)) HẾT (ORDER BY orderdate 

Truy vấn này tạo ra kết quả sau trong đó mỗi ngày đặt hàng chỉ xuất hiện một lần:

 orderdate daytotal runningsum ---------- ---------------- 2017-07-04 440.00 440.00 2017-07-05 1863.40 2303.40 2017-07-08 2206.66 4510.06 2017-07-09 3597.90 8107.96 ... 

Dù sao đi nữa, hãy nhớ ghi nhớ phương pháp hay nhất ở đây!

Tin tốt là nếu bạn đang chạy trên SQL Server 2016 trở lên và có chỉ mục cột lưu trữ hiện diện trên dữ liệu (ngay cả khi đó là chỉ mục cửa hàng lưu trữ giả mạo được lọc) hoặc nếu bạn đang chạy trên SQL Server 2019 trở lên, hoặc trên Cơ sở dữ liệu Azure SQL, bất kể sự hiện diện của chỉ mục cột lưu trữ, cả ba truy vấn nói trên đều được tối ưu hóa với toán tử Tổng hợp cửa sổ chế độ hàng loạt. Với toán tử này, nhiều lỗi xử lý chế độ hàng không hiệu quả được loại bỏ. Toán tử này hoàn toàn không sử dụng bộ đệm, vì vậy không có vấn đề gì về bộ đệm trong bộ nhớ so với bộ đệm trên đĩa. Nó sử dụng quy trình xử lý phức tạp hơn, trong đó nó có thể áp dụng nhiều lần chuyển song song qua cửa sổ của các hàng trong bộ nhớ cho cả ROWS và RANGE.

Để chứng minh bằng cách sử dụng tối ưu hóa chế độ hàng loạt, hãy đảm bảo mức độ tương thích cơ sở dữ liệu của bạn được đặt thành 150 hoặc cao hơn:

 ALTER DATABASE TSQLV5 SET COMPATIBILITY_LEVEL =150; 

Chạy lại Truy vấn 1:

 CHỌN actid, tranid, val, SUM (val) OVER (PHẦN BẰNG LỆNH CỦA HOẠT ĐỘNG THEO LỆNH CỦA tranid ROWS UNBOUNDED PRECEDING) NHƯ số dư TỪ dbo.Transactions; 

Kế hoạch cho truy vấn này được thể hiện trong Hình 3.

Hình 3:Kế hoạch cho Truy vấn 1, xử lý theo chế độ hàng loạt

Dưới đây là thống kê hiệu suất mà tôi nhận được cho truy vấn này:

Thời gian CPU:937 ms, thời gian đã trôi qua:983 ms.
Số lần đọc logic của bảng 'Giao dịch':6208.

Thời gian chạy giảm xuống còn 1 giây!

Chạy lại Truy vấn 2 với tùy chọn RANGE rõ ràng:

 CHỌN actid, tranid, val, SUM (val) OVER (PHẦN THEO LỆNH CỦA HOẠT ĐỘNG THEO LỆNH CỦA tranid RANGE UNBOUNDED PRECEDING) NHƯ số dư TỪ dbo.Transators; 

Kế hoạch cho truy vấn này được thể hiện trong Hình 4.

Hình 2:Kế hoạch cho Truy vấn 2, xử lý theo chế độ hàng loạt

Dưới đây là thống kê hiệu suất mà tôi nhận được cho truy vấn này:

Thời gian CPU:969 ms, thời gian đã trôi qua:1048 ms.
Số lần đọc logic của bảng 'Giao dịch':6208.

Hiệu suất giống như đối với Truy vấn 1.

Chạy lại Truy vấn 3, với tùy chọn RANGE ngầm định:

 CHỌN actid, tranid, val, SUM (val) OVER (PHẦN THEO LỆNH CỦA actid THEO tranid) NHƯ số dư TỪ dbo.Transactions; 

Kế hoạch và số hiệu suất tất nhiên giống như đối với Truy vấn 2.

Khi bạn hoàn tất, hãy chạy mã sau để tắt thống kê hiệu suất:

 ĐẶT THỜI GIAN THỐNG KÊ, IO TẮT; 

Ngoài ra, đừng quên tắt tùy chọn Loại bỏ kết quả sau khi thực thi trong SSMS.

Khung ngầm định với FIRST_VALUE và LAST_VALUE

Các hàm FIRST_VALUE và LAST_VALUE là các hàm cửa sổ bù trừ trả về một biểu thức tương ứng từ hàng đầu tiên hoặc hàng cuối cùng trong khung cửa sổ. Phần khó khăn về chúng là thường khi mọi người sử dụng chúng lần đầu tiên, họ không nhận ra rằng chúng hỗ trợ một khung, thay vì nghĩ rằng chúng áp dụng cho toàn bộ phân vùng.

Hãy xem xét nỗ lực sau để trả lại thông tin đơn đặt hàng, cộng với giá trị của đơn đặt hàng đầu tiên và đơn hàng cuối cùng của khách hàng:

 CHỌN custid, orderdate, orderid, val, FIRST_VALUE (val) OVER (PARTITION BY custid ORDER BY orderdate, orderid) AS firstval, LAST_VALUE (val) OVER (PARTITION BY custid ORDER BY orderdate, orderid) AS lastval from Sales. OrderValues ​​ĐẶT HÀNG THEO custid, orderdate, orderid; 

Nếu bạn tin tưởng không chính xác rằng các hàm này hoạt động trên toàn bộ phân vùng cửa sổ, đây là niềm tin của nhiều người sử dụng các hàm này lần đầu tiên, bạn sẽ tự nhiên mong đợi FIRST_VALUE trả về giá trị thứ tự của đơn hàng đầu tiên của khách hàng và LAST_VALUE trả về giá trị đơn hàng của đơn hàng cuối cùng của khách hàng. Tuy nhiên, trong thực tế, các chức năng này hỗ trợ một khung. Xin nhắc lại, với các hàm hỗ trợ khung, khi bạn chỉ định mệnh đề thứ tự cửa sổ nhưng không chỉ định đơn vị khung cửa sổ và phạm vi liên quan của nó, bạn nhận được RANGE UNBOUNDED PRECEDING theo mặc định. Với hàm FIRST_VALUE, bạn sẽ nhận được kết quả mong đợi, nhưng nếu truy vấn của bạn được tối ưu hóa với các toán tử chế độ hàng, bạn sẽ phải trả tiền phạt khi sử dụng bộ đệm trên đĩa. Với hàm LAST_VALUE, điều đó thậm chí còn tệ hơn. Không chỉ vậy, bạn sẽ phải trả tiền phạt của bộ đệm trên đĩa, mà thay vì nhận giá trị từ hàng cuối cùng trong phân vùng, bạn sẽ nhận giá trị từ hàng hiện tại!

Đây là kết quả của truy vấn trên:

 custid orderdate orderid val firstval lastval ---------------- ---------------- ---- ---------- 1 2018-08-25 10643 814.50 814.50 814.50 1 2018-10-03 10692 878.00 814.50 878.00 1 2018-10-13 10702 330.00 814.50 330.00 1 2019-01-15 10835 845.80 814.50 845.80 1 2019-03-16 10952 471.20 814.50 471.20 1 2019-04-09 11011 933.50 814.50 933.50 2 2017-09-18 10308 88.80 88.80 88.80 2 2018-08-08 10625 479.75 88.80 479.75 2 2018-11-28 10759 320,00 88,80 320,00 2 2019-03-04 10926 514,40 88,80 514,40 3 2017-11-27 10365 403.20 403.20 403.20 3 2018-04-15 10507 749,06 403.20 749,06 3 2018-05-13 10535 1940,85 403.20 1940,85 3 2018-06-19 10573 2082,00 403.20 2082,00 3 2018-09-22 10677 813,37 403.20 813,37 3 2018-09-25 10682 375,50 403.20 375,50 3 2019-01-28 10856 660,00 403.20 660,00 ... 

Thông thường, khi mọi người nhìn thấy đầu ra như vậy lần đầu tiên, họ nghĩ rằng SQL Server có lỗi. Nhưng tất nhiên, nó không; nó chỉ đơn giản là mặc định của tiêu chuẩn SQL. Có một lỗi trong truy vấn. Nhận thấy rằng có một khung liên quan, bạn muốn rõ ràng về đặc điểm khung và sử dụng khung tối thiểu để nắm bắt hàng mà bạn đang theo dõi. Ngoài ra, hãy đảm bảo rằng bạn sử dụng đơn vị ROWS. Vì vậy, để lấy hàng đầu tiên trong phân vùng, hãy sử dụng hàm FIRST_VALUE với khung ROWS GIỮA ROW CHÍNH XÁC VÀ HIỆN TẠI. Để lấy hàng cuối cùng trong phân vùng, hãy sử dụng hàm LAST_VALUE với khung ĐƯỜNG LỐI GIỮA ĐƯỜNG HIỆN TẠI VÀ ĐƯỜNG DẪN SAU KHÔNG ĐƯỢC TỔNG HỢP.

Đây là truy vấn đã sửa đổi của chúng tôi với lỗi đã được sửa:

 CHỌN custid, orderdate, orderid, val, FIRST_VALUE (val) OVER (PARTITION BY custid ORDER BY orderdate, orderid ROWS GIỮA ĐƯỜNG CHÍNH XÁC CHUNG VÀ HIỆN TẠI) AS đầu tiên, LAST_VALUE (val) OVER (PARTITION BY custidate ORDER BY orderid ROWS GIỮA ROW HIỆN TẠI VÀ KHÔNG ĐƯỢC TỔNG HỢP THEO DÕI) như giá trị cuối cùng TỪ Sales.OrderValues ​​ĐẶT HÀNG THEO custid, orderdate, orderid; 

Lần này bạn nhận được kết quả chính xác:

 custid orderdate orderid val firstval lastval ---------------- ---------------- ---- ---------- 1 2018-08-25 10643 814.50 814.50 933.50 1 2018-10-03 10692 878.00 814.50 933.50 1 2018-10-13 10702 330.00 814.50 933.50 1 2019-01-15 10835 845.80 814.50 933.50 1 2019-03-16 10952 471.20 814.50 933.50 1 2019-04-09 11011 933.50 814.50 933.50 2 2017-09-18 10308 88.80 88.80 514.40 2 2018-08-08 10625 479.75 88.80 514.40 2 2018-11-28 10759 320,00 88,80 514,40 2 2019-03-04 10926 514,40 88,80 514,40 3 2017-11-27 10365 403.20 403.20 660,00 3 2018-04-15 10507 749,06 403.20 660,00 3 2018-05-13 10535 1940,85 403.20 660,00 3 2018-06-19 10573 2082,00 403.20 660,00 3 2018-09-22 10677 813,37 403.20 660,00 3 2018-09-25 10682 375,50 403.20 660.00 3 2019-01-28 10856 660.00 403.20 660.00 ... 

Người ta tự hỏi đâu là động lực để tiêu chuẩn thậm chí hỗ trợ một khung có các chức năng này. Nếu bạn nghĩ về nó, bạn sẽ chủ yếu sử dụng chúng để lấy thứ gì đó từ hàng đầu tiên hoặc hàng cuối cùng trong phân vùng. Nếu bạn cần giá trị từ, chẳng hạn như hai hàng trước dòng hiện tại, thay vì sử dụng FIRST_VALUE với khung bắt đầu bằng 2 PRECEDING, thì việc sử dụng LAG với offset rõ ràng là 2 có dễ dàng hơn nhiều không:

 CHỌN custid, orderdate, orderid, val, LAG (val, 2) OVER (PARTITION BY custid ORDER BY orderdate, orderid) NHƯ hai giá trị từ Sales.OrderValues ​​ORDER BY custid, orderdate, orderid; 

Truy vấn này tạo ra kết quả sau:

 custid orderdate orderid val prevtwoval ------- ---------- ----------------- ---------------- ---- 1 2018-08-25 10643 814.50 NULL 1 2018-10-03 10692 878.00 NULL 1 2018-10-13 10702 330.00 814.50 1 2019-01-15 10835 845.80 878.00 1 2019-03-16 10952 471.20 330.00 1 2019-04-09 11011 933.50 845.80 2 2017-09-18 10308 88.80 NULL 2 2018-08-08 10625 479.75 NULL 2 2018-11-28 10759 320.00 88.80 2 2019-03-04 10926 514.40 479.75 3 2017-11-27 10365 403.20 NULL 3 2018-04-15 10507 749.06 NULL 3 2018-05-13 10535 1940.85 403.20 3 2018-06-19 10573 2082.00 749.06 3 2018-09-22 10677 813.37 1940.85 3 2018-09-25 10682 375.50 2082.00 3 2019 -01-28 10856 660,00 813,37 ... 

Rõ ràng, có sự khác biệt về ngữ nghĩa giữa việc sử dụng hàm LAG và FIRST_VALUE ở trên với khung bắt đầu bằng 2 PRECEDING. Với trước đó, nếu một hàng không tồn tại trong khoảng chênh lệch mong muốn, bạn sẽ nhận được NULL theo mặc định. Với cái thứ hai, bạn vẫn nhận được giá trị từ hàng đầu tiên hiện diện, tức là giá trị từ hàng đầu tiên trong phân vùng. Hãy xem xét truy vấn sau:

 CHỌN custid, orderdate, orderid, val, FIRST_VALUE (val) OVER (PARTITION BY custid ORDER BY orderdate, orderid ROWS GIỮA 2 ROW CHÍNH XÁC VÀ HIỆN TẠI) Như hai lần từ Sales.OrderValues ​​ORDER BY custid, orderdate, orderid; trước> 

Truy vấn này tạo ra kết quả sau:

 custid orderdate orderid val prevtwoval ------- ---------- ----------------- ---------------- ---- 1 2018-08-25 10643 814.50 814.50 1 2018-10-03 10692 878.00 814.50 1 2018-10-13 10702 330.00 814.50 1 2019-01-15 10835 845.80 878.00 1 2019-03-16 10952 471.20 330.00 1 2019-04-09 11011 933.50 845.80 2 2017-09-18 10308 88.80 88.80 2 2018-08-08 10625 479.75 88.80 2 2018-11-28 10759 320.00 88.80 2 2019-03-04 10926 514.40 479.75 3 2017-11-27 10365 403.20 403.20 3 2018-04-15 10507 749.06 403.20 3 2018-05-13 10535 1940.85 403.20 3 2018-06-19 10573 2082.00 749.06 3 2018-09-22 10677 813.37 1940.85 3 2018-09-25 10682 375.50 2082.00 3 2019 -01-28 10856 660,00 813,37 ... 

Quan sát rằng lần này không có NULL trong đầu ra. Vì vậy, có một số giá trị trong việc hỗ trợ khung với FIRST_VALUE và LAST_VALUE. Chỉ cần đảm bảo rằng bạn nhớ phương pháp hay nhất để luôn trình bày rõ ràng về đặc điểm kỹ thuật khung với các chức năng này và sử dụng tùy chọn ROWS với khung tối thiểu chứa hàng mà bạn đang theo dõi.

Kết luận

Bài viết này tập trung vào các lỗi, cạm bẫy và các phương pháp hay nhất liên quan đến chức năng cửa sổ. Hãy nhớ rằng cả hai hàm tổng hợp cửa sổ và hàm bù cửa sổ FIRST_VALUE và LAST_VALUE đều hỗ trợ một khung và nếu bạn chỉ định mệnh đề thứ tự cửa sổ nhưng bạn không chỉ định đơn vị khung cửa sổ và phạm vi liên quan của nó, bạn sẽ nhận được RANGE UNBOUNDED PRECEDING trước mặc định. Điều này phải chịu một hình phạt về hiệu suất khi truy vấn được tối ưu hóa với các toán tử chế độ hàng. Với hàm LAST_VALUE, điều này dẫn đến việc nhận các giá trị từ hàng hiện tại thay vì hàng cuối cùng trong phân vùng. Hãy nhớ rõ ràng về khung và thường thích tùy chọn ROWS hơn RANGE. Thật tuyệt khi thấy những cải tiến về hiệu suất với toán tử Window Aggregate ở chế độ hàng loạt. Khi có thể áp dụng, ít nhất lỗi hiệu suất sẽ được loại bỏ.


  1. Database
  2.   
  3. Mysql
  4.   
  5. Oracle
  6.   
  7. Sqlserver
  8.   
  9. PostgreSQL
  10.   
  11. Access
  12.   
  13. SQLite
  14.   
  15. MariaDB
  1. Thêm các tính năng nâng cao khác như quản lý danh mục và bỏ phiếu cho chủ đề và bài đăng

  2. Một số cách để chèn các chuỗi phân tách được phân tách trong một cột

  3. Sự khác biệt giữa Lược đồ và Cơ sở dữ liệu là gì?

  4. Cách sắp xếp trong SQL

  5. Cách viết mệnh đề ORDER BY có ngoại lệ bằng SQL