내 SELECT DISTINCT TOP N 쿼리가 전체 테이블을 스캔하는 이유는 무엇입니까?


27

SELECT DISTINCT TOP NSQL Server 쿼리 최적화 프로그램에서 제대로 최적화되지 않은 것으로 보이는 몇 가지 쿼리를 실행했습니다 . 간단한 예 : 두 개의 교대 값을 가진 백만 개의 행 테이블을 고려하여 시작하겠습니다. 내가 사용합니다 GetNums의 데이터를 생성하는 기능을 :

DROP TABLE IF EXISTS X_2_DISTINCT_VALUES;

CREATE TABLE X_2_DISTINCT_VALUES (PK INT IDENTITY (1, 1), VAL INT NOT NULL);

INSERT INTO X_2_DISTINCT_VALUES WITH (TABLOCK) (VAL)
SELECT N % 2
FROM dbo.GetNums(1000000);

UPDATE STATISTICS X_2_DISTINCT_VALUES WITH FULLSCAN;

다음 쿼리의 경우 :

SELECT DISTINCT TOP 2 VAL
FROM X_2_DISTINCT_VALUES
OPTION (MAXDOP 1);

SQL Server는 테이블의 첫 번째 데이터 페이지를 검색하여 두 가지 고유 한 값을 찾을 수 있지만 대신 모든 데이터를 검색합니다 . SQL Server가 요청 된 고유 값 수를 찾을 때까지 왜 스캔하지 않습니까?

이 질문에는 블록으로 생성 된 10 개의 고유 한 값을 가진 천만 개의 행이 포함 된 다음 테스트 데이터를 사용하십시오.

DROP TABLE IF EXISTS X_10_DISTINCT_HEAP;

CREATE TABLE X_10_DISTINCT_HEAP (VAL VARCHAR(10) NOT NULL);

INSERT INTO X_10_DISTINCT_HEAP WITH (TABLOCK)
SELECT REPLICATE(CHAR(65 + (N / 100000 ) % 10 ), 10)
FROM dbo.GetNums(10000000);

UPDATE STATISTICS X_10_DISTINCT_HEAP WITH FULLSCAN;

클러스터형 인덱스가있는 테이블에 대한 답변도 허용됩니다.

DROP TABLE IF EXISTS X_10_DISTINCT_CI;

CREATE TABLE X_10_DISTINCT_CI (PK INT IDENTITY (1, 1), VAL VARCHAR(10) NOT NULL, PRIMARY KEY (PK));

INSERT INTO X_10_DISTINCT_CI WITH (TABLOCK) (VAL)
SELECT REPLICATE(CHAR(65 + (N / 100000 ) % 10 ), 10)
FROM dbo.GetNums(10000000);

UPDATE STATISTICS X_10_DISTINCT_CI WITH FULLSCAN;

다음 쿼리 는 테이블에서 천만 행을 모두 검색합니다 . 전체 테이블을 스캔하지 않는 것을 어떻게 얻을 수 있습니까? SQL Server 2016 SP1을 사용하고 있습니다.

SELECT DISTINCT TOP 10 VAL
FROM X_10_DISTINCT_HEAP
OPTION (MAXDOP 1);

커서가 10 분 동안 작동 할 수도 있습니다
paparazzo

답변:


29

DISTINCT위 쿼리에서 작업을 수행 할 수있는 세 가지 최적화 알고리즘이 있습니다 . 다음 쿼리는 목록이 완전한 것을 제안하는 오류를 발생시킵니다.

SELECT DISTINCT TOP 10 ID
FROM X_10_DISTINCT_HEAP
OPTION (MAXDOP 1, QUERYRULEOFF GbAggToSort, QUERYRULEOFF GbAggToHS, QUERYRULEOFF GbAggToStrm);

메시지 8622, 수준 16, 상태 1, 줄 1

이 쿼리에 정의 된 힌트로 인해 쿼리 프로세서가 쿼리 계획을 생성 할 수 없습니다. 힌트를 지정하지 않고 SET FORCEPLAN을 사용하지 않고 쿼리를 다시 제출하십시오.

GbAggToSort그룹 별 집계 (고유)를 고유 한 정렬로 구현합니다. 행을 생성하기 전에 입력에서 모든 데이터를 읽는 차단 연산자입니다. GbAggToStrm그룹 별 집계를 스트림 집계로 구현합니다 (이 경우 입력 정렬도 필요함). 이것은 또한 차단 연산자입니다. GbAggToHS는 해시 일치로 구현합니다.이 질문에서 잘못된 계획에서 보았지만 해시 일치 (집계) 또는 해시 일치 (흐름 구분)로 구현할 수 있습니다.

해시 일치 ( flow distinct ) 연산자는 차단되지 않기 때문에이 문제를 해결하는 한 가지 방법입니다. SQL Server는 충분한 고유 값을 찾으면 검색을 중지 할 수 있어야합니다.

