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

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

Bài viết này là phần thứ năm trong loạt bài về T-SQL bug, 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 xác định, truy vấn con, phép nối và cửa sổ. Trong tháng này, tôi đề cập đến việc xoay vòng và bỏ quay. 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 đã chia sẻ đề xuất 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.

Tạo nhóm ngầm với PIVOT

Khi mọi người muốn xoay dữ liệu bằng T-SQL, họ sử dụng giải pháp tiêu chuẩn với truy vấn được nhóm và biểu thức CASE hoặc toán tử bảng PIVOT độc quyền. Lợi ích chính của toán tử PIVOT là nó có xu hướng dẫn đến mã ngắn hơn. Tuy nhiên, toán tử này có một vài thiếu sót, trong số đó có một cái bẫy thiết kế cố hữu có thể dẫn đến lỗi trong mã của bạn. Sau đây, tôi sẽ mô tả cái bẫy, lỗi tiềm ẩn và phương pháp tốt nhất để ngăn chặn lỗi. Tôi cũng sẽ mô tả một đề xuất để nâng cao cú pháp của toán tử PIVOT theo cách giúp tránh lỗi.

Khi bạn xoay dữ liệu, có ba bước liên quan đến giải pháp, với ba yếu tố liên quan:

  1. Nhóm dựa trên phần tử nhóm / trên hàng
  2. Chênh lệch dựa trên phần tử spread / on cols
  3. Tổng hợp dựa trên phần tử tổng hợp / dữ liệu

Sau đây là cú pháp của toán tử PIVOT:

 CHỌN  TỪ  PIVOT ( () FOR  IN ()) AS ; 

Thiết kế của toán tử PIVOT yêu cầu bạn chỉ định rõ ràng các phần tử tập hợp và dàn trải, nhưng cho phép SQL Server tìm ra phần tử nhóm một cách ngầm định bằng cách loại bỏ. Bất kỳ cột nào xuất hiện trong bảng nguồn được cung cấp làm đầu vào cho toán tử PIVOT, chúng mặc nhiên trở thành phần tử nhóm.

Giả sử chẳng hạn bạn muốn truy vấn bảng Sales.Orders trong cơ sở dữ liệu mẫu TSQLV5. Bạn muốn trả lại ID người gửi hàng trên các hàng, năm vận chuyển trên các cột và tổng số đơn đặt hàng cho mỗi người gửi hàng và năm làm tổng hợp.

Nhiều người gặp khó khăn trong việc tìm ra cú pháp của toán tử PIVOT và điều này thường dẫn đến việc nhóm dữ liệu theo các phần tử không mong muốn. Như một ví dụ về nhiệm vụ của chúng tôi, giả sử rằng bạn không nhận ra rằng phần tử nhóm được xác định ngầm và bạn đưa ra truy vấn sau:

 CHỌN shipperid, [2017], [2018], [2019] TỪ Bộ phận bán hàng. ÁP DỤNG CHÉO đơn hàng (GIÁ TRỊ (NĂM (ngày vận chuyển))) NHƯ D (năm vận chuyển) PIVOT (COUNT (ngày vận chuyển) CHO năm vận chuyển VÀO ([2017] , [2018], [2019])) AS P; 

Chỉ có ba người gửi hàng hiện diện trong dữ liệu, với ID người gửi hàng là 1, 2 và 3. Vì vậy, bạn hy vọng sẽ chỉ thấy ba hàng trong kết quả. Tuy nhiên, kết quả truy vấn thực tế hiển thị nhiều hàng hơn:

 shipperid 2017 2018 2019 ------------- ---------------- -3 1 0 01 1 0 02 1 0 01 1 0 02 1 0 02 1 0 02 1 0 03 1 0 02 1 0 03 1 0 0 ... 3 0 1 03 0 1 03 0 1 01 0 1 03 0 1 01 0 1 03 0 1 03 0 1 03 0 1 01 0 1 0 ... 3 0 0 11 0 0 12 0 0 11 0 0 12 0 0 11 0 0 13 0 0 13 0 0 12 0 1 0 ... (830 hàng bị ảnh hưởng) 

Điều gì đã xảy ra?

Bạn có thể tìm ra manh mối sẽ giúp bạn tìm ra lỗi trong mã bằng cách xem kế hoạch truy vấn được hiển thị trong Hình 1.

