“EXISTS (…) OR EXISTS (…)”의 절 순서


11

두 가지 중 하나의 존재를 테스트하는 쿼리 클래스가 있습니다. 그것은 형태입니다

SELECT CASE
  WHEN EXISTS (SELECT 1 FROM ...)
  OR EXISTS (SELECT 1 FROM ...)
THEN 1 ELSE 0 END;

실제 명령문은 C로 생성되며 ODBC 연결을 통한 임시 쿼리로 실행됩니다.

최근 대부분의 경우 두 번째 SELECT가 첫 번째 SELECT보다 빠르며 두 EXISTS 절의 순서를 변경하면 방금 만든 하나 이상의 끔찍한 테스트 사례에서 속도가 크게 향상된다는 사실이 밝혀졌습니다.

분명히해야 할 일은 두 절을 바꾸는 것이지만 SQL Server에 더 익숙한 사람이이 점을 고려할 것인지 알고 싶었습니다. 우연의 일치와 "구현 세부 사항"에 의존하는 것 같습니다.

(또한 SQL Server가 더 똑똑한 경우 두 EXISTS 절을 병렬로 실행하고 둘 중 하나가 먼저 단락을 완료하도록 할 것입니다.)

SQL Server가 그러한 쿼리의 실행 시간을 지속적으로 향상시킬 수있는 더 좋은 방법이 있습니까?

최신 정보

내 질문에 관심을 가져 주셔서 감사합니다. 실제 쿼리 계획에 대한 질문은 기대하지 않았지만 공유 할 계획입니다.

SQL Server 2008R2 이상을 지원하는 소프트웨어 구성 요소를위한 것입니다. 데이터의 모양은 구성 및 사용에 따라 상당히 다를 수 있습니다. 내 동료는 (예에서) dbf_1162761$z$rv$1257927703테이블이 항상 테이블보다 행의 수와 같거나 그보다 많을 dbf_1162761$z$dd$1257927703때도 있기 때문에 쿼리를 이와 같이 변경하려고 생각 했습니다 .

여기에 내가 언급 한 욕설이 있습니다. 첫 번째 쿼리는 느린 쿼리이며 약 20 초가 걸립니다. 두 번째 쿼리는 즉시 완료됩니다.

가치있는 것을 위해, 매개 변수 스니핑이 특정 사례를 버리고 있기 때문에 "알 수없는 최적화"비트도 최근에 추가되었습니다.

원래 검색어 :

SELECT CASE
  WHEN EXISTS (SELECT 1 FROM zumero.dbf_1162761$z$rv$1257927703 rv INNER JOIN zumero.dbf_1162761$t$tx tx ON tx.txid=rv.txid WHERE tx.generation BETWEEN 1500 AND 2502)
  OR EXISTS (SELECT 1 FROM zumero.dbf_1162761$z$dd$1257927703 dd INNER JOIN zumero.dbf_1162761$t$tx tx ON tx.txid=dd.txid WHERE tx.generation BETWEEN 1500 AND 2502)
THEN 1 ELSE 0 END
OPTION (OPTIMIZE FOR UNKNOWN)

원래 계획 :

