Truy vấn chậm hơn 100 lần trong SQL Server 2014, Row Spool Row ước tính thủ phạm?


11

Tôi có một truy vấn chạy trong 800 mili giây trong SQL Server 2012 và mất khoảng 170 giây trong SQL Server 2014 . Tôi nghĩ rằng tôi đã thu hẹp điều này xuống mức ước tính cardinality kém cho Row Count Spoolnhà điều hành. Tôi đã đọc một chút về các toán tử bộ đệm (ví dụ, ở đâyở đây ), nhưng vẫn gặp khó khăn khi hiểu một số điều:

  • Tại sao truy vấn này cần một Row Count Spooltoán tử? Tôi không nghĩ rằng nó cần thiết cho tính chính xác, vậy nó đang cố gắng cung cấp tối ưu hóa cụ thể nào?
  • Tại sao SQL Server ước tính rằng phép nối với Row Count Spooltoán tử loại bỏ tất cả các hàng?
  • Đây có phải là một lỗi trong SQL Server 2014 không? Nếu vậy, tôi sẽ gửi trong Connect. Nhưng tôi muốn hiểu sâu hơn trước.

Lưu ý: Tôi có thể viết lại truy vấn dưới dạng LEFT JOINhoặc thêm chỉ mục vào các bảng để đạt được hiệu suất chấp nhận được trong cả SQL Server 2012 và SQL Server 2014. Vì vậy, câu hỏi này là về cách hiểu sâu hơn về truy vấn cụ thể này và lên kế hoạch làm thế nào để cụm từ truy vấn khác nhau.


Truy vấn chậm

Xem Pastebin này cho một kịch bản thử nghiệm đầy đủ. Đây là truy vấn kiểm tra cụ thể mà tôi đang xem:

-- Prune any existing customers from the set of potential new customers
-- This query is much slower than expected in SQL Server 2014 
SELECT *
FROM #potentialNewCustomers -- 10K rows
WHERE cust_nbr NOT IN (
    SELECT cust_nbr
    FROM #existingCustomers -- 1MM rows
)


SQL Server 2014: Gói truy vấn ước tính

SQL Server tin rằng Left Anti Semi Joinđến Row Count Spoolsẽ lọc 10.000 hàng xuống 1 hàng. Vì lý do này, nó chọn một LOOP JOINtham gia tiếp theo #existingCustomers.

nhập mô tả hình ảnh ở đây


SQL Server 2014: Gói truy vấn thực tế

Như mong đợi (bởi tất cả mọi người trừ SQL Server!), Row Count SpoolĐã không xóa bất kỳ hàng nào. Vì vậy, chúng tôi đang lặp 10.000 lần khi SQL Server dự kiến ​​lặp lại một lần.

nhập mô tả hình ảnh ở đây


SQL Server 2012: Gói truy vấn ước tính

Khi sử dụng SQL Server 2012 (hoặc OPTION (QUERYTRACEON 9481)trong SQL Server 2014), việc Row Count Spoolkhông giảm số lượng hàng ước tính và tham gia băm được chọn, dẫn đến một kế hoạch tốt hơn nhiều.

nhập mô tả hình ảnh ở đây

THAM GIA TRẢ LẠI viết lại

Để tham khảo, đây là cách tôi có thể viết lại truy vấn để đạt được hiệu suất tốt trong tất cả SQL Server 2012, 2014 và 2016. Tuy nhiên, tôi vẫn quan tâm đến hành vi cụ thể của truy vấn ở trên và liệu nó có là một lỗi trong Công cụ ước tính Cardinality SQL Server 2014 mới.

