SQL Server 2014에서 쿼리 속도가 100 배 느리면 행 수 스풀 행이 원인을 추정합니까?


13

SQL Server 2012 에서 800 밀리 초로 실행 되고 SQL Server 2014에서170 초가 걸리는 쿼리가 있습니다. 나는 이것을 Row Count Spool운영자 의 열악한 카디널리티 추정치로 좁혔다 고 생각합니다 . 스풀 연산자 (예 : herehere ) 에 대해 약간 읽었 지만 여전히 몇 가지 사항을 이해하는 데 어려움이 있습니다.

  • 이 쿼리에 Row Count Spool연산자 가 필요한 이유는 무엇 입니까? 정확성이 필요하다고 생각하지 않으므로 특정 최적화를 제공하려고합니까?
  • SQL Server가 Row Count Spool연산자에 대한 조인이 모든 행을 제거 한다고 추정하는 이유는 무엇 입니까?
  • SQL Server 2014의 버그입니까? 그렇다면 Connect에 제출하겠습니다. 그러나 먼저 더 깊이 이해하고 싶습니다.

참고 : LEFT JOINSQL Server 2012 및 SQL Server 2014 모두에서 허용 가능한 성능을 달성하기 위해 쿼리를 테이블 로 다시 작성 하거나 인덱스를 테이블에 추가 할 수 있습니다. 따라서이 질문은이 특정 쿼리를 이해하는 것에 대한 자세한 내용과 그에 대한 자세한 내용은 검색어를 다르게 표현하는 방법


느린 쿼리

전체 테스트 스크립트는 이 Pastebin 을 참조하십시오 . 내가보고있는 특정 테스트 쿼리는 다음과 같습니다.

-- 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 : 예상 쿼리 계획

SQL Server는 믿고 Left Anti Semi Join받는 사람은 Row Count Spool한 행으로 만 개 행을 필터링합니다. 이러한 이유로 LOOP JOIN이후 조인에 대한를 선택합니다 #existingCustomers.

여기에 이미지 설명을 입력하십시오


SQL Server 2014 : 실제 쿼리 계획

예상대로 (SQL Server를 제외한 모든 사람이) Row Count Spool행을 제거하지 않았습니다. 따라서 SQL Server가 한 번만 반복 될 것으로 예상되는 경우 10,000 회 반복합니다.

여기에 이미지 설명을 입력하십시오


SQL Server 2012 : 예상 쿼리 계획

SQL Server 2012 (또는 OPTION (QUERYTRACEON 9481)SQL Server 2014)를 사용하는 Row Count Spool경우 예상 행 수를 줄이고 해시 조인을 선택하면 훨씬 더 나은 계획을 얻을 수 있습니다.

여기에 이미지 설명을 입력하십시오

왼쪽 가입 재 작성

참고로 다음은 모든 SQL Server 2012, 2014 및 2016에서 우수한 성능을 달성하기 위해 쿼리를 다시 작성할 수있는 방법입니다. 그러나 여전히 위 쿼리의 특정 동작 및 쿼리 여부에 관심이 있습니다. 새로운 SQL Server 2014 Cardinality Estimator의 버그입니다.

-- 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

여기에 이미지 설명을 입력하십시오

답변:


10

이 쿼리에 행 개수 스풀 연산자가 필요한 이유는 무엇입니까? ... 어떤 특정 최적화를 제공하려고합니까?

cust_nbr#existingCustomers이 널 입력 가능합니다. 실제로 null이 포함되어 있으면 올바른 응답은 0 행을 반환 NOT IN (NULL,...) 하는 것입니다 (항상 빈 결과 집합을 생성합니다).

따라서 쿼리는 다음과 같이 생각할 수 있습니다.

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) 

행 개수 스풀을 사용하면

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

두 번 이상

이것은 약간의 가정 차이가 성능에 치명적인 차이를 만들 수있는 경우 인 것 같습니다.

아래와 같이 단일 행을 업데이트 한 후 ...

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

... 쿼리가 1 초 이내에 완료되었습니다. 계획의 실제 버전과 예상 버전의 행 수는 이제 거의 자리에 있습니다.

SET STATISTICS TIME ON;
SET STATISTICS IO ON;

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

여기에 이미지 설명을 입력하십시오

위에서 설명한대로 제로 행이 출력됩니다.

SQL Server의 통계 히스토그램 및 자동 업데이트 임계 값은 이러한 종류의 단일 행 변경을 감지하기에 충분하지 않습니다. 논란의 여지가 있지만 열이 널 입력 가능 NULL하면 통계 히스토그램에 현재 아무 것도없는 것으로 표시되어 있어도 열이 하나 이상 포함되어 있다는 사실에 근거하여 작업하는 것이 합리적 일 수 있습니다 .


9

