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 - truy vấn con

Bài viết này là bài thứ hai 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. Lần này tôi tập trung vào các lỗi cổ điển liên quan đến truy vấn con. Đặc biệt, tôi bao gồm các lỗi thay thế và các rắc rối logic ba giá trị. Một số chủ đề mà tôi đề cập trong loạt bài này đã được các MVP đồng nghiệp gợi ý trong một cuộc thảo luận mà chúng tôi đã có về chủ đề này. 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 đã góp ý!

Lỗi thay thế

Để giải thích lỗi thay thế cổ điển, tôi sẽ sử dụng một tình huống đơn đặt hàng của khách hàng. Chạy mã sau để tạo một hàm trợ giúp được gọi là GetNums và để tạo và điền bảng Khách hàng và Đơn hàng:

 ĐẶT SỐ TÀI KHOẢN BẬT; SỬ DỤNG tempdb; ĐI BẢNG DROP NẾU TỒN TẠI dbo.Orders; BẢNG DROP NẾU TỒN TẠI dbo .Khách hàng; DROP FUNP NẾU TỒN TẠI dbo.GetNums; ĐI TẠO CHỨC NĂNG dbo.GetNums (@low AS BIGINT, @high AS BIGINT) BẢNG QUAY LẠI (CHỌN c TỪ (CHỌN 1 ĐOÀN KẾT TẤT CẢ CHỌN 1) NHƯ D (c)), L1 NHƯ (CHỌN 1 NHƯ c TỪ L0 NHƯ A CROSS JOIN L0 AS B), L2 AS (CHỌN 1 NHƯ c TỪ L1 NHƯ MỘT THAM GIA CHÉO L1 AS B), L3 AS (CHỌN 1 AS c TỪ L2 AS A CROSS JOIN L2 AS B), L4 AS (CHỌN 1 AS c TỪ L3 AS A CROSS JOIN L3 AS B), L5 AS (CHỌN 1 AS c TỪ L4 NHƯ CHÉO THAM GIA L4 NHƯ B), Số tổng NHƯ (CHỌN ROW_NUMBER () HẾT (ĐẶT HÀNG BẰNG (CHỌN NULL)) NHƯ rownum TỪ L5) CHỌN ĐẦU (@high - @low + 1) @low + rownum - 1 NHƯ n TỪ Nums ORDER BY rownum; GO CREATE TABLE dbo.Customers (custid INT NOT NULL CONSTRAINT PK_Customers PRIMARY KEY, companyname VARCHAR (50) NOT NULL); CHÈN VÀO dbo.Customers WITH (TABLOCK) (custid, companyname) SELECT n AS custid, CONCAT ('Cust', CAST (n AS VARCHAR (10))) AS companyname FROM dbo.GetNums (1, 100); CREATE TABLE dbo.Orders (orderid INT NOT NULL IDENTITY CONSTRAINT PK_Orders PRIMARY KEY, customerid INT NOT NULL, điền BINARY (100) NOT NULL - đại diện cho các cột khác CONSTRAINT DFT_Orders_filler DEFAULT (0x)); INSERT INTO dbo.Orders WITH (TABLOCK) (customerid) CHỌN C.n AS customerid FROM dbo.GetNums (1, 10000) AS O CROSS JOIN dbo.GetNums (1, 100) AS C WHERE C.n NOT IN (17, 59); TẠO CHỈ SỐ idx_customerid TRÊN dbo.Orders (customerid); 

Hiện tại, bảng Khách hàng có 100 khách hàng có ID khách hàng liên tiếp trong phạm vi từ 1 đến 100. 98 trong số đó có các đơn hàng tương ứng trong bảng Đơn hàng. Khách hàng có ID 17 và 59 chưa đặt bất kỳ đơn hàng nào và do đó không có mặt trong bảng Đơn hàng.

Bạn chỉ theo đuổi những khách hàng đã đặt hàng và bạn cố gắng đạt được điều này bằng cách sử dụng truy vấn sau (gọi là Truy vấn 1):

 ĐẶT TẮT SỐ TÀI KHOẢN; CHỌN custid, companynameFROM dbo.CustomersWHERE custid IN (CHỌN custid TỪ dbo.Orders); 