Hình 1:Kế hoạch cho truy vấn tổng hợp với nhóm ẩn

Đừng để việc sử dụng toán tử ÁP DỤNG CROSS với mệnh đề VALUES trong truy vấn làm bạn bối rối. Điều này được thực hiện đơn giản để tính toán cột kết quả được vận chuyển trong năm dựa trên cột ngày vận chuyển nguồn và được xử lý bởi toán tử Tính vô hướng đầu tiên trong kế hoạch.

Bảng đầu vào cho toán tử PIVOT chứa tất cả các cột từ bảng Sales.Orders, cộng với cột kết quả được vận chuyển trong năm. Như đã đề cập, SQL Server xác định phần tử nhóm một cách ngầm định bằng cách loại bỏ dựa trên những gì bạn không chỉ định là phần tử tổng hợp (ngày vận chuyển) và phần tử dàn trải (năm vận chuyển). Có lẽ bạn đã trực giác mong đợi cột shipperid là cột nhóm vì nó xuất hiện trong danh sách CHỌN, nhưng như bạn có thể thấy trong kế hoạch, trên thực tế, bạn có một danh sách cột dài hơn nhiều, bao gồm orderid, là cột khóa chính trong bảng nguồn. Điều này có nghĩa là thay vì nhận được một hàng cho mỗi người giao hàng, bạn sẽ nhận được một hàng cho mỗi đơn hàng. Vì trong danh sách CHỌN, bạn chỉ chỉ định các cột shipperid, [2017], [2018] và [2019], bạn không thấy phần còn lại, điều này làm tăng thêm sự nhầm lẫn. Nhưng những người còn lại đã tham gia vào nhóm ngụ ý.

Điều gì có thể tuyệt vời là nếu cú ​​pháp của toán tử PIVOT hỗ trợ một mệnh đề trong đó bạn có thể chỉ ra rõ ràng phần tử nhóm / trên hàng. Một cái gì đó như thế này:

 CHỌN  TỪ  PIVOT ( () CHO  IN () TRÊN ROWS ) AS ; 

Dựa trên cú pháp này, bạn sẽ sử dụng mã sau để xử lý tác vụ của chúng tôi:

 CHỌN shipperid, [2017], [2018], [2019] TỪ Bộ phận bán hàng. ÁP DỤNG CHÉO đơn hàng (GIÁ TRỊ (NĂM (ngày vận chuyển))) NHƯ D (năm vận chuyển) PIVOT (COUNT (ngày vận chuyển) CHO năm vận chuyển VÀO ([2017] , [2018], [2019]) TRÊN ROWS shipperid) NHƯ P; 

Bạn có thể tìm thấy mục phản hồi với đề xuất cải thiện cú pháp của nhà điều hành PIVOT tại đây. Để thực hiện cải tiến này là một thay đổi không đột phá, điều khoản này có thể được thực hiện tùy chọn, với mặc định là hành vi hiện có. Có những đề xuất khác để cải thiện cú pháp của toán tử PIVOT bằng cách làm cho nó năng động hơn và bằng cách hỗ trợ nhiều tổng hợp.

Trong khi đó, có một phương pháp hay nhất có thể giúp bạn tránh lỗi. Sử dụng biểu thức bảng chẳng hạn như CTE hoặc bảng dẫn xuất trong đó bạn chỉ chiếu ba phần tử mà bạn cần tham gia vào hoạt động xoay vòng, sau đó sử dụng biểu thức bảng làm đầu vào cho toán tử PIVOT. Bằng cách này, bạn hoàn toàn kiểm soát phần tử nhóm. Đây là cú pháp chung sau phương pháp hay nhất này:

 WITH  AS (SELECT , ,  FROM ) CHỌN  FROM  PIVOT ( () FOR  IN ( )) NHƯ ; 

Được áp dụng cho nhiệm vụ của chúng tôi, bạn sử dụng mã sau:

 VỚI C AS (CHỌN shipperid, NĂM (ngày vận chuyển) NHƯ năm xuất xưởng, ngày vận chuyển TỪ Đơn đặt hàng. 2017], [2018], [2019])) AS P; 

Lần này, bạn chỉ nhận được ba hàng kết quả như mong đợi:

 shipperid 2017 2018 2019 ------------- ---------------- -3 51 125 731 36 130 792 56 143 116 