|--Compute Scalar(DEFINE:([Expr1006]=CASE WHEN [Expr1007] THEN (1) ELSE (0) END))
     |--Nested Loops(Left Semi Join, DEFINE:([Expr1007] = [PROBE VALUE]))
          |--Constant Scan
          |--Concatenation
               |--Nested Loops(Inner Join, WHERE:([scale].[zumero].[dbf_1162761$z$rv$1257927703].[txid] as [rv].[txid]=[scale].[zumero].[dbf_1162761$t$tx].[txid] as [tx].[txid]))
               |    |--Clustered Index Scan(OBJECT:([scale].[zumero].[dbf_1162761$z$rv$1257927703].[PK__dbf_1162__97770A2F62EEAE79] AS [rv]), WHERE:([scale].[zumero].[dbf_1162761$z$rv$1257927703].[txid] as [rv].[txid]>(0)))
               |    |--Index Seek(OBJECT:([scale].[zumero].[dbf_1162761$t$tx].[gendex] AS [tx]), SEEK:([tx].[generation] >= (1500) AND [tx].[generation] <= (2502)) ORDERED FORWARD)
               |--Nested Loops(Inner Join, OUTER REFERENCES:([tx].[txid]))
                    |--Clustered Index Scan(OBJECT:([scale].[zumero].[dbf_1162761$t$tx].[PK__dbf_1162__E3BA953EC2197789] AS [tx]),  WHERE:([scale].[zumero].[dbf_1162761$t$tx].[generation] as [tx].[generation]>=(1500) AND [scale].[zumero].[dbf_1162761$t$tx].[generation] as [tx].[generation]<=(2502)) ORDERED FORWARD)
                    |--Index Seek(OBJECT:([scale].[zumero].[dbf_1162761$z$dd$1257927703].[n$dbf_1162761$z$dd$txid$1257927703] AS [dd]), SEEK:([dd].[txid]=[scale].[zumero].[dbf_1162761$t$tx].[txid] as [tx].[txid]),  WHERE:([scale].[zumero].[dbf_1162761$z$dd$1257927703].[txid] as [dd].[txid]>(0)) ORDERED FORWARD)

고정 쿼리 :

SELECT CASE
  WHEN EXISTS (SELECT 1 FROM zumero.dbf_1162761$z$dd$1257927703 dd INNER JOIN zumero.dbf_1162761$t$tx tx ON tx.txid=dd.txid WHERE tx.generation BETWEEN 1500 AND 2502)
  OR EXISTS (SELECT 1 FROM zumero.dbf_1162761$z$rv$1257927703 rv INNER JOIN zumero.dbf_1162761$t$tx tx ON tx.txid=rv.txid WHERE tx.generation BETWEEN 1500 AND 2502)
THEN 1 ELSE 0 END
OPTION (OPTIMIZE FOR UNKNOWN)

고정 계획 :

|--Compute Scalar(DEFINE:([Expr1006]=CASE WHEN [Expr1007] THEN (1) ELSE (0) END))
     |--Nested Loops(Left Semi Join, DEFINE:([Expr1007] = [PROBE VALUE]))
          |--Constant Scan
          |--Concatenation
               |--Nested Loops(Inner Join, OUTER REFERENCES:([tx].[txid]))
               |    |--Clustered Index Scan(OBJECT:([scale].[zumero].[dbf_1162761$t$tx].[PK__dbf_1162__E3BA953EC2197789] AS [tx]),  WHERE:([scale].[zumero].[dbf_1162761$t$tx].[generation] as [tx].[generation]>=(1500) AND [scale].[zumero].[dbf_1162761$t$tx].[generation] as [tx].[generation]<=(2502)) ORDERED FORWARD)
               |    |--Index Seek(OBJECT:([scale].[zumero].[dbf_1162761$z$dd$1257927703].[n$dbf_1162761$z$dd$txid$1257927703] AS [dd]), SEEK:([dd].[txid]=[scale].[zumero].[dbf_1162761$t$tx].[txid] as [tx].[txid]),  WHERE:([scale].[zumero].[dbf_1162761$z$dd$1257927703].[txid] as [dd].[txid]>(0)) ORDERED FORWARD)
               |--Nested Loops(Inner Join, WHERE:([scale].[zumero].[dbf_1162761$z$rv$1257927703].[txid] as [rv].[txid]=[scale].[zumero].[dbf_1162761$t$tx].[txid] as [tx].[txid]))
                    |--Clustered Index Scan(OBJECT:([scale].[zumero].[dbf_1162761$z$rv$1257927703].[PK__dbf_1162__97770A2F62EEAE79] AS [rv]), WHERE:([scale].[zumero].[dbf_1162761$z$rv$1257927703].[txid] as [rv].[txid]>(0)))
                    |--Index Seek(OBJECT:([scale].[zumero].[dbf_1162761$t$tx].[gendex] AS [tx]), SEEK:([tx].[generation] >= (1500) AND [tx].[generation] <= (2502)) ORDERED FORWARD)

답변:


11