Flow Distinct 논리 연산자는 입력을 스캔하여 중복을 제거합니다. Distinct 연산자는 출력을 생성하기 전에 모든 입력을 소비하는 반면 Flow Distinct 연산자는 입력에서 얻은대로 각 행을 반환합니다 (해당 행이 중복되지 않은 경우 폐기 됨).

질문의 쿼리가 해시 일치 (흐름 구분) 대신 해시 일치 (집계)를 사용하는 이유는 무엇입니까? 테이블의 고유 값 수가 변경되면 테이블에 스캔 해야하는 행 수의 추정치가 감소해야하기 때문에 해시 일치 (흐름 고유) 쿼리 비용이 감소 할 것으로 예상됩니다. 빌드 해야하는 해시 테이블이 커지기 때문에 해시 일치 (집계) 계획의 비용이 증가 할 것으로 예상됩니다. 이를 조사하는 한 가지 방법 은 계획 지침작성하는 것 입니다. 두 개의 데이터 복사본을 만들지 만 그 중 하나에 계획 지침을 적용하면 동일한 데이터에 대해 해시 일치 (집계)와 해시 일치 (고유)를 나란히 비교할 수 있어야합니다. 동일한 규칙이 두 계획 ( GbAggToHS) 모두에 적용되므로 쿼리 최적화 프로그램 규칙을 비활성화하여이 작업을 수행 할 수 없습니다 .

다음은 계획 가이드를 얻는 한 가지 방법입니다.

DROP TABLE IF EXISTS X_PLAN_GUIDE_TARGET;

CREATE TABLE X_PLAN_GUIDE_TARGET (VAL VARCHAR(10) NOT NULL);

INSERT INTO X_PLAN_GUIDE_TARGET WITH (TABLOCK)
SELECT CAST(N % 10000 AS VARCHAR(10))
FROM dbo.GetNums(10000000);

UPDATE STATISTICS X_PLAN_GUIDE_TARGET WITH FULLSCAN;

-- run this query
SELECT DISTINCT TOP 10 VAL  FROM X_PLAN_GUIDE_TARGET  OPTION (MAXDOP 1)

계획 핸들을 가져 와서 계획 지침을 작성하는 데 사용하십시오.

-- plan handle is 0x060007009014BC025097E88F6C01000001000000000000000000000000000000000000000000000000000000
SELECT qs.plan_handle, st.text FROM 
sys.dm_exec_query_stats AS qs   
CROSS APPLY sys.dm_exec_sql_text(qs.sql_handle) AS st  
WHERE st.text LIKE '%X[_]PLAN[_]GUIDE[_]TARGET%'
ORDER BY last_execution_time DESC;

EXEC sp_create_plan_guide_from_handle 
'EVIL_PLAN_GUIDE', 
0x060007009014BC025097E88F6C01000001000000000000000000000000000000000000000000000000000000;

계획 지침은 정확한 쿼리 텍스트에서만 작동하므로 계획 지침에서 다시 복사 해 보겠습니다.

SELECT query_text
FROM sys.plan_guides
WHERE name = 'EVIL_PLAN_GUIDE';

데이터를 재설정하십시오.

TRUNCATE TABLE X_PLAN_GUIDE_TARGET;

INSERT INTO X_PLAN_GUIDE_TARGET WITH (TABLOCK)
SELECT REPLICATE(CHAR(65 + (N / 100000 ) % 10 ), 10)
FROM dbo.GetNums(10000000);

오는 Get 쿼리 계획을 계획 지침 적용으로 쿼리 :

SELECT DISTINCT TOP 10 VAL  FROM X_PLAN_GUIDE_TARGET  OPTION (MAXDOP 1)

여기에는 테스트 데이터와 함께 원하는 해시 일치 (흐름 구분) 연산자가 있습니다. SQL Server는 테이블에서 모든 행을 읽을 것으로 예상되며 추정 비용은 해시 일치 (집계) 계획과 동일합니다. 필자가 제안한 테스트는 계획의 행 목표가 SQL Server가 테이블에서 기대하는 고유 값의 수보다 크거나 같을 때 두 계획의 비용이 동일하다는 것을 제안했습니다.이 경우 단순히 통계. 불행히도 (우리 쿼리의 경우) 최적화 프로그램은 비용이 동일 할 때 해시 일치 (흐름 구분)보다 해시 일치 (집계)를 선택합니다. 그래서 우리는 우리가 원하는 계획에서 멀어지면 0.0000001 매직 최적화 장치입니다.

이 문제를 공격하는 한 가지 방법은 행 목표를 줄이는 것입니다. 뷰 시점에서 행 목표가 옵티 마이저 인 경우 고유 행 수보다 적은 경우 해시 일치 (흐름 고유)를 얻게됩니다. 이것은 OPTIMIZE FOR쿼리 힌트 로 수행 할 수 있습니다 .