이 쿼리에 행 개수 스풀 연산자가 필요한 이유는 무엇입니까? 정확성이 필요하다고 생각하지 않으므로 특정 최적화를 제공하려고합니까?

이 질문에 대한 Martin의 철저한 답변 을 참조하십시오 . 중요한 점은 내 하나의 행이있는 경우이다 NOT INNULL, 부울 논리가 "올바른 응답이 제로 행을 반환하는 것입니다"같은 것을 사용할 수 있습니다. Row Count Spool조작이 (필요) 로직을 최적화한다.

SQL Server가 Row Count Spool 연산자에 대한 조인이 모든 행을 제거한다고 추정하는 이유는 무엇입니까?

Microsoft는 SQL 2014 Cardinality Estimator에 대한 우수한 백서를 제공합니다 . 이 문서에서 다음 정보를 찾았습니다.

새 CE는 값이 히스토그램 범위를 벗어나도 쿼리 된 값이 데이터 세트에 존재한다고 가정합니다. 이 예에서 새 CE는 테이블 카디널리티에 밀도를 곱하여 계산 된 평균 빈도를 사용합니다.

종종 그러한 변화는 매우 좋은 변화입니다. 오름차순 주요 문제를 크게 완화 하고 일반적으로 통계 히스토그램을 기반으로 범위를 벗어난 값에 대해보다 보수적 인 쿼리 계획 (높은 행 추정치)을 생성합니다.

그러나이 특정 경우에 NULL값을 찾은 것으로 가정하면에 조인하면의 Row Count Spool모든 행이 필터링 된다는 가정으로 이어집니다 #potentialNewCustomers. 실제로 NULL행이있는 경우 (마틴의 대답에서 볼 수 있듯이) 정확한 추정치입니다. 그러나 NULL행이 없는 경우 입력 행의 수에 관계없이 SQL Server가 조인 후 1 행의 추정값을 생성하기 때문에 그 결과가 치명적일 수 있습니다. 이로 인해 나머지 쿼리 계획에서 조인 선택이 매우 열악해질 수 있습니다.

이것은 SQL 2014의 버그입니까? 그렇다면 Connect에 제출하겠습니다. 그러나 먼저 더 깊이 이해하고 싶습니다.

버그와 SQL Server의 새로운 카디널리티 추정기의 성능에 영향을 미치는 가정 또는 제한 사이의 회색 영역에 있다고 생각합니다. 그러나이 NOT IN문제로 인해 NULL값 이없는 nullable 절의 특정 경우 SQL 2012에 비해 성능이 크게 저하 될 수 있습니다 .

따라서 SQL 팀이 Cardinality Estimator에 대한이 변경의 잠재적 영향을 인식 할 수 있도록 Connect 문제를 제기 했습니다 .

업데이트 : 우리는 지금 SQL16을 위해 CTP3을 사용하고 있으며 문제가 발생하지 않는다는 것을 확인했습니다.


5

Martin Smith의 답변 과 귀하의 자체 답변 은 모든 주요 요점을 올바르게 해결했습니다. 앞으로 독자를위한 영역을 강조하고 싶습니다.

따라서이 질문은이 특정 쿼리를 이해하고 깊이있게 계획하는 것이 아니라 쿼리를 다르게 표현하는 방법에 대한 것이 아닙니다.

쿼리의 명시된 목적은 다음과 같습니다.

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

이 요구 사항은 여러 가지 방법으로 SQL로 표현하기 쉽습니다. 어떤 것을 선택 하느냐는 다른 것만큼이나 스타일 문제이지만, 모든 경우에 올바른 결과를 반환하도록 쿼리 사양을 작성해야합니다. 여기에는 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
    DPNNC.cust_nbr NOT IN
    (
        SELECT 
            EC.cust_nbr 
        FROM #existingCustomers AS EC 
        WHERE 
            EC.cust_nbr IS NOT NULL
    );

효율적인 실행 계획을 생성하여 올바른 결과를 반환합니다.

실행 계획

우리는 표현할 수 NOT IN<> ALL또는 NOT = ANY계획이나 결과에 영향을주지 않고 :

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
    );

또는 사용 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
    );

이 아무것도 마법 이것에 대해, 또는 사용에 대해 아무것도 특히 제기 IN, ANY또는 ALL- 우리는 항상 올바른 결과를 얻을 수 있도록, 제대로 쿼리를 작성해야합니다.

가장 컴팩트 한 형태는 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;

비트 맵 필터링이 없기 때문에 실행 계획의 효율성이 떨어질 수 있지만 올바른 결과도 얻을 수 있습니다.

비 비트 맵 실행 계획

원래의 질문은 필요한 null check 구현에서 성능에 영향을 미치는 문제를 노출시키기 때문에 흥미 롭습니다. 이 답변의 요점은 쿼리를 올바르게 작성 하면 문제를 피할 수 있다는 것입니다.

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.