Bài viết này là phần thứ ba 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 và các truy vấn con. Lần này tôi tập trung vào việc tham gia. Một số lỗi và các phương pháp hay nhất mà tôi đề cập ở đây là kết quả của một cuộc khảo sát mà tôi đã thực hiện giữa các MVP đồng nghiệp. 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 đã cung cấp thông tin chi tiế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.
Trong bài viết này, tôi tập trung vào bốn lỗi phổ biến cổ điển:COUNT (*) trong các phép nối bên ngoài, gộp kép, mâu thuẫn ON-WHERE và mâu thuẫn nối OUTER-INNER. Tất cả các lỗi này đều liên quan đến các nguyên tắc cơ bản về truy vấn T-SQL và dễ dàng tránh được nếu bạn làm theo các phương pháp hay nhất đơn giản.
COUNT (*) trong các liên kết bên ngoài
Lỗi đầu tiên của chúng tôi liên quan đến số lượng không chính xác được báo cáo cho các nhóm trống do sử dụng liên kết ngoài và tổng hợp COUNT (*). Hãy xem xét truy vấn sau đây tính toán số lượng đơn đặt hàng và tổng số tiền vận chuyển cho mỗi khách hàng:
SỬ DỤNG TSQLV5; - http://tsql.solidq.com/SampleDatabases/TSQLV5.zip CHỌN custid, COUNT (*) AS số lượng, SUM (cước phí) NHƯ tổng trọng lượng từ Sales.Orders GROUP BY custid ORDER BY custid;
Truy vấn này tạo ra kết quả sau (viết tắt):
custid numorders totalfreight ------------- ---------------- 1 6 225,58 2 4 97,42 3 7 268,52 4 13 471,95 5 18 1559,52 ... 21 7 232,75 23 5 637,94 ... 56 10 862,74 58 6 277,96 ... 87 15 822,48 88 9 194,71 89 14 1353,06 90 7 88,41 91 7 175,74 (89 hàng bị ảnh hưởng)
Có 91 khách hàng hiện đang có mặt trong bảng Khách hàng, trong đó 89 khách hàng đã đặt hàng; do đó kết quả đầu ra của truy vấn này hiển thị 89 nhóm khách hàng và số lượng đơn đặt hàng chính xác của họ và tổng số tiền vận chuyển. Khách hàng có ID 22 và 57 có mặt trong bảng Khách hàng nhưng chưa đặt bất kỳ đơn hàng nào và do đó họ không hiển thị trong kết quả.
Giả sử rằng bạn được yêu cầu đưa những khách hàng không có bất kỳ đơn hàng liên quan nào vào kết quả truy vấn. Điều tự nhiên cần làm trong trường hợp này là thực hiện kết nối bên ngoài bên trái giữa Khách hàng và Đơn đặt hàng để bảo toàn những khách hàng không có đơn đặt hàng. Tuy nhiên, một lỗi điển hình khi chuyển đổi giải pháp hiện có thành giải pháp áp dụng phép kết hợp là để tính toán số lượng đơn hàng là COUNT (*), như được hiển thị trong truy vấn sau (gọi nó là Truy vấn 1):
SELECT C.custid, COUNT (*) AS numorders, SUM (O.freight) AS totalfreight TỪ Bán hàng. Khách hàng NHƯ C TRÁI RA THAM GIA Bán hàng. Đơn đặt hàng AS O ON C.custid =O.custid GROUP BY C.custid ĐẶT HÀNG BỞI C.custid;
Truy vấn này tạo ra kết quả sau:
custid numorders totalfreight ------------- ---------------- 1 6 225,58 2 4 97,42 3 7 268,52 4 13 471,95 5 18 1559,52 ... 21 7 232,75 22 1 NULL 23 5 637,94 ... 56 10 862,74 57 1 NULL 58 6 277,96 ... 87 15 822,48 88 9 194,71 89 14 1353,06 90 7 88,41 91 7 175,74 (91 hàng bị ảnh hưởng)Quan sát rằng khách hàng 22 và 57 lần này xuất hiện trong kết quả, nhưng số lượng đơn đặt hàng của họ hiển thị 1 thay vì 0 vì COUNT (*) đếm hàng chứ không phải đơn đặt hàng. Tổng cước phí được báo cáo chính xác vì SUM (cước phí) bỏ qua đầu vào NULL.
Kế hoạch cho truy vấn này được thể hiện trong Hình 1.
Hình 1:Kế hoạch cho Truy vấn 1
Trong kế hoạch này, Expr1002 đại diện cho số lượng hàng trên mỗi nhóm, do kết quả của phép nối bên ngoài, ban đầu được đặt thành NULL cho những khách hàng không có lệnh khớp. Toán tử Tính vô hướng ngay bên dưới nút CHỌN gốc sau đó chuyển NULL thành 1. Đó là kết quả của việc đếm các hàng thay vì đếm các lệnh.
Để sửa lỗi này, bạn muốn áp dụng tổng hợp COUNT cho một phần tử từ phía không được đặt trước của phép nối bên ngoài và bạn muốn đảm bảo rằng sử dụng cột không thể NULL làm đầu vào. Cột khóa chính sẽ là một lựa chọn tốt. Đây là truy vấn giải pháp (gọi là Truy vấn 2) với lỗi đã được sửa:
SELECT C.custid, COUNT (O.orderid) AS numorders, SUM (O.freight) AS totalfreight TỪ Bán hàng. Khách hàng AS C TRÁI NGOÀI THAM GIA Bán hàng. Đơn hàng AS O ON C.custid =O.custid GROUP BY C .custid ĐẶT HÀNG CỦA C.custid;Đây là kết quả của truy vấn này:
custid numorders totalfreight ------------- ---------------- 1 6 225,58 2 4 97,42 3 7 268,52 4 13 471,95 5 18 1559,52 ... 21 7 232,75 22 0 NULL 23 5 637,94 ... 56 10 862,74 57 0 NULL 58 6 277,96 ... 87 15 822,48 88 9 194,71 89 14 1353,06 90 7 88,41 91 7 175,74 (91 hàng bị ảnh hưởng)Quan sát rằng lần này khách hàng 22 và 57 hiển thị số đếm chính xác là 0.
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
Bạn cũng có thể thấy sự thay đổi trong kế hoạch, trong đó NULL đại diện cho số lượng khách hàng không có lệnh khớp được chuyển đổi thành 0 chứ không phải 1 lần này.
Khi sử dụng phép nối, hãy thận trọng với việc áp dụng tổng hợp COUNT (*). Khi sử dụng các phép nối bên ngoài, nó thường là một lỗi. Phương pháp hay nhất là áp dụng tổng hợp COUNT cho một cột không thể NULL từ nhiều phía của phép nối một-nhiều. Cột khóa chính là một lựa chọn tốt cho mục đích này vì nó không cho phép NULL. Đây có thể là một phương pháp hay ngay cả khi sử dụng các phép nối bên trong, vì bạn không bao giờ biết liệu sau này bạn có cần thay đổi phép nối bên trong thành phép nối bên ngoài do thay đổi yêu cầu hay không.
Tổng hợp nhúng kép
Lỗi thứ hai của chúng tôi cũng liên quan đến việc trộn các liên kết và tổng hợp, lần này sẽ tính đến các giá trị nguồn nhiều lần. Hãy xem xét truy vấn sau đây làm ví dụ:
CHỌN C.custid, COUNT (O.orderid) AS số, SUM (O.freight) AS totalfreight, CAST (SUM (OD.qty * OD.unitprice * (1 - OD.discount)) AS SỐ (12 , 2)) NHƯ tổng giá trị TỪ Bán hàng. Khách hàng NHƯ C TRÁI RA THAM GIA Bán hàng. Đơn đặt hàng NHƯ O TRÊN C.custid =O.custid TRÁI OUTER THAM GIA Bán hàng.OrderDetails AS OD ON O.orderid =OD.orderid GROUP BY C.custid ORDER THEO C.custid;Truy vấn này kết hợp với Khách hàng, Đơn đặt hàng và Chi tiết đơn hàng, nhóm các hàng theo custid và được cho là tính toán các tổng hợp như số đơn đặt hàng, tổng cước phí và tổng giá trị trên mỗi khách hàng. Truy vấn này tạo ra kết quả sau:
custid numorders totalfreight totalval ------------- ---------------- 1 12 419,60 4273,00 2 10 306,59 1402,95 3 17 667,29 7023,98 4 30 1447,14 13390,65 5 52 4835,18 24927,58 ... 87 37 2611,93 15648,70 88 19 546,96 6068,20 89 40 4017,32 27363,61 90 17 262,16 3161,35 91 16 461,53 3531,95Bạn có thể phát hiện ra lỗi ở đây không?
Tiêu đề đơn hàng được lưu trữ trong bảng Đơn hàng và các dòng đơn hàng tương ứng của chúng được lưu trữ trong bảng Chi tiết đơn hàng. Khi bạn nối các tiêu đề đơn hàng với các dòng đặt hàng tương ứng của chúng, tiêu đề được lặp lại trong kết quả của phép nối trên mỗi dòng. Do đó, tổng hợp COUNT (O.orderid) phản ánh không chính xác số lượng dòng đặt hàng chứ không phải số lượng đơn đặt hàng. Tương tự, SUM (O.freight) tính không chính xác tiền cước vận chuyển nhiều lần cho mỗi đơn đặt hàng — nhiều như số dòng đặt hàng trong đơn đặt hàng. Phép tính tổng hợp chính xác duy nhất trong truy vấn này là phép tính được sử dụng để tính tổng giá trị vì nó được áp dụng cho các thuộc tính của các dòng đơn hàng:SUM (OD.qty * OD.unitprice * (1 - OD.discount).
Để có được số lượng đơn hàng chính xác, chỉ cần sử dụng tổng hợp số lượng riêng biệt:COUNT (DISTINCT O.orderid). Bạn có thể nghĩ rằng bản sửa lỗi tương tự có thể được áp dụng cho việc tính toán tổng cước phí, nhưng điều này sẽ chỉ tạo ra một lỗi mới. Dưới đây là truy vấn của chúng tôi với các tổng hợp riêng biệt được áp dụng cho các thước đo của tiêu đề đơn đặt hàng:
SELECT C.custid, COUNT (DISTINCT O.orderid) AS numorders, SUM (DISTINCT O.freight) AS totalfreight, CAST (SUM (OD.qty * OD.unitprice * (1 - OD.discount)) AS SỐ (12, 2)) AS totalval FROM Sales.Các khách hàng AS C TRÁI OUTER THAM GIA Sales.Orders AS O ON C.custid =O.custid LEFT OUTER JOIN Sales.OrderDetails AS OD ON O.orderid =OD.orderid GROUP BY C. custid ĐẶT HÀNG CỦA C.custid;Truy vấn này tạo ra kết quả sau:
custid numorders totalfreight totalval ------------- ---------------- 1 6 225,58 4273,00 2 4 97,42 1402,95 3 7 268,52 7023,98 4 13 448,23 13390,65 ***** 5 18 1559,52 24927,58 ... 87 15 822,48 15648,70 88 9 194,71 6068,20 89 14 1353,06 27363,61 90 7 87,66 3161,35 ***** 91 7 175,74 3531,95Số lượng đơn hàng hiện là chính xác, nhưng tổng giá trị hàng hóa thì không. Bạn có thể phát hiện ra lỗi mới không?
Lỗi mới khó nắm bắt hơn vì nó chỉ xuất hiện khi cùng một khách hàng có ít nhất một trường hợp nhiều đơn đặt hàng có cùng giá trị cước vận chuyển. Trong trường hợp như vậy, bây giờ bạn đang tính cước vận chuyển chỉ một lần cho mỗi khách hàng, chứ không phải một lần cho mỗi đơn hàng như bạn nên làm.
Sử dụng truy vấn sau (yêu cầu SQL Server 2017 trở lên) để xác định các giá trị cước phí riêng cho cùng một khách hàng:
WITH C AS (CHỌN custid, cước phí, STRING_AGG (CAST (orderid NHƯ VARCHAR (MAX)), ',') TRONG NHÓM (ORDER BY orderid) NHƯ đơn đặt hàng TỪ Sales. NHÓM ĐƠN HÀNG THEO custid, cước phí CÓ ĐẾM (* )> 1) CHỌN custid, STRING_AGG (CONCAT ('(cước:', cước vận chuyển, ', đơn hàng:', đơn hàng, ')'), ',') làm bản sao TỪ C GROUP BY custid;Truy vấn này tạo ra kết quả sau:
custid trùng lặp -------------------------------------------------- - 4 (cước:23,72, đơn hàng:10743, 10953) 90 (cước:0,75, đơn hàng:10615, 11005)Với những phát hiện này, bạn nhận ra rằng truy vấn có lỗi đã báo cáo tổng giá trị cước vận chuyển không chính xác cho khách hàng 4 và 90. Truy vấn đã báo cáo tổng giá trị cước phí chính xác cho những khách hàng còn lại vì giá trị cước vận chuyển của họ là duy nhất.
Để khắc phục lỗi, bạn cần tách việc tính toán tổng hợp các đơn đặt hàng và các dòng đặt hàng thành các bước khác nhau bằng cách sử dụng biểu thức bảng, như sau:
WITH O AS (SELECT custid, COUNT (orderid) AS numorders, SUM (cước phí) AS tổng giá trị từ Sales.Orders GROUP BY custid), OD AS (SELECT O.custid, CAST (SUM (OD.qty * OD). unitprice * (1 - OD.discount)) AS NUMERIC (12, 2)) AS totalval FROM Sales.Orders AS O INNER JOIN Sales.OrderDetails AS OD ON O.orderid =OD.orderid GROUP BY O.custid) CHỌN C. custid, O.numorders, O.totalfreight, OD.totalval TỪ Bán hàng. Khách hàng NHƯ C TRÁI RA THAM GIA O ON C.custid =O.custid LEFT OUTER JOIN OD ON C.custid =OD.custid ORDER BY C.custid;Truy vấn này tạo ra kết quả sau:
custid numorders totalfreight totalval ------------- ---------------- 1 6 225,58 4273,00 2 4 97,42 1402,95 3 7 268,52 7023,98 4 13 471,95 13390,65 ***** 5 18 1559,52 24927,58 ... 87 15 822,48 15648,70 88 9 194,71 6068,20 89 14 1353,06 27363,61 90 7 88,41 3161,35 ***** 91 7 175,74 3531,95Quan sát tổng giá trị cước vận chuyển của khách hàng 4 và 90 hiện đã cao hơn. Đây là những con số chính xác.
Thực tiễn tốt nhất ở đây là hãy lưu tâm khi tham gia và tổng hợp dữ liệu. Bạn muốn cảnh giác với những trường hợp như vậy khi tham gia nhiều bảng và áp dụng tổng hợp để đo lường từ một bảng không phải là cạnh hoặc lá, bảng trong các phép nối. Trong trường hợp như vậy, bạn thường cần áp dụng các phép tính tổng hợp trong các biểu thức bảng và sau đó nối các biểu thức bảng.
Vì vậy, lỗi tổng hợp nhúng kép đã được sửa. Tuy nhiên, có thể có một lỗi khác trong truy vấn này. Bạn có thể phát hiện ra nó không? Tôi sẽ cung cấp thông tin chi tiết về một lỗi tiềm ẩn như trường hợp thứ tư mà tôi sẽ đề cập ở phần sau trong “Xung đột tham gia OUTER-INNER.”
mâu thuẫn ON-WHERE
Lỗi thứ ba của chúng tôi là kết quả của việc nhầm lẫn các vai trò mà mệnh đề ON và WHERE phải đóng. Ví dụ:giả sử bạn được giao nhiệm vụ đối sánh khách hàng và đơn đặt hàng mà họ đã đặt kể từ ngày 12 tháng 2 năm 2019, nhưng cũng bao gồm những khách hàng đầu ra không đặt hàng kể từ đó. Bạn cố gắng giải quyết nhiệm vụ bằng cách sử dụng truy vấn sau (gọi nó là Truy vấn 3):
CHỌN C.custid, C.companyname, O.orderid, O.orderdate FROM Sales.Các khách hàng AS C LEFT OUTER THAM GIA Sales.Orders AS O ON O.custid =C.custid WHERE O.orderdate> ='20190212';Khi sử dụng phép nối bên trong, cả ON và WHERE đều đóng vai trò lọc giống nhau và do đó, bạn tổ chức các vị từ giữa các mệnh đề này như thế nào không quan trọng. Tuy nhiên, khi sử dụng một phép nối ngoài như trong trường hợp của chúng ta, các mệnh đề này có nghĩa khác.
Mệnh đề ON đóng vai trò đối sánh, có nghĩa là tất cả các hàng từ phía được bảo toàn của phép nối (Khách hàng trong trường hợp của chúng tôi) sẽ được trả về. Các kết quả phù hợp dựa trên vị từ BẬT được kết nối với các kết quả phù hợp của chúng và kết quả là lặp lại mỗi trận đấu. Những cái không có bất kỳ kết quả phù hợp nào được trả về với NULL dưới dạng trình giữ chỗ trong thuộc tính của bên không được đặt trước.
Ngược lại, mệnh đề WHERE, đóng vai trò lọc đơn giản hơn — luôn luôn. Điều này có nghĩa là các hàng mà vị từ lọc đánh giá là true sẽ được trả về và tất cả các hàng còn lại sẽ bị loại bỏ. Do đó, một số hàng từ phía được bảo toàn của phép nối có thể bị xóa hoàn toàn.
Hãy nhớ rằng các thuộc tính từ phía không được đặt trước của phép nối bên ngoài (Đơn hàng trong trường hợp của chúng tôi) được đánh dấu là NULL cho các hàng bên ngoài (không khớp). Bất cứ khi nào bạn áp dụng bộ lọc liên quan đến một phần tử từ phía không được đặt trước của phép nối, vị từ bộ lọc sẽ đánh giá là không xác định cho tất cả các hàng bên ngoài, dẫn đến việc loại bỏ chúng. Điều này phù hợp với logic vị từ ba giá trị mà SQL tuân theo. Một cách hiệu quả, kết quả sẽ trở thành một liên kết bên trong. Một ngoại lệ đối với quy tắc này là khi bạn đặc biệt tìm kiếm một NULL trong một phần tử từ phía không đặt trước để xác định các điểm không khớp (phần tử LÀ NULL).
Truy vấn lỗi của chúng tôi tạo ra kết quả sau:
custid companyname orderid orderdate ----------------- ---------------- 1 Khách hàng NRZBB 11011 2019-04-09 1 Khách hàng NRZBB 10952 2019-03-16 2 Khách hàng MLTDN 10926 2019-03-04 4 Khách hàng HFBZG 11016 2019-04-10 4 Khách hàng HFBZG 10953 2019-03-16 4 Khách hàng HFBZG 10920 2019-03- 03 5 Khách hàng HGVLZ 10924 2019-03-04 6 Khách hàng XHXJV 11058 2019-04-29 6 Khách hàng XHXJV 10956 2019-03-17 8 Khách hàng QUHWH 10970 2019-03-24 ... 20 Khách hàng THHDP 10979 2019-03-26 20 Khách hàng THHDP 10968 2019-03-23 20 Khách hàng THHDP 10895 2019-02-18 24 Khách hàng CYZTN 11050 2019-04-27 24 Khách hàng CYZTN 11001 2019-04-06 24 Khách hàng CYZTN 10993 2019-04-01 ... (195 hàng bị ảnh hưởng)Đầu ra mong muốn được cho là có 213 hàng, bao gồm 195 hàng đại diện cho các đơn đặt hàng đã được đặt kể từ ngày 12 tháng 2 năm 2019 và 18 hàng bổ sung đại diện cho những khách hàng đã không đặt hàng kể từ đó. Như bạn có thể thấy, sản lượng thực tế không bao gồm những khách hàng chưa đặt hàng kể từ ngày được chỉ định.
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 3
Quan sát rằng trình tối ưu hóa đã phát hiện ra mâu thuẫn và chuyển đổi nội bộ liên kết bên ngoài thành liên kết bên trong. Điều đó thật tốt khi thấy điều đó, nhưng đồng thời cũng là một dấu hiệu rõ ràng rằng có một lỗi trong truy vấn.
Tôi đã gặp trường hợp mọi người cố gắng sửa lỗi bằng cách thêm vị từ HOẶC O.orderid LÀ KHÔNG ĐỦ vào mệnh đề WHERE, như sau:
CHỌN C.custid, C.companyname, O.orderid, O.orderdate FROM Sales.Các khách hàng AS C LEFT OUTER THAM GIA Sales.Orders AS O ON O.custid =C.custid WHERE O.orderdate> ='20190212' HOẶC O.orderid LÀ KHÔNG;Vị từ phù hợp duy nhất là vị từ so sánh ID khách hàng từ hai bên. Vì vậy, bản thân tham gia trả về những khách hàng đã đặt hàng nói chung, cùng với các lệnh khớp của họ, cũng như những khách hàng hoàn toàn không đặt hàng, với NULL trong thuộc tính đơn đặt hàng của họ. Sau đó, các vị từ lọc sẽ lọc những khách hàng đã đặt hàng kể từ ngày được chỉ định, cũng như những khách hàng chưa đặt hàng (khách hàng 22 và 57). Truy vấn thiếu những khách hàng đã đặt một số đơn đặt hàng, nhưng không phải kể từ ngày được chỉ định!
Truy vấn này tạo ra kết quả sau:
custid companyname orderid orderdate ----------------- ---------------- 1 Khách hàng NRZBB 11011 2019-04-09 1 Khách hàng NRZBB 10952 2019-03-16 2 Khách hàng MLTDN 10926 2019-03-04 4 Khách hàng HFBZG 11016 2019-04-10 4 Khách hàng HFBZG 10953 2019-03-16 4 Khách hàng HFBZG 10920 2019-03- 03 5 Khách hàng HGVLZ 10924 2019-03-04 6 Khách hàng XHXJV 11058 2019-04-29 6 Khách hàng XHXJV 10956 2019-03-17 8 Khách hàng QUHWH 10970 2019-03-24 ... 20 Khách hàng THHDP 10979 2019-03-26 20 Khách hàng THHDP 10968 2019-03-23 20 Khách hàng THHDP 10895 2019-02-18 22 Khách hàng DTDMN NULL NULL 24 Khách hàng CYZTN 11050 2019-04-27 24 Khách hàng CYZTN 11001 2019-04-06 24 Khách hàng CYZTN 10993 2019-04-01. .. (197 hàng bị ảnh hưởng)Để sửa lỗi một cách chính xác, bạn cần cả vị từ so sánh ID khách hàng từ hai phía và vị từ so với ngày đặt hàng để được coi là các vị từ phù hợp. Để đạt được điều này, cả hai cần phải được chỉ định trong mệnh đề ON, như vậy (gọi Câu hỏi này là 4):
CHỌN C.custid, C.companyname, O.orderid, O.orderdate FROM Sales.Các khách hàng AS C LEFT OUTER THAM GIA Bán hàng. Đơn hàng AS O ON O.custid =C.custid AND O.orderdate> ='20190212';Truy vấn này tạo ra kết quả sau:
custid companyname orderid orderdate ----------------- ---------------- 1 Khách hàng NRZBB 11011 2019-04-09 1 Khách hàng NRZBB 10952 2019-03-16 2 Khách hàng MLTDN 10926 2019-03-04 3 Khách hàng KBUDE NULL NULL 4 Khách hàng HFBZG 11016 2019-04-10 4 Khách hàng HFBZG 10953 2019-03-16 4 Khách hàng HFBZG 10920 2019-03-03 5 Khách hàng HGVLZ 10924 2019-03-04 6 Khách hàng XHXJV 11058 2019-04-29 6 Khách hàng XHXJV 10956 2019-03-17 7 Khách hàng QXVLA NULL NULL 8 Khách hàng QUHWH 10970 2019-03-24 ... 20 Khách hàng THHDP 10979 2019-03-26 20 Khách hàng THHDP 10968 2019-03-23 20 Khách hàng THHDP 10895 2019-02-18 21 Khách hàng KIDPX NULL NULL 22 Khách hàng DTDMN NULL 23 Khách hàng WVFAF NULL 24 Khách hàng CYZTN 11050 2019-04- 27 24 Khách hàng CYZTN 11001 2019-04-06 24 Khách hàng CYZTN 10993 2019-04-01 ... (213 hàng bị ảnh hưởng)Kế hoạch cho truy vấn này được thể hiện trong Hình 4.
Hình 4:Kế hoạch cho Truy vấn 4
Như bạn có thể thấy, lần này trình tối ưu hóa đã xử lý phép nối như một phép nối bên ngoài.
Đây là một truy vấn rất đơn giản mà tôi đã sử dụng cho mục đích minh họa. Với các truy vấn phức tạp và phức tạp hơn nhiều, ngay cả những nhà phát triển có kinh nghiệm cũng có thể gặp khó khăn trong việc tìm ra một vị từ thuộc mệnh đề ON hay mệnh đề WHERE. Điều làm cho mọi thứ trở nên dễ dàng đối với tôi là chỉ cần tự hỏi bản thân xem vị từ là vị từ phù hợp hay là vị từ lọc. Nếu cái trước, nó thuộc mệnh đề ON; nếu cái sau, nó thuộc mệnh đề WHERE.
Xung đột tham gia OUTER-INNER
Lỗi thứ tư và cuối cùng của chúng tôi là một biến thể của lỗi thứ ba. Điều này thường xảy ra trong các truy vấn nhiều kết hợp trong đó bạn kết hợp các loại kết hợp. Ví dụ:giả sử bạn cần tham gia các bảng Khách hàng, Đơn đặt hàng, Chi tiết đơn hàng, Sản phẩm và Nhà cung cấp để xác định các cặp khách hàng-nhà cung cấp đã có hoạt động chung. Bạn viết truy vấn sau (gọi nó là Truy vấn 5):
CHỌN DISTINCT C.custid, C.companyname AS customer, S.supplierid, S.companyname AS nhà cung cấp TỪ Bán hàng. Khách hàng AS C INNER Tham gia Bán hàng. Đơn hàng AS O ON O.custid =C.custid INNER JOIN Sales.OrderDetails AS OD ON OD.orderid =O.orderid INNER JOIN Production. Sản phẩm AS P ON P.productid =OD.productid INNER JOIN Production.Supplier AS S ON S.supplierid =P.supplierid;Truy vấn này tạo ra kết quả sau với 1.236 hàng:
custid khách hàng nhà cung cấp supplierid ----------------- ---------------- ----- 1 Khách hàng NRZBB 1 Nhà cung cấp SWRXU 1 Khách hàng NRZBB 3 Nhà cung cấp STUAZ 1 Khách hàng NRZBB 7 Nhà cung cấp GQRCV ... 21 Khách hàng KIDPX 24 Nhà cung cấp JNNES 21 Khách hàng KIDPX 25 Nhà cung cấp ERVYZ 21 Khách hàng KIDPX 28 Nhà cung cấp OAVQT 23 Khách hàng WVFAF 3 Nhà cung cấp STUAZ 23 Khách hàng WVFAF 7 Nhà cung cấp GQRCV 23 Khách hàng WVFAF 8 Nhà cung cấp BWGYE ... 56 Khách hàng QNIVZ 26 Nhà cung cấp ZWZDM 56 Khách hàng QNIVZ 28 Nhà cung cấp OAVQT 56 Khách hàng QNIVZ 29 Nhà cung cấp OGLRK 58 Khách hàng AHXHT 1 Nhà cung cấp SWRXU 58 Khách hàng AHXHT 5 Nhà cung cấp EQPNC 58 Khách hàng AHXHT 5 Nhà cung cấp EQPNC 58 QWUSF ... (1236 hàng bị ảnh hưởng)Kế hoạch cho truy vấn này được thể hiện trong Hình 5.
Hình 5:Kế hoạch cho Truy vấn 5
Tất cả các liên kết trong kế hoạch được xử lý dưới dạng liên kết nội bộ như bạn mong đợi.
Bạn cũng có thể quan sát trong kế hoạch mà trình tối ưu hóa đã áp dụng tối ưu hóa sắp xếp kết hợp. Với các phép nối bên trong, trình tối ưu hóa biết rằng nó có thể sắp xếp lại thứ tự vật lý của các phép nối theo bất kỳ cách nào mà nó thích trong khi vẫn giữ nguyên ý nghĩa của truy vấn ban đầu, vì vậy nó có rất nhiều tính linh hoạt. Ở đây, việc tối ưu hóa dựa trên chi phí của nó dẫn đến thứ tự:tham gia (Khách hàng, tham gia (Đơn hàng, tham gia (tham gia (Nhà cung cấp, Sản phẩm), Chi tiết đơn hàng))).
Giả sử rằng bạn nhận được yêu cầu thay đổi truy vấn để truy vấn bao gồm những khách hàng chưa đặt hàng. Nhớ lại rằng chúng tôi hiện có hai khách hàng như vậy (với ID 22 và 57), vì vậy kết quả mong muốn được cho là có 1.238 hàng. Một lỗi phổ biến trong trường hợp này là thay đổi liên kết bên trong giữa Khách hàng và Đơn đặt hàng thành liên kết bên ngoài bên trái, nhưng để tất cả các liên kết còn lại là liên kết bên trong, như sau:
CHỌN DISTINCT C.custid, C.companyname AS customer, S.supplierid, S.companyname AS nhà cung cấp TỪ Bán hàng. Khách hàng AS C TRÁI OUTER THAM GIA Bán hàng. Đơn đặt hàng AS O ON O.custid =C.custid INNER JOIN Sales. OrderDetails AS OD ON OD.orderid =O.orderid INNER JOIN Production.Products AS P ON P.productid =OD.productid INNER JOIN Production.Supplier AS S ON S.supplierid =P.supplierid;Khi một phép nối ngoài cùng bên trái sau đó được theo sau bởi các phép nối bên trong hoặc bên phải và vị từ nối so sánh một thứ gì đó từ phía không được đặt trước của phép nối bên ngoài bên trái với một số phần tử khác, kết quả của vị từ là giá trị logic chưa biết và phần tử bên ngoài ban đầu hàng bị loại bỏ. Phép nối bên ngoài bên trái sẽ trở thành một phép nối bên trong.
Kết quả là, truy vấn này tạo ra cùng một đầu ra giống như đối với Truy vấn 5, chỉ trả về 1.236 hàng. Cũng tại đây, trình tối ưu hóa phát hiện sự mâu thuẫn và chuyển đổi liên kết bên ngoài thành liên kết bên trong, tạo ra cùng một kế hoạch được hiển thị trước đó trong Hình 5.
Một nỗ lực phổ biến để sửa lỗi là làm cho tất cả các liên kết bên trái tham gia bên ngoài, như sau:
CHỌN DISTINCT C.custid, C.companyname AS khách hàng, S.supplierid, S.companyname AS nhà cung cấp TỪ Bộ phận Bán hàng. .OrderDetails AS OD ON OD.orderid =O.orderid LEFT OUTER JOIN Production.Products AS P ON P.productid =OD.productid LEFT OUTER JOIN Production.Supplier AS S ON S.supplierid =P.supplierid;Truy vấn này tạo ra kết quả sau, bao gồm các khách hàng 22 và 57:
custid khách hàng nhà cung cấp supplierid ----------------- ---------------- ----- 1 Khách hàng NRZBB 1 Nhà cung cấp SWRXU 1 Khách hàng NRZBB 3 Nhà cung cấp STUAZ 1 Khách hàng NRZBB 7 Nhà cung cấp GQRCV ... 21 Khách hàng KIDPX 24 Nhà cung cấp JNNES 21 Khách hàng KIDPX 25 Nhà cung cấp ERVYZ 21 Khách hàng KIDPX 28 Nhà cung cấp OAVQT 22 Khách hàng DTDMN NULL NULL 23 Khách hàng WVFAF 3 Nhà cung cấp STUAZ 23 Khách hàng WVFAF 7 Nhà cung cấp GQRCV 23 Khách hàng WVFAF 8 Nhà cung cấp BWGYE ... 56 Khách hàng QNIVZ 26 Nhà cung cấp ZWZDM 56 Khách hàng QNIVZ 28 Nhà cung cấp OAVQT 56 Khách hàng QNIVZ 29 Nhà cung cấp OGLRK 57 Khách hàng WVAXS NULL NULL 58 Khách hàng AHXHTU 58 Nhà cung cấp AHXHXTU 1 Khách hàng AHXHT 5 Nhà cung cấp EQPNC 58 Khách hàng AHXHT 6 Nhà cung cấp QWUSF ... (1238 hàng liên hệ cted)Tuy nhiên, có hai vấn đề với giải pháp này. Giả sử rằng bên cạnh Khách hàng, bạn có thể có các hàng trong một bảng khác trong truy vấn mà không có hàng nào phù hợp trong bảng tiếp theo và trong trường hợp đó, bạn không muốn giữ các hàng bên ngoài đó. Ví dụ:điều gì sẽ xảy ra nếu trong môi trường của bạn, nó được phép tạo tiêu đề cho một đơn đặt hàng và sau đó, hãy điền vào nó với các dòng đơn đặt hàng. Giả sử rằng trong trường hợp như vậy, truy vấn không được trả về các tiêu đề đơn hàng trống như vậy. Tuy nhiên, truy vấn được cho là sẽ trả lại khách hàng mà không có đơn đặt hàng. Vì phép nối giữa Order và OrderDetails là phép nối bên ngoài bên trái, nên truy vấn này sẽ trả về các đơn hàng trống như vậy, mặc dù không phải vậy.
Một vấn đề khác là khi sử dụng các phép nối bên ngoài, bạn đặt ra nhiều hạn chế hơn đối với trình tối ưu hóa về các cách sắp xếp lại mà nó được phép khám phá như một phần của tối ưu hóa thứ tự nối của nó. Trình tối ưu hóa có thể sắp xếp lại phép nối A LEFT OUTER JOIN B thành B RIGHT OUTER JOIN A, nhưng đó là cách sắp xếp lại duy nhất mà nó được phép khám phá. Với các phép nối bên trong, trình tối ưu hóa cũng có thể sắp xếp lại thứ tự các bảng ngoài việc chỉ lật các mặt, ví dụ, nó có thể sắp xếp lại thứ tự nối (phép nối (join (join (A, B), C), D), E)))) để tham gia (A, join (B, join (join (E, D), C))) như thể hiện trước đó trong Hình 5.
Nếu bạn nghĩ về điều đó, thì điều bạn thực sự cần là rời khỏi Khách hàng tham gia với kết quả của các liên kết bên trong giữa các phần còn lại của bảng. Rõ ràng, bạn có thể đạt được điều này với các biểu thức bảng. Tuy nhiên, T-SQL hỗ trợ một thủ thuật khác. Thứ thực sự xác định thứ tự nối logic không chính xác là thứ tự của các bảng trong mệnh đề FROM, mà là thứ tự của các mệnh đề ON. Tuy nhiên, để truy vấn hợp lệ, mỗi mệnh đề BẬT phải xuất hiện ngay bên dưới hai đơn vị mà nó đang tham gia. Vì vậy, để coi sự kết hợp giữa Khách hàng và phần còn lại là cuối cùng, tất cả những gì bạn cần làm là chuyển điều khoản BẬT kết nối Khách hàng và phần còn lại xuất hiện cuối cùng, như sau:
CHỌN DISTINCT C.custid, C.companyname AS khách hàng, S.supplierid, S.companyname AS nhà cung cấp TỪ Bán hàng. Khách hàng AS C TRÁI NGOÀI THAM GIA Bán hàng. Đơn hàng AS O - chuyển từ đây ------- ---------------- INNER tham gia bán hàng.OrderDetails AS OD - ON OD.orderid =O.orderid - INNER tham gia sản xuất. Sản phẩm AS P - ON P.productid =OD .productid - INNER JOIN Production.Supplier AS S - ON S.supplierid =P.supplierid - ON O.custid =C.custid; - <- đến đây -Bây giờ thứ tự tham gia hợp lý là:leftjoin (Khách hàng, tham gia (tham gia (tham gia (Đơn hàng, Chi tiết đặt hàng), Sản phẩm), Nhà cung cấp)). Lần này, bạn sẽ giữ chân những khách hàng chưa đặt hàng, nhưng bạn sẽ không giữ lại những tiêu đề đơn hàng không có dòng đơn hàng phù hợp. Ngoài ra, bạn cho phép trình tối ưu hóa linh hoạt sắp xếp thứ tự kết hợp đầy đủ trong các kết hợp bên trong giữa Đơn đặt hàng, Chi tiết đơn hàng, Sản phẩm và Nhà cung cấp.
Một nhược điểm của cú pháp này là tính dễ đọc. Tin tốt là điều này có thể dễ dàng sửa chữa bằng cách sử dụng dấu ngoặc đơn, như vậy (gọi đây là Truy vấn 6):
CHỌN DISTINCT C.custid, C.companyname AS customer, S.supplierid, S.companyname AS nhà cung cấp TỪ Bán hàng. Khách hàng AS C LEFT OUTER JOIN (Bán hàng. Đơn hàng AS O INNER JOIN Sales.OrderDetails AS OD ON OD.orderid =O.orderid INNER JOIN Production. Sản phẩm AS P TRÊN P.productid =OD.productid INNER JOIN Production.Supplier AS S ON S.supplierid =P.supplierid) ON O.custid =C.custid;Đừng nhầm lẫn việc sử dụng dấu ngoặc đơn ở đây với một bảng dẫn xuất. Đây không phải là một bảng dẫn xuất, mà chỉ là một cách để tách một số toán tử bảng thành đơn vị của riêng chúng, cho rõ ràng. Ngôn ngữ không thực sự cần những dấu ngoặc đơn này, nhưng chúng rất được khuyến khích để dễ đọc.
Kế hoạch cho truy vấn này được thể hiện trong Hình 6.
Hình 6:Kế hoạch cho Truy vấn 6
Hãy quan sát rằng lần này liên kết giữa Khách hàng và những người còn lại được xử lý như một liên kết ngoài và trình tối ưu hóa đã áp dụng tối ưu hóa thứ tự liên kết.
Kết luận
Trong bài viết này, tôi đã đề cập đến bốn lỗi cổ điển liên quan đến phép nối. When using outer joins, computing the COUNT(*) aggregate typically results in a bug. The best practice is to apply the aggregate to a non-NULLable column from the nonpreserved side of the join.
When joining multiple tables and involving aggregate calculations, if you apply the aggregates to a nonleaf table in the joins, it’s usually a bug resulting in double-dipping aggregates. The best practice is then to apply the aggregates within table expressions and joining the table expressions.
It’s common to confuse the meanings of the ON and WHERE clauses. With inner joins, they’re both filters, so it doesn’t really matter how you organize your predicates within these clauses. However, with outer joins the ON clause serves a matching role whereas the WHERE clause serves a filtering role. Understanding this helps you figure out how to organize your predicates within these clauses.
In multi-join queries, a left outer join that is subsequently followed by an inner join, or a right outer join, where you compare an element from the nonpreserved side of the join with others (other than the IS NULL test), the outer rows of the left outer join are discarded. To avoid this bug, you want to apply the left outer join last, and this can be achieved by shifting the ON clause that connects the preserved side of this join with the rest to appear last. Use parentheses for clarity even though they are not required.