DECLARE @j INT = 10;

SELECT DISTINCT TOP (@j) VAL
FROM X_10_DISTINCT_HEAP
OPTION (MAXDOP 1, OPTIMIZE FOR (@j = 1));

이 쿼리의 경우 옵티마이 저는 쿼리에 첫 번째 행만 필요한 것처럼 계획을 작성하지만 쿼리가 실행될 때 처음 10 개의 행을 다시 가져옵니다. 내 컴퓨터에서이 쿼리는 X_10_DISTINCT_HEAP250ms의 CPU 시간과 2537 개의 논리 읽기로 292ms에서 892800 개의 행을 스캔 하고 완료합니다.

통계가 왜곡 된 데이터에 대해 샘플링 된 통계에 대해 발생할 수있는 하나의 고유 한 값만보고하는 경우이 기술이 작동하지 않습니다. 그러나이 경우 이와 같은 기술을 사용하여 정당화하기에 충분히 밀도가 높은 데이터는 없을 것입니다. 특히 병렬로 수행 할 수있는 경우 테이블의 모든 데이터를 스캔해도 크게 손실되지 않을 수 있습니다.

이 문제를 공격하는 또 다른 방법은 SQL Server가 기본 테이블에서 얻을 것으로 예상되는 고유 값의 수를 늘리는 것입니다. 이것은 예상보다 어렵다. 결정적 함수를 적용하면 결과의 뚜렷한 개수를 늘릴 수 없습니다. 쿼리 최적화 프로그램이 수학적 사실을 알고 있다면 (일부 테스트는 적어도 우리의 목적을위한 것임을 시사합니다) 결정 성 함수 ( 모든 문자열 함수 포함 )를 적용해도 예상되는 개별 행 수는 증가하지 않습니다.

NEWID()및 의 명백한 선택을 포함하여 많은 비 결정적 기능도 작동하지 않았습니다 RAND(). 그러나이 LAG()쿼리에 대한 트릭을 수행합니다. 쿼리 최적화 프로그램 LAG해시 일치 (흐름 구분) 계획을 권장하는 표현식 에 대해 천만개의 고유 값을 예상합니다 .

SELECT DISTINCT TOP 10 LAG(VAL, 0) OVER (ORDER BY (SELECT NULL)) AS ID
FROM X_10_DISTINCT_HEAP
OPTION (MAXDOP 1);

내 컴퓨터에서이 쿼리는 1109ms X_10_DISTINCT_HEAP의 CPU 시간과 2537 개의 논리적 읽기로 1165ms에서 892800 개의 행을 스캔 하고 완료하므로 LAG()상대적으로 약간의 오버 헤드 가 추가됩니다. @Paul White는이 쿼리에 대해 배치 모드 처리를 시도 할 것을 제안했습니다. SQL Server 2016에서는로도 일괄 처리 모드 처리를 수행 할 수 있습니다 MAXDOP 1. rowstore 테이블에 대한 배치 모드 처리를 얻는 한 가지 방법은 다음과 같이 빈 CCI에 조인하는 것입니다.

CREATE TABLE #X_DUMMY_CCI (ID INT NOT NULL);

CREATE CLUSTERED COLUMNSTORE INDEX X_DUMMY_CCI ON #X_DUMMY_CCI;

SELECT DISTINCT TOP 10 VAL
FROM
(
    SELECT LAG(VAL, 1) OVER (ORDER BY (SELECT NULL)) AS VAL
    FROM X_10_DISTINCT_HEAP
    LEFT OUTER JOIN #X_DUMMY_CCI ON 1 = 0
) t
WHERE t.VAL IS NOT NULL
OPTION (MAXDOP 1);

이 코드는 이 쿼리 계획을 생성 합니다.

Paul은 Window Aggregate 최적화에 적합하지 않기 LAG(..., 1)때문에 사용할 쿼리를 변경해야한다고 지적했습니다 LAG(..., 0). 이 변경은 경과 시간을 520ms로, CPU 시간을 454ms로 줄였습니다.

주의 LAG()접근 방식은 가장 안정적인 하나가 아닙니다. Microsoft가 기능에 대한 고유성 가정을 변경하면 더 이상 작동하지 않을 수 있습니다. 기존 CE와는 다른 추정치가 있습니다. 또한 힙에 대한 이러한 유형의 최적화는 좋은 생각이 아닙니다. 테이블을 다시 작성하면 테이블에서 거의 모든 행을 읽어야하는 최악의 경우가 발생할 수 있습니다.