Bạn có thể lấy lại 98 khách hàng, nhưng thay vào đó, bạn có được tất cả 100 khách hàng, bao gồm cả những người có ID 17 và 59:

 custid companyname ------------ 1 Cust 12 Cust 23 Cust 3 ... 16 Cust 1617 Cust 1718 Cust 18 ... 58 Cust 5859 Cust 5960 Cust 60 ... 98 Cust 9899 Cust 99100 Cust 100 (100 hàng bị ảnh hưởng) 

Bạn có thể tìm ra điều gì sai không?

Để thêm vào sự nhầm lẫn, hãy kiểm tra kế hoạch cho Truy vấn 1 như thể hiện trong Hình 1.

Hình 1:Kế hoạch cho Truy vấn 1

Kế hoạch hiển thị toán tử Vòng lặp lồng nhau (Nối bán phần bên trái) không có vị từ tham gia, có nghĩa là điều kiện duy nhất để trả lại khách hàng là phải có bảng Đơn hàng không có gì, như thể truy vấn bạn đã viết như sau:

 CHỌN custid, companynameFROM dbo.CustomersWHERE TỒN TẠI (CHỌN * TỪ dbo.Orders); 

Bạn có thể mong đợi một kế hoạch tương tự như kế hoạch được thể hiện trong Hình 2.

Hình 2:Kế hoạch dự kiến ​​cho Truy vấn 1

Trong kế hoạch này, bạn sẽ thấy toán tử Vòng lặp lồng nhau (Nối bán phần bên trái), với việc quét chỉ mục được phân nhóm trên Khách hàng làm đầu vào bên ngoài và tìm kiếm trong chỉ mục trên cột customerid trong Đơn hàng làm đầu vào bên trong. Bạn cũng thấy một tham chiếu bên ngoài (tham số tương quan) dựa trên cột custid trong Khách hàng và vị từ tìm kiếm Order.customerid =Khách hàng.custid.

Vậy tại sao bạn nhận được kế hoạch trong Hình 1 mà không phải là kế hoạch trong Hình 2? Nếu bạn chưa tìm ra, hãy xem kỹ định nghĩa của cả hai bảng — cụ thể là tên cột — và tên cột được sử dụng trong truy vấn. Bạn sẽ nhận thấy rằng bảng Khách hàng giữ ID khách hàng trong một cột có tên là custid và bảng Đơn đặt hàng chứa ID khách hàng trong một cột có tên là customerid. Tuy nhiên, mã sử dụng custid trong cả truy vấn bên ngoài và bên trong. Vì tham chiếu đến custid trong truy vấn bên trong là không đủ điều kiện, SQL Server phải giải quyết bảng nào đến từ cột. Theo tiêu chuẩn SQL, SQL Server phải tìm kiếm cột trong bảng được truy vấn trong cùng một phạm vi trước tiên, nhưng vì không có cột nào được gọi là custid trong Đơn hàng, sau đó nó phải tìm kiếm cột đó trong bảng ở bên ngoài phạm vi, và lần này có một trận đấu. Vì vậy, tham chiếu đến custid mặc nhiên trở thành tham chiếu tương quan, như thể bạn đã viết truy vấn sau:

 CHỌN custid, companynameFROM dbo.CustomersWHERE custid IN (CHỌN Khách hàng.custid TỪ dbo.Orders); 

Với điều kiện là Đơn hàng không trống và giá trị custid bên ngoài không phải là NULL (không thể có trong trường hợp của chúng tôi vì cột được xác định là KHÔNG ĐỦ), bạn sẽ luôn nhận được kết quả khớp vì bạn so sánh giá trị với chính nó . Vì vậy, Truy vấn 1 trở thành tương đương với:

 CHỌN custid, companynameFROM dbo.CustomersWHERE TỒN TẠI (CHỌN * TỪ dbo.Orders); 

