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

Đặt hàng có điều kiện bởi

Một tình huống phổ biến trong nhiều ứng dụng máy khách-máy chủ là cho phép người dùng cuối ra lệnh sắp xếp thứ tự kết quả. Một số người muốn xem các mặt hàng có giá thấp nhất trước, một số muốn xem các mặt hàng mới nhất trước và một số muốn xem chúng theo thứ tự bảng chữ cái. Đây là một điều phức tạp để đạt được trong Transact-SQL vì bạn không thể chỉ nói:

 TẠO THỦ TỤC dbo.SortOnSomeTable @SortColumn NVARCHAR (128) =N'key_col ', @SortDirection VARCHAR (4) =' ASC'ASBEGIN ... ORDER BY @SortColumn; - hoặc ... ĐẶT HÀNG BẰNG @SortColumn @SortDirection; ENDGO 

Điều này là do T-SQL không cho phép các biến ở những vị trí này. Nếu bạn chỉ sử dụng @SortColumn, bạn sẽ nhận được:

Msg 1008, Mức 16, Trạng thái 1, Dòng x
Mục SELECT được xác định bởi ORDER BY số 1 chứa một biến như một phần của biểu thức xác định vị trí cột. Các biến chỉ được phép khi sắp xếp thứ tự bởi một biểu thức tham chiếu đến tên cột.

(Và khi thông báo lỗi cho biết, "một biểu thức tham chiếu đến tên cột", bạn có thể thấy nó không rõ ràng và tôi đồng ý. Nhưng tôi có thể đảm bảo với bạn rằng điều này không có nghĩa là một biến là một biểu thức phù hợp.)

Nếu bạn cố gắng thêm @SortDirection, thông báo lỗi sẽ mờ hơn một chút:

Msg 102, Mức 15, Trạng thái 1, Dòng x
Cú pháp không chính xác gần '@SortDirection'.

Có một số cách giải quyết vấn đề này và bản năng đầu tiên của bạn có thể là sử dụng SQL động hoặc giới thiệu biểu thức CASE. Nhưng như với hầu hết mọi thứ, có những phức tạp có thể buộc bạn đi theo con đường này hay con đường khác. Vì vậy, cái nào bạn nên sử dụng? Hãy cùng khám phá cách các giải pháp này có thể hoạt động và so sánh các tác động đến hiệu suất của một số cách tiếp cận khác nhau.

Dữ liệu mẫu

Sử dụng chế độ xem danh mục mà chúng ta có lẽ đều hiểu khá rõ, sys.all_objects, tôi đã tạo bảng sau dựa trên kết hợp chéo, giới hạn bảng ở 100.000 hàng (tôi muốn dữ liệu lấp đầy nhiều trang nhưng điều đó không mất nhiều thời gian để truy vấn và kiểm tra):

 TẠO CƠ SỞ DỮ LIỆU OrderBy; GOUSE OrderBy; ĐI CHỌN ĐẦU (100000) key_col =ROW_NUMBER () OVER (ORDER BY s1. [object_id]), - một LỚN với chỉ mục nhóm s1. [object_id], - một INT không có tên chỉ mục =s1.name - một NVARCHAR có chỉ mục hỗ trợ COLLATE SQL_Latin1_General_CP1_CI_AS, type_desc =s1.type_desc - một NVARCHAR (60) không có chỉ mục COLLATE SQL_Latin1_General_CP1_CobI_AS, s1.modify_date - một ngày giờ không có chỉ mục FROMsysINTOjectify_date sys.all_objects AS s1 CROSS THAM GIA sys.all_objects AS s2ORDER BY s1. [object_id]; 

(Thủ thuật COLLATE là do nhiều chế độ xem danh mục có các cột khác nhau với các đối chiếu khác nhau và điều này đảm bảo rằng hai cột sẽ khớp với mục đích của bản trình diễn này.)