고유 한 열이있는 테이블 (예 : 문제의 클러스터형 인덱스 예)에 대해 더 나은 옵션이 있습니다. 예를 들어 SUBSTRING항상 빈 문자열을 반환하는 식을 사용하여 최적화 프로그램을 속일 수 있습니다 . SQL Server는 SUBSTRING고유 값의 수를 변경 한다고 생각하지 않으므로 PK와 같은 고유 열에 적용하면 예상 행 수는 천만입니다. 다음 쿼리 는 해시 일치 (흐름 구분) 연산자를 가져옵니다.

SELECT DISTINCT TOP 10 VAL + SUBSTRING(CAST(PK AS VARCHAR(10)), 11, 1)
FROM X_10_DISTINCT_CI
OPTION (MAXDOP 1);

내 컴퓨터에서이 쿼리는 297ms X_10_DISTINCT_CI의 CPU 시간과 3011 개의 논리적 읽기로 333ms에서 900000 개의 행을 스캔 하고 완료합니다.

요약하면, 쿼리 최적화 프로그램은 > =가 테이블에서 추정 된 개별 행의 수인 SELECT DISTINCT TOP N경우 쿼리 에 대해 테이블에서 모든 행을 읽은 것으로 가정 N합니다. 해시 일치 (집계) 연산자는 해시 일치 (흐름 구분) 연산자와 동일한 비용을 가질 수 있지만 옵티마이 저는 항상 집계 연산자를 선택합니다. 이로 인해 충분한 스캔 값이 테이블 스캔 시작 근처에있을 때 불필요한 논리적 읽기가 발생할 수 있습니다. 해시 일치 (흐름 구분) 연산자를 사용하여 옵티 마이저를 속이는 두 가지 방법은 OPTIMIZE FOR힌트를 사용하여 행 목표를 낮추 LAG()거나 SUBSTRING고유 한 열을 사용 하거나 고유 한 열 에서 예상되는 고유 행 수를 늘리는 것 입니다.


12

당신은 이미 자신의 질문에 올바르게 대답했습니다.

가장 효율적인 방법은 실제로 전체 테이블을 스캔하는 것입니다 -columnstore 'heap' 으로 구성 할 수 있다면 관찰을 추가하고 싶습니다 .

CREATE CLUSTERED COLUMNSTORE INDEX CCSI 
ON dbo.X_10_DISTINCT_HEAP;

간단한 쿼리 :

SELECT DISTINCT TOP (10)
    XDH.VAL 
FROM dbo.X_10_DISTINCT_HEAP AS XDH
OPTION (MAXDOP 1);

그런 다음 :

실행 계획

테이블 'X_10_DISTINCT_HEAP'. 스캔 카운트 1,
 논리적 읽기 0, 물리적 읽기 0, 미리 읽기 0, 
 lob 논리적 읽기 66 , lob 물리적 읽기 0, lob 미리 읽기는 0을 읽습니다.
테이블 'X_10_DISTINCT_HEAP'. 세그먼트 읽기 13, 세그먼트 건너 뛰기 0

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

해시 일치 (Flow Distinct)는 현재 배치 모드에서 실행할 수 없습니다. 이것을 사용하는 방법은 배치에서 행 처리로 (보이지 않는) 고가의 전환으로 인해 훨씬 ​​느립니다. 예를 들면 다음과 같습니다.

SET ROWCOUNT 10;

SELECT DISTINCT 
    XDH.VAL
FROM dbo.X_10_DISTINCT_HEAP AS XDH
OPTION (FAST 1);

SET ROWCOUNT 0;

제공합니다 :

흐름 고유 실행 계획

테이블 'X_10_DISTINCT_HEAP'. 스캔 카운트 1,
 논리적 읽기 0, 물리적 읽기 0, 미리 읽기 0, 
 lob 논리적 읽기 20 , lob 물리적 읽기 0, lob 미리 읽기는 0을 읽습니다.
테이블 'X_10_DISTINCT_HEAP'. 세그먼트 4를 읽고 세그먼트 0을 건너 뜁니다.

 SQL Server 실행 시간 :
   CPU 시간 = 640ms, 경과 시간 = 680ms

테이블이 행 저장소 힙으로 구성 될 때보 다 느립니다.


4

재귀 CTE를 사용하여 반복 된 부분 스캔 (스킵 스캔과 유사하지만 동일하지는 않음)을 에뮬레이트하려는 시도가 있습니다. 목표는 인덱스가 없기 때문에 (id)테이블에서 정렬 및 여러 스캔을 피하는 것입니다.

재귀 적 CTE 제한을 우회하는 몇 가지 트릭을 수행합니다.

  • TOP재귀 부분에는 허용 되지 않습니다. 하위 쿼리를 ROW_NUMBER()대신 사용합니다.
  • 상수 부분에 대한 참조를 여러 개 가질 수 없으며 재귀 부분에서 사용 LEFT JOIN하거나 사용할 수 없습니다 NOT IN (SELECT id FROM cte). 우회하기 위해 우리는 hierarchyID와 비슷 하거나 비슷한 VARCHAR모든 id값 을 누적 하는 문자열을 STRING_AGG만든 다음와 비교합니다 LIKE.