Một tùy chọn khác là sử dụng giải pháp tiêu chuẩn cũ và cổ điển để xoay vòng bằng cách sử dụng truy vấn được nhóm và biểu thức CASE, như sau:

 CHỌN shipperid, COUNT (TRƯỜNG HỢP KHI vận chuyển năm =2017 SAU 1 KẾT THÚC) NHƯ [2017], COUNT (TRƯỜNG HỢP KHI vận chuyển năm =2018 THÌ 1 KẾT THÚC) NHƯ [2018], COUNT (TRƯỜNG HỢP KHI vận chuyển năm =2019 SAU 1 KẾT THÚC) NHƯ [2019] TỪ Bộ phận bán hàng. ÁP DỤNG CHÉO đơn hàng (GIÁ TRỊ (NĂM (ngày vận chuyển))) NHƯ D (năm vận chuyển) NƠI ngày vận chuyển KHÔNG PHẢI LÀ NULLGROUP BỞI shipperid; 

Với cú pháp này, tất cả ba bước xoay vòng và các phần tử liên quan của chúng phải rõ ràng trong mã. Tuy nhiên, khi bạn có một số lượng lớn các giá trị trải rộng, cú pháp này có xu hướng dài dòng. Trong những trường hợp như vậy, mọi người thường thích sử dụng toán tử PIVOT.

Loại bỏ hoàn toàn NULL với UNPIVOT

Mục tiếp theo trong bài viết này là một cạm bẫy hơn là một lỗi. Nó liên quan đến toán tử T-SQL UNPIVOT độc quyền, cho phép bạn giải nén dữ liệu từ trạng thái cột sang trạng thái hàng.

Tôi sẽ sử dụng bảng có tên là CustOrders làm dữ liệu mẫu của mình. Sử dụng mã sau để tạo, điền và truy vấn bảng này để hiển thị nội dung của nó:

 DROP TABLE NẾU TỒN TẠI dbo.CustOrders; ĐI VỚI C AS (CHỌN custid, NĂM (ngày đặt hàng) NHƯ đơn hàng năm, giá trị TỪ Sales.OrderValues) CHỌN custid, [2017], [2018], [2019] VÀO dbo.CustOrdersFROM C PIVOT (SUM (val) FOR orderyearyear IN ([2017], [2018], [2019])) AS P; CHỌN * TỪ dbo.CustOrders; 

Mã này tạo ra kết quả sau:

 custid 2017 2018 2019 ------------- ---------------- 1 NULL 2022.50 2250.502 88.80 799.75 514.403 403.20 5960.78 660.004 1379.00 6406.90 5604.755 4324.40 13849.02 6754.166 NULL 1079.80 2160.007 9986.20 7817.88 730.008 982.00 3026.85 224.009 4074.28 11208.36 6680.6110 1832.80 7630.25 11338.5611 479.40 3179.50 2431.0012 NULL 238.00 1576.8013 100.80 NULL NULL14 1674.22 6516.40 4158.2615 2169.00 1128.00 513.7516 NULL 787.60 931.5017 533.60 420.00 2809.6118 268.80 487.00 860.1019 950.00 4514,35 9296,6920 15568,07 48096,27 41210,65 ... 

Bảng này chứa tổng giá trị đơn đặt hàng cho mỗi khách hàng và năm. NULL đại diện cho các trường hợp khách hàng không có bất kỳ hoạt động đặt hàng nào trong năm mục tiêu.

Giả sử rằng bạn muốn bỏ chia dữ liệu khỏi bảng CustOrders, trả về một hàng cho mỗi khách hàng và năm, với cột kết quả được gọi là val chứa tổng giá trị đơn đặt hàng cho khách hàng hiện tại và năm. Mọi tác vụ giải nén thường liên quan đến ba yếu tố:

  1. Tên của các cột nguồn hiện có mà bạn đang bỏ chia sẻ:[2017], [2018], [2019] trong trường hợp của chúng tôi
  2. Tên bạn gán cho cột mục tiêu sẽ giữ các tên cột nguồn:trong trường hợp của chúng tôi là orderyear
  3. Tên bạn gán cho cột mục tiêu sẽ chứa các giá trị của cột nguồn:val trong trường hợp của chúng tôi