Sau đó, tôi đã tạo một cặp chỉ mục được phân cụm / không phân cụm điển hình có thể tồn tại trên một bảng như vậy, trước khi tối ưu hóa (tôi không thể sử dụng object_id cho khóa, vì phép nối chéo tạo ra các bản sao):

 TẠO CHỈ SỐ ĐƯỢC CHỈNH LẬP DUY NHẤT trên key_col TRÊN dbo.sys_objects (key_col); TẠO INDEX tên TRÊN dbo.sys_objects (name); 

Các trường hợp sử dụng

Như đã đề cập ở trên, người dùng có thể muốn xem dữ liệu này được sắp xếp theo nhiều cách khác nhau, vì vậy hãy đặt ra một số trường hợp sử dụng điển hình mà chúng tôi muốn hỗ trợ (và bằng cách hỗ trợ, ý tôi là chứng minh):

  • Thứ tự theo key_col tăng dần ** mặc định nếu người dùng không quan tâm
  • Được sắp xếp theo object_id (tăng dần / giảm dần)
  • Được sắp xếp theo tên (tăng dần / giảm dần)
  • Được sắp xếp theo type_desc (tăng dần / giảm dần)
  • Được sắp xếp theo mod_date (tăng dần / giảm dần)

Chúng tôi sẽ để thứ tự key_col làm mặc định vì nó sẽ hiệu quả nhất nếu người dùng không có tùy chọn; vì key_col là một đại diện tùy ý không có ý nghĩa gì đối với người dùng (và thậm chí có thể không được tiếp xúc với họ), không có lý do gì để cho phép sắp xếp ngược lại trên cột đó.

Phương pháp tiếp cận không hiệu quả

Cách tiếp cận phổ biến nhất mà tôi thấy khi ai đó lần đầu tiên bắt đầu giải quyết vấn đề này là giới thiệu logic điều khiển luồng cho truy vấn. Họ mong đợi có thể làm được điều này:

 SELECT key_col, [object_id], name, type_desc, mod_dateFROM dbo.sys_objectsORDER BY IF @SortColumn ='key_col' key_colIF @SortColumn ='object_id' [object_id] IF @SortColumn ='name' name ... IF @SortDirection ='ASC' MÔ TẢ ASCELSE; 

Điều này rõ ràng là không hoạt động. Tiếp theo, tôi thấy CASE được giới thiệu không chính xác, sử dụng cú pháp tương tự:

 SELECT key_col, [object_id], name, type_desc, mod_dateFROM dbo.sys_objectsORDER BY CASE @SortColumn KHI 'key_col' THÌ key_col KHI 'object_id' THÌ [object_id] KHI 'name' THÌ tên ... HẾT CASE @SortDirection WHEN 'ASC' THÌ ASC ELSE DESC END; 

Điều này gần hơn, nhưng nó không thành công vì hai lý do. Một là CASE là một biểu thức trả về chính xác một giá trị của một kiểu dữ liệu cụ thể; điều này hợp nhất các kiểu dữ liệu không tương thích và do đó sẽ phá vỡ biểu thức CASE. Khác là không có cách nào để áp dụng có điều kiện hướng sắp xếp theo cách này mà không sử dụng SQL động.

Phương pháp tiếp cận hiệu quả

Ba cách tiếp cận chính mà tôi đã thấy như sau:

Nhóm các loại và chỉ đường tương thích lại với nhau

Để sử dụng CASE với ORDER BY, phải có một biểu thức riêng biệt cho từng tổ hợp các loại và hướng tương thích. Trong trường hợp này, chúng tôi sẽ phải sử dụng một cái gì đó như thế này:

 TẠO THỦ TỤC dbo.Sort_CaseExpanded @SortColumn NVARCHAR (128) =N'key_col ', @SortDirection VARCHAR (4) =' ASC'ASBEGIN BẬT TÀI KHOẢN BẬT; CHỌN key_col, [object_id], name, type_desc, edit_date FROM dbo.sys_objects ORDER THEO TRƯỜNG HỢP KHI @SortDirection ='ASC' THÌ CASE @SortColumn KHI 'key_col' THÌ key_col KHI 'object_id' THÌ [object_id] KẾT THÚC KẾT THÚC, CASE WHEN [object_id] END END, CASE WHEN SortDirection ='DESC' THÌ CASE @SortColumn KHI 'key_col' THEN key_col KHI 'object_id' THÌ [object_id] END END DESC, CASE WHEN @SortDirection ='ASC' THÌ CASE @SortColumn KHI 'name' THEN name WHEN 'type_desc' THEN type_desc END END, CASE WHEN @SortDirection ='DESC' THEN CASE @SortColumn KHI 'name' THEN name WHEN 'type_desc' THEN type_desc END END DESC, CASE WHEN @SortColumn ='mod_date' VÀ @SortDirection ='ASC' THÌ sửa đổi , CASE WHEN @SortColumn ='mod_date' AND @SortDirection ='DESC' SAU ĐÓ edit_date END DESC; END 

