본문 바로가기
Develop/MSSQL

EXCEPT 구문 성능 이슈

by 3-stack 2022. 12. 7.

1. 사전구성

SELECT number custid
  , NEWID() code
  , CASE WHEN number <= 10 THEN 5
    WHEN number <= 110 THEN 4
    WHEN number <= 410 THEN 3
    WHEN number <= 710 THEN 2
    ELSE 1 END level
INTO TB_Cust01
FROM master..spt_values
WHERE type = 'P'
  AND number BETWEEN 1 AND 1000;
  
SELECT a.custid, DATEADD(dd, b.number, '2000-01-01') date
INTO TB_Pay01
FROM TB_Cust01 a, master..spt_values b
WHERE b.type = 'P'
  AND b.number BETWEEN 1 AND 1000
  AND (
    (level IN (4, 5) AND custid%2=0)
    OR (level IN (1,2,3) AND custid%100<>0)
  );

CREATE INDEX NIDX01_Cust01 ON TB_Cust01 (level) INCLUDE (custid);
CREATE INDEX NIDX01_Pay01 ON TB_Pay01 (date) INCLUDE (custid);

 

table_name index_name index_description Is_Unique indexed_columns included_columns
TB_Cust01 NIDX01_Cust01 nonclustered 0 level custid
TB_Pay01 NIDX01_Pay01 nonclustered 0 date custid

 

2. AS-IS

SELECT custid
FROM TB_Cust01
WHERE level = 2
EXCEPT
SELECT custid
FROM TB_Pay01
WHERE date >= '2000-01-01';

  • [TB_Cust01] 테이블에서 [TB_Pay01] 테이블의 데이터를 제외하기 위해
    조건절을 필터하고 정렬을 수행한 뒤 Anti Semi Join으로 조인에 성공하지 못하는 데이터만을 최종 출력한다.
  • [TB_Pay01] 테이블에서 사용된 인덱스에는 조인 조건인 [custid] 컬럼이 키로 구성되어 있지 않기 때문에
    [date] >= '2000-01-01' 조건만 인덱스로 탐색되며 선행 테이블의 결과 집합 각 행마다 조인에 성공하는 데이터를 찾을 때까지 읽게 되면서 I/O가 높게 사용된다.

 

3. TO_BE

CREATE INDEX NIDX02_Pay01 ON TB_Pay01 (custid, date)
;
SELECT custid
FROM TB_Cust01 a
WHERE level = 2
  AND NOT EXISTS (
    SELECT 'x'
    FROM TB_Pay01 b
    WHERE a.custid = b.custid
      AND date >= '2000-01-01');

  • NOT EXISTS 구문으로 변경하면 동일한 결과를 도출하면서 불필요한 정렬 과정을 제거할 수 있다.
  • 결국 인덱스 쓰라는 말... NOT EXISTS도 Anti Semi Join으로 수행되며 조인에 성공하는 데이터를 찾으면 해당 지점에서 중단하지만
    조인에 성공하는 데이터를 찾지 못한다면 모든 데이터를 읽어내야 하기 때문에 많은 I/O가 사용될 수 있다.
  • [TB_Pay01] 테이블의 조인 조건인 [custid] 컬럼이 인덱스에 선행 키로 포함되어 있다면
    데이터가 정렬된 상태로 유지되기 때문에 첫 행만 읽어내도 조인되는 데이터가 있는지에 대한 판별이 가능하여 I/O를 개선할 수 있다.

댓글