EXISTS가 COUNT보다 성능이 우수한지 확인하십시오! … 아닙니다?


35

한 행의 존재가 있어야 확인했을 때 나는 종종 읽은 항상 대신 COUNT와의 EXISTS와 함께 할 수.

그러나 몇 가지 최근 시나리오에서 카운트를 사용할 때 성능 향상을 측정했습니다.
패턴은 다음과 같습니다.

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

"내부"SQL Server에서 어떤 일이 일어나고 있는지 알려주는 방법에 익숙하지 않으므로 EXISTS에 포함되지 않은 결함이 있었는지 궁금했습니다.

그 현상에 대한 설명이 있습니까?

편집하다:

실행할 수있는 전체 스크립트는 다음과 같습니다.

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 

SQL Server 2008R2 (7 64 비트) 에서이 결과를 얻습니다.

COUNT 번역:

표 '# 455F344D'. 스캔 카운트 1, 논리적 읽기 8, 물리적 읽기 0, 미리 읽기 0, lob 논리적 읽기 0, lob 물리적 읽기 0, lob 미리 읽기 0.
표 '# 492FC531'. 스캔 횟수 1, 논리적 읽기 30, 물리적 읽기 0, 미리 읽기 0, lob 논리적 읽기 0, lob 물리적 읽기 0, lob 미리 읽기 0

SQL Server 실행 시간 :
CPU 시간 = 0ms, 경과 시간 = 81ms

EXISTS 번역:

표 '# 492FC531'. 스캔 횟수 1, 논리적 읽기 96, 물리적 읽기 0, 미리 읽기 0, lob 논리적 읽기 0, lob 물리적 읽기 0, lob 미리 읽기 0.
표 '# 455F344D'. 스캔 카운트 1, 논리적 읽기 8, 물리적 읽기 0, 미리 읽기 0, lob 논리적 읽기 0, lob 물리적 읽기 0, lob 미리 읽기 0.

SQL Server 실행 시간 :
CPU 시간 = 0ms, 경과 시간 = 76ms

답변:


43

한 행의 존재가 있어야 확인했을 때 나는 종종 읽은 항상 대신 COUNT와의 EXISTS와 함께 할 수.

특히 데이터베이스 와 관련하여 항상 사실이 되는 것은 매우 드 rare니다 . SQL에서 동일한 의미를 표현하는 방법에는 여러 가지가 있습니다. 유용한 경험 법칙이 있다면 가장 자연스러운 구문을 사용하여 쿼리를 작성하고 (주로 주관적인) 쿼리 계획이나 성능이 허용되지 않는 경우에만 다시 작성하는 것이 좋습니다.

가치있는 것에 대해, 나 자신의 문제는 존재 쿼리가를 사용하여 가장 자연스럽게 표현된다는 것 EXISTS입니다. 또한 거부 대안 보다 EXISTS 더 잘 최적화 하는 경향이 있습니다. 사용 및 필터링 은 SQL Server 쿼리 최적화 프로그램에서 일부 지원을받는 또 다른 대안이지만 개인적으로 더 복잡한 쿼리에서는 신뢰할 수없는 것으로 나타났습니다. 어쨌든, 그 대안 중 하나보다 훨씬 자연스럽게 보입니다.OUTER JOINNULLCOUNT(*)=0EXISTS

내가 한 측정에 완벽하게 부합하는 EXISTS의 전례가없는 결함이 있는지 궁금합니다.

옵티마이 저가 CASE표현식 (및 EXISTS특히 테스트)의 서브 쿼리를 처리하는 방식을 강조하므로 특정 예제가 흥미 롭습니다 .

CASE 표현식의 서브 쿼리

다음과 같은 (완전히 합법적 인) 쿼리를 고려하십시오.

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;

의미CASEWHEN/ELSE일반적으로 문구가 텍스트 순서로 평가 된다는 것 입니다. 위의 쿼리에서 ELSE하위 쿼리가 WHEN절을 만족 하는 경우 하위 쿼리가 둘 이상의 행 을 반환하면 SQL Server에서 오류를 반환하는 것이 올바르지 않습니다 . 이러한 의미를 존중하기 위해 옵티마이 저는 통과 술어를 사용하는 계획을 생성합니다.

통과 술어

중첩 루프 조인의 내부는 통과 술어가 false를 리턴 할 때만 평가됩니다. 전반적인 효과는 CASE식이 순서대로 테스트되고 하위 쿼리는 이전식이 만족되지 않는 경우에만 평가됩니다.

EXISTS 서브 쿼리가있는 CASE 표현식