Nếu bảng bên ngoài hỗ trợ NULL trong cột custid, thì Truy vấn 1 sẽ tương đương với:

 CHỌN custid, tên công ty 

Giờ thì bạn đã hiểu tại sao Truy vấn 1 được tối ưu hóa với kế hoạch trong Hình 1 và tại sao bạn thu hút được tất cả 100 khách hàng trở lại.

Cách đây một thời gian, tôi đã ghé thăm một khách hàng gặp lỗi tương tự, nhưng rất tiếc với câu lệnh DELETE. Hãy suy nghĩ một chút điều này có nghĩa là gì. Tất cả các hàng trong bảng đã bị xóa và không chỉ những hàng mà họ định xóa ban đầu!

Đối với các phương pháp hay nhất có thể giúp bạn tránh những lỗi như vậy, có hai phương pháp chính. Trước tiên, bạn có thể kiểm soát nó nhiều nhất có thể, hãy đảm bảo rằng bạn sử dụng tên cột nhất quán trên các bảng cho các thuộc tính đại diện cho cùng một thứ. Thứ hai, hãy đảm bảo rằng bạn lập bảng tham chiếu cột đủ điều kiện trong các truy vấn con, kể cả trong các truy vấn độc lập, nơi đây không phải là một thực tế phổ biến. Tất nhiên, bạn có thể sử dụng bí danh bảng nếu bạn không muốn sử dụng tên bảng đầy đủ. Áp dụng phương pháp này cho truy vấn của chúng tôi, giả sử rằng nỗ lực ban đầu của bạn đã sử dụng mã sau:

 CHỌN custid, companynameFROM dbo.CustomersWHERE custid IN (CHỌN O.custid TỪ dbo.Orders AS O); 

Ở đây, bạn không cho phép phân giải tên cột ngầm định và do đó Máy chủ SQL tạo ra lỗi sau:

 Bản tin thứ 207, Mức 16, Trạng thái 1, Dòng 108 Tên cột không hợp lệ 'custid'. 

Bạn đi và kiểm tra siêu dữ liệu cho bảng Đơn hàng, nhận ra rằng bạn đã sử dụng sai tên cột và sửa truy vấn (gọi đây là Truy vấn 2), như sau:

 CHỌN custid, companynameFROM dbo.CustomersWHERE custid IN (CHỌN O.customerid TỪ dbo.Orders AS O); 

Lần này, bạn có được đầu ra phù hợp với 98 khách hàng, không bao gồm các khách hàng có ID 17 và 59:

 custid companyname ------------ 1 Khách hàng 12 Khách hàng 23 Khách hàng 3 ... 16 Khách hàng 1618 Khách hàng 18..58 Khách hàng 5860 Khách hàng 60 ... 98 Cust 9899 Cust 99100 Cust 100 (98 hàng bị ảnh hưởng) 

Bạn cũng nhận được kế hoạch dự kiến ​​được hiển thị trước đó trong Hình 2.

Ngoài ra, rõ ràng tại sao khách hàng.custid là một tham chiếu bên ngoài (tham số tương quan) trong toán tử Vòng lặp lồng nhau (Kết nối bán bên trái) trong Hình 2. Điều ít rõ ràng hơn là tại sao Expr1004 cũng xuất hiện trong kế hoạch dưới dạng tham chiếu bên ngoài. Paul White, thành viên của SQL Server MVP, đưa ra giả thuyết rằng nó có thể liên quan đến việc sử dụng thông tin từ lá của đầu vào bên ngoài để gợi ý cho công cụ lưu trữ nhằm tránh nỗ lực trùng lặp bởi các cơ chế đọc trước. Bạn có thể tìm thấy thông tin chi tiết tại đây.

Rắc rối logic ba giá trị