일반적으로 SQL Server는 CASE명령문 의 일부를 순서대로 실행하지만 OR조건을 자유롭게 재정렬 할 수 있습니다. 일부 쿼리의 WHEN경우 CASE명령문 내 표현식 의 순서를 변경하여 일관되게 더 나은 성능을 얻을 수 있습니다 . OR명령문 에서 조건 순서를 변경할 때 성능이 향상되는 경우도 있지만 동작이 보장되는 것은 아닙니다.

간단한 예를 들어 살펴 보는 것이 가장 좋습니다. SQL Server 2016에 대해 테스트 중이므로 컴퓨터에서 동일한 결과를 얻지 못할 수도 있지만 동일한 원칙이 적용되는 한 알고 있습니다. 먼저 두 개의 테이블에 1에서 1000000 사이의 백만 개의 정수를 넣습니다. 하나는 클러스터 된 인덱스가 있고 다른 하나는 힙입니다.

CREATE TABLE dbo.X_HEAP (ID INT NOT NULL, FLUFF VARCHAR(100));

INSERT INTO dbo.X_HEAP  WITH (TABLOCK)
SELECT TOP (1000000) ROW_NUMBER() OVER (ORDER BY (SELECT NULL)), REPLICATE('Z', 100)
FROM master..spt_values t1
CROSS JOIN master..spt_values t2
OPTION (MAXDOP 1);

CREATE TABLE dbo.X_CI (ID INT NOT NULL, FLUFF VARCHAR(100), PRIMARY KEY (ID));

INSERT INTO dbo.X_CI  WITH (TABLOCK)
SELECT TOP (1000000) ROW_NUMBER() OVER (ORDER BY (SELECT NULL)), REPLICATE('Z', 100)
FROM master..spt_values t1
CROSS JOIN master..spt_values t2
OPTION (MAXDOP 1);

다음 쿼리를 고려하십시오.

SELECT CASE
  WHEN EXISTS (SELECT 1 FROM dbo.X_HEAP WHERE ID = 500000)
  OR EXISTS (SELECT 1 FROM dbo.X_CI WHERE ID = 500000)
THEN 1 ELSE 0 END;

하위 쿼리를 평가하는 X_CI것이 X_HEAP특히 일치하는 행이없는 경우 하위 쿼리를 평가하는 것보다 훨씬 저렴하다는 것을 알고 있습니다 . 일치하는 행이 없으면 클러스터 된 인덱스가있는 테이블에 대해 몇 가지 논리적 읽기만 수행하면됩니다. 그러나 일치하는 행이 없다는 것을 알기 위해 힙의 모든 행을 스캔해야합니다. 최적화 프로그램도 이것을 알고 있습니다. 대체로 클러스터형 인덱스를 사용하여 하나의 행을 조회하는 것은 테이블 스캔에 비해 매우 저렴합니다.

이 예제 데이터의 경우 다음과 같이 쿼리를 작성합니다.

SELECT CASE
  WHEN EXISTS (SELECT 1 FROM dbo.X_CI WHERE ID = 500000) THEN 1 
  WHEN EXISTS (SELECT 1 FROM dbo.X_HEAP WHERE ID = 500000) THEN 1 
ELSE 0 END;

이렇게하면 SQL Server가 먼저 클러스터 된 인덱스가있는 테이블에 대해 하위 쿼리를 강제로 실행합니다. 결과는 다음과 같습니다 SET STATISTICS IO, TIME ON.

테이블 'X_CI'. 스캔 카운트 0, 논리적 읽기 3, 물리적 읽기 0

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

쿼리 계획을 살펴보면 레이블 1에서 찾기가 레이블 2에서 스캔하는 것보다 데이터를 반환하지 않으면 발생하지 않습니다.

좋은 질문

다음 쿼리는 훨씬 덜 효율적입니다.

SELECT CASE
  WHEN EXISTS (SELECT 1 FROM dbo.X_HEAP WHERE ID = 500000) THEN 1 
  WHEN EXISTS (SELECT 1 FROM dbo.X_CI WHERE ID = 500000) THEN 1 
ELSE 0 END
OPTION (MAXDOP 1);

쿼리 계획을 보면 레이블 2의 스캔이 항상 발생한다는 것을 알 수 있습니다. 행이 발견되면 레이블 1에서 찾기를 건너 뜁니다. 그것은 우리가 원하는 순서가 아닙니다.