힙 들어 (열이라는 가정 id) 시험 1에서 rextester.com .

테스트에서 알 수 있듯이 여러 스캔을 피할 수는 없지만 처음 몇 페이지에 다른 값이 있으면 확인이 수행됩니다. 그러나 값이 균등하게 분산되지 않으면 테이블의 큰 부분에서 여러 번 스캔을 수행 할 수 있으므로 성능이 저하됩니다.

WITH ct (id, found, list) AS
  ( SELECT TOP (1) id, 1, CAST('/' + id + '/' AS VARCHAR(MAX))
    FROM x_large_table_2
  UNION ALL
    SELECT y.ID, ct.found + 1, CAST(ct.list + y.id + '/' AS VARCHAR(MAX))
    FROM ct
      CROSS APPLY 
      ( SELECT x.id, 
               rn = ROW_NUMBER() OVER (ORDER BY (SELECT NULL))
        FROM x_large_table_2 AS x
        WHERE ct.list NOT LIKE '%/' + id + '/%'
      ) AS y
    WHERE ct.found < 3         -- the TOP (n) parameter here
      AND y.rn = 1
  )
SELECT id FROM ct ;

그리고 테이블이 클러스터링되면 (CI on unique_key), rextester.com 에서 test-2 .

WHERE x.unique_key > ct.unique_key여러 개의 스캔을 피하기 위해 클러스터형 인덱스 ( )를 사용 합니다.

WITH ct (unique_key, id, found, list) AS
  ( SELECT TOP (1) unique_key, id, 1, CAST(CONCAT('/',id, '/') AS VARCHAR(MAX))
    FROM x_large_table_2
  UNION ALL
    SELECT y.unique_key, y.ID, ct.found + 1, 
        CAST(CONCAT(ct.list, y.id, '/') AS VARCHAR(MAX))
    FROM ct
      CROSS APPLY 
      ( SELECT x.unique_key, x.id, 
               rn = ROW_NUMBER() OVER (ORDER BY (SELECT NULL))
        FROM x_large_table_2 AS x
        WHERE x.unique_key > ct.unique_key
          AND ct.list NOT LIKE '%/' + id + '/%'
      ) AS y
    WHERE ct.found < 5       -- the TOP (n) parameter here
      AND y.rn = 1
  )
-- SELECT * FROM ct ;        -- for debugging
SELECT id FROM ct ;

이 솔루션에는 상당히 미묘한 성능 문제가 있습니다. N 번째 값을 찾은 후 테이블에서 추가 탐색을 수행합니다. 따라서 상위 10 개에 10 개의 고유 값이 있으면 11 번째 값을 찾습니다. 추가 전체 스캔으로 끝나고 1,000 만 개의 ROW_NUMBER () 계산이 실제로 추가됩니다. 내 컴퓨터에서 쿼리 속도를 20 배 빠르게하는 해결 방법이 있습니다. 어떻게 생각해? brentozar.com/pastetheplan/?id=SkDhAmFKe
Joe Obbish 2012

2

완벽을 기하기 위해이 문제에 접근하는 또 다른 방법은 OUTER APPLY 를 사용하는 것 입니다. OUTER APPLY찾아야 할 각각의 고유 한 값에 대해 연산자를 추가 할 수 있습니다 . 이것은 개념적으로 이퍼 큐브의 재귀 적 접근과 유사하지만 재귀를 손으로 직접 작성합니다. 한 가지 장점은 해결 방법 TOP대신 파생 테이블에서 사용할 수 있다는 것입니다 ROW_NUMBER(). 한 가지 큰 단점은 쿼리 텍스트가 N늘어날 수록 길어진 다는 것입니다.

힙에 대한 쿼리 구현은 다음과 같습니다.