Bạn có thể nói, ồ, đó là một đoạn mã xấu xí, và tôi đồng ý với bạn. Tôi nghĩ đây là lý do tại sao rất nhiều người lưu trữ dữ liệu của họ trên giao diện người dùng và để tầng trình bày giải quyết việc sắp xếp nó theo các thứ tự khác nhau. :-)

Bạn có thể thu gọn logic này hơn một chút bằng cách chuyển đổi tất cả các loại không phải chuỗi thành chuỗi sẽ sắp xếp chính xác, ví dụ:

 TẠO THỦ TỤC dbo.Sort_CaseCollapsed @SortColumn NVARCHAR (128) =N'key_col ', @SortDirection VARCHAR (4) =' ASC'ASBEGIN BẬT SỐ KHOẢN; CHỌN key_col, [object_id], name, type_desc, mod_date FROM dbo.sys_objects ORDER THEO TRƯỜNG HỢP KHI @SortDirection ='ASC' THÌ CASE @SortColumn KHI 'key_col' THÌ PHẢI ('000000000000' + RTRIM (key_col), 12) KHI NÀO ' object_id 'THEN RIGHT (COALESCE (NULLIF (LEFT (RTRIM ([object_id]), 1),' - '),' 0 ') + REPLICATE (' 0 ', 23) + RTRIM ([object_id]), 24) WHEN 'name' THÌ tên KHI 'type_desc' THÌ type_desc KHI 'edit_date' THÌ CHUYỂN ĐỔI (CHAR (19), mod_date, 120) KẾT THÚC KẾT THÚC, CASE WHEN @SortDirection ='DESC' THÌ CASE @SortColumn WHEN 'key_col' THEN RIGHT (' 000000000000 '+ RTRIM (key_col), 12) KHI' object_id 'THÌ PHẢI (COALESCE (NULLIF (LEFT) (RTRIM ([object_id]), 1),' - '),' 0 ') + REPLICATE (' 0 ', 23 ) + RTRIM ([object_id]), 24) KHI 'name' THÌ tên KHI 'type_desc' THEN type_desc KHI 'mod_date' THÌ CHUYỂN ĐỔI (CHAR (19), mod_date, 120) END END DESC; END 

Tuy nhiên, đó là một mớ hỗn độn khá xấu xí và bạn phải lặp lại các biểu thức hai lần để xử lý các hướng sắp xếp khác nhau. Tôi cũng nghi ngờ rằng việc sử dụng OPTION RECOMPILE trên truy vấn đó sẽ giúp bạn không bị khó chịu bởi tính năng đánh hơi tham số. Ngoại trừ trường hợp mặc định, phần lớn công việc đang được thực hiện ở đây sẽ không phải là biên dịch.

Áp dụng xếp hạng bằng cách sử dụng các hàm cửa sổ

