Kiểm tra sự tồn tại với EXISTS vượt trội hơn COUNT! … Không phải?


35

Tôi thường đọc khi người ta phải kiểm tra sự tồn tại của một hàng nên luôn luôn được thực hiện với EXISTS thay vì với COUNT.

Tuy nhiên, trong một số tình huống gần đây, tôi đã đo lường sự cải thiện hiệu suất khi sử dụng số lượng.
Mô hình đi như thế này:

LEFT JOIN (
    SELECT
        someID
        , COUNT(*)
    FROM someTable
    GROUP BY someID
) AS Alias ON (
    Alias.someID = mainTable.ID
)

Tôi không quen với các phương pháp để nói những gì đang xảy ra "bên trong" Máy chủ SQL, vì vậy tôi đã tự hỏi liệu có một lỗ hổng không rõ ràng nào với EXISTS có ý nghĩa hoàn hảo đối với các phép đo mà tôi đã thực hiện không (có thể EXISTS là RBAR?!).

Bạn có một số lời giải thích cho hiện tượng đó?

CHỈNH SỬA:

Đây là một kịch bản đầy đủ mà bạn có thể chạy:

SET NOCOUNT ON
SET STATISTICS IO OFF

DECLARE @tmp1 TABLE (
    ID INT UNIQUE
)


DECLARE @tmp2 TABLE (
    ID INT
    , X INT IDENTITY
    , UNIQUE (ID, X)
)

; WITH T(n) AS (
    SELECT
        ROW_NUMBER() OVER (ORDER BY (SELECT NULL))
    FROM master.dbo.spt_values AS S
) 
, tally(n) AS (
    SELECT
        T2.n * 100 + T1.n
    FROM T AS T1
    CROSS JOIN T AS T2
    WHERE T1.n <= 100
    AND T2.n <= 100
)
INSERT @tmp1
SELECT n
FROM tally AS T1
WHERE n < 10000


; WITH T(n) AS (
    SELECT
        ROW_NUMBER() OVER (ORDER BY (SELECT NULL))
    FROM master.dbo.spt_values AS S
) 
, tally(n) AS (
    SELECT
        T2.n * 100 + T1.n
    FROM T AS T1
    CROSS JOIN T AS T2
    WHERE T1.n <= 100
    AND T2.n <= 100
)
INSERT @tmp2
SELECT T1.n
FROM tally AS T1
CROSS JOIN T AS T2
WHERE T1.n < 10000
AND T1.n % 3 <> 0
AND T2.n < 1 + T1.n % 15

PRINT '
COUNT Version:
'

WAITFOR DELAY '00:00:01'

SET STATISTICS IO ON
SET STATISTICS TIME ON

SELECT
    T1.ID
    , CASE WHEN n > 0 THEN 1 ELSE 0 END AS DoesExist
FROM @tmp1 AS T1
LEFT JOIN (
    SELECT
        T2.ID
        , COUNT(*) AS n
    FROM @tmp2 AS T2
    GROUP BY T2.ID
) AS T2 ON (
    T2.ID = T1.ID
)
WHERE T1.ID BETWEEN 5000 AND 7000
OPTION (RECOMPILE) -- Required since table are filled within the same scope

SET STATISTICS TIME OFF

PRINT '

EXISTS Version:'

WAITFOR DELAY '00:00:01'

SET STATISTICS TIME ON

SELECT
    T1.ID
    , CASE WHEN EXISTS (
        SELECT 1
        FROM @tmp2 AS T2
        WHERE T2.ID = T1.ID
    ) THEN 1 ELSE 0 END AS DoesExist
FROM @tmp1 AS T1
WHERE T1.ID BETWEEN 5000 AND 7000
OPTION (RECOMPILE) -- Required since table are filled within the same scope

SET STATISTICS TIME OFF 

Trên SQL Server 2008R2 (Bảy 64 bit) tôi nhận được kết quả này

COUNT Phiên bản:

Bảng '# 455F344D'. Quét số 1, đọc logic 8, đọc vật lý 0, đọc trước đọc 0, đọc logic 0, đọc vật lý 0, đọc đọc trước 0, đọc trước
bảng 0 # 492FC531 '. Quét số 1, đọc logic 30, đọc vật lý 0, đọc trước đọc 0, đọc logic 0, đọc vật lý lob 0, đọc trước đọc 0, đọc trước 0.

Thời gian thực hiện máy chủ SQL:
Thời gian CPU = 0 ms, thời gian trôi qua = 81 ms.

EXISTS Phiên bản:

Bảng '# 492FC531'. Quét số 1, đọc logic 96, đọc vật lý 0, đọc trước đọc 0, đọc logic 0, đọc vật lý 0, đọc đọc trước 0, đọc trước
"Bảng" # 455F344D '. Quét số 1, đọc logic 8, đọc vật lý 0, đọc trước đọc 0, đọc logic 0, đọc vật lý lob 0, đọc trước đọc 0, đọc trước 0.

Thời gian thực thi máy chủ SQL:
Thời gian CPU = 0 ms, thời gian trôi qua = 76 ms.

Câu trả lời:


43

Tôi thường đọc khi người ta phải kiểm tra sự tồn tại của một hàng nên luôn luôn được thực hiện với EXISTS thay vì với COUNT.

Rất hiếm khi mọi thứ luôn luôn đúng, đặc biệt là khi nói đến cơ sở dữ liệu. Có bất kỳ số cách nào để diễn đạt cùng một ngữ nghĩa trong SQL. Nếu có một quy tắc hữu ích, có thể là viết các truy vấn bằng cách sử dụng cú pháp tự nhiên nhất có sẵn (và, vâng, đó là chủ quan) và chỉ xem xét viết lại nếu kế hoạch truy vấn hoặc hiệu suất bạn nhận được là không thể chấp nhận được.

Đối với những gì nó có giá trị, riêng tôi về vấn đề là các truy vấn tồn tại được thể hiện một cách tự nhiên nhất bằng cách sử dụng EXISTS. Đó cũng là kinh nghiệm của tôi EXISTS có xu hướng tối ưu hóa tốt hơn so với lựa chọn OUTER JOINtừ chối NULL. Sử dụng COUNT(*)và lọc trên =0là một cách khác, có thể có một số hỗ trợ trong trình tối ưu hóa truy vấn SQL Server, nhưng cá nhân tôi thấy điều này không đáng tin cậy trong các truy vấn phức tạp hơn. Trong mọi trường hợp, EXISTSdường như tự nhiên hơn (đối với tôi) so với một trong những lựa chọn thay thế đó.

Tôi đã tự hỏi nếu có một lỗ hổng không mong muốn với EXISTS có ý nghĩa hoàn hảo với các phép đo tôi đã thực hiện

Ví dụ cụ thể của bạn rất thú vị, vì nó làm nổi bật cách trình tối ưu hóa xử lý các truy vấn con trong các CASEbiểu thức (và EXISTSđặc biệt là các bài kiểm tra).

Truy vấn con trong biểu thức CASE

Hãy xem xét các truy vấn (hoàn toàn hợp pháp) sau đây:

DECLARE @Base AS TABLE (a integer NULL);
DECLARE @When AS TABLE (b integer NULL);
DECLARE @Then AS TABLE (c integer NULL);
DECLARE @Else AS TABLE (d integer NULL);

SELECT
    CASE
        WHEN (SELECT W.b FROM @When AS W) = 1
            THEN (SELECT T.c FROM @Then AS T)
        ELSE (SELECT E.d FROM @Else AS E)
    END
FROM @Base AS B;

Các ngữ nghĩa củaCASE là rằng WHEN/ELSEđiều khoản được thường được đánh giá theo thứ tự văn bản. Trong truy vấn trên, SQL Server sẽ không trả về lỗi nếu ELSEtruy vấn con trả về nhiều hơn một hàng, nếu WHENmệnh đề được thỏa mãn. Để tôn trọng các ngữ nghĩa này, trình tối ưu hóa tạo ra một kế hoạch sử dụng các biến vị ngữ đi qua:

