Đây là một vấn đề tôi đưa ra chống lại định kỳ và chưa tìm thấy một giải pháp tốt cho.
Giả sử cấu trúc bảng sau
CREATE TABLE T
(
A INT PRIMARY KEY,
B CHAR(1000) NULL,
C CHAR(1000) NULL
)
và yêu cầu là xác định xem một trong hai cột có thể rỗng B
hay C
thực sự có chứa bất kỳ NULL
giá trị nào không (và nếu đó là một (các)).
Cũng giả sử bảng chứa hàng triệu hàng (và không có số liệu thống kê cột nào có thể được xem qua vì tôi quan tâm đến một giải pháp chung hơn cho lớp truy vấn này).
Tôi có thể nghĩ ra một vài cách tiếp cận điều này nhưng tất cả đều có điểm yếu.
Hai EXISTS
tuyên bố riêng biệt . Điều này sẽ có lợi thế là cho phép các truy vấn dừng quét sớm ngay khi NULL
tìm thấy. Nhưng nếu cả hai cột trong thực tế không chứa NULL
s thì sẽ có hai lần quét đầy đủ.
Truy vấn tổng hợp đơn
SELECT
MAX(CASE WHEN B IS NULL THEN 1 ELSE 0 END) AS B,
MAX(CASE WHEN C IS NULL THEN 1 ELSE 0 END) AS C
FROM T
Điều này có thể xử lý cả hai cột cùng một lúc để có trường hợp xấu nhất là quét toàn bộ. Nhược điểm là ngay cả khi nó gặp một NULL
trong cả hai cột từ rất sớm trên truy vấn vẫn sẽ quét toàn bộ phần còn lại của bảng.
Biến người dùng
Tôi có thể nghĩ ra cách thứ ba để làm việc này
BEGIN TRY
DECLARE @B INT, @C INT, @D INT
SELECT
@B = CASE WHEN B IS NULL THEN 1 ELSE @B END,
@C = CASE WHEN C IS NULL THEN 1 ELSE @C END,
/*Divide by zero error if both @B and @C are 1.
Might happen next row as no guarantee of order of
assignments*/
@D = 1 / (2 - (@B + @C))
FROM T
OPTION (MAXDOP 1)
END TRY
BEGIN CATCH
IF ERROR_NUMBER() = 8134 /*Divide by zero*/
BEGIN
SELECT 'B,C both contain NULLs'
RETURN;
END
ELSE
RETURN;
END CATCH
SELECT ISNULL(@B,0),
ISNULL(@C,0)
nhưng điều này không phù hợp với mã sản xuất vì hành vi chính xác cho truy vấn nối ghép tổng hợp không được xác định. và chấm dứt quá trình quét bằng cách ném một lỗi là một giải pháp khá kinh khủng.
Có lựa chọn nào khác kết hợp các điểm mạnh của các phương pháp trên không?
Chỉnh sửa
Chỉ cần cập nhật kết quả này với kết quả tôi nhận được về các lần đọc cho các câu trả lời được gửi cho đến nay (sử dụng dữ liệu kiểm tra của @ ypercube)
+----------+------------+------+---------+----------+----------------------+----------+------------------+
| | 2 * EXISTS | CASE | Kejser | Kejser | Kejser | ypercube | 8kb |
+----------+------------+------+---------+----------+----------------------+----------+------------------+
| | | | | MAXDOP 1 | HASH GROUP, MAXDOP 1 | | |
| No Nulls | 15208 | 7604 | 8343 | 7604 | 7604 | 15208 | 8346 (8343+3) |
| One Null | 7613 | 7604 | 8343 | 7604 | 7604 | 7620 | 7630 (25+7602+3) |
| Two Null | 23 | 7604 | 8343 | 7604 | 7604 | 30 | 30 (18+12) |
+----------+------------+------+---------+----------+----------------------+----------+------------------+
Đối với câu trả lời @ Thomas của tôi đã thay đổi TOP 3
để TOP 2
cho có khả năng cho phép nó để thoát trước đó. Tôi đã có một kế hoạch song song theo mặc định cho câu trả lời đó vì vậy cũng đã thử nó với một MAXDOP 1
gợi ý để làm cho số lần đọc tương đương với các kế hoạch khác. Tôi hơi ngạc nhiên với kết quả như trong thử nghiệm trước đây của tôi, tôi đã thấy truy vấn đó ngắn mạch mà không đọc toàn bộ bảng.
Kế hoạch cho dữ liệu thử nghiệm của tôi rằng ngắn mạch dưới đây
Kế hoạch cho dữ liệu của ypercube là
Vì vậy, nó thêm một toán tử sắp xếp chặn vào kế hoạch. Tôi cũng đã thử với HASH GROUP
gợi ý nhưng cuối cùng vẫn đọc được tất cả các hàng
Vì vậy, chìa khóa dường như là để có được một hash match (flow distinct)
nhà điều hành cho phép kế hoạch này bị đoản mạch vì các giải pháp thay thế khác sẽ chặn và tiêu thụ tất cả các hàng. Tôi không nghĩ có gợi ý để ép buộc điều này một cách cụ thể nhưng rõ ràng "nói chung, trình tối ưu hóa chọn Dòng phân biệt trong đó xác định rằng cần ít hàng đầu ra hơn so với có các giá trị riêng biệt trong bộ đầu vào." .
Dữ liệu của @ ypercube chỉ có 1 hàng trong mỗi cột với NULL
các giá trị (cardinality = 30300) và các hàng ước tính đi vào và ra khỏi toán tử là cả hai 1
. Bằng cách làm cho vị từ mờ hơn một chút đối với trình tối ưu hóa, nó tạo ra một kế hoạch với toán tử Flow Distinc.
SELECT TOP 2 *
FROM (SELECT DISTINCT
CASE WHEN b IS NULL THEN NULL ELSE 'foo' END AS b
, CASE WHEN c IS NULL THEN NULL ELSE 'bar' END AS c
FROM test T
WHERE LEFT(b,1) + LEFT(c,1) IS NULL
) AS DT
Chỉnh sửa 2
Một điều chỉnh cuối cùng xảy ra với tôi là truy vấn ở trên vẫn có thể xử lý nhiều hàng hơn mức cần thiết trong trường hợp hàng đầu tiên mà nó gặp phải NULL
có NULL trong cả hai cột B
và C
. Nó sẽ tiếp tục quét chứ không thoát ra ngay lập tức. Một cách để tránh điều này sẽ là hủy bỏ các hàng khi chúng được quét. Vì vậy, sửa đổi cuối cùng của tôi cho câu trả lời của Thomas Kejers bên dưới
SELECT DISTINCT TOP 2 NullExists
FROM test T
CROSS APPLY (VALUES(CASE WHEN b IS NULL THEN 'b' END),
(CASE WHEN c IS NULL THEN 'c' END)) V(NullExists)
WHERE NullExists IS NOT NULL
Có lẽ sẽ tốt hơn cho vị từ WHERE (b IS NULL OR c IS NULL) AND NullExists IS NOT NULL
nhưng ngược lại với dữ liệu thử nghiệm trước đó mà người ta không đưa ra cho tôi một kế hoạch với Dòng chảy riêng biệt, trong khi đó thì NullExists IS NOT NULL
có (kế hoạch bên dưới).
TOP 3
chỉ có thể làTOP 2
hiện vì nó sẽ quét cho đến khi nó tìm thấy một trong mỗi điều sau đây(NOT_NULL,NULL)
,(NULL,NOT_NULL)
,(NULL,NULL)
. Bất kỳ 2 trong số 3 sẽ là đủ - và nếu nó tìm thấy(NULL,NULL)
đầu tiên thì thứ hai cũng sẽ không cần thiết. Ngoài ra, để đoản mạch, kế hoạch sẽ cần triển khai sự khác biệt thông qua mộthash match (flow distinct)
nhà điều hành thay vìhash match (aggregate)
hoặcdistinct sort