Một lỗi phổ biến liên quan đến truy vấn con liên quan đến các trường hợp truy vấn bên ngoài sử dụng vị từ NOT IN và truy vấn con có thể trả về NULL trong số các giá trị của nó. Ví dụ:giả sử rằng bạn cần có thể lưu trữ đơn đặt hàng trong bảng Đơn hàng của chúng tôi với NULL làm ID khách hàng. Trường hợp như vậy sẽ đại diện cho một đơn đặt hàng không liên quan đến bất kỳ khách hàng nào; ví dụ:một đơn đặt hàng bù đắp cho sự không nhất quán giữa số lượng sản phẩm thực tế và số lượng được ghi lại trong cơ sở dữ liệu.

Sử dụng mã sau để tạo lại bảng Đơn hàng với cột custid cho phép NULL và bây giờ điền vào bảng này với cùng một dữ liệu mẫu như trước đây (với các đơn đặt hàng theo ID khách hàng từ 1 đến 100, không bao gồm 17 và 59):

 DROP TABLE NẾU TỒN TẠI dbo.Orders; ĐI TẠO BẢNG dbo.Orders (orderid INT NOT NULL IDENTITY CONSTRAINT PK_Orders PRIMARY KEY, custid INT NULL, điền BINARY (100) NOT NULL - đại diện cho các cột khác CONSTRAINT DFT_Orders_fillerx DEFAULT (0) ); INSERT INTO dbo.Orders WITH (TABLOCK) (custid) SELECT C.n AS customerid FROM dbo.GetNums (1, 10000) AS O CROSS JOIN dbo.GetNums (1, 100) AS C WHERE C.n NOT IN (17, 59); TẠO CHỈ SỐ idx_custid TRÊN dbo.Orders (custid); 

Lưu ý rằng trong khi chúng tôi đang thực hiện, tôi đã làm theo phương pháp hay nhất đã thảo luận trong phần trước để sử dụng tên cột nhất quán trên các bảng cho các thuộc tính giống nhau và đặt tên cột trong bảng Đơn hàng là custid giống như trong bảng Khách hàng.

Giả sử rằng bạn cần viết một truy vấn trả lại những khách hàng không đặt hàng. Bạn đưa ra giải pháp đơn giản sau bằng cách sử dụng vị từ NOT IN (gọi nó là Truy vấn 3, lần thực thi đầu tiên):

 CHỌN custid, tên công ty 

Truy vấn này trả về kết quả mong đợi với khách hàng 17 và 59:

 custid companyname ------------ 17 Cust 1759 Cust 59 (2 hàng bị ảnh hưởng) 

Việc kiểm kê được thực hiện trong kho của công ty và có sự không nhất quán giữa số lượng thực tế của một số sản phẩm và số lượng được ghi lại trong cơ sở dữ liệu. Vì vậy, bạn thêm một lệnh bù đắp giả để giải thích cho sự không nhất quán. Vì không có khách hàng thực tế nào được liên kết với đơn đặt hàng, bạn sử dụng NULL làm ID khách hàng. Chạy mã sau để thêm tiêu đề đơn đặt hàng như vậy:

 CHÈN VÀO GIÁ TRỊ dbo.Orders (custid) (NULL); 

Chạy Truy vấn 3 lần thứ hai:

 CHỌN custid, tên công ty 

Lần này, bạn nhận được một kết quả trống:

 custid companyname ------------ (0 hàng bị ảnh hưởng) 

Rõ ràng là có gì đó không ổn. Bạn biết rằng khách hàng 17 và 59 không đặt bất kỳ đơn hàng nào và thực sự họ xuất hiện trong bảng Khách hàng nhưng không xuất hiện trong bảng Đơn hàng. Tuy nhiên, kết quả truy vấn khẳng định rằng không có khách hàng nào không đặt hàng. Bạn có thể tìm ra lỗi ở đâu và cách khắc phục không?