Tôi đã phát hiện ra thủ thuật nhỏ gọn này từ AndriyM, mặc dù nó hữu ích nhất trong trường hợp tất cả các cột sắp xếp tiềm năng đều thuộc loại tương thích, nếu không thì biểu thức được sử dụng cho ROW_NUMBER () cũng phức tạp như nhau. Phần thông minh nhất là để chuyển đổi giữa thứ tự tăng dần và giảm dần, chúng tôi chỉ cần nhân ROW_NUMBER () với 1 hoặc -1. Chúng ta có thể áp dụng nó trong tình huống này như sau:

 TẠO THỦ TỤC dbo.Sort_RowNumber @SortColumn NVARCHAR (128) =N'key_col ', @SortDirection VARCHAR (4) =' ASC'ASBEGIN BẬT TÀI KHOẢN BẬT;; WITH x AS (SELECT key_col, [object_id], name, type_desc, mod_date, rn =ROW_NUMBER () OVER (ORDER BY CASE @SortColumn WHEN 'key_col' THEN RIGHT ('000000000000' + RTRIM (key_col), 12) WHEN ' object_id 'THEN RIGHT (COALESCE (NULLIF (LEFT (RTRIM ([object_id]), 1),' - '),' 0 ') + REPLICATE (' 0 ', 23) + RTRIM ([object_id]), 24) WHEN 'name' THÌ tên KHI 'type_desc' THÌ type_desc KHI 'mod_date' THÌ CHUYỂN ĐỔI (CHAR (19), mod_date, 120) END) * TRƯỜNG HỢP @SortDirection KHI 'ASC' THÌ 1 ELSE -1 KẾT THÚC TỪ dbo.sys_objects) CHỌN key_col , [object_id], name, type_desc, mod_date FROM x ORDER BY rn; ENDGO 

Một lần nữa, OPTION RECOMPILE có thể giúp bạn ở đây. Ngoài ra, bạn có thể nhận thấy trong một số trường hợp các mối quan hệ được xử lý khác nhau bởi các gói khác nhau - ví dụ:khi sắp xếp theo tên, bạn thường sẽ thấy key_col đi qua theo thứ tự tăng dần trong mỗi nhóm tên trùng lặp, nhưng bạn cũng có thể thấy các giá trị trộn lẫn với nhau. Để cung cấp hành vi dễ dự đoán hơn trong trường hợp có ràng buộc, bạn luôn có thể thêm một mệnh đề ORDER BY bổ sung. Lưu ý rằng nếu bạn thêm key_col vào ví dụ đầu tiên, bạn sẽ cần đặt nó thành một biểu thức để key_col không được liệt kê trong ORDER BY hai lần (bạn có thể làm điều này bằng cách sử dụng key_col + 0, chẳng hạn).

SQL động

Rất nhiều người dè dặt về SQL động - nó không thể đọc được, nó là nơi sinh sôi của SQL injection, nó dẫn đến kế hoạch bộ nhớ cache phình ra, nó đánh bại mục đích của việc sử dụng các thủ tục được lưu trữ… Một số trong số này chỉ đơn giản là không đúng sự thật và một số trong số đó dễ giảm thiểu. Tôi đã thêm một số xác thực ở đây để có thể dễ dàng thêm vào bất kỳ quy trình nào ở trên:

 TẠO QUY TRÌNH dbo.Sort_DynamicSQL @SortColumn NVARCHAR (128) =N'key_col ', @SortDirection VARCHAR (4) =' ASC'ASBEGIN BẬT TÀI KHOẢN BẬT; - từ chối mọi hướng sắp xếp không hợp lệ:IF UPPER (@SortDirection) NOT IN ('ASC', 'DESC') BEGIN RAISERROR ('Tham số không hợp lệ cho @SortDirection:% s', 11, 1, @SortDirection); QUAY LẠI -1; KẾT THÚC - từ chối mọi tên cột không mong muốn:IF LOWER (@SortColumn) NOT IN (N'key_col ', N'object_id', N'name ', N'type_desc', N'modify_date ') BEGIN RAISERROR (' Tham số không hợp lệ cho @SortColumn:% s ', 11, 1, @SortColumn); QUAY LẠI -1; KẾT THÚC ĐẶT @SortColumn =QUOTENAME (@SortColumn); DECLARE @sql NVARCHAR (MAX); SET @sql =N'SELECT key_col, [object_id], name, type_desc, mod_date FROM dbo.sys_objects ORDER BY '+ @SortColumn +' '+ @SortDirection +'; '; EXEC sp_executesql @sql; HẾT 

