서브쿼리에서 옵티마이저의 실행계획
아래와 같은 쿼리가 있다고 가정해 봅니다.
1 2 3 4 5 6 | SELECT TOP 20 * FROM T1 WHERE C2 IN ( SELECT C2 FROM T2 WHERE C3 = 'ABC' ) ORDER BY C1 | cs |
보통의 경우 옵티마이저는 아래와 같은 실행계획을 세우게 됩니다.
1. T2 테이블 C3='ABC' 인 C2 컬럼 데이터를 가져온다.
2. T1 테이블의 C2 컬럼에 가져온 데이터가 존재하는 데이터를 가져온다.
3. 이렇게 최종적으로 나온 데이터를 C1 컬럼으로 정렬하여 20 건만 출력한다.
이번 포스팅은 조금 특수한 경우로 아래 쿼리의 경우,
부모쿼리부터 데이터를 가져오면서 서브쿼리와 비교하는 실행계획을 세우게 된 케이스 입니다.
1 2 3 4 5 6 7 | SELECT TOP 20 T1C1 FROM dbo.T1 AS A WITH (NOLOCK) WHERE T1C2 IN (SELECT T2C2 FROM dbo.T2 WITH (NOLOCK) WHERE T2C3 IN ('ABC', 'ABC_Reserved')) ORDER BY T1C1 DESC | cs |
전제 조건
1. 서브쿼리의 테이블에서 출력되는 데이터는 테이블의 전체 건수와 동일합니다.
1 2 3 4 | SELECT C3, count(*) AS Cnt FROM dbo.T2 WITH(NOLOCK) GROUP BY C2 | cs |
strstoragetable |
Cnt |
ABC |
3872 |
ABC_Reserved |
9 |
2. 테이블의 인덱스는 아래와 같이 걸려 있습니다.
1 | sp_helpindex 'T2' | cs |
인덱스 종류 |
인덱스 순서 |
Non Clustered Index |
C3, C2 |
Clustered Index + PK |
C2 |
1 | sp_helpindex 'T1' | cs |
인덱스 종류 |
인덱스 순서 |
Non Clustered Index |
C2, C1 |
Clustered Index + PK |
C1 |
3. 부모 쿼리 테이블은 많은 건수를 가진 테이블 입니다.
1 | sp_spaceused 'T1' | cs |
건수 |
1,219,880 |
실행계획 확인
1. 서브쿼리 선택도가 나쁜 쿼리 ( 위와 동일한 쿼리 )
1 2 3 4 5 6 7 8 | SELECT TOP 20 T1C1 FROM dbo.T1 AS A WITH (NOLOCK) WHERE T1C2 IN (SELECT T2C2 FROM dbo.T2 WITH (NOLOCK) WHERE T2C3 IN ('ABC', 'ABC_Reserved')) ORDER BY T1C1 DESC | cs |
* 실행계획 해석
1. 부모쿼리 T1 테이블의 T1C1 로 Scan 을 진행하면서 T1C2 값을 출력합니다.
2. 출력된 T1C2 값이 서브쿼리 T2 테이블의 T2C2 값과 매칭되는게 있는지 확인합니다.
3. 매칭이 되면 T1 테이블의 T1C1 을 출력합니다.
4. 위의 단계를 T1C1 가 20 개가 출력될 때까지 계속합니다.
- 왜 이렇게 되었을까?
옵티마이저가 서브쿼리부터 접근한다고 가정해 봅시다.
1. 서브쿼리의 선택도는 매우 낮습니다. 즉, 테이블의 모든 건수를 다 출력해야 합니다.
2. 다 출력된 데이터를 부모 테이블의 T1C2 와 다 비교해야 합니다.
3. 이렇게 비교된 데이터를 다시한번 T1C1 로 정렬해야 합니다.
옵티마이저는 계획 유추
서브쿼리 선택도가 매우 낮기 때문에,
부모테이블의 Cluster Index + PK 로 구성된 T1C1 컬럼을 뒤에서부터 순차적으로 읽어가며 T1C2 값을 서브쿼리를 읽어서 나온 값과 비교하는게 더 좋다고 판단했기 때문일 것입니다.
2. 서브쿼리 선택도가 좋은 쿼리 ( 위 쿼리에서 선택도를 좋게 변경 )
서브쿼리에서 출력되는 데이터의 선택도를 좋게 변경해 보았습니다.
서브쿼리 T2 테이블의 T2C3 는 PK 입니다.
1 2 3 4 5 6 7 8 | SELECT TOP 20 T1C1 FROM dbo.T1 AS A WITH (NOLOCK) WHERE T1C2 IN (SELECT T2C2 FROM dbo.T2 WITH (NOLOCK) WHERE T2C3 IN (31, 229, 249, 615, 11880) ORDER BY T1C1 DESC | cs |
* 실행계획 해석
1. 서브쿼리 T2 테이블의 T2C3 값을 가져옵니다.
2. 가져온 T2C3 값을 부모쿼리 T1 테이블의 T1C2 값 Merge Join 으로 비교합니다.
4. 비교를 완료한 뒤에 나온 T1C1 를 정렬하여 20 개를 출력합니다.
옵티마이저는 서브쿼리의 T1C2 로 나오는 값이 5개라는 걸 알기 때문에,
서브쿼리를 먼저 읽고 나온 값으로 비교를 진행하였기 때문입니다.
참고로 force order 옵션을 부여하여 쿼리 실행계획을 부모부터 읽을 수 있도록 강제할 수 있습니다.
1 2 3 4 5 6 7 8 9 | SELECT TOP 20 T1C1 FROM dbo.T1 AS A WITH (NOLOCK) WHERE T1C2 IN (SELECT T2C2 FROM dbo.T2 WITH (NOLOCK) WHERE T2C3 IN (31, 229, 249, 615, 11880) ORDER BY T1C1 DESC OPTION (FORCE ORDER) | cs |
결론
서브쿼리를 사용한 경우 서브쿼리 선택도에 따라 쿼리를 읽는 순서가 달라지게 됩니다.
쿼리를 구성할 때 읽는 순서를 결정지을 수 없기 때문에, 이후에 선택도가 달라져 장애가 발생할 수도 있게 됩니다.
따라서 IN 절을 사용할 때는 힌트를 주어서 쿼리순서를 강제하도록 진행하거나
IN 절 대신 JOIN 방식으로 변경하여 순서를 지정하는 것도 좋은 방법이 될 것 같습니다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | -- IN 서브쿼리 SELECT TOP 20 T1C1 FROM dbo.T1 AS A WITH (NOLOCK) WHERE T1C2 IN (SELECT T2C2 FROM dbo.T2 WITH (NOLOCK) WHERE T2C3 IN ('ABC', 'ABC_Reserved')) ORDER BY T1C1 DESC -- JOIN SELECT TOP 20 A.T1C1 FROM dbo.T1 AS A WITH (NOLOCK) INNER JOIN dbo.T2 AS B WITH(NOLOCK) ON A.T1C2 = B.T2C2 AND B.T2C3 IN ('ABC', 'ABC_Reserved')) ORDER BY A.T1C1 DESC | cs |
'SQL Server > SQL Server 이슈' 카테고리의 다른 글
[MSSQL] 데이터 파일 사용량이 풀일 때 해결방법 (0) | 2020.03.09 |
---|---|
[MSSQL] 단순모드에서 트랜잭션 로그가 계속 증가하는 현상 해결하기 (0) | 2020.02.28 |
[MSSQL] SQLTELEMTERY, SSASTELEMETRY, SSISTELEMETRY 세션이란, Ceip 서비스 중지하는 방법 (0) | 2019.12.10 |
[MSSQL] SSISDB cleanup_server_retention_window 배치작업 execution_cursor 인 커서가 없습니다. 오류 해결하기 (0) | 2019.08.29 |
[MSSQL] 구성 요소 'Ad Hoc Distributed Queries'이(가) SQL Server 보안 구성의 일부로 해제 해결하기 (0) | 2019.05.22 |