-- Re-writing with LEFT JOIN yields much better performance in 2012/2014/2016
SELECT n.*
FROM #potentialNewCustomers n
LEFT JOIN (SELECT 1 AS test, cust_nbr FROM #existingCustomers) c
    ON c.cust_nbr = n.cust_nbr
WHERE c.test IS NULL

nhập mô tả hình ảnh ở đây

Câu trả lời:


8

Tại sao truy vấn này cần một toán tử Row Count Spool? ... nó đang cố gắng cung cấp tối ưu hóa cụ thể nào?

Các cust_nbrcột #existingCustomerslà nullable. Nếu nó thực sự chứa bất kỳ null nào, phản hồi chính xác ở đây là trả về các hàng 0 ( NOT IN (NULL,...) sẽ luôn mang lại một tập kết quả trống.).

Vì vậy, truy vấn có thể được coi là

SELECT p.*
FROM   #potentialNewCustomers p
WHERE  NOT EXISTS (SELECT *
                   FROM   #existingCustomers e1
                   WHERE  p.cust_nbr = e1.cust_nbr)
       AND NOT EXISTS (SELECT *
                       FROM   #existingCustomers e2
                       WHERE  e2.cust_nbr IS NULL) 

Với bộ đệm hàng có để tránh phải đánh giá

EXISTS (SELECT *
        FROM   #existingCustomers e2
        WHERE  e2.cust_nbr IS NULL) 

Nhiều hơn một lần.

Đây dường như chỉ là một trường hợp mà một sự khác biệt nhỏ trong các giả định có thể tạo ra một sự khác biệt khá lớn trong hiệu suất.

Sau khi cập nhật một hàng như dưới đây ...

UPDATE #existingCustomers
SET    cust_nbr = NULL
WHERE  cust_nbr = 1;

... truy vấn hoàn thành trong chưa đầy một giây. Các hàng được tính trong các phiên bản thực tế và ước tính của kế hoạch hiện đã gần đạt được.

SET STATISTICS TIME ON;
SET STATISTICS IO ON;

SELECT *
FROM   #potentialNewCustomers
WHERE  cust_nbr NOT IN (SELECT cust_nbr
                        FROM   #existingCustomers 
                       ) 

nhập mô tả hình ảnh ở đây

Hàng không là đầu ra như mô tả ở trên.

Biểu đồ thống kê và ngưỡng cập nhật tự động trong SQL Server không đủ chi tiết để phát hiện loại thay đổi hàng đơn này. Có thể cho rằng nếu cột là null, có thể hợp lý khi làm việc trên cơ sở rằng nó chứa ít nhất một NULLcột ngay cả khi biểu đồ thống kê hiện không chỉ ra rằng có bất kỳ.


8

Tại sao truy vấn này cần một toán tử Row Count Spool? Tôi không nghĩ rằng nó cần thiết cho tính chính xác, vậy nó đang cố gắng cung cấp tối ưu hóa cụ thể nào?

Xem câu trả lời thấu đáo của Martin cho câu hỏi này. Điểm mấu chốt ở đây là nếu một hàng duy nhất trong NOT INNULL, logic boolean hoạt động ra ví dụ rằng "câu trả lời đúng là để trở về zero hàng". Các Row Count Spoolnhà điều hành được tối ưu hóa này (cần thiết) logic.

Tại sao SQL Server ước tính rằng phép nối với toán tử Row Count Spool loại bỏ tất cả các hàng?

Microsoft cung cấp một trang giấy trắng xuất sắc về Công cụ ước tính Cardinality SQL 2014 . Trong tài liệu này, tôi tìm thấy thông tin sau:

CE mới giả định rằng các giá trị được truy vấn tồn tại trong tập dữ liệu ngay cả khi giá trị nằm ngoài phạm vi của biểu đồ. CE mới trong ví dụ này sử dụng tần số trung bình được tính bằng cách nhân số lượng thẻ của bảng với mật độ.

Thông thường, một sự thay đổi như vậy là một điều rất tốt; nó làm giảm đáng kể vấn đề chính tăng dần và thường mang lại một kế hoạch truy vấn bảo thủ hơn (ước tính hàng cao hơn) cho các giá trị nằm ngoài phạm vi dựa trên biểu đồ thống kê.

Tuy nhiên, trong trường hợp cụ thể này, giả sử rằng một NULLgiá trị sẽ được tìm thấy dẫn đến giả định rằng việc tham gia vào Row Count Spoolsẽ lọc tất cả các hàng từ đó #potentialNewCustomers. Trong trường hợp trên thực tế có một NULLhàng, đây là một ước tính chính xác (như đã thấy trong câu trả lời của Martin). Tuy nhiên, trong trường hợp không xảy ra một NULLhàng, hiệu ứng có thể bị tàn phá vì SQL Server tạo ra ước tính sau khi tham gia là 1 hàng bất kể có bao nhiêu hàng đầu vào xuất hiện. Điều này có thể dẫn đến các lựa chọn tham gia rất kém trong phần còn lại của kế hoạch truy vấn.

Đây có phải là một lỗi trong SQL 2014? Nếu vậy, tôi sẽ gửi trong Connect. Nhưng tôi muốn hiểu sâu hơn trước.

Tôi nghĩ rằng đó là trong khu vực màu xám giữa một lỗi và một giả định hoặc giới hạn ảnh hưởng đến hiệu suất của Công cụ ước tính Cardinality mới của SQL Server. Tuy nhiên, việc giải quyết vấn đề này có thể gây ra sự hồi quy đáng kể về hiệu suất so với SQL 2012 trong trường hợp cụ thể của một NOT INmệnh đề nullable không xảy ra có bất kỳ NULLgiá trị nào .

Do đó, tôi đã gửi vấn đề Kết nối để nhóm SQL nhận thức được ý nghĩa tiềm ẩn của thay đổi này đối với Công cụ ước tính Cardinality.

Cập nhật: Hiện tại chúng tôi đang sử dụng CTP3 cho SQL16 và tôi đã xác nhận rằng sự cố không xảy ra ở đó.


4

Câu trả lời của Martin Smith và câu trả lời tự trả lời của bạn đã giải quyết chính xác tất cả các điểm chính, tôi chỉ muốn nhấn mạnh một lĩnh vực cho độc giả tương lai:

Vì vậy, câu hỏi này là nhiều hơn về việc hiểu truy vấn cụ thể này và lập kế hoạch chuyên sâu và ít hơn về cách diễn đạt các truy vấn khác nhau.

Mục đích đã nêu của truy vấn là:

-- Prune any existing customers from the set of potential new customers

Yêu cầu này dễ dàng diễn đạt bằng SQL, theo nhiều cách. Cái nào được chọn cũng là một vấn đề về kiểu dáng, nhưng đặc tả truy vấn vẫn phải được viết để trả về kết quả chính xác trong mọi trường hợp.Điều này bao gồm kế toán cho null.

Thể hiện đầy đủ yêu cầu logic:

  • Trả lại khách hàng tiềm năng chưa phải là khách hàng
  • Liệt kê từng khách hàng tiềm năng nhiều nhất một lần
  • Loại trừ tiềm năng null và khách hàng hiện tại (bất kể khách hàng null có nghĩa là gì)

Sau đó chúng ta có thể viết một truy vấn phù hợp với các yêu cầu đó bằng cách sử dụng cú pháp nào chúng ta thích. Ví dụ:

WITH DistinctPotentialNonNullCustomers AS
(
    SELECT DISTINCT 
        PNC.cust_nbr 
    FROM #potentialNewCustomers AS PNC
    WHERE 
        PNC.cust_nbr IS NOT NULL
)
SELECT
    DPNNC.cust_nbr
FROM DistinctPotentialNonNullCustomers AS DPNNC
WHERE
    DPNNC.cust_nbr NOT IN
    (
        SELECT 
            EC.cust_nbr 
        FROM #existingCustomers AS EC 
        WHERE 
            EC.cust_nbr IS NOT NULL
    );

Điều này tạo ra một kế hoạch thực hiện hiệu quả, trả về kết quả chính xác:

Kế hoạch thực hiện

Chúng tôi có thể thể hiện NOT INnhư là <> ALLhoặc NOT = ANYkhông ảnh hưởng đến kế hoạch hoặc kết quả:

WITH DistinctPotentialNonNullCustomers AS
(
    SELECT DISTINCT 
        PNC.cust_nbr 
    FROM #potentialNewCustomers AS PNC
    WHERE 
        PNC.cust_nbr IS NOT NULL
)
SELECT
    DPNNC.cust_nbr
FROM DistinctPotentialNonNullCustomers AS DPNNC
WHERE
    DPNNC.cust_nbr <> ALL
    (
        SELECT 
            EC.cust_nbr 
        FROM #existingCustomers AS EC 
        WHERE 
            EC.cust_nbr IS NOT NULL
    );
WITH DistinctPotentialNonNullCustomers AS
(
    SELECT DISTINCT 
        PNC.cust_nbr 
    FROM #potentialNewCustomers AS PNC
    WHERE 
        PNC.cust_nbr IS NOT NULL
)
SELECT
    DPNNC.cust_nbr
FROM DistinctPotentialNonNullCustomers AS DPNNC
WHERE
    NOT DPNNC.cust_nbr = ANY
    (
        SELECT 
            EC.cust_nbr 
        FROM #existingCustomers AS EC 
        WHERE 
            EC.cust_nbr IS NOT NULL
    );

Hoặc sử dụng NOT EXISTS:

WITH DistinctPotentialNonNullCustomers AS
(
    SELECT DISTINCT 
        PNC.cust_nbr 
    FROM #potentialNewCustomers AS PNC
    WHERE 
        PNC.cust_nbr IS NOT NULL
)
SELECT
    DPNNC.cust_nbr
FROM DistinctPotentialNonNullCustomers AS DPNNC
WHERE 
    NOT EXISTS
    (
        SELECT * 
        FROM #existingCustomers AS EC
        WHERE
            EC.cust_nbr = DPNNC.cust_nbr
            AND EC.cust_nbr IS NOT NULL
    );

Không có gì kỳ diệu về vấn đề này, hoặc bất cứ điều gì đặc biệt phản đối về việc sử dụng IN, ANYhoặcALL - chúng ta chỉ cần viết truy vấn chính xác, vì vậy nó sẽ luôn tạo ra kết quả đúng.

Hình thức nhỏ gọn nhất sử dụng EXCEPT:

SELECT 
    PNC.cust_nbr 
FROM #potentialNewCustomers AS PNC
WHERE 
    PNC.cust_nbr IS NOT NULL
EXCEPT
SELECT
    EC.cust_nbr 
FROM #existingCustomers AS EC
WHERE 
    EC.cust_nbr IS NOT NULL;

Điều này cũng tạo ra kết quả chính xác, mặc dù kế hoạch thực hiện có thể kém hiệu quả hơn do không có bộ lọc bitmap:

Kế hoạch thực hiện phi bitmap

Câu hỏi ban đầu rất thú vị vì nó phơi bày một vấn đề ảnh hưởng đến hiệu suất với việc thực hiện kiểm tra null cần thiết. Điểm của câu trả lời này là viết đúng truy vấn cũng tránh được vấn đề.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.