Xử lý NULL là một trong những khía cạnh phức tạp hơn của mô hình dữ liệu và thao tác dữ liệu với SQL. Hãy bắt đầu với thực tế là nỗ lực giải thích chính xác NULL là gì không phải là tầm thường trong và của chính nó. Ngay cả trong số những người đã nắm bắt tốt lý thuyết quan hệ và SQL, bạn sẽ nghe thấy những ý kiến rất ủng hộ và phản đối việc sử dụng NULL trong cơ sở dữ liệu của bạn. Dù muốn hay không, với tư cách là người thực hành cơ sở dữ liệu, bạn thường phải xử lý chúng và do NULL làm tăng thêm độ phức tạp cho việc viết mã SQL của bạn, bạn nên ưu tiên hiểu rõ chúng. Bằng cách này, bạn có thể tránh được các lỗi và cạm bẫy không cần thiết.
Bài viết này là bài đầu tiên trong loạt bài về độ phức tạp NULL. Tôi bắt đầu với phạm vi bao quát về NULL là gì và cách chúng hoạt động khi so sánh. Sau đó, tôi đề cập đến sự mâu thuẫn trong điều trị NULL trong các yếu tố ngôn ngữ khác nhau. Cuối cùng, tôi đề cập đến các tính năng tiêu chuẩn còn thiếu liên quan đến xử lý NULL trong T-SQL và đề xuất các giải pháp thay thế có sẵn trong T-SQL.
Hầu hết các phạm vi đều có liên quan đến bất kỳ nền tảng nào triển khai phương ngữ SQL, nhưng trong một số trường hợp, tôi đề cập đến các khía cạnh cụ thể cho T-SQL.
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.
NULL làm điểm đánh dấu cho giá trị bị thiếu
Hãy bắt đầu với việc hiểu NULL là gì. Trong SQL, NULL là một điểm đánh dấu hoặc một trình giữ chỗ cho một giá trị bị thiếu. Đó là nỗ lực của SQL trong việc thể hiện trong cơ sở dữ liệu của bạn một thực tế mà một giá trị thuộc tính nhất định đôi khi có mặt và đôi khi bị thiếu. Ví dụ:giả sử rằng bạn cần lưu trữ dữ liệu nhân viên trong bảng Nhân viên. Bạn có các thuộc tính cho họ, tên đệm và họ. Thuộc tính tên và họ là bắt buộc và do đó bạn xác định chúng là không cho phép NULL. Thuộc tính tên đệm là tùy chọn và do đó bạn xác định nó là cho phép NULL.
Nếu bạn đang thắc mắc mô hình quan hệ nói gì về các giá trị bị thiếu, thì người tạo ra mô hình đó là Edgar F. Codd đã tin vào chúng. Trên thực tế, anh ấy thậm chí còn phân biệt được hai loại giá trị bị thiếu:Thiếu nhưng có thể áp dụng (điểm đánh dấu A-Values) và Thiếu nhưng không thể áp dụng (điểm đánh dấu I-Values). Nếu chúng tôi lấy thuộc tính tên đệm làm ví dụ, trong trường hợp nhân viên có tên đệm, nhưng vì lý do riêng tư chọn không chia sẻ thông tin, bạn sẽ sử dụng điểm đánh dấu A-Values. Trong trường hợp nhân viên hoàn toàn không có tên đệm, bạn sẽ sử dụng điểm đánh dấu I-Values. Ở đây, cùng một thuộc tính đôi khi có thể có liên quan và hiện tại, đôi khi Thiếu nhưng Có thể áp dụng và đôi khi Thiếu nhưng Không thể áp dụng. Các trường hợp khác có thể được cắt rõ ràng hơn, chỉ hỗ trợ một loại giá trị bị thiếu. Ví dụ:giả sử bạn có bảng Đơn đặt hàng với thuộc tính được gọi là ngày vận chuyển chứa ngày giao hàng của đơn đặt hàng. Một đơn đặt hàng đã được vận chuyển sẽ luôn có ngày giao hàng hiện tại và có liên quan. Trường hợp duy nhất không xác định được ngày giao hàng sẽ là đối với các đơn hàng chưa được giao. Vì vậy, ở đây, phải có giá trị ngày vận chuyển có liên quan hoặc phải sử dụng điểm đánh dấu I-Values.
Các nhà thiết kế của SQL đã chọn không phân biệt các giá trị thiếu có thể áp dụng và không thể áp dụng, và cung cấp cho chúng tôi NULL làm điểm đánh dấu cho bất kỳ loại giá trị bị thiếu nào. Đối với hầu hết các phần, SQL được thiết kế để giả định rằng các NULL đại diện cho loại giá trị bị thiếu nhưng có thể áp dụng. Do đó, đặc biệt là khi việc bạn sử dụng NULL làm trình giữ chỗ cho một giá trị không thể áp dụng được, việc xử lý SQL NULL mặc định có thể không phải là cách bạn cho là đúng. Đôi khi, bạn sẽ cần thêm logic xử lý NULL rõ ràng để có được cách xử lý mà bạn coi là đúng cho mình.
Cách tốt nhất, nếu bạn biết rằng một thuộc tính không được phép NULL, hãy đảm bảo rằng bạn thực thi nó với ràng buộc NOT NULL như một phần của định nghĩa cột. Có một số lý do quan trọng cho điều này. Một lý do là nếu bạn không thực thi điều này, lúc này hay lúc khác, NULL sẽ đến đó. Nó có thể là kết quả của một lỗi trong ứng dụng hoặc nhập dữ liệu xấu. Sử dụng một ràng buộc, bạn biết rằng NULL sẽ không bao giờ có mặt trên bảng. Một lý do khác là trình tối ưu hóa đánh giá các ràng buộc như NOT NULL để tối ưu hóa tốt hơn, tránh các công việc không cần thiết tìm kiếm NULL và bật các quy tắc chuyển đổi nhất định.
Các so sánh liên quan đến NULL
Có một số khó khăn trong đánh giá của SQL về các vị từ khi NULL có liên quan. Đầu tiên tôi sẽ đề cập đến các phép so sánh liên quan đến các hằng số. Sau đó, tôi sẽ đề cập đến các phép so sánh liên quan đến các biến, tham số và cột.
Khi bạn sử dụng các vị từ so sánh các toán hạng trong các phần tử truy vấn như WHERE, ON và HAVING, kết quả có thể có của phép so sánh phụ thuộc vào việc liệu bất kỳ toán hạng nào có thể là NULL hay không. Nếu bạn biết chắc chắn rằng không có toán hạng nào có thể là NULL, thì kết quả của vị từ sẽ luôn là TRUE hoặc FALSE. Đây là cái được gọi là logic vị từ hai giá trị, hay nói ngắn gọn, đơn giản là logic hai giá trị. Đây là trường hợp, ví dụ:khi bạn đang so sánh một cột được xác định là không cho phép NULL với một số toán hạng không phải NULL khác.
Nếu bất kỳ toán hạng nào trong phép so sánh có thể là NULL, chẳng hạn, một cột cho phép NULL, sử dụng cả toán tử bình đẳng (=) và bất bình đẳng (<>,>, <,> =, <=, v.v.), bạn đang bây giờ dựa trên lôgic vị từ ba giá trị. Nếu trong một phép so sánh nhất định mà hai toán hạng xảy ra không phải là giá trị NULL, bạn vẫn nhận được kết quả là TRUE hoặc FALSE. Tuy nhiên, nếu bất kỳ toán hạng nào là NULL, bạn sẽ nhận được giá trị logic thứ ba được gọi là UNKNOWN. Lưu ý rằng đó là trường hợp ngay cả khi so sánh hai NULL. Việc xử lý TRUE và FALSE bởi hầu hết các phần tử của SQL là khá trực quan. Việc điều trị UNKNOWN không phải lúc nào cũng trực quan như vậy. Hơn nữa, các phần tử khác nhau của SQL xử lý trường hợp UNKNOWN theo cách khác nhau, vì tôi sẽ giải thích chi tiết ở phần sau của bài viết dưới “Sự mâu thuẫn về cách xử lý NULL”.
Ví dụ:giả sử rằng bạn cần truy vấn bảng Sales.Orders trong cơ sở dữ liệu mẫu TSQLV5 và trả lại các đơn hàng đã được giao vào ngày 2 tháng 1 năm 2019. Bạn sử dụng truy vấn sau:
SỬ DỤNG TSQLV5; CHỌN orderid, ngày vận chuyểnFROM Bán hàng.OrdersWHERE ngày vận chuyển ='20190102';
Rõ ràng rằng vị từ bộ lọc đánh giá là TRUE cho các hàng có ngày vận chuyển là ngày 2 tháng 1 năm 2019 và các hàng đó sẽ được trả lại. Cũng rõ ràng rằng vị từ đánh giá thành FALSE cho các hàng có ngày vận chuyển nhưng không phải là ngày 2 tháng 1 năm 2019 và những hàng đó nên bị loại bỏ. Nhưng những hàng có ngày vận chuyển NULL thì sao? Hãy nhớ rằng cả vị từ dựa trên đẳng thức và vị từ dựa trên bất bình đẳng đều trả về UNKNOWN nếu bất kỳ toán hạng nào là NULL. Bộ lọc WHERE được thiết kế để loại bỏ các hàng như vậy. Bạn cần nhớ rằng bộ lọc WHERE trả về các hàng mà vị từ bộ lọc đánh giá là TRUE và loại bỏ các hàng mà vị từ đánh giá thành FALSE hoặc UNKNOWN.
Truy vấn này tạo ra kết quả sau:
orderid ngày vận chuyển ----------- ----------- 10771 2019-01-0210794 2019-01-0210802 2019-01-02
Giả sử rằng bạn cần trả lại các đơn đặt hàng chưa được vận chuyển vào ngày 2 tháng 1 năm 2019. Theo như bạn được biết, các đơn đặt hàng chưa được vận chuyển được cho là sẽ được đưa vào đầu ra. Bạn sử dụng một truy vấn tương tự như truy vấn cuối cùng, chỉ phủ định vị từ, như sau:
SELECT orderid, shippingdateFROM Sales.OrdersWHERE NOT (shippingdate ='20190102');
Truy vấn này trả về kết quả sau:
orderid ngày vận chuyển ----------- ----------- 10249 2017-07-1010252 2017-07-1110250 2017-07-12 ... 11050 2019-05 -0511055 2019-05-0511063 2019-05-0611067 2019-05-0611069 2019-05-06 (806 hàng bị ảnh hưởng)
Đầu ra đương nhiên loại trừ các hàng có ngày vận chuyển là ngày 2 tháng 1 năm 2019, nhưng cũng loại trừ các hàng có ngày vận chuyển NULL. Điều có thể phản trực quan ở đây là điều gì sẽ xảy ra khi bạn sử dụng toán tử NOT để phủ định một vị từ đánh giá là UNKNOWN. Rõ ràng, KHÔNG ĐÚNG là SAI và KHÔNG SAI là ĐÚNG. Tuy nhiên, NOT UNKNOWN vẫn là UNKNOWN. Logic của SQL đằng sau thiết kế này là nếu bạn không biết liệu một mệnh đề có đúng hay không, bạn cũng không biết liệu mệnh đề đó có đúng hay không. Điều này có nghĩa là khi sử dụng toán tử bình đẳng và bất bình đẳng trong vị từ bộ lọc, cả dạng tích cực và phủ định của vị từ đều không trả về các hàng có NULL.
Ví dụ này khá đơn giản. Có những trường hợp phức tạp hơn liên quan đến truy vấn con. Có một lỗi phổ biến khi bạn sử dụng vị từ NOT IN với truy vấn con, khi truy vấn con trả về giá trị NULL trong số các giá trị được trả về. Truy vấn luôn trả về một kết quả trống. Lý do là dạng tích cực của vị từ (phần IN) trả về giá trị TRUE khi giá trị bên ngoài được tìm thấy và UNKNOWN khi không tìm thấy giá trị này do so sánh với giá trị NULL. Khi đó, sự phủ định của vị từ với toán tử NOT luôn trả về FALSE hoặc UNKNOWN, tương ứng - không bao giờ là TRUE. Tôi trình bày chi tiết về lỗi này trong các lỗi T-SQL, cạm bẫy và các phương pháp hay nhất - truy vấn con, bao gồm các giải pháp được đề xuất, cân nhắc tối ưu hóa và các phương pháp hay nhất. Nếu bạn chưa quen với lỗi cổ điển này, hãy đảm bảo bạn kiểm tra bài viết này vì lỗi này khá phổ biến và có các biện pháp đơn giản mà bạn có thể thực hiện để tránh nó.
Trở lại nhu cầu của chúng tôi, còn việc cố gắng trả lại các đơn đặt hàng có ngày giao hàng khác với ngày 2 tháng 1 năm 2019, sử dụng toán tử khác với (<>):
CHỌN orderid, ngày vận chuyểnFROM Bán hàng.OrdersWHERE ngày vận chuyển <> '20190102';
Thật không may, cả hai toán tử bình đẳng và bất bình đẳng đều mang lại kết quả KHÔNG CÓ khi bất kỳ toán hạng nào là NULL, vì vậy truy vấn này tạo ra kết quả sau giống như truy vấn trước đó, ngoại trừ NULL:
orderid ngày vận chuyển ----------- ----------- 10249 2017-07-1010252 2017-07-1110250 2017-07-12 ... 11050 2019-05 -0511055 2019-05-0511063 2019-05-0611067 2019-05-0611069 2019-05-06 (806 hàng bị ảnh hưởng)
Để tách biệt vấn đề so sánh với NULL tạo ra UNKNOWN bằng cách sử dụng bình đẳng, bất bình đẳng và phủ định của hai loại toán tử, tất cả các truy vấn sau đây trả về tập kết quả trống:
CHỌN orderid, ngày vận chuyểnFROM Bán hàng.OrdersWHERE ngày vận chuyển =NULL; CHỌN orderid, ngày vận chuyểnFROM Sales.OrdersWHERE NOT (ngày vận chuyển =NULL); CHỌN orderid, ngày vận chuyểnFROM Sales.OrdersWHERE ngày vận chuyển <> NULL; CHỌN orderid, ngày vận chuyểnFROM Sales.OrdersWHERE NOT (ngày vận chuyển <> NULL);
Theo SQL, bạn không nên kiểm tra xem thứ gì đó bằng NULL hay khác NULL, thay vào đó nếu thứ gì đó là NULL hoặc không phải NULL, bằng cách sử dụng các toán tử đặc biệt IS NULL và IS NOT NULL, tương ứng. Các toán tử này sử dụng logic hai giá trị, luôn trả về TRUE hoặc FALSE. Ví dụ:sử dụng toán tử IS NULL để trả về các đơn hàng chưa được vận chuyển, như sau:
CHỌN orderid, ngày vận chuyểnFROM Bán hàng. Đơn hàngWHERE ngày vận chuyển LÀ KHÔNG;
Truy vấn này tạo ra kết quả sau:
orderid ngày vận chuyển ----------- ----------- 11008 NULL11019 NULL11039 NULL ... (21 hàng bị ảnh hưởng)
Sử dụng toán tử IS NOT NULL để trả lại các đơn hàng đã giao, như sau:
CHỌN orderid, ngày vận chuyểnTruy vấn này tạo ra kết quả sau:
orderid ngày vận chuyển ----------- ----------- 10249 2017-07-1010252 2017-07-1110250 2017-07-12 ... 11050 2019-05 -0511055 2019-05-0511063 2019-05-0611067 2019-05-0611069 2019-05-06 (809 hàng bị ảnh hưởng)Sử dụng mã sau để trả lại các đơn đặt hàng đã được giao vào ngày khác với ngày 2 tháng 1 năm 2019, cũng như các đơn đặt hàng chưa được vận chuyển:
CHỌN orderid, ngày vận chuyểnTruy vấn này tạo ra kết quả sau:
orderid ngày vận chuyển ----------- ----------- 11008 NULL11019 NULL11039 NULL ... 10249 2017-07-1010252 2017-07-1110250 2017-07-12 ... 11050 2019-05-0511055 2019-05-0511063 2019-05-0611067 2019-05-0611069 2019-05-06 (827 hàng bị ảnh hưởng)Trong phần sau của loạt bài này, tôi đề cập đến các tính năng tiêu chuẩn để xử lý NULL hiện đang bị thiếu trong T-SQL, bao gồm cả vị từ DISTINCT , có khả năng đơn giản hóa việc xử lý NULL rất nhiều.
So sánh với các biến, tham số và cột
Phần trước tập trung vào các vị từ so sánh một cột với một hằng số. Tuy nhiên, trong thực tế, bạn chủ yếu sẽ so sánh một cột với các biến / tham số hoặc với các cột khác. Những so sánh như vậy liên quan đến sự phức tạp hơn nữa.
Từ quan điểm xử lý NULL, các biến và tham số được xử lý giống nhau. Tôi sẽ sử dụng các biến trong các ví dụ của mình, nhưng những điểm tôi đưa ra về việc xử lý chúng cũng liên quan đến các tham số.
Hãy xem xét truy vấn cơ bản sau (tôi sẽ gọi nó là Truy vấn 1), lọc các đơn đặt hàng đã được giao vào một ngày nhất định:
DECLARE @dt AS DATE ='20190212'; CHỌN orderid, ngày vận chuyểnFROM Bán hàng.OrdersWHERE ngày vận chuyển =@dt;Tôi sử dụng một biến trong ví dụ này và khởi tạo nó với một số ngày mẫu, nhưng đây cũng có thể là một truy vấn được tham số hóa trong một thủ tục được lưu trữ hoặc một hàm do người dùng xác định.
Việc thực thi truy vấn này tạo ra kết quả sau:
orderid ngày vận chuyển ----------- ----------- 10865 2019-02-1210866 2019-02-1210876 2019-02-1210878 2019-02-1210879 2019- 02-12Kế hoạch cho Truy vấn 1 được thể hiện trong Hình 1.
Hình 1:Kế hoạch cho Truy vấn 1
Bảng có một chỉ mục bao trùm để hỗ trợ truy vấn này. Chỉ mục được gọi là idx_nc_shippeddate và nó được xác định với danh sách khóa (ngày vận chuyển, orderid). Vị từ bộ lọc của truy vấn được biểu thị dưới dạng đối số tìm kiếm (SARG) , nghĩa là nó cho phép trình tối ưu hóa xem xét áp dụng thao tác tìm kiếm trong chỉ mục hỗ trợ, đi thẳng đến phạm vi các hàng đủ điều kiện. Điều khiến bộ lọc trở thành vị từ SARGable là nó sử dụng toán tử đại diện cho một loạt các hàng đủ điều kiện liên tiếp trong chỉ mục và nó không áp dụng thao tác cho cột được lọc. Kế hoạch mà bạn nhận được là kế hoạch tối ưu cho truy vấn này.
Nhưng nếu bạn muốn cho phép người dùng yêu cầu các đơn hàng chưa được vận chuyển thì sao? Các đơn đặt hàng như vậy có ngày vận chuyển NULL. Đây là nỗ lực chuyển NULL làm ngày nhập:
DECLARE @dt AS DATE =NULL; CHỌN orderid, ngày vận chuyểnFROM Bán hàng.OrdersWHERE ngày vận chuyển =@dt;Như bạn đã biết, một vị từ sử dụng toán tử bình đẳng tạo ra UNKNOWN khi bất kỳ toán hạng nào là NULL. Do đó, truy vấn này trả về một kết quả trống:
orderid ngày vận chuyển ----------- ----------- (0 hàng bị ảnh hưởng)Mặc dù T-SQL hỗ trợ toán tử IS NULL, nó không hỗ trợ toán tử IS
rõ ràng. Vì vậy, bạn không thể sử dụng một vị từ bộ lọc, chẳng hạn như ngày vận chuyển WHERE IS @dt. Một lần nữa, tôi sẽ nói về giải pháp thay thế tiêu chuẩn không được hỗ trợ trong một bài viết trong tương lai. Những gì nhiều người làm để giải quyết nhu cầu này trong T-SQL là sử dụng các hàm ISNULL hoặc COALESCE để thay thế một NULL bằng một giá trị thường không thể xuất hiện trong dữ liệu ở cả hai phía, như vậy (tôi sẽ gọi đây là Truy vấn 2): DECLARE @dt AS DATE =NULL; CHỌN orderid, ngày vận chuyểnTruy vấn này tạo ra kết quả chính xác:
orderid ngày vận chuyển ------------- 11008 NULL11019 NULL11039 NULL ... 11075 NULL11076 NULL11077 NULL (21 hàng bị ảnh hưởng)Nhưng kế hoạch cho truy vấn này, như thể hiện trong Hình 2, không phải là tối ưu.
Hình 2:Kế hoạch cho Truy vấn 2
Vì bạn đã áp dụng thao tác cho cột được lọc, vị từ bộ lọc không được coi là một SARG nữa. Chỉ số vẫn đang bao phủ, vì vậy nó có thể được sử dụng; nhưng thay vì áp dụng tìm kiếm trong chỉ mục đi thẳng đến phạm vi các hàng đủ điều kiện, toàn bộ lá chỉ mục được quét. Giả sử rằng bảng có 50.000.000 đơn đặt hàng, chỉ có 1.000 là đơn đặt hàng chưa được vận chuyển. Kế hoạch này sẽ quét tất cả 50.000.000 hàng thay vì thực hiện tìm kiếm đi thẳng đến 1.000 hàng đủ điều kiện.
Một dạng của vị từ bộ lọc mà cả hai đều có nghĩa chính xác mà chúng ta đang theo đuổi và được coi là đối số tìm kiếm là (ngày vận chuyển =@dt HOẶC (ngày vận chuyển LÀ KHÔNG ĐỦ VÀ @dt LÀ ĐỦ)). Đây là một truy vấn sử dụng vị từ SARGable này (chúng tôi sẽ gọi nó là Truy vấn 3):
DECLARE @dt AS DATE =NULL; CHỌN orderid, ngày vận chuyểnKế 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
Như bạn có thể thấy, kế hoạch áp dụng tìm kiếm trong chỉ số hỗ trợ. Vị từ tìm kiếm cho biết là shippingdate =@dt, nhưng nó được thiết kế bên trong để xử lý các NULL giống như các giá trị không phải NULL nhằm mục đích so sánh.
Giải pháp này thường được coi là một trong những giải pháp hợp lý. Nó là tiêu chuẩn, tối ưu và chính xác. Hạn chế chính của nó là dài dòng. Điều gì sẽ xảy ra nếu bạn có nhiều vị từ bộ lọc dựa trên các cột NULLable? Bạn sẽ nhanh chóng kết thúc với một mệnh đề WHERE dài dòng và rườm rà. Và nó trở nên tồi tệ hơn nhiều khi bạn cần viết một vị từ bộ lọc liên quan đến cột NULLable để tìm kiếm các hàng mà cột đó khác với tham số đầu vào. Vị từ sau đó trở thành:(ngày vận chuyển <> @dt VÀ ((ngày vận chuyển LÀ KHÔNG ĐỦ VÀ @dt KHÔNG ĐỦ) HOẶC (ngày vận chuyển KHÔNG ĐỦ và @dt LÀ KHÔNG ĐỦ))).
Bạn có thể thấy rõ sự cần thiết của một giải pháp thanh lịch hơn, vừa ngắn gọn vừa tối ưu. Thật không may, một số sử dụng giải pháp không tiêu chuẩn trong đó bạn tắt tùy chọn phiên ANSI_NULLS. Tùy chọn này khiến SQL Server sử dụng xử lý không chuẩn đối với các toán tử bình đẳng (=) và khác với (<>) với logic hai giá trị thay vì logic ba giá trị, coi NULL giống như các giá trị không phải NULL cho mục đích so sánh. Đó là trường hợp ít nhất miễn là một trong các toán hạng là một tham số / biến hoặc một ký tự.
Chạy mã sau để tắt tùy chọn ANSI_NULLS trong phiên:
TẮT ANSI_NULLS;Chạy truy vấn sau bằng cách sử dụng một vị từ dựa trên đẳng thức đơn giản:
DECLARE @dt AS DATE =NULL; CHỌN orderid, ngày vận chuyểnFROM Bán hàng.OrdersWHERE ngày vận chuyển =@dt;Truy vấn này trả về 21 đơn đặt hàng chưa được vận chuyển. Bạn nhận được cùng một kế hoạch được hiển thị trước đó trong Hình 3, hiển thị một tìm kiếm trong chỉ mục.
Chạy mã sau để chuyển trở lại hành vi tiêu chuẩn khi ANSI_NULLS được bật:
BẬT ANSI_NULLS;Việc dựa vào những hành vi không chuẩn mực như vậy rất không được khuyến khích. Tài liệu cũng nói rằng hỗ trợ cho tùy chọn này sẽ bị xóa trong một số phiên bản SQL Server trong tương lai. Hơn nữa, nhiều người không nhận ra rằng tùy chọn này chỉ có thể áp dụng khi ít nhất một trong các toán hạng là tham số / biến hoặc hằng số, mặc dù tài liệu hướng dẫn khá rõ ràng về nó. Nó không áp dụng khi so sánh hai cột chẳng hạn như trong một phép nối.
Vì vậy, làm thế nào để bạn xử lý các phép nối liên quan đến các cột tham gia NULLable nếu bạn muốn khớp khi hai bên là NULL? Ví dụ:sử dụng mã sau để tạo và điền các bảng T1 và T2:
DROP TABLE NẾU TỒN TẠI dbo.T1, dbo.T2; ĐI TẠO BẢNG dbo.T1 (k1 INT NULL, k2 INT NULL, k3 INT NULL, val1 VARCHAR (10) NOT NULL, CONSTRAINT UNQ_T1 UNIQUE CLUSTERED (k1, k2) , k3)); TẠO BẢNG dbo.T2 (k1 INT NULL, k2 INT NULL, k3 INT NULL, val2 VARCHAR (10) NOT NULL, CONSTRAINT UNQ_T2 UNIQUE CLUSTERED (k1, k2, k3)); CHÈN VÀO Dbo.T1 (k1, k2, k3, val1) CÁC GIÁ TRỊ (1, NULL, 0, 'A'), (NULL, NULL, 1, 'B'), (0, NULL, NULL, 'C') , (1, 1, 0, 'D'), (0, NULL, 1, 'F'); CHÈN VÀO Dbo.T2 (k1, k2, k3, val2) CÁC GIÁ TRỊ (0, 0, 0, 'G'), (1, 1, 1, 'H'), (0, NULL, NULL, 'I') , (NULL, NULL, NULL, 'J'), (0, NULL, 1, 'K');Mã này tạo các chỉ mục bao trùm trên cả hai bảng để hỗ trợ một phép nối dựa trên các khóa nối (k1, k2, k3) ở cả hai phía.
Sử dụng mã sau để cập nhật thống kê số lượng, làm tăng các con số để trình tối ưu hóa nghĩ rằng bạn đang xử lý các bảng lớn hơn:
CẬP NHẬT THỐNG KÊ dbo.T1 (UNQ_T1) VỚI ROWCOUNT =1000000; CẬP NHẬT THỐNG KÊ dbo.T2 (UNQ_T2) VỚI ROWCOUNT =1000000;Sử dụng mã sau để cố gắng nối hai bảng bằng cách sử dụng các vị từ dựa trên đẳng thức đơn giản:
CHỌN T1.k1, T1.K2, T1.K3, T1.val1, T2.val2FROM dbo.T1 INNER JOIN dbo.T2 ON T1.k1 =T2.k1 AND T1.k2 =T2.k2 AND T1. k3 =T2.k3;Cũng giống như các ví dụ lọc trước đó, cũng ở đây so sánh giữa các NULL bằng cách sử dụng toán tử bình đẳng mang lại UNKNOWN, dẫn đến các kết quả không khớp. Truy vấn này tạo ra một đầu ra trống:
k1 K2 K3 val1 val2 ----------- ----------- ------------- - ---------- (0 hàng bị ảnh hưởng)Sử dụng ISNULL hoặc COALESCE như trong ví dụ lọc trước đó, thay thế NULL bằng một giá trị thường không thể xuất hiện trong dữ liệu ở cả hai phía, dẫn đến một truy vấn chính xác (tôi sẽ gọi truy vấn này là Truy vấn 4):
CHỌN T1.k1, T1.K2, T1.K3, T1.val1, T2.val2FROM dbo.T1 INNER JOIN dbo.T2 ON ISNULL (T1.k1, -2147483648) =ISNULL (T2.k1, -2147483648 ) AND ISNULL (T1.k2, -2147483648) =ISNULL (T2.k2, -2147483648) AND ISNULL (T1.k3, -2147483648) =ISNULL (T2.k3, -2147483648);Truy vấn này tạo ra kết quả sau:
k1 K2 K3 val1 val2 ----------- ----------- ------------- - ---------- 0 NULL NULL C I0 NULL 1 F KTuy nhiên, giống như thao tác với cột được lọc sẽ phá vỡ khả năng SARGability của vị từ bộ lọc, thao tác với cột nối sẽ ngăn khả năng dựa vào thứ tự chỉ mục. Điều này có thể được nhìn thấy trong kế hoạch cho truy vấn này như thể hiện trong Hình 4.
Hình 4:Kế hoạch cho Truy vấn 4
Phương án tối ưu cho truy vấn này là phương án áp dụng các bản quét có thứ tự của hai chỉ mục bao gồm theo sau là thuật toán Kết hợp Hợp nhất, không có sắp xếp rõ ràng. Trình tối ưu hóa đã chọn một kế hoạch khác vì nó không thể dựa vào thứ tự lập chỉ mục. Nếu bạn cố gắng ép buộc một thuật toán Kết hợp bằng cách sử dụng INNER MERGE JOIN, thì kế hoạch sẽ vẫn dựa trên việc quét các chỉ mục không có thứ tự, sau đó là sắp xếp rõ ràng. Hãy thử nó!
Tất nhiên, bạn có thể sử dụng các vị từ dài dòng tương tự như các vị từ SARGable được hiển thị trước đó cho các tác vụ lọc:
CHỌN T1.k1, T1.K2, T1.K3, T1.val1, T2.val2FROM dbo.T1 INNER JOIN dbo.T2 ON (T1.k1 =T2.k1 OR (T1.k1 LÀ KHÔNG VÀ T2. K1 LÀ KHÔNG ĐỦ)) VÀ (T1.k2 =T2.k2 HOẶC (T1.k2 LÀ KHÔNG VÀ T2.K2 LÀ KHÔNG)) VÀ (T1.k3 =T2.k3 HOẶC (T1.k3 LÀ KHÔNG VÀ T2.K3 LÀ KHÔNG));Truy vấn này tạo ra kết quả mong muốn và cho phép trình tối ưu hóa dựa vào thứ tự chỉ mục. Tuy nhiên, hy vọng của chúng tôi là tìm ra một giải pháp vừa tối ưu vừa ngắn gọn.
Có một kỹ thuật ngắn gọn và trang nhã ít được biết đến mà bạn có thể sử dụng trong cả phép nối và bộ lọc, cho cả mục đích xác định kết quả phù hợp và xác định các kết quả không khớp. Kỹ thuật này đã được phát hiện và ghi lại cách đây nhiều năm, chẳng hạn như trong bài viết xuất sắc của Paul White về Kế hoạch truy vấn không có tài liệu:So sánh bình đẳng từ năm 2011. Nhưng vì một số lý do mà có vẻ như vẫn còn nhiều người không biết về nó, và tiếc là cuối cùng lại sử dụng các giải pháp không tiêu chuẩn. Nó chắc chắn xứng đáng được tiếp xúc và yêu thích nhiều hơn.
Kỹ thuật này dựa trên thực tế là các toán tử tập hợp như INTERSECT và EXCEPT sử dụng phương pháp so sánh dựa trên tính khác biệt khi so sánh các giá trị chứ không phải phương pháp so sánh dựa trên bình đẳng hoặc bất bình đẳng.
Hãy xem xét nhiệm vụ tham gia của chúng tôi như một ví dụ. Nếu chúng tôi không cần trả về các cột khác với các khóa nối, chúng tôi sẽ sử dụng một truy vấn đơn giản (tôi sẽ gọi nó là Truy vấn 5) với toán tử INTERSECT, như sau:
CHỌN k1, k2, k3 FROM dbo.T1INTERSECTSELECT k1, k2, k3 FROM dbo.T2;Truy vấn này tạo ra kết quả sau:
k1 k2 k3 ----------- ----------- ----------- 0 NULL NULL0 NULL 1Kế hoạch cho truy vấn này được hiển thị trong Hình 5, xác nhận rằng trình tối ưu hóa có thể dựa vào thứ tự chỉ mục và sử dụng thuật toán Kết hợp Hợp nhất.
Hình 5:Kế hoạch cho Truy vấn 5
Như Paul lưu ý trong bài viết của mình, kế hoạch XML cho toán tử tập hợp sử dụng toán tử so sánh IS ngầm định ( CompareOp ="IS" ) trái ngược với toán tử so sánh EQ được sử dụng trong phép nối bình thường ( CompareOp ="EQ" ). Vấn đề với giải pháp chỉ dựa vào toán tử tập hợp là nó giới hạn bạn chỉ trả về các cột mà bạn đang so sánh. Những gì chúng ta thực sự cần là một phép lai giữa một phép nối và một toán tử tập hợp, cho phép bạn so sánh một tập hợp con của các phần tử trong khi trả về những phần tử bổ sung giống như một phép nối đã làm và sử dụng phép so sánh dựa trên tính khác biệt (IS) giống như một toán tử tập hợp. Điều này có thể đạt được bằng cách sử dụng một phép nối làm cấu trúc bên ngoài và một vị từ TỒN TẠI trong mệnh đề BẬT của phép nối dựa trên một truy vấn với toán tử INTERSECT so sánh các khóa tham gia từ hai phía, giống như vậy (Tôi sẽ gọi giải pháp này là Truy vấn 6):
CHỌN T1.k1, T1.K2, T1.K3, T1.val1, T2.val2FROM dbo.T1 INNER JOIN dbo.T2 TRÊN TỒN TẠI (CHỌN T1.k1, T1.k2, T1.k3 LỰA CHỌN INTERSECT T2. k1, T2.k2, T2.k3);Toán tử INTERSECT hoạt động trên hai truy vấn, mỗi truy vấn tạo thành một tập hợp một hàng dựa trên các khóa kết hợp từ hai bên. Khi hai hàng giống nhau, truy vấn INTERSECT trả về một hàng; vị từ EXISTS trả về TRUE, dẫn đến kết quả khớp. Khi hai hàng không giống nhau, truy vấn INTERSECT trả về một tập hợp trống; vị từ EXISTS trả về FALSE, dẫn đến giá trị không khớp.
Giải pháp này tạo ra kết quả mong muốn:
k1 K2 K3 val1 val2 ----------- ----------- ------------- - ---------- 0 NULL NULL C I0 NULL 1 F KKế hoạch cho truy vấn này được hiển thị trong Hình 6, xác nhận rằng trình tối ưu hóa có thể dựa vào thứ tự chỉ mục.
Hình 6:Kế hoạch cho Truy vấn 6
Bạn có thể sử dụng cấu trúc tương tự làm vị từ bộ lọc liên quan đến cột và tham số / biến để tìm kiếm các kết quả phù hợp dựa trên sự khác biệt, như sau:
DECLARE @dt AS DATE =NULL; CHỌN orderid, ngày vận chuyểnFROM Bán hàng.OrdersWHERE TỒN TẠI (CHỌN ngày vận chuyểnKế hoạch giống như kế hoạch được thể hiện trước đó trong Hình 3.
Bạn cũng có thể phủ định vị từ để tìm kiếm các từ không trùng khớp, như sau:
DECLARE @dt AS DATE ='20190212'; CHỌN orderid, ngày vận chuyểnFROM Bán hàng.Đơn hàng KHÔNG TỒN TẠI (CHỌN ngày vận chuyểnTruy vấn này tạo ra kết quả sau:
orderid ngày vận chuyển ----------- ----------- 11008 NULL11019 NULL11039 NULL ... 10847 2019-02-1010856 2019-02-1010871 2019-02-1010867 2019-02-1110874 2019-02-1110870 2019-02-1310884 2019-02-1310840 2019-02-1610887 2019-02-16 ... (825 hàng bị ảnh hưởng)Ngoài ra, bạn có thể sử dụng một vị từ khẳng định, nhưng thay INTERSECT bằng EXCEPT, như sau:
DECLARE @dt AS DATE ='20190212'; CHỌN orderid, ngày vận chuyểnLưu ý rằng kế hoạch trong hai trường hợp có thể khác nhau, vì vậy hãy đảm bảo thử nghiệm cả hai cách với lượng lớn dữ liệu.
Kết luận
NULL thêm phần phức tạp của họ vào việc viết mã SQL của bạn. Bạn luôn muốn nghĩ về khả năng xuất hiện của NULL trong dữ liệu và đảm bảo rằng bạn sử dụng cấu trúc truy vấn phù hợp và thêm logic có liên quan vào các giải pháp của bạn để xử lý NULL một cách chính xác. Bỏ qua chúng là một cách chắc chắn để kết thúc với lỗi trong mã của bạn. Tháng này, tôi tập trung vào NULL là gì và chúng được xử lý như thế nào trong các phép so sánh liên quan đến hằng số, biến, tham số và cột. Tháng tới, tôi sẽ tiếp tục đề cập bằng cách thảo luận về sự mâu thuẫn trong cách xử lý NULL trong các yếu tố ngôn ngữ khác nhau và việc thiếu các tính năng tiêu chuẩn để xử lý NULL.