Tất nhiên, lỗi liên quan đến NULL trong bảng Đơn hàng. Đối với SQL, NULL là một điểm đánh dấu cho một giá trị bị thiếu có thể đại diện cho một khách hàng thích hợp. SQL không biết rằng đối với chúng tôi, NULL đại diện cho một khách hàng bị thiếu và không thể áp dụng (không liên quan). Đối với tất cả khách hàng trong bảng Khách hàng có mặt trong bảng Đơn đặt hàng, vị từ IN tìm thấy kết quả phù hợp là ĐÚNG và phần KHÔNG VÀO khiến nó trở thành SAI, do đó hàng khách hàng bị loại bỏ. Càng xa càng tốt. Nhưng đối với khách hàng 17 và 59, vị từ IN mang lại giá trị UNKNOWN vì tất cả các so sánh có giá trị không phải NULL đều cho kết quả FALSE và so sánh với NULL cho ra UNKNOWN. Hãy nhớ rằng, SQL giả định rằng NULL có thể đại diện cho bất kỳ khách hàng hiện hành nào, vì vậy giá trị logic UNKNOWN chỉ ra rằng không biết liệu ID khách hàng bên ngoài có bằng với ID khách hàng NULL bên trong hay không. FALSE OR FALSE… OR UNKNOWN là UNKNOWN. Sau đó, phần KHÔNG VÀO được áp dụng cho UNKNOWN vẫn tạo ra UNKNOWN.

Nói theo thuật ngữ tiếng Anh đơn giản hơn, bạn đã yêu cầu trả lại những khách hàng không đặt hàng. Vì vậy, theo lẽ tự nhiên, truy vấn loại bỏ tất cả khách hàng khỏi bảng Khách hàng có mặt trong bảng Đơn đặt hàng vì người ta biết chắc rằng họ đã đặt hàng. Đối với phần còn lại (17 và 59 trong trường hợp của chúng tôi), truy vấn loại bỏ chúng kể từ SQL, giống như không biết liệu họ có đặt hàng hay không, cũng như không biết liệu họ có đặt hàng hay không và bộ lọc cần sự chắc chắn (TRUE) trong để trả lại một hàng. Thật là một dưa chua!

Vì vậy, ngay khi NULL đầu tiên vào bảng Đơn hàng, kể từ thời điểm đó, bạn luôn nhận được kết quả trống từ truy vấn NOT IN. Còn đối với trường hợp bạn thực sự không có NULL trong dữ liệu nhưng cột cho phép NULL thì sao? Như bạn đã thấy trong lần thực thi đầu tiên của Truy vấn 3, trong trường hợp như vậy, bạn sẽ nhận được kết quả chính xác. Có lẽ bạn đang nghĩ rằng ứng dụng sẽ không bao giờ đưa NULL vào dữ liệu, vì vậy bạn không có gì phải lo lắng. Đó là một thực tiễn không tốt vì một vài lý do. Đối với một, nếu một cột được xác định là cho phép NULL, thì khá chắc chắn rằng các NULL cuối cùng sẽ đến đó ngay cả khi chúng không được phép; nó chỉ là một vấn đề thời gian. Đó có thể là kết quả của việc nhập dữ liệu không hợp lệ, lỗi trong ứng dụng và các lý do khác. Đối với một điều khác, ngay cả khi dữ liệu không chứa NULL, nếu cột cho phép những điều đó, trình tối ưu hóa phải tính đến khả năng NULL sẽ có mặt khi nó tạo kế hoạch truy vấn và trong truy vấn NOT IN của chúng tôi, điều này sẽ dẫn đến một hình phạt về hiệu suất . Để chứng minh điều này, hãy xem xét kế hoạch cho lần thực thi đầu tiên của Truy vấn 3 trước khi bạn thêm hàng với NULL, như thể hiện trong Hình 3.

Hình 3:Kế hoạch thực thi Truy vấn 3 đầu tiên

Toán tử Vòng lặp lồng nhau trên cùng xử lý logic Kết nối Chống Bán lại Bên trái. Về cơ bản, đó là về việc xác định các điểm không khớp và đoản mạch hoạt động bên trong ngay khi tìm thấy một điểm trùng khớp. Phần bên ngoài của vòng lặp kéo tất cả 100 khách hàng từ bảng Khách hàng, do đó phần bên trong của vòng lặp được thực hiện 100 lần.