Nếu bạn quyết định sử dụng toán tử UNPIVOT để xử lý tác vụ giải nén, trước tiên bạn phải tìm ra ba phần tử trên, sau đó sử dụng cú pháp sau:

 CHỌN , ,  FROM  UNPIVOT ( FOR  IN ()) AS ; 

Được áp dụng cho nhiệm vụ của chúng tôi, bạn sử dụng truy vấn sau:

 SELECT custid, orderyear, valFROM dbo.CustOrders UNPIVOT (val FOR orderyear IN ([2017], [2018], [2019])) AS U; 

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

 custid orderyear val ----------------------------- 1 2018 2022.501 2019 2250.502 2017 88.802 2018 799.752 2019 514.403 2017 403.203 2018 5960.783 2019 660.004 2017 1379.004 2018 6406.904 2019 5604.755 2017 4324.405 2018 13849.025 2019 6754.166 2018 1079.806 2019 2160.007 2017 9986.207 2018 7817.887 2019 730.00 ... 

Nhìn vào dữ liệu nguồn và kết quả truy vấn, bạn có nhận thấy điều gì còn thiếu không?

Thiết kế của toán tử UNPIVOT liên quan đến việc loại bỏ ngầm định các hàng kết quả có NULL trong cột giá trị — val trong trường hợp của chúng ta. Nhìn vào kế hoạch thực thi cho truy vấn này được hiển thị trong Hình 2, bạn có thể thấy toán tử Bộ lọc loại bỏ các hàng có NULL trong cột val (Expr1007 trong kế hoạch).

Hình 2:Kế hoạch hủy chia truy vấn với việc loại bỏ ngầm các NULL

Đôi khi hành vi này là mong muốn, trong trường hợp đó, bạn không cần phải làm bất cứ điều gì đặc biệt. Vấn đề là đôi khi bạn muốn giữ các hàng có NULL. Cạm bẫy là khi bạn muốn giữ lại các NULL và thậm chí bạn không nhận ra rằng toán tử UNPIVOT được thiết kế để loại bỏ chúng.

Điều tuyệt vời là nếu toán tử UNPIVOT có một mệnh đề tùy chọn cho phép bạn chỉ định xem bạn muốn xóa hay giữ NULL, với điều khoản trước đây là mặc định cho khả năng tương thích ngược. Dưới đây là một ví dụ về cú pháp này có thể trông như thế nào:

 SELECT , ,  FROM  UNPIVOT ( FOR  IN () [XÓA NULLS | GIỮ NULLS]) AS ; 

Nếu bạn muốn giữ NULL, dựa trên cú pháp này, bạn sẽ sử dụng truy vấn sau:

 SELECT custid, orderyear, valFROM dbo.CustOrders UNPIVOT (val FOR orderyear IN ([2017], [2018], [2019]) KEEP NULLS) AS U; 

Bạn có thể tìm thấy mục phản hồi với đề xuất để cải thiện cú pháp của toán tử UNPIVOT theo cách này tại đây.

Trong khi đó, nếu bạn muốn giữ các hàng có NULL, bạn phải đưa ra giải pháp thay thế. Nếu bạn khăng khăng sử dụng toán tử UNPIVOT, bạn cần áp dụng hai bước. Trong bước đầu tiên, bạn xác định một biểu thức bảng dựa trên truy vấn sử dụng hàm ISNULL hoặc COALESCE để thay thế NULL trong tất cả các cột không được phân chia bằng một giá trị thường không thể xuất hiện trong dữ liệu, ví dụ:-1 trong trường hợp của chúng tôi. Trong bước thứ hai, bạn sử dụng hàm NULLIF trong truy vấn bên ngoài đối với cột giá trị để thay thế -1 trở lại bằng NULL. Đây là mã giải pháp hoàn chỉnh:

 WITH C AS (SELECT custid, ISNULL ([2017], -1.0) AS [2017], ISNULL ([2018], -1.0) AS [2018], ISNULL ([2019], -1.0) AS [2019 ] FROM dbo.CustOrders) CHỌN custid, orderyear, NULLIF (val, -1.0) AS valFROM C UNPIVOT (val FOR orderyear IN ([2017], [2018], [2019])) AS U; 

