행을 반환하지 않는 쿼리에 ORDER BY를 포함 시키면 성능에 큰 영향을 미칩니다.


15

간단한 3 개의 테이블 조인이 주어지면 ORDER BY가 행을 반환하지 않아도 포함되면 쿼리 성능이 크게 변경됩니다. 실제 문제 시나리오는 30 초가 걸리고 0 개의 행을 반환하지만 ORDER BY가 포함되지 않은 경우 즉시 발생합니다. 왜?

SELECT * 
FROM tinytable t                          /* one narrow row */
JOIN smalltable s on t.id=s.tinyId        /* one narrow row */
JOIN bigtable b on b.smallGuidId=s.GuidId /* a million narrow rows */
WHERE t.foreignId=3                       /* doesn't match */
ORDER BY b.CreatedUtc          /* try with and without this ORDER BY */

bigtable.smallGuidId에 대한 색인을 가질 수 있음을 이해하지만 실제로이 경우에는 더 나빠질 것이라고 생각합니다.

다음은 테스트 할 테이블을 작성하고 채우는 스크립트입니다. 흥미롭게도 smalltable에는 nvarchar (max) 필드가 있다는 것이 중요합니다. 또한 guid와 함께 빅 테이블에 합류한다는 것이 중요합니다 (해시 일치를 사용하고 싶다고 생각합니다).

CREATE TABLE tinytable
  (
     id        INT PRIMARY KEY IDENTITY(1, 1),
     foreignId INT NOT NULL
  )

CREATE TABLE smalltable
  (
     id     INT PRIMARY KEY IDENTITY(1, 1),
     GuidId UNIQUEIDENTIFIER NOT NULL DEFAULT NEWID(),
     tinyId INT NOT NULL,
     Magic  NVARCHAR(max) NOT NULL DEFAULT ''
  )

CREATE TABLE bigtable
  (
     id          INT PRIMARY KEY IDENTITY(1, 1),
     CreatedUtc  DATETIME NOT NULL DEFAULT GETUTCDATE(),
     smallGuidId UNIQUEIDENTIFIER NOT NULL
  )

INSERT tinytable
       (foreignId)
VALUES(7)

INSERT smalltable
       (tinyId)
VALUES(1)

-- make a million rows 
DECLARE @i INT;

SET @i=20;

INSERT bigtable
       (smallGuidId)
SELECT GuidId
FROM   smalltable;

WHILE @i > 0
  BEGIN
      INSERT bigtable
             (smallGuidId)
      SELECT smallGuidId
      FROM   bigtable;

      SET @i=@i - 1;
  END 

SQL 2005, 2008 및 2008R2에서 동일한 결과를 테스트했습니다.

답변:


32

나는 Martin Smith의 대답에 동의하지만 문제는 단순히 통계 중 하나가 아닙니다. foreignId 열에 대한 통계 (자동 통계가 사용 가능하다고 가정)는 값 3에 대해 행이 존재하지 않음을 나타냅니다 (값이 7 인 값이 하나만 있음).

DBCC SHOW_STATISTICS (tinytable, foreignId) WITH HISTOGRAM

통계 출력

SQL Server는 통계가 캡처 된 후 상황이 변경 되었을 수 있으므로 계획이 실행될 때 값 3에 대한 행 이있을 있음을 알고 있습니다 . 또한 계획 컴파일과 실행 사이에 어느 정도의 시간이 소요될 수 있습니다 (계획은 결국 재사용을 위해 캐시됩니다). Martin이 말했듯이 SQL Server에는 최적의 이유로 캐시 된 계획을 다시 컴파일하는 것을 정당화하기 위해 충분한 수정이 이루어진시기를 감지하는 논리가 포함되어 있습니다.

그러나이 중 어느 것도 중요하지 않습니다. 한 가지 경우를 제외하고, 옵티마이 저는 테이블 조작으로 생성 된 행 수가 0으로 추정되지 않습니다. 출력이 항상 제로 행이어야한다고 정적으로 판별 할 수 있으면 조작이 중복되어 완전히 제거됩니다.

대신 옵티 마이저 모델은 최소 하나의 행을 추정합니다 . 이 휴리스틱을 사용하면 더 낮은 추정이 가능한 경우보다 평균적으로 더 나은 계획을 생성하는 경향이 있습니다. 어떤 단계에서 제로 행 추정치를 생성하는 계획은 비용 기반 결정을 내릴 근거가 없기 때문에 처리 스트림의 해당 시점에서 쓸모가 없습니다 (무엇이든 제로 행은 제로 행임). 추정치가 틀린 경우, 0 행 추정치 위의 계획 형태는 거의 합리적이지 않습니다.

두 번째 요소는 포함 가정이라고하는 또 다른 모델링 가정입니다. 이것은 본질적으로 쿼리가 다른 범위의 값과 값 범위를 결합하는 경우 범위가 겹치기 때문이라고 말합니다. 이것을 넣는 또 다른 방법은 행이 리턴 될 것으로 예상되므로 조인이 지정되고 있다는 것입니다. 이러한 추론이 없으면 비용은 일반적으로 과소 평가되어 광범위한 공통 쿼리에 대한 계획이 잘못됩니다.

본질적으로 여기에있는 것은 최적화 프로그램의 모델에 맞지 않는 쿼리입니다. 다중 열 또는 필터링 된 인덱스로 추정치를 '향상'시키기 위해 할 수있는 일은 없습니다. 여기에서 1 행보다 낮은 추정값을 얻는 방법은 없습니다. 실제 데이터베이스에는 이러한 상황이 발생하지 않도록 외래 키가있을 수 있지만 여기에 해당되지 않는다고 가정하면 모델 외부 조건을 수정하기 위해 힌트를 사용합니다. 이 쿼리에는 다양한 힌트 접근 방식이 작동합니다. OPTION (FORCE ORDER)작성된대로 쿼리에서 잘 작동합니다.