Phần bên trong của vòng lặp trên cùng thực thi toán tử Vòng lặp lồng nhau (Tham gia bên trong). Phần bên ngoài của vòng lặp dưới cùng tạo hai hàng cho mỗi khách hàng — một hàng cho trường hợp NULL và một hàng khác cho ID khách hàng hiện tại, theo thứ tự này. Đừng để toán tử Khoảng thời gian hợp nhất làm bạn bối rối. Nó thường được sử dụng để hợp nhất các khoảng chồng chéo, ví dụ:một vị từ như col1 GIỮA 20 VÀ 30 HOẶC col1 GIỮA 25 VÀ 35 được chuyển đổi thành col1 GIỮA 20 VÀ 35. Ý tưởng này có thể được khái quát hóa để loại bỏ các bản sao trong một vị từ IN. Trong trường hợp của chúng tôi, thực sự không thể có bất kỳ bản sao nào. Theo thuật ngữ đơn giản, như đã đề cập, hãy nghĩ về phần bên ngoài của vòng lặp là tạo hai hàng cho mỗi khách hàng — hàng đầu tiên cho trường hợp NULL và hàng thứ hai cho ID khách hàng hiện tại. Sau đó, phần bên trong của vòng lặp đầu tiên thực hiện tìm kiếm trong chỉ mục idx_custid trên Đơn hàng để tìm kiếm NULL. Nếu tìm thấy NULL, nó không kích hoạt lần tìm kiếm thứ hai cho ID khách hàng hiện tại (hãy nhớ lỗi ngắn mạch được xử lý bởi vòng lặp Anti Semi Join trên cùng). Trong trường hợp như vậy, khách hàng bên ngoài sẽ bị loại bỏ. Nhưng nếu không tìm thấy NULL, vòng lặp dưới cùng sẽ kích hoạt lần tìm kiếm thứ hai để tìm ID khách hàng hiện tại trong Đơn đặt hàng. Nếu nó được tìm thấy, khách hàng bên ngoài sẽ bị loại bỏ. Nếu không tìm thấy, khách hàng bên ngoài sẽ được trả lại. Điều này có nghĩa là khi NULL không có trong Đơn đặt hàng, kế hoạch này thực hiện hai lần tìm kiếm cho mỗi khách hàng! Điều này có thể được quan sát trong kế hoạch là số hàng 200 trong đầu vào bên ngoài của vòng lặp dưới cùng. Do đó, đây là số liệu thống kê I / O được báo cáo cho lần thực thi đầu tiên:

 Bảng 'Đơn hàng'. Quét đếm 200, đọc logic 603 

Kế hoạch cho lần thực thi thứ hai của Truy vấn 3, sau khi một hàng có NULL được thêm vào bảng Đơn hàng, được hiển thị trong Hình 4.

Hình 4:Kế hoạch thực thi Truy vấn 3

Vì NULL có trong bảng nên đối với tất cả các khách hàng, lần thực thi đầu tiên của toán tử Tìm kiếm Chỉ mục sẽ tìm thấy một kết quả phù hợp và do đó tất cả các khách hàng sẽ bị loại bỏ. Vì vậy, chúng tôi chỉ thực hiện một lượt tìm kiếm cho mỗi khách hàng chứ không phải hai lượt tìm kiếm, vì vậy lần này bạn nhận được 100 lượt tìm kiếm chứ không phải 200 lượt tìm kiếm; tuy nhiên, đồng thời điều này có nghĩa là bạn đang nhận lại một kết quả trống!

Dưới đây là số liệu thống kê I / O được báo cáo cho lần thực thi thứ hai:

 Bảng 'Đơn hàng'. Quét đếm 100, đọc lôgic 300 

Một giải pháp cho tác vụ này khi có thể có NULL trong số các giá trị trả về trong truy vấn con là chỉ cần lọc những giá trị đó ra, giống như vậy (gọi nó là Giải pháp 1 / Truy vấn 4):

 CHỌN custid, tên công ty 

