Điều này chắc chắn có vẻ như hành vi ngoài ý muốn. Đúng là ước tính cardinality không cần nhất quán ở mỗi bước của kế hoạch nhưng đây là kế hoạch truy vấn tương đối đơn giản và ước tính cardinality cuối cùng không phù hợp với những gì truy vấn đang thực hiện. Ước tính số lượng thẻ thấp như vậy có thể dẫn đến các lựa chọn kém cho các loại tham gia và phương thức truy cập cho các bảng khác ở hạ lưu trong một kế hoạch phức tạp hơn.
Thông qua thử nghiệm và lỗi, chúng tôi có thể đưa ra một vài truy vấn tương tự mà vấn đề không xuất hiện:
SELECT
ID
, CASE
WHEN ID <> 0
THEN (SELECT TOP 1 ID FROM dbo.X_OTHER_TABLE)
ELSE (SELECT -1)
END AS ID2
FROM dbo.X_HEAP;
SELECT
ID
, CASE
WHEN ID < 500
THEN (SELECT TOP 1 ID FROM dbo.X_OTHER_TABLE)
WHEN ID >= 500
THEN (SELECT TOP 1 ID FROM dbo.X_OTHER_TABLE_2)
END AS ID2
FROM dbo.X_HEAP;
Chúng tôi cũng có thể đưa ra nhiều truy vấn hơn mà vấn đề xuất hiện:
SELECT
ID
, CASE
WHEN ID < 500
THEN (SELECT TOP 1 ID FROM dbo.X_OTHER_TABLE)
WHEN ID >= 500
THEN (SELECT TOP 1 ID FROM dbo.X_OTHER_TABLE_2)
ELSE (SELECT TOP 1 ID FROM X_OTHER_TABLE)
END AS ID2
FROM dbo.X_HEAP;
SELECT
ID
, CASE
WHEN ID = 0
THEN (SELECT TOP 1 ID FROM dbo.X_OTHER_TABLE)
ELSE (SELECT -1)
END AS ID2
FROM dbo.X_HEAP;
SELECT
ID
, CASE
WHEN ID = 0
THEN (SELECT TOP 1 ID FROM dbo.X_OTHER_TABLE)
ELSE (SELECT TOP 1 ID FROM dbo.X_OTHER_TABLE_2)
END AS ID2
FROM dbo.X_HEAP;
Dường như có một mẫu: nếu có một biểu thức bên trong CASE
dự kiến sẽ không được thực thi và biểu thức kết quả là một truy vấn con đối với bảng thì ước tính hàng giảm xuống 1 sau biểu thức đó.
Nếu tôi viết truy vấn đối với một bảng có chỉ mục được nhóm, các quy tắc sẽ thay đổi phần nào. Chúng tôi có thể sử dụng cùng một dữ liệu:
CREATE TABLE dbo.X_CI (ID INT NOT NULL, PRIMARY KEY (ID))
INSERT INTO dbo.X_CI WITH (TABLOCK)
SELECT * FROM dbo.X_HEAP;
UPDATE STATISTICS X_CI WITH FULLSCAN;
Truy vấn này có ước tính 1000 hàng cuối cùng:
SELECT
ID
, CASE
WHEN ID = 0
THEN (SELECT TOP 1 ID FROM dbo.X_OTHER_TABLE_2)
ELSE (SELECT TOP 1 ID FROM dbo.X_OTHER_TABLE)
END
FROM dbo.X_CI;
Nhưng truy vấn này có ước tính 1 hàng cuối cùng:
SELECT
ID
, CASE
WHEN ID <> 0
THEN (SELECT TOP 1 ID FROM dbo.X_OTHER_TABLE)
ELSE (SELECT TOP 1 ID FROM dbo.X_OTHER_TABLE_2)
END
FROM dbo.X_CI;
Để tìm hiểu sâu hơn về vấn đề này, chúng ta có thể sử dụng cờ theo dõi không có giấy tờ 2363 để lấy thông tin về cách trình tối ưu hóa truy vấn thực hiện các phép tính chọn lọc. Tôi thấy hữu ích khi ghép cờ theo dõi đó với cờ theo dõi không có giấy tờ 8606 . TF 2363 dường như đưa ra các tính toán chọn lọc cho cả cây được đơn giản hóa và cây sau khi chuẩn hóa dự án. Có cả hai cờ theo dõi được kích hoạt làm cho nó rõ ràng tính toán nào áp dụng cho cây nào.
Hãy thử truy vấn ban đầu được đăng trong câu hỏi:
SELECT
ID
, CASE
WHEN ID <> 0
THEN (SELECT TOP 1 ID FROM X_OTHER_TABLE)
ELSE (SELECT TOP 1 ID FROM X_OTHER_TABLE_2)
END AS ID2
FROM X_HEAP
OPTION (QUERYTRACEON 3604, QUERYTRACEON 2363, QUERYTRACEON 8606);
Đây là một phần của đầu ra mà tôi nghĩ là có liên quan cùng với một số ý kiến:
Plan for computation:
CSelCalcColumnInInterval -- this is the type of calculator used
Column: QCOL: [SE_DB].[dbo].[X_HEAP].ID -- this is the column used for the calculation
Pass-through selectivity: 0 -- all rows are expected to have a true value for the case expression
Stats collection generated:
CStCollOuterJoin(ID=8, CARD=1000 x_jtLeftOuter) -- the row estimate after the join will still be 1000
CStCollBaseTable(ID=1, CARD=1000 TBL: X_HEAP)
CStCollBaseTable(ID=2, CARD=1 TBL: X_OTHER_TABLE)
...
Plan for computation:
CSelCalcColumnInInterval
Column: QCOL: [SE_DB].[dbo].[X_HEAP].ID
Pass-through selectivity: 1 -- no rows are expected to have a true value for the case expression
Stats collection generated:
CStCollOuterJoin(ID=9, CARD=1 x_jtLeftOuter) -- the row estimate after the join will still be 1
CStCollOuterJoin(ID=8, CARD=1000 x_jtLeftOuter) -- here is the row estimate after the previous join
CStCollBaseTable(ID=1, CARD=1000 TBL: X_HEAP)
CStCollBaseTable(ID=2, CARD=1 TBL: X_OTHER_TABLE)
CStCollBaseTable(ID=3, CARD=1 TBL: X_OTHER_TABLE_2)
Bây giờ chúng ta hãy thử nó cho một truy vấn tương tự không có vấn đề. Tôi sẽ sử dụng cái này:
SELECT
ID
, CASE
WHEN ID <> 0
THEN (SELECT TOP 1 ID FROM dbo.X_OTHER_TABLE)
ELSE (SELECT -1)
END AS ID2
FROM dbo.X_HEAP
OPTION (QUERYTRACEON 3604, QUERYTRACEON 2363, QUERYTRACEON 8606);
Đầu ra gỡ lỗi ở cuối:
Plan for computation:
CSelCalcColumnInInterval
Column: QCOL: [SE_DB].[dbo].[X_HEAP].ID
Pass-through selectivity: 1
Stats collection generated:
CStCollOuterJoin(ID=9, CARD=1000 x_jtLeftOuter)
CStCollOuterJoin(ID=8, CARD=1000 x_jtLeftOuter)
CStCollBaseTable(ID=1, CARD=1000 TBL: dbo.X_HEAP)
CStCollBaseTable(ID=2, CARD=1 TBL: dbo.X_OTHER_TABLE)
CStCollConstTable(ID=4, CARD=1) -- this is different than before because we select a constant instead of from a table
Hãy thử một truy vấn khác có ước tính hàng xấu:
SELECT
ID
, CASE
WHEN ID < 500
THEN (SELECT TOP 1 ID FROM dbo.X_OTHER_TABLE)
WHEN ID >= 500
THEN (SELECT TOP 1 ID FROM dbo.X_OTHER_TABLE_2)
ELSE (SELECT TOP 1 ID FROM X_OTHER_TABLE)
END AS ID2
FROM dbo.X_HEAP
OPTION (QUERYTRACEON 3604, QUERYTRACEON 2363, QUERYTRACEON 8606);
Cuối cùng, ước tính cardinality giảm xuống 1 hàng, một lần nữa sau khi chọn lọc Pass-through = 1. Ước tính cardinality được bảo toàn sau độ chọn lọc là 0,501 và 0,499.
Plan for computation:
CSelCalcColumnInInterval
Column: QCOL: [SE_DB].[dbo].[X_HEAP].ID
Pass-through selectivity: 0.501
...
Plan for computation:
CSelCalcColumnInInterval
Column: QCOL: [SE_DB].[dbo].[X_HEAP].ID
Pass-through selectivity: 0.499
...
Plan for computation:
CSelCalcColumnInInterval
Column: QCOL: [SE_DB].[dbo].[X_HEAP].ID
Pass-through selectivity: 1
Stats collection generated:
CStCollOuterJoin(ID=12, CARD=1 x_jtLeftOuter) -- this is associated with the ELSE expression
CStCollOuterJoin(ID=11, CARD=1000 x_jtLeftOuter)
CStCollOuterJoin(ID=10, CARD=1000 x_jtLeftOuter)
CStCollBaseTable(ID=1, CARD=1000 TBL: dbo.X_HEAP)
CStCollBaseTable(ID=2, CARD=1 TBL: dbo.X_OTHER_TABLE)
CStCollBaseTable(ID=3, CARD=1 TBL: dbo.X_OTHER_TABLE_2)
CStCollBaseTable(ID=4, CARD=1 TBL: X_OTHER_TABLE)
Chúng ta lại chuyển sang một truy vấn tương tự khác không có vấn đề. Tôi sẽ sử dụng cái này:
SELECT
ID
, CASE
WHEN ID < 500
THEN (SELECT TOP 1 ID FROM dbo.X_OTHER_TABLE)
WHEN ID >= 500
THEN (SELECT TOP 1 ID FROM dbo.X_OTHER_TABLE_2)
END AS ID2
FROM dbo.X_HEAP
OPTION (QUERYTRACEON 3604, QUERYTRACEON 2363, QUERYTRACEON 8606);
Trong đầu ra gỡ lỗi, không bao giờ có bước nào có độ chọn lọc vượt qua là 1. Ước tính cardinality duy trì ở 1000 hàng.
Plan for computation:
CSelCalcColumnInInterval
Column: QCOL: [SE_DB].[dbo].[X_HEAP].ID
Pass-through selectivity: 0.499
Stats collection generated:
CStCollOuterJoin(ID=9, CARD=1000 x_jtLeftOuter)
CStCollOuterJoin(ID=8, CARD=1000 x_jtLeftOuter)
CStCollBaseTable(ID=1, CARD=1000 TBL: dbo.X_HEAP)
CStCollBaseTable(ID=2, CARD=1 TBL: dbo.X_OTHER_TABLE)
CStCollBaseTable(ID=3, CARD=1 TBL: dbo.X_OTHER_TABLE_2)
End selectivity computation
Điều gì về truy vấn khi nó liên quan đến một bảng có chỉ mục được nhóm? Xem xét các truy vấn sau đây với vấn đề ước tính hàng:
SELECT
ID
, CASE
WHEN ID <> 0
THEN (SELECT TOP 1 ID FROM dbo.X_OTHER_TABLE)
ELSE (SELECT TOP 1 ID FROM dbo.X_OTHER_TABLE_2)
END
FROM dbo.X_CI
OPTION (QUERYTRACEON 3604, QUERYTRACEON 2363, QUERYTRACEON 8606);
Kết thúc của đầu ra gỡ lỗi tương tự như những gì chúng ta đã thấy:
Plan for computation:
CSelCalcColumnInInterval
Column: QCOL: [SE_DB].[dbo].[X_CI].ID
Pass-through selectivity: 1
Stats collection generated:
CStCollOuterJoin(ID=9, CARD=1 x_jtLeftOuter)
CStCollOuterJoin(ID=8, CARD=1000 x_jtLeftOuter)
CStCollBaseTable(ID=1, CARD=1000 TBL: dbo.X_CI)
CStCollBaseTable(ID=2, CARD=1 TBL: dbo.X_OTHER_TABLE)
CStCollBaseTable(ID=3, CARD=1 TBL: dbo.X_OTHER_TABLE_2)
Tuy nhiên, truy vấn đối với CI mà không có vấn đề có đầu ra khác nhau. Sử dụng truy vấn này:
SELECT
ID
, CASE
WHEN ID = 0
THEN (SELECT TOP 1 ID FROM dbo.X_OTHER_TABLE_2)
ELSE (SELECT TOP 1 ID FROM dbo.X_OTHER_TABLE)
END
FROM dbo.X_CI
OPTION (QUERYTRACEON 3604, QUERYTRACEON 2363, QUERYTRACEON 8606);
Kết quả trong các máy tính khác nhau đang được sử dụng. CSelCalcColumnInInterval
không còn xuất hiện:
Plan for computation:
CSelCalcFixedFilter (0.559)
Pass-through selectivity: 0.559
Stats collection generated:
CStCollOuterJoin(ID=8, CARD=1000 x_jtLeftOuter)
CStCollBaseTable(ID=1, CARD=1000 TBL: dbo.X_CI)
CStCollBaseTable(ID=2, CARD=1 TBL: dbo.X_OTHER_TABLE_2)
...
Plan for computation:
CSelCalcUniqueKeyFilter
Pass-through selectivity: 0.001
Stats collection generated:
CStCollOuterJoin(ID=9, CARD=1000 x_jtLeftOuter)
CStCollOuterJoin(ID=8, CARD=1000 x_jtLeftOuter)
CStCollBaseTable(ID=1, CARD=1000 TBL: dbo.X_CI)
CStCollBaseTable(ID=2, CARD=1 TBL: dbo.X_OTHER_TABLE_2)
CStCollBaseTable(ID=3, CARD=1 TBL: dbo.X_OTHER_TABLE)
Để kết luận, chúng tôi dường như nhận được một ước tính hàng xấu sau khi truy vấn con theo các điều kiện sau:
Máy CSelCalcColumnInInterval
tính chọn lọc được sử dụng. Tôi không biết chính xác khi nào nó được sử dụng nhưng nó dường như xuất hiện thường xuyên hơn khi bảng cơ sở là một đống.
Độ chọn lọc chuyển qua = 1. Nói cách khác, một trong các CASE
biểu thức được dự kiến sẽ được ước tính thành sai cho tất cả các hàng. Không thành vấn đề nếu CASE
biểu thức đầu tiên ước tính là đúng cho tất cả các hàng.
Có một tham gia bên ngoài để CStCollBaseTable
. Nói cách khác, CASE
biểu thức kết quả là một truy vấn con đối với bảng. Một giá trị không đổi sẽ không hoạt động.
Có lẽ trong các điều kiện đó, trình tối ưu hóa truy vấn vô tình áp dụng tính chọn lọc chuyển qua cho ước tính hàng của bảng bên ngoài thay vì cho công việc được thực hiện ở phần bên trong của vòng lặp lồng nhau. Điều đó sẽ làm giảm ước tính hàng xuống 1.
Tôi đã có thể tìm thấy hai cách giải quyết. Tôi đã không thể tái tạo vấn đề khi sử dụng APPLY
thay vì truy vấn con. Đầu ra của cờ theo dõi 2363 rất khác với APPLY
. Đây là một cách để viết lại truy vấn ban đầu trong câu hỏi:
SELECT
h.ID
, a.ID2
FROM X_HEAP h
OUTER APPLY
(
SELECT CASE
WHEN ID <> 0
THEN (SELECT TOP 1 ID FROM X_OTHER_TABLE)
ELSE (SELECT TOP 1 ID FROM X_OTHER_TABLE_2)
END
) a(ID2);
Các di sản CE xuất hiện để tránh vấn đề là tốt.
SELECT
ID
, CASE
WHEN ID <> 0
THEN (SELECT TOP 1 ID FROM X_OTHER_TABLE)
ELSE (SELECT TOP 1 ID FROM X_OTHER_TABLE_2)
END AS ID2
FROM X_HEAP
OPTION (USE HINT('FORCE_LEGACY_CARDINALITY_ESTIMATION'));
Một mục kết nối đã được gửi cho vấn đề này (với một số chi tiết mà Paul White cung cấp trong câu trả lời của anh ấy).