So sánh Hiệu suất

Tôi đã tạo một thủ tục được lưu trữ trong trình bao bọc cho từng quy trình ở trên để tôi có thể dễ dàng kiểm tra tất cả các tình huống. Bốn thủ tục trình bao bọc trông giống như thế này, với tên thủ tục tất nhiên là khác nhau:

 TẠO THỦ TỤC dbo.Test_Sort_CaseExpandedASBEGIN ĐẶT TÀI KHOẢN BẬT; EXEC dbo.Sort_CaseExpanded; - EXEC mặc định dbo.Sort_CaseExpanded N'name ',' ASC '; EXEC dbo.Sort_CaseExpanded N'name ',' DESC '; EXEC dbo.Sort_CaseExpanded N'object_id ',' ASC '; EXEC dbo.Sort_CaseExpanded N'object_id ',' DESC '; EXEC dbo.Sort_CaseExpanded N'type_desc ',' ASC '; EXEC dbo.Sort_CaseExpanded N'type_desc ',' DESC '; EXEC dbo.Sort_CaseExpanded N'modify_date ',' ASC '; EXEC dbo.Sort_CaseExpanded N'modify_date ',' DESC '; END 

Và sau đó bằng cách sử dụng SQL Sentry Plan Explorer, tôi đã tạo các kế hoạch thực thi thực tế (và các chỉ số đi kèm với nó) với các truy vấn sau và lặp lại quá trình này 10 lần để tính tổng thời lượng:

 DBCC DROPCLEANBUFFERS; DBCC FREEPROCCACHE; EXEC dbo.Test_Sort_CaseExpanded; - EXEC dbo.Test_Sort_CaseCollapsed; - EXEC dbo.Test_Sort_RowNumber; - EXEC dbo.Test_Sort_DynamicSQL; ĐI 10  

Tôi cũng đã thử nghiệm ba trường hợp đầu tiên với OPTION RECOMPILE (không có ý nghĩa nhiều đối với trường hợp SQL động, vì chúng tôi biết rằng mỗi lần đó sẽ là một kế hoạch mới) và cả bốn trường hợp với MAXDOP 1 để loại bỏ can thiệp song song. Đây là kết quả:

Kết luận

Đối với hiệu suất hoàn toàn, SQL động luôn thắng (mặc dù chỉ bằng một biên độ nhỏ trên tập dữ liệu này). Phương pháp ROW_NUMBER (), mặc dù thông minh, nhưng lại là kẻ thua cuộc trong mỗi bài kiểm tra (xin lỗi AndriyM).

Nó thậm chí còn thú vị hơn khi bạn muốn giới thiệu một mệnh đề WHERE, đừng bận tâm đến việc phân trang. Ba thứ này giống như một cơn bão hoàn hảo để đưa sự phức tạp vào những gì bắt đầu như một truy vấn tìm kiếm đơn giản. Truy vấn của bạn càng có nhiều hoán vị, thì bạn càng có nhiều khả năng muốn loại bỏ khả năng đọc ra khỏi cửa sổ và sử dụng SQL động kết hợp với cài đặt "tối ưu hóa cho khối lượng công việc đặc biệt" để giảm thiểu tác động của các kế hoạch sử dụng một lần trong bộ nhớ cache kế hoạch của bạn.


  1. Database
  2.   
  3. Mysql
  4.   
  5. Oracle
  6.   
  7. Sqlserver
  8.   
  9. PostgreSQL
  10.   
  11. Access
  12.   
  13. SQLite
  14.   
  15. MariaDB
  1. Mệnh đề GROUP BY trong SQL dành cho người mới bắt đầu

  2. Các nguyên tắc cơ bản về biểu thức bảng, Phần 5 - CTE, các cân nhắc logic

  3. Kiểm tra tình trạng trên Exadata bằng Exachk Utility

  4. Thu hẹp khoảng cách Azure:Phiên bản được quản lý

  5. Toán tử KHÔNG PHẢI của SQL