Mã này tạo ra kết quả mong đợi:

 custid companyname ------------ 17 Cust 1759 Cust 59 (2 hàng bị ảnh hưởng) 

Nhược điểm của giải pháp này là bạn cần nhớ thêm bộ lọc. Tôi thích giải pháp sử dụng vị từ KHÔNG TỒN TẠI, trong đó truy vấn con có mối tương quan rõ ràng so sánh ID khách hàng của đơn đặt hàng với ID khách hàng của khách hàng, như vậy (gọi là Giải pháp 2 / Truy vấn 5):

 CHỌN C.custid, C.companynameFROM dbo.Các khách hàng NHƯ CWHERE KHÔNG TỒN TẠI (CHỌN * TỪ dbo. Đơn đặt hàng NHƯ O WHERE O.custid =C.custid); 

Hãy nhớ rằng so sánh dựa trên bình đẳng giữa NULL và bất kỳ thứ gì tạo ra UNKNOWN và UNKNOWN sẽ bị bộ lọc WHERE loại bỏ. Vì vậy, nếu NULL tồn tại trong Đơn đặt hàng, chúng sẽ bị bộ lọc của truy vấn bên trong loại bỏ mà bạn không cần thêm xử lý NULL rõ ràng và do đó bạn không cần lo lắng về việc NULL có hay không tồn tại trong dữ liệu.

Truy vấn này tạo ra kết quả mong đợi:

 custid companyname ------------ 17 Cust 1759 Cust 59 (2 hàng bị ảnh hưởng) 

Kế hoạch cho cả hai giải pháp được thể hiện trong Hình 5.

Hình 5:Kế hoạch cho Truy vấn 4 (Giải pháp 1) và Truy vấn 5 (Giải pháp 2 )

Như bạn có thể thấy các kế hoạch gần như giống hệt nhau. Chúng cũng khá hiệu quả, bằng cách sử dụng tối ưu hóa Nối bán bên trái với một đoạn ngắn mạch. Cả hai chỉ thực hiện 100 lần tìm kiếm trong chỉ mục idx_custid trên Đơn hàng và với toán tử Top, áp dụng ngắn mạch sau khi chạm vào một hàng trong lá.

Thống kê I / O cho cả hai truy vấn đều giống nhau:

 Bảng 'Đơn hàng'. Quét đếm 100, đọc lôgic 348 

Tuy nhiên, một điều cần xem xét là liệu có bất kỳ cơ hội nào để bảng bên ngoài có NULL trong cột tương quan hay không (trong trường hợp của chúng tôi là custid). Rất khó có thể phù hợp trong một tình huống như đơn đặt hàng của khách hàng, nhưng có thể phù hợp trong các tình huống khác. Nếu thực sự là như vậy, cả hai giải pháp đều xử lý NULL bên ngoài không chính xác.

Để chứng minh điều này, hãy thả và tạo lại bảng Khách hàng với NULL làm một trong các ID khách hàng bằng cách chạy mã sau:

 DROP TABLE NẾU TỒN TẠI dbo.Customers; ĐI TẠO BẢNG dbo.Customers (custid INT NULL CONSTRAINT UNQ_Customers_custid UNIQUE CLUSTERED, companyname VARCHAR (50) NOT NULL); INSERT INTO dbo.Customers WITH (TABLOCK) (custid, companyname) SELECT CAST (NULL AS INT) AS custid, 'Cust NULL' AS companyname UNION ALL SELECT n AS custid, CONCAT ('Cust', CAST (n AS VARCHAR (10 ))) NHƯ tên công ty TỪ dbo.GetNums (1, 100); 

Giải pháp 1 sẽ không trả về NULL bên ngoài bất kể NULL bên trong có xuất hiện hay không.

Giải pháp 2 sẽ trả về NULL bên ngoài bất kể NULL bên trong có xuất hiện hay không.