경우 CASE하위 쿼리가 사용하는 EXISTS논리 존재 시험은 일반적으로 거부 될 반에 참여하지만 행으로 구현되는 반 조인 이후의 절을 필요로하는 경우에 유지해야합니다. 이 특별한 종류의 semi-join을 통과하는 행은 semi-join이 일치하는지 여부를 나타내는 플래그를 얻습니다. 이 플래그를 프로브 열이라고 합니다.

구현의 세부 사항은 논리 서브 쿼리가 프로브 컬럼과 상관 된 조인 ( '적용')으로 대체된다는 것입니다. 작업은 쿼리 최적화 프로그램 RemoveSubqInPrj(프로젝션에서 하위 쿼리 제거) 이라는 단순화 규칙에 의해 수행됩니다 . 추적 플래그 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);

EXISTS테스트를 보여주는 입력 트리의 일부 는 다음과 같습니다.

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

이것은 다음 RemoveSubqInPrj과 같은 구조로 변환 됩니다.

LogOp_Apply (x_jtLeftSemi probe PROBE:COL: Expr1008)

이것은 앞에서 설명한 프로브로 왼쪽 semi-join 적용입니다. 이 초기 변환은 현재까지 SQL Server 쿼리 최적화 프로그램에서 사용할 수있는 유일한 변환이며이 변환을 비활성화하면 컴파일이 실패합니다.

이 쿼리에 가능한 실행 계획 형태 중 하나는 해당 논리 구조를 직접 구현하는 것입니다.

NLJ Semi 프로브와 결합

최종 Compute Scalar CASE는 프로브 열 값을 사용하여 표현식 의 결과를 평가합니다 .

계산 스칼라 식

최적화에서 세미 조인에 다른 물리적 조인 유형을 고려하면 계획 트리의 기본 모양이 유지됩니다. 병합 조인 만 프로브 열을 지원하므로 논리적으로 가능하지만 해시 세미 조인은 고려되지 않습니다.

프로브 컬럼과 병합

병합 Expr1008은 계획의 연산자에 표시되지 않지만 레이블이 붙은 표현식 (이름은 이전과 동일 함)을 출력합니다 . 이것은 다시 프로브 열입니다. 이전과 마찬가지로 최종 Compute Scalar는이 프로브 값을 사용하여를 평가합니다 CASE.

문제는 옵티마이 저가 병합 (또는 해시) 세미 조인으로 만 가치가있는 대안을 완전히 탐색하지 않는다는 것입니다. 중첩 루프 계획에서는 T2모든 반복에서 범위의 행 이 범위와 일치 하는지 확인하는 이점이 없습니다 . 병합 또는 해시 계획을 사용하면 유용한 최적화가 될 수 있습니다.

쿼리에 일치하는 BETWEEN술어를 추가 T2하면 병합 세미 조인의 잔차로 각 행에 대해이 검사가 수행됩니다 (실행 계획에서 파악하기는 어렵지만 거기에 있음).

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;

잔여 술어

우리는 BETWEEN술어가 대신 T2탐색 을하도록 강요 되기를 희망합니다 . 일반적으로 옵티마이 저는이를 수행 할 것을 고려합니다 (쿼리에 추가 술어가없는 경우에도). 그것은 인식 암시 적 술어를 ( BETWEENT1와 사이에 조인 조건 T1T2함께 의미 BETWEEN에를 T2) 그들이 원래 쿼리 텍스트에 존재하지 않고. 불행하게도, 적용-프로브 패턴은 이것이 탐구되지 않음을 의미합니다.

쿼리를 작성하여 두 입력 모두 병합 세미 조인에 대한 탐색을 생성하는 방법이 있습니다. 한 가지 방법은 매우 부 자연스러운 방법으로 쿼리를 작성하는 것입니다 (일반적으로 선호하는 이유를 잃음 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;

TOP 트릭 계획

프로덕션 환경에서 해당 쿼리를 작성하는 것은 기쁘지 않으며 원하는 계획 형태가 가능하다는 것을 보여주는 것입니다. 작성해야하는 실제 쿼리 CASE가 이러한 특정 방식으로 사용 되며 병합 semi-join의 프로브 측에서 탐색을 수행하지 않아 성능이 저하되는 경우 올바른 결과를 생성하는 다른 구문을 사용하여 쿼리를 작성하는 것이 좋습니다 보다 효율적인 실행 계획.


6

인수는 "대 COUNT (*)는 EXISTS" 레코드가 존재하는지 여부를 확인 함께 할 것입니다. 예를 들면 다음과 같습니다.

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

vs

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

SQL 스크립트가 COUNT(*)레코드 존재 확인으로 사용하지 않으므로 시나리오에서 해당 스크립트가 적용 가능하다고 말하지 않습니다.


내가 게시 한 스크립트를 기반으로 한 그럼에도 불구하고 결론?
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.