21

여기서 기본적인 문제는 통계 중 하나입니다.

두 쿼리의 예상 행 수는 최종 결과 SELECTbigtable실제로 발생하는 0이 아니라 1,048,580 개의 행 (에 존재하는 것으로 추정되는 행과 동일한 수)을 반환 한다고 생각 합니다.

JOIN조건이 일치하고 모든 행을 유지합니다. 단일 행 in tinytablet.foreignId=3술어 와 일치하지 않으므로 제거 됩니다.

당신이 실행하는 경우

SELECT * 
FROM tinytable t  
WHERE t.foreignId=3  AND id=1 

그리고 그것이 예상 행 수를 보면 1보다는 0및 계획 전반에 걸쳐이 오류 전파합니다. tinytable현재 1 행을 포함합니다. 500 개의 행 수정 이 발생할 때까지이 테이블에 대한 통계는 재 컴파일되지 않으므로 일치하는 행이 추가 될 수 있으며 재 컴파일을 트리거하지 않습니다.

ORDER BY절 을 추가 할 때 조인 순서가 변경 되고 varchar(max)smalltable이있는 이유는 varchar(max)열이 평균적으로 4,000 바이트 씩 행 크기를 증가시킬 것으로 추정하기 때문 입니다. 1048580 행을 곱하면 정렬 작업에 약 4GB가 필요하므로 SORT작업 전에 작업 을 수행하기로 결정 합니다 JOIN.

아래와 같이 힌트를 사용 하여 ORDER BY쿼리가 비 ORDER BY조인 전략 을 강제 로 적용하도록 할 수 있습니다 .

SELECT *
FROM   tinytable t /* one narrow row */
       INNER MERGE JOIN smalltable s /* one narrow row */
                        INNER LOOP JOIN bigtable b
                          ON b.smallGuidId = s.GuidId /* a million narrow rows */
         ON t.id = s.tinyId
WHERE  t.foreignId = 3 /* doesn't match */
ORDER  BY b.CreatedUtc
OPTION (MAXDOP 1) 

이 계획은 추정 된 하위 트리 비용이 거의 12,000및 잘못된 예상 행 수와 예상 데이터 크기를 갖는 정렬 연산자를 보여줍니다 .

계획

BTW UNIQUEIDENTIFIER열을 정수로 바꾸는 것을 찾지 못했습니다 .


2

Show Execution Plan 버튼을 켜면 진행 상황을 확인할 수 있습니다. "느린"쿼리 계획은 다음과 같습니다. 여기에 이미지 설명을 입력하십시오

다음은 "빠른"쿼리입니다. 여기에 이미지 설명을 입력하십시오

이것보세요-함께 실행하면 첫 번째 쿼리는 ~ 33 배 더 "고가"(97 : 3 비율)입니다. SQL은 날짜 시간별로 BigTable을 주문하도록 첫 번째 쿼리를 최적화 한 다음 SmallTable & TinyTable에 대해 작은 "검색"루프를 실행하여 각각 백만 번 실행합니다 ( "Clustered Index Seek"아이콘 위에 마우스를 올려 놓으면 더 많은 통계를 얻을 수 있습니다). 따라서 작은 테이블 (23 % 및 46 %)의 정렬 (27 %) 및 2 x 1 백만 "Seeks"는 값 비싼 쿼리의 대량입니다. 비 ORDER BY조회는 총 3 회의 스캔을 수행합니다.

기본적으로 특정 시나리오에 대한 SQL 옵티 마이저 로직에 구멍이 있습니다. 그러나 TysHTTP에서 언급했듯이 색인을 추가하면 (삽입 속도가 느려짐) 스캔 속도가 빨라집니다.


2

일어나고있는 일은 SQL이 제한 전에 명령을 실행하기로 결정하고 있습니다.

이 시도:

SELECT *
(
SELECT * 
FROM tinytable t
    INNER JOIN smalltable s on t.id=s.tinyId
    INNER JOIN bigtable b on b.smallGuidId=s.GuidId
WHERE t.foreignId=3
) X
ORDER BY b.CreatedUtc

이렇게하면 실제로 다른 인덱스를 추가해도 성능에 영향을주지 않으면 서 성능이 향상됩니다 (이 경우 반환 결과 수가 매우 적은 경우). SQL 옵티마이 저가 조인 전까지 순서를 수행하기로 결정하는 것은 이상하지만 실제로 데이터를 반환 한 경우 조인 후 정렬하면 정렬하지 않고 정렬하는 것보다 시간이 오래 걸리기 때문일 수 있습니다.

마지막으로 다음 스크립트를 실행 한 다음 업데이트 된 통계 및 색인으로 인해 문제가 해결되는지 확인하십시오.

EXEC [sp_MSforeachtable] @command1="RAISERROR('UPDATE STATISTICS(''?'') ...',10,1) WITH NOWAIT UPDATE STATISTICS ? "

EXEC [sp_MSforeachtable] @command1="RAISERROR('DBCC DBREINDEX(''?'') ...',10,1) WITH NOWAIT DBCC DBREINDEX('?')"

EXEC [sp_MSforeachtable] @command1="RAISERROR('UPDATE STATISTICS(''?'') ...',10,1) WITH NOWAIT UPDATE STATISTICS ? "

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