Vị ngữ đi qua

Phía bên trong của các phép nối vòng lặp lồng nhau chỉ được đánh giá khi vị từ truyền qua trả về false. Hiệu quả tổng thể là các CASEbiểu thức được kiểm tra theo thứ tự và các truy vấn con chỉ được đánh giá nếu không có biểu thức nào trước đó được thỏa mãn.

Biểu thức CASE với truy vấn con EXISTS

Khi CASEsử dụng truy vấn con EXISTS, kiểm tra tồn tại logic được triển khai dưới dạng bán tham gia, nhưng các hàng thường bị loại bỏ bởi liên kết bán phải được giữ lại trong trường hợp mệnh đề sau cần chúng. Các dòng chảy qua loại bán tham gia đặc biệt này có được một cờ để cho biết liệu bán tham gia có tìm thấy kết quả khớp hay không. Cờ này được gọi là cột thăm dò .

Các chi tiết của việc triển khai là truy vấn con logic được thay thế bằng phép nối tương quan ('áp dụng') bằng cột thăm dò. Công việc được thực hiện theo quy tắc đơn giản hóa trong trình tối ưu hóa truy vấn được gọi RemoveSubqInPrj(loại bỏ truy vấn con trong phép chiếu). Chúng ta có thể xem chi tiết bằng cờ theo dõi 8606:

SELECT
    T1.ID,
    CASE
        WHEN EXISTS 
        (
            SELECT 1
            FROM #T2 AS T2
            WHERE T2.ID = T1.ID
        ) THEN 1 
    ELSE 0
    END AS DoesExist
FROM #T1 AS T1
WHERE T1.ID BETWEEN 5000 AND 7000
OPTION (QUERYTRACEON 3604, QUERYTRACEON 8606);

Một phần của cây đầu vào hiển thị EXISTS thử nghiệm được hiển thị bên dưới:

ScaOp_Exists 
    LogOp_Project
        LogOp_Select
            LogOp_Get TBL: #T2
            ScaOp_Comp x_cmpEq
                ScaOp_Identifier [T2].ID
                ScaOp_Identifier [T1].ID

Điều này được chuyển đổi bởi RemoveSubqInPrj thành một cấu trúc đứng đầu bởi:

LogOp_Apply (x_jtLeftSemi probe PROBE:COL: Expr1008)

Đây là bán tham gia bên trái áp dụng với thăm dò được mô tả trước đây. Chuyển đổi ban đầu này là duy nhất có sẵn trong các trình tối ưu hóa truy vấn SQL Server cho đến nay và quá trình biên dịch sẽ thất bại nếu chuyển đổi này bị vô hiệu hóa.

Một trong những hình dạng kế hoạch thực hiện có thể có cho truy vấn này là triển khai trực tiếp cấu trúc logic đó:

NLJ Semi Tham gia với thăm dò

Hàm vô hướng tính toán cuối cùng đánh giá kết quả của CASEbiểu thức bằng giá trị cột thăm dò:

Tính biểu thức vô hướng

Hình dạng cơ bản của cây kế hoạch được bảo tồn khi tối ưu hóa xem xét các loại kết nối vật lý khác cho bán tham gia. Chỉ hợp nhất tham gia hỗ trợ một cột thăm dò, do đó, một phép nối băm, mặc dù có thể hợp lý, không được xem xét:

Hợp nhất với cột thăm dò

Lưu ý kết hợp đầu ra một biểu thức được gắn nhãn Expr1008(rằng tên giống như trước là trùng hợp ngẫu nhiên) mặc dù không có định nghĩa nào cho nó xuất hiện trên bất kỳ toán tử nào trong kế hoạch. Đây chỉ là cột thăm dò một lần nữa. Như trước đây, vô hướng tính toán cuối cùng sử dụng giá trị thăm dò này để đánh giáCASE .