Đây là kết quả của truy vấn này cho thấy rằng các hàng có NULL trong cột val được giữ nguyên:

 custid orderyear val ----------------------------- 1 2017 NULL1 2018 2022.501 2019 2250.502 2017 88.802 2018 799.752 2019 514.403 2017 403.203 2018 5960.783 2019 660.004 2017 1379.004 2018 6406.904 2019 5604.755 2017 4324.405 2018 13849.025 2019 6754.166 2017 NULL6 2018 1079.806 2019 2160.007 2017 9986.207 2018 7817.887 2019 730.00 ... 

Cách tiếp cận này rất khó xử lý, đặc biệt là khi bạn có một số lượng lớn các cột cần bỏ chia.

Một giải pháp thay thế sử dụng kết hợp toán tử ÁP DỤNG và mệnh đề GIÁ TRỊ. Bạn tạo một hàng cho mỗi cột chưa được phân chia, với một cột đại diện cho cột tên mục tiêu (trong trường hợp của chúng tôi là thứ tự) và một cột khác đại diện cho cột giá trị mục tiêu (trong trường hợp của chúng tôi là val). Bạn cung cấp năm không đổi cho cột tên và cột nguồn tương quan có liên quan cho cột giá trị. Đây là mã giải pháp hoàn chỉnh:

 SELECT custid, orderyear, valFROM dbo.CustOrders CROSS ÁP DỤNG (VALUES (2017, [2017]), (2018, [2018]), (2019, [2019])) AS A (orderyear, val);  

Điều thú vị ở đây là trừ khi bạn quan tâm đến việc loại bỏ các hàng có NULL trong cột val, bạn không cần phải làm bất kỳ điều gì đặc biệt. Không có bước ẩn nào ở đây xóa các hàng có NULLS. Hơn nữa, vì bí danh cột val được tạo như một phần của mệnh đề FROM, nên mệnh đề WHERE có thể truy cập được. Vì vậy, nếu bạn quan tâm đến việc loại bỏ NULL, bạn có thể nói rõ về nó trong mệnh đề WHERE bằng cách tương tác trực tiếp với bí danh cột giá trị, như sau:

 CHỌN custid, orderyear, valFROM dbo. KHÔNG ĐẦY ĐỦ; 

Vấn đề là cú pháp này cho phép bạn kiểm soát việc bạn muốn giữ hay xóa NULL. Nó linh hoạt hơn toán tử UNPIVOT theo một cách khác, cho phép bạn xử lý nhiều biện pháp chưa được phân chia, chẳng hạn như cả val và qty. Mặc dù vậy, trọng tâm của tôi trong bài viết này là cạm bẫy liên quan đến NULL nên tôi không đi sâu vào khía cạnh này.

Kết luận

Việc thiết kế các toán tử PIVOT và UNPIVOT đôi khi dẫn đến lỗi và cạm bẫy trong mã của bạn. Cú pháp của toán tử PIVOT không cho phép bạn chỉ ra thành phần nhóm một cách rõ ràng. Nếu bạn không nhận ra điều này, bạn có thể kết thúc với các phần tử nhóm không mong muốn. Phương pháp hay nhất là bạn nên sử dụng biểu thức bảng làm đầu vào cho toán tử PIVOT và đây là lý do tại sao kiểm soát rõ ràng phần tử nhóm là gì.

Cú pháp của toán tử UNPIVOT không cho phép bạn kiểm soát việc xóa hay giữ các hàng có NULL trong cột giá trị kết quả. Để giải quyết vấn đề này, bạn có thể sử dụng giải pháp khó xử với hàm ISNULL và NULLIF hoặc giải pháp dựa trên toán tử ÁP DỤNG và mệnh đề GIÁ TRỊ.

Tôi cũng đã đề cập đến hai mục phản hồi với các đề xuất để cải thiện toán tử PIVOT và UNPIVOT với các tùy chọn rõ ràng hơn để kiểm soát hành vi của toán tử và các phần tử của 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. SQL, thêm dữ liệu vào bảng

  2. Câu hỏi phỏng vấn kỹ sư dữ liệu với Python

  3. Người thiết kế cơ sở dữ liệu làm gì?

  4. Khóa ứng viên trong thiết kế cơ sở dữ liệu là gì?

  5. Giảm thiểu tác động của việc mở rộng cột IDENTITY - phần 2