Nếu bạn muốn xử lý NULL giống như bạn xử lý các giá trị không phải NULL, tức là trả về NULL nếu có trong Khách hàng nhưng không có trong Đơn hàng và không trả lại nếu có trong cả hai, bạn cần thay đổi logic của giải pháp để sử dụng tính riêng biệt so sánh dựa trên cơ sở thay vì so sánh dựa trên bình đẳng. Điều này có thể đạt được bằng cách kết hợp vị từ EXISTS và toán tử bộ EXCEPT, giống như vậy (gọi đây là Giải pháp 3 / Truy vấn 6):

 CHỌN C.custid, C.companynameFROM dbo.Các khách hàng NHƯ CWHERE TỒN TẠI (CHỌN C.custid NGOẠI TRỪ CHỌN O.custid TỪ dbo.Orders AS O); 

Vì hiện tại có NULL trong cả Khách hàng và Đơn đặt hàng, nên truy vấn này không trả về NULL một cách chính xác. Đây là kết quả truy vấn:

 custid companyname ------------ 17 Cust 1759 Cust 59 (2 hàng bị ảnh hưởng) 

Chạy mã sau để xóa hàng có NULL khỏi bảng Đơn hàng và chạy lại Giải pháp 3:

 XÓA khỏi dbo.Orders KHI custid KHÔNG CÓ; CHỌN C.custid, C.companynameFROM dbo.Các khách hàng NHƯ CWHERE TỒN TẠI (CHỌN C.custid NGOẠI LỆ CHỌN O.custid TỪ dbo.Orders AS O); 

Lần này, vì NULL có trong Khách hàng nhưng không có trong Đơn hàng, nên kết quả bao gồm NULL:

 custid companyname ------------ NULL Cust NULL17 Cust 1759 Cust 59 (3 hàng bị ảnh hưởng) 

Kế hoạch cho giải pháp này được thể hiện trong Hình 6:

Hình 6:Kế hoạch cho Truy vấn 6 (Giải pháp 3)

Mỗi khách hàng, kế hoạch sử dụng toán tử Quét liên tục để tạo một hàng với khách hàng hiện tại và áp dụng một lần tìm kiếm trong chỉ mục idx_custid trên Đơn hàng để kiểm tra xem khách hàng có tồn tại trong Đơn hàng hay không. Bạn kết thúc với một lần tìm kiếm cho mỗi khách hàng. Vì chúng tôi hiện có 101 khách hàng trong bảng, chúng tôi nhận được 101 lượt tìm kiếm.

Đây là số liệu thống kê I / O cho truy vấn này:

 Bảng 'Đơn hàng'. Quét đếm 101, đọc logic 415 

Kết luận

Tháng này, tôi đã đề cập đến các lỗi liên quan đến truy vấn con, cạm bẫy và các phương pháp hay nhất. Tôi đã đề cập đến các lỗi thay thế và các rắc rối logic ba giá trị. Hãy nhớ sử dụng tên cột nhất quán giữa các bảng và luôn lập bảng các cột đủ điều kiện trong truy vấn con, ngay cả khi chúng là những cột độc lập. Ngoài ra, hãy nhớ thực thi ràng buộc NOT NULL khi cột không được phép cho phép NULL và luôn tính đến NULL khi chúng có thể có trong dữ liệu của bạn. Đảm bảo rằng bạn bao gồm NULL trong dữ liệu mẫu của mình khi chúng được phép để bạn có thể dễ dàng bắt lỗi trong mã của mình hơn khi kiểm tra nó. Hãy cẩn thận với vị từ NOT IN khi kết hợp với các truy vấn con. Nếu có thể có NULL trong kết quả của truy vấn bên trong, thì vị từ NOT EXISTS thường là lựa chọn thay thế ưu tiê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ẫu dữ liệu tham chiếu:Có thể mở rộng và linh hoạt

  2. Lập chỉ mục hoạt động như thế nào

  3. Sử dụng ODBC với Salesforce và OneLogin Đăng nhập một lần (SSO)

  4. Intel có bị diệt vong trong không gian CPU máy chủ không?

  5. Hiểu về triển khai Amazon Auroras Multi-AZ