Vấn đề là trình tối ưu hóa không khám phá đầy đủ các lựa chọn thay thế chỉ trở nên đáng giá khi kết hợp (hoặc băm) semi jo. Trong kế hoạch các vòng lặp lồng nhau, không có lợi thế để kiểm tra nếu các hàng trongT2 khớp với phạm vi trên mỗi lần lặp không. Với gói hợp nhất hoặc hàm băm, đây có thể là một tối ưu hóa hữu ích.

Nếu chúng ta thêm một biến BETWEENvị ngữ phù hợp T2vào truy vấn, thì tất cả những gì xảy ra là kiểm tra này được thực hiện cho mỗi hàng dưới dạng phần dư trên phép nối bán kết hợp (khó phát hiện trong kế hoạch thực hiện, nhưng nó ở đó):

SELECT
    T1.ID,
    CASE
        WHEN EXISTS 
        (
            SELECT 1
            FROM #T2 AS T2
            WHERE T2.ID = T1.ID
            AND T2.ID BETWEEN 5000 AND 7000 -- New
        ) THEN 1 
    ELSE 0
    END AS DoesExist
FROM #T1 AS T1
WHERE T1.ID BETWEEN 5000 AND 7000;

Vị ngữ còn lại

Chúng tôi hy vọng rằng BETWEENvị ngữ thay vào đó sẽ được đẩy xuống để T2dẫn đến một tìm kiếm. Thông thường, trình tối ưu hóa sẽ xem xét thực hiện việc này (ngay cả khi không có biến vị ngữ phụ trong truy vấn). Nó nhận ra các biến vị ngữ ngụ ý ( BETWEENtrên T1và biến vị ngữ nối giữa T1T2với nhau ngụ ý BETWEENbậtT2 ) mà không có chúng trong văn bản truy vấn ban đầu. Thật không may, mẫu thăm dò ứng dụng có nghĩa là điều này không được khám phá.

Có nhiều cách để viết truy vấn để tạo ra các tìm kiếm trên cả hai đầu vào thành một phép nối bán kết hợp. Một cách liên quan đến việc viết truy vấn theo cách khá không tự nhiên (đánh bại lý do tôi thường thích EXISTS):

WITH T2 AS
(
    SELECT TOP (9223372036854775807) * 
    FROM #T2 AS T2 
    WHERE ID BETWEEN 5000 AND 7000
)
SELECT 
    T1.ID, 
    DoesExist = 
        CASE 
            WHEN EXISTS 
            (
                SELECT * FROM T2 
                WHERE T2.ID = T1.ID
            ) THEN 1 ELSE 0 END
FROM #T1 AS T1
WHERE T1.ID BETWEEN 5000 AND 7000;

Kế hoạch lừa đảo hàng đầu

Tôi sẽ không vui khi viết truy vấn đó trong môi trường sản xuất, nó chỉ để chứng minh rằng hình dạng kế hoạch mong muốn là có thể. Nếu truy vấn thực sự bạn cần viết sử dụng CASEtheo cách cụ thể này và hiệu suất bị ảnh hưởng bởi không có tìm kiếm ở phía thăm dò của bán kết hợp, bạn có thể xem xét viết truy vấn bằng cách sử dụng cú pháp khác nhau tạo ra kết quả đúng và kế hoạch thực hiện hiệu quả hơn.


6

Đối số "COUNT (*) so với EXISTS" là để kiểm tra xem một bản ghi có tồn tại hay không. Ví dụ:

WHERE (SELECT COUNT(*) FROM Table WHERE ID=@ID)>0

đấu với

WHERE EXISTS(SELECT ID FROM Table WHERE ID=@ID)

Tập lệnh SQL của bạn không sử dụng COUNT(*)làm kiểm tra tồn tại bản ghi và do đó tôi sẽ không nói rằng tập lệnh đó có thể áp dụng trong kịch bản của bạn.


Bất kỳ / kết luận dựa trên kịch bản tôi đã đăng?
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.