SELECT VAL
FROM (
    SELECT t1.VAL VAL1, t2.VAL VAL2, t3.VAL VAL3, t4.VAL VAL4, t5.VAL VAL5, t6.VAL VAL6, t7.VAL VAL7, t8.VAL VAL8, t9.VAL VAL9, t10.VAL VAL10
    FROM 
    ( 
    SELECT TOP 1 VAL FROM X_10_DISTINCT_HEAP 
    ) t1
    OUTER APPLY
    ( 
    SELECT TOP 1 VAL FROM X_10_DISTINCT_HEAP t2 WHERE t2.VAL NOT IN (t1.VAL)
    ) t2
    OUTER APPLY
    ( 
    SELECT TOP 1 VAL FROM X_10_DISTINCT_HEAP t3 WHERE t3.VAL NOT IN (t1.VAL, t2.VAL)
    ) t3
    OUTER APPLY
    ( 
    SELECT TOP 1 VAL FROM X_10_DISTINCT_HEAP t4 WHERE t4.VAL NOT IN (t1.VAL, t2.VAL, t3.VAL)
    ) t4
    OUTER APPLY
    ( 
    SELECT TOP 1 VAL FROM X_10_DISTINCT_HEAP t5 WHERE t5.VAL NOT IN (t1.VAL, t2.VAL, t3.VAL, t4.VAL)
    ) t5
    OUTER APPLY
    ( 
    SELECT TOP 1 VAL FROM X_10_DISTINCT_HEAP t6 WHERE t6.VAL NOT IN (t1.VAL, t2.VAL, t3.VAL, t4.VAL, t5.VAL)
    ) t6
    OUTER APPLY
    ( 
    SELECT TOP 1 VAL FROM X_10_DISTINCT_HEAP t7 WHERE t7.VAL NOT IN (t1.VAL, t2.VAL, t3.VAL, t4.VAL, t5.VAL, t6.VAL)
    ) t7
    OUTER APPLY
    ( 
    SELECT TOP 1 VAL FROM X_10_DISTINCT_HEAP t8 WHERE t8.VAL NOT IN (t1.VAL, t2.VAL, t3.VAL, t4.VAL, t5.VAL, t6.VAL, t7.VAL)
    ) t8
    OUTER APPLY
    ( 
    SELECT TOP 1 VAL FROM X_10_DISTINCT_HEAP t9 WHERE t9.VAL NOT IN (t1.VAL, t2.VAL, t3.VAL, t4.VAL, t5.VAL, t6.VAL, t7.VAL, t8.VAL)
    ) t9
    OUTER APPLY
    ( 
    SELECT TOP 1 VAL FROM X_10_DISTINCT_HEAP t10 WHERE t10.VAL NOT IN (t1.VAL, t2.VAL, t3.VAL, t4.VAL, t5.VAL, t6.VAL, t7.VAL, t8.VAL, t9.VAL)
    ) t10
) t
UNPIVOT 
(
  VAL FOR VALS IN (VAL1, VAL2, VAL3, VAL4, VAL5, VAL6, VAL7, VAL8, VAL9, VAL10)
) AS upvt;

위 쿼리에 대한 실제 쿼리 계획은 다음과 같습니다 . 내 컴퓨터 에서이 쿼리는 625ms의 CPU 시간과 12605의 논리적 읽기로 713ms에 완료됩니다. 우리는 100k 행마다 새로운 고유 값을 얻으 므로이 쿼리가 약 900000 * 10 * 0.5 = 4500000 행을 스캔 할 것으로 기대합니다. 이론적 으로이 쿼리는 다른 쿼리 에서이 쿼리의 논리적 읽기를 5 배 수행해야합니다.

DECLARE @j INT = 10;

SELECT DISTINCT TOP (@j) VAL
FROM X_10_DISTINCT_HEAP
OPTION (MAXDOP 1, OPTIMIZE FOR (@j = 1));

이 쿼리는 2537 개의 논리적 읽기를 수행했습니다. 2537 * 5 = 12685는 12605와 거의 비슷합니다.

클러스터형 인덱스가있는 테이블의 경우 더 잘 수행 할 수 있습니다. 동일한 행을 두 번 스캔하지 않도록 마지막 클러스터 된 키 값을 파생 테이블로 전달할 수 있기 때문입니다. 하나의 구현 :