잘못된 쿼리 계획

성능 결과는 다음과 같습니다.

테이블 'X_HEAP'. 스캔 카운트 1, 논리적 읽기 7247

SQL Server 실행 시간 : CPU 시간 = 15ms, 경과 시간 = 22ms

원래 쿼리로 돌아가서이 쿼리에 대해 검색 및 스캔이 성능에 적합한 순서로 평가 된 것을 볼 수 있습니다.

SELECT CASE
  WHEN EXISTS (SELECT 1 FROM dbo.X_HEAP WHERE ID = 500000)
  OR EXISTS (SELECT 1 FROM dbo.X_CI WHERE ID = 500000)
THEN 1 ELSE 0 END;

그리고이 쿼리에서 반대 순서로 평가됩니다.

SELECT CASE
  WHEN EXISTS (SELECT 1 FROM dbo.X_CI WHERE ID = 500000)
  OR EXISTS (SELECT 1 FROM dbo.X_HEAP WHERE ID = 500000)
THEN 1 ELSE 0 END;

그러나 이전 쿼리 쌍과 달리 SQL Server 쿼리 최적화 프로그램이 서로를 먼저 평가하도록 강요하는 것은 없습니다. 중요한 행동에 의존해서는 안됩니다.

결론적으로 하나의 하위 쿼리를 다른 하위 쿼리보다 먼저 평가해야하는 경우 CASE명령문이나 다른 방법을 사용하여 순서를 강제합니다. 그렇지 않으면 OR원하는 조건 에서 하위 쿼리를 자유롭게 주문할 수 있지만 최적화 프로그램이 작성된 순서대로 하위 쿼리를 실행할 것이라는 보장은 없습니다.

추가:

자연스러운 후속 질문은 SQL Server가 어떤 쿼리가 더 저렴한 쿼리를 결정하고 먼저 쿼리를 실행하도록하려면 어떻게해야합니까? 지금까지 모든 메서드는 동작이 보장되지 않더라도 쿼리 작성 순서대로 SQL Server에서 구현 된 것으로 보입니다.

다음은 간단한 데모 테이블에서 작동하는 것으로 보이는 옵션 중 하나입니다.

SELECT CASE
  WHEN EXISTS (
    SELECT 1
    FROM (
        SELECT TOP 2 1 t
        FROM 
        (
            SELECT 1 ID

            UNION ALL

            SELECT TOP 1 ID 
            FROM dbo.X_HEAP 
            WHERE ID = 50000 
        ) h
        CROSS JOIN
        (
            SELECT 1 ID

            UNION ALL

            SELECT TOP 1 ID 
            FROM dbo.X_CI
            WHERE ID = 50000
        ) ci
    ) cnt
    HAVING COUNT(*) = 2
)
THEN 1 ELSE 0 END;

여기 에서 db 바이올린 데모를 찾을 수 있습니다 . 파생 테이블의 순서를 변경해도 쿼리 계획은 변경되지 않습니다. 두 쿼리에서 X_HEAP테이블은 건드리지 않습니다. 즉, 쿼리 최적화 프로그램이 가장 저렴한 쿼리를 먼저 실행하는 것으로 보입니다. 나는 프로덕션에서 이와 같은 것을 사용하는 것을 추천 할 수 없으므로 대부분 호기심 가치를 위해 여기에 있습니다. 동일한 것을 달성하는 훨씬 간단한 방법이있을 수 있습니다.


4
또는 CASE WHEN EXISTS (SELECT 1 FROM dbo.X_CI WHERE ID = 500000 UNION ALL SELECT 1 FROM dbo.X_HEAP WHERE ID = 500000) THEN 1 ELSE 0 END대안이 될 수 있지만 여전히 어떤 쿼리가 더 빠른 쿼리를 수동으로 결정하고 먼저 쿼리에 의존하는지에 달려 있습니다. SQL Server가 자동으로 재정렬되도록 저렴한 표현이 먼저 평가되도록 표현 방법이 있는지 확실하지 않습니다.
Martin Smith
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.