SELECT VAL
FROM (
    SELECT t1.VAL VAL1, t2.VAL VAL2, t3.VAL VAL3, t4.VAL VAL4, t5.VAL VAL5, t6.VAL VAL6, t7.VAL VAL7, t8.VAL VAL8, t9.VAL VAL9, t10.VAL VAL10
    FROM 
    ( 
    SELECT TOP 1 PK, VAL FROM X_10_DISTINCT_CI 
    ) t1
    OUTER APPLY
    ( 
    SELECT TOP 1 PK, VAL FROM X_10_DISTINCT_CI t2 WHERE PK > t1.PK AND t2.VAL NOT IN (t1.VAL)
    ) t2
    OUTER APPLY
    ( 
    SELECT TOP 1 PK, VAL FROM X_10_DISTINCT_CI t3 WHERE PK > t2.PK AND t3.VAL NOT IN (t1.VAL, t2.VAL)
    ) t3
    OUTER APPLY
    ( 
    SELECT TOP 1 PK, VAL FROM X_10_DISTINCT_CI t4 WHERE PK > t3.PK AND t4.VAL NOT IN (t1.VAL, t2.VAL, t3.VAL)
    ) t4
    OUTER APPLY
    ( 
    SELECT TOP 1 PK, VAL FROM X_10_DISTINCT_CI t5 WHERE PK > t4.PK AND t5.VAL NOT IN (t1.VAL, t2.VAL, t3.VAL, t4.VAL)
    ) t5
    OUTER APPLY
    ( 
    SELECT TOP 1 PK, VAL FROM X_10_DISTINCT_CI t6 WHERE PK > t5.PK AND t6.VAL NOT IN (t1.VAL, t2.VAL, t3.VAL, t4.VAL, t5.VAL)
    ) t6
    OUTER APPLY
    ( 
    SELECT TOP 1 PK, VAL FROM X_10_DISTINCT_CI t7 WHERE PK > t6.PK AND t7.VAL NOT IN (t1.VAL, t2.VAL, t3.VAL, t4.VAL, t5.VAL, t6.VAL)
    ) t7
    OUTER APPLY
    ( 
    SELECT TOP 1 PK, VAL FROM X_10_DISTINCT_CI t8 WHERE PK > t7.PK AND t8.VAL NOT IN (t1.VAL, t2.VAL, t3.VAL, t4.VAL, t5.VAL, t6.VAL, t7.VAL)
    ) t8
    OUTER APPLY
    ( 
    SELECT TOP 1 PK, VAL FROM X_10_DISTINCT_CI t9 WHERE PK > t8.PK AND t9.VAL NOT IN (t1.VAL, t2.VAL, t3.VAL, t4.VAL, t5.VAL, t6.VAL, t7.VAL, t8.VAL)
    ) t9
    OUTER APPLY
    ( 
    SELECT TOP 1 PK, VAL FROM X_10_DISTINCT_CI t10 WHERE PK > t9.PK AND t10.VAL NOT IN (t1.VAL, t2.VAL, t3.VAL, t4.VAL, t5.VAL, t6.VAL, t7.VAL, t8.VAL, t9.VAL)
    ) t10
) t
UNPIVOT 
(
  VAL FOR VALS IN (VAL1, VAL2, VAL3, VAL4, VAL5, VAL6, VAL7, VAL8, VAL9, VAL10)
) AS upvt;

위 쿼리에 대한 실제 쿼리 계획은 다음과 같습니다 . 내 컴퓨터에서이 쿼리는 140ms의 CPU 시간과 3203 개의 논리 읽기로 154ms 안에 완료됩니다. 이것은 OPTIMIZE FOR클러스터형 인덱스 테이블에 대한 쿼리 보다 약간 더 빠르게 실행되는 것 같습니다. 나는 기대하지 않았으므로 성능을보다 신중하게 측정하려고했습니다. 내 방법은 결과 집합없이 각 쿼리 열 시간을 실행하고에서 총 숫자를보고 있었다 sys.dm_exec_sessions그리고 sys.dm_exec_session_wait_stats. 세션 56은 APPLY쿼리이고 세션 63은 OPTIMIZE FOR쿼리입니다.

출력 sys.dm_exec_sessions:

╔════════════╦══════════╦════════════════════╦═══════════════╗
 session_id  cpu_time  total_elapsed_time  logical_reads 
╠════════════╬══════════╬════════════════════╬═══════════════╣
         56      1360                1373          32030 
         63      2094                2091          30400 
╚════════════╩══════════╩════════════════════╩═══════════════╝

APPLY쿼리의 cpu_time 및 elapsed_time에는 분명한 이점이 있습니다.

출력 sys.dm_exec_session_wait_stats:

╔════════════╦════════════════════════════════╦═════════════════════╦══════════════╦══════════════════╦═════════════════════╗
 session_id            wait_type             waiting_tasks_count  wait_time_ms  max_wait_time_ms  signal_wait_time_ms 
╠════════════╬════════════════════════════════╬═════════════════════╬══════════════╬══════════════════╬═════════════════════╣
         56  SOS_SCHEDULER_YIELD                             340             0                 0                    0 
         56  MEMORY_ALLOCATION_EXT                            38             0                 0                    0 
         63  SOS_SCHEDULER_YIELD                             518             0                 0                    0 
         63  MEMORY_ALLOCATION_EXT                            98             0                 0                    0 
         63  RESERVED_MEMORY_ALLOCATION_EXT                  400             0                 0                    0 
╚════════════╩════════════════════════════════╩═════════════════════╩══════════════╩══════════════════╩═════════════════════╝

OPTIMIZE FOR쿼리는 추가 대기 유형이 RESERVED_MEMORY_ALLOCATION_EXT . 이것이 무엇을 의미하는지 정확히 모르겠습니다. 해시 일치 (흐름 구분) 연산자에서 오버 헤드를 측정 한 것일 수 있습니다. 어쨌든 CPU 시간에서 70ms의 차이에 대해 걱정할 가치가 없습니다.


1

왜 이것이
문제를 해결할 수 있는지에 대한 답변이 있다고 생각합니다.
지저분 해 보이지만 실행 계획에 따르면 상위 2 위는 비용의 84 %입니다

SELECT distinct top (2)  [enumID]
FROM [ENRONbbb].[dbo].[docSVenum1]

declare @table table (enumID tinyint);
declare @enumID tinyint;
set @enumID = (select top (1) [enumID] from [docSVenum1]);
insert into @table values (@enumID);
set @enumID = (select top (1) [enumID] from [docSVenum1] where [enumID] not in (select enumID from @table));
insert into @table values (@enumID);
set @enumID = (select top (1) [enumID] from [docSVenum1] where [enumID] not in (select enumID from @table));
insert into @table values (@enumID);
set @enumID = (select top (1) [enumID] from [docSVenum1] where [enumID] not in (select enumID from @table));
insert into @table values (@enumID);
set @enumID = (select top (1) [enumID] from [docSVenum1] where [enumID] not in (select enumID from @table));
insert into @table values (@enumID);
set @enumID = (select top (1) [enumID] from [docSVenum1] where [enumID] not in (select enumID from @table));
insert into @table values (@enumID);
set @enumID = (select top (1) [enumID] from [docSVenum1] where [enumID] not in (select enumID from @table));
insert into @table values (@enumID);
set @enumID = (select top (1) [enumID] from [docSVenum1] where [enumID] not in (select enumID from @table));
insert into @table values (@enumID);
set @enumID = (select top (1) [enumID] from [docSVenum1] where [enumID] not in (select enumID from @table));
insert into @table values (@enumID);
set @enumID = (select top (1) [enumID] from [docSVenum1] where [enumID] not in (select enumID from @table));
insert into @table values (@enumID);
select enumID from @table;

이 코드는 내 컴퓨터에서 5 초가 걸렸습니다. 테이블 변수에 대한 조인이 약간의 오버 헤드를 추가하는 것처럼 보입니다. 최종 쿼리에서 테이블 변수는 892800 번 스캔되었습니다. 이 쿼리에는 1359ms의 CPU 시간과 1374ms의 경과 시간이 걸렸습니다. 내가 기대했던 것보다 훨씬 더. 확실하지 않지만 테이블 변수에 기본 키를 추가하면 도움이되는 것 같습니다. 다른 가능한 최적화가있을 수 있습니다.
Joe Obbish 2012

-4

보고있는 내용을 이해하기 위해 물러서서 객관적으로 질문을 검토해야한다고 생각합니다.

쿼리 최적화 프로그램이 고유 한 값의 전체 목록을 먼저 식별하지 않고 상위 10 개의 고유 한 값을 어떻게 선택할 수 있습니까?

고유를 선택하면 결과 세트를 식별하기 위해 전체 테이블 (또는 포함 인덱스) 스캔이 필요합니다. 생각해보십시오-표의 마지막 행에는 이전에는 보지 못한 값이 포함될 수 있습니다.

Select Distinct는 매우 무딘 무기입니다.


2
실제로는 아닙니다. 테이블을 스캔하고 처음 20 개의 행에 10 개의 고유 값이있는 경우 나머지 테이블을 계속 스캔해야하는 이유는 무엇입니까?
ypercubeᵀᴹ

2
10 만 요청할 때 왜 계속 찾아야합니까? 이미 10 개의 고유 한 값을 찾았으므로 중지해야합니다. 이것이 문제의 문제입니다.
ypercubeᵀᴹ

3
상위 N 검색에서 전체 결과 세트를 먼저보아야하는 이유는 무엇입니까? 10 개의 고유 한 값이 있고 그게 마음에 드는 전부라면 다른 값을 찾지 못할 수 있습니다. 전체 결과 집합을 정렬하여 첫 번째 "10"이 무엇인지 아는 것은 다른 이야기이지만 10 개를 신경 쓰지 않고 10 개의 고유 한 값만 원하는 경우 전체 결과 집합을 얻는 데 논리적 요구 사항이 없습니다.
Tom V-팀 모니카

2
요청한 세트를 반환하는 작업을 수행했다고 상상해보십시오. 수천만 개 중 상위 10 개의 값을 지정하라는 요청을 받았으며 정렬 순서를 따르도록 지시받지 않았습니다. 예를 들어, 처음 100 개를보고 결과에 도달하면 전체 값 집합을 거쳐야한다고 생각하십니까? 이제는 데이터베이스 제품에서 해당 논리를 구현하는 것이 또 다른 문제이지만 ,이 문제에 대해 전체 테이블을 스캔하는 것이 논리적으로 필요하다고 제안하는 것 같습니다 .
Andriy M

4
@Marco : 나는 동의,이 입니다 대답은. 응답자는 질문의 전제에 동의하지 않고 OP의 오해라고 생각하는 것에 대답합니다.
Andriy M
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.