728x90

이번에 소개해드릴 내용은 실제 색인 생성을 하는 부분입니다.

실제로 색인을 생성하면서 이리저리 살펴 보도록 하지요.

간단한 클러스터드 인덱스와 넌클러스터드 인덱스의 생성은 앞에서 보았으니

이번엔 유니크 인덱스 생성을 간단히 알아보고 이어서 Fill Factor를 알아 보도록 하지요.

먼저 유니크 인덱스 입니다.

 

USE konanTestDB
GO

--데이터 조회
SELECT TOP 100 * FROM konan_test_table
SELECT COUNT(konan_id) FROM konan_test_table

--유니크 인덱스 생성
CREATE UNIQUE INDEX idx ON konan_test_table (konan_data)

--중복 데이터 삽입 - 에러
INSERT INTO konan_test_table(konan_data, konan_date) VALUES('1', getdate())

서버: 메시지 2601, 수준 14, 상태 3, 줄 1
고유 인덱스 'idx'이(가) 있는 'konan_test_table' 개체에 중복 키 행을 삽입할 수 없습니다.
문이 종료되었습니다.

--인덱스 삭제
DROP INDEX konan_test_table.idx

--유니크 인덱스 생성 - WITH IGNORE_DUP_KEY
CREATE UNIQUE INDEX idx ON konan_test_table (konan_data)
WITH IGNORE_DUP_KEY

INSERT INTO konan_test_table(konan_data, konan_date) VALUES('1', getdate())

서버: 메시지 3604, 수준 16, 상태 1, 줄 1
중복 키가 무시되었습니다.

 

테이블에 이미 PRIMARY KEY가 있더라도 얼마든지 유니크한 값을 구별할 수 있는

유니크 인덱스로 고유성을 보장받을 수 있게 됩니다.

 

다음으로 중요한 이야기인 Fill Factor를 이야기해 보도록 하지요.

인덱스를 생성하면 데이터가 변경될 경우 인덱스 페이지 역시 변경이 되어야 합니다.

이때 데이터의 삽입이 상당히 많다면 인덱스 페이지는 계속적으로 변경되므로

시스템에 어느정도 부하를 줄 수 있게 되지요. 이때 fill factor를 적절히 이용해

인덱스의 리프레벨과 중간레벨에 여유 공간을 두는 겁니다.

자 여기서 그렇다면!!!

만약 Fill Factor가 100%라면? 이는 현재 인덱스의 리프 레벨과 중간레벨이 모두 100%로

차 있는 상태인 겁니다. 적절한 수량만큼만 채우는 80% 정도로 값을 주면?

인덱스의 리프레벨은 20%정도의 여유 공간이 생기지요. 하지만!! 그만큼 SELECT의

속도는 느려질 수 있는 겁니다.

우선 샘플을 또한 봐 보도록 하지요.

 

USE konanTestDB
GO

--데이터 조회
SELECT TOP 100 * FROM konan_test_table
SELECT COUNT(konan_id) FROM konan_test_table

--인덱스 삭제
DROP INDEX konan_test_table.idx

CREATE CLUSTERED INDEX idx ON konan_test_table (konan_id)


--인덱스 정보 조회
sp_helpindex konan_test_table


--평균 페이지 밀도 판단.
DBCC SHOWCONTIG(konan_test_table)

DBCC SHOWCONTIG이(가) 'konan_test_table' 테이블을 스캔하는 중...
테이블: 'konan_test_table' (1977058079); 인덱스 ID: 0, 데이터베이스 ID: 18
TABLE 수준 스캔이 수행되었습니다.
- 스캔한 페이지................................: 141
- 스캔한 익스텐트..............................: 18
- 전환된 익스텐트..............................: 17
- 익스텐트 당 평균 페이지 수........................: 7.8
- 스캔 밀도[최적:실제].......: 100.00% [18:18]
- 익스텐트 스캔 조각화 상태 ...................: 0.00%
- 페이지 당 사용 가능한 평균 바이트 수.....................: 81.8
- 평균 페이지 밀도(전체).....................: 98.99%
DBCC 실행이 완료되었습니다. DBCC에서 오류 메시지를 출력하면 시스템 관리자에게 문의하십시오.

--하나건너 하나씩 데이터 삭제
BEGIN TRAN
DELETE FROM konan_test_table WHERE konan_id % 2 = 0
COMMIT TRAN

--평균 페이지 밀도 판단.
DBCC SHOWCONTIG(konan_test_table)

DBCC SHOWCONTIG이(가) 'konan_test_table' 테이블을 스캔하는 중...
테이블: 'konan_test_table' (1977058079); 인덱스 ID: 0, 데이터베이스 ID: 18
TABLE 수준 스캔이 수행되었습니다.
- 스캔한 페이지................................: 141
- 스캔한 익스텐트..............................: 18
- 전환된 익스텐트..............................: 17
- 익스텐트 당 평균 페이지 수........................: 7.8
- 스캔 밀도[최적:실제].......: 100.00% [18:18]
- 익스텐트 스캔 조각화 상태 ...................: 0.00%
- 페이지 당 사용 가능한 평균 바이트 수.....................: 4018.8
- 평균 페이지 밀도(전체).....................: 50.35%
DBCC 실행이 완료되었습니다. DBCC에서 오류 메시지를 출력하면 시스템 관리자에게 문의하십시오.


--인덱스 재생성
CREATE CLUSTERED INDEX idx ON konan_test_table (konan_id)
WITH FILLFACTOR = 80, DROP_EXISTING

--평균 페이지 밀도 판단.
DBCC SHOWCONTIG(konan_test_table)

DBCC SHOWCONTIG이(가) 'konan_test_table' 테이블을 스캔하는 중...
테이블: 'konan_test_table' (1977058079); 인덱스 ID: 1, 데이터베이스 ID: 18
TABLE 수준 스캔이 수행되었습니다.
- 스캔한 페이지................................: 87
- 스캔한 익스텐트..............................: 11
- 전환된 익스텐트..............................: 10
- 익스텐트 당 평균 페이지 수........................: 7.9
- 스캔 밀도[최적:실제].......: 100.00% [11:11]
- 논리 스캔 조각화 상태 ..................: 11.49%
- 익스텐트 스캔 조각화 상태 ...................: 9.09%
- 페이지 당 사용 가능한 평균 바이트 수.....................: 1601.7
- 평균 페이지 밀도(전체).....................: 80.21%
DBCC 실행이 완료되었습니다. DBCC에서 오류 메시지를 출력하면 시스템 관리자에게 문의하십시오.

이와 비슷하게 생각하실 것이 PAD_INDEX 입니다.

PAD_INDEX와 FILL_FACTOR의 차이는 PAD_INDEX는 중간레벨까지 여유 공간을

둔다는 점입니다.

 

 

--인덱스 재생성
CREATE CLUSTERED INDEX idx ON konan_test_table (konan_id)
WITH FILLFACTOR = 80, PAD_INDEX, DROP_EXISTING

 

 

이와 비슷한 작업을 하는 인덱스를 재구성하는 DBCC명령이 두가지 있습니다.

먼저 DBCC DBREINDEX명령과

그다음 DBCC INDEXDEFRAG 입니다.

 

--DBCC DBREINDEX - 1개 인덱스인 idx인덱스를 fillfactor 90으로 재생성
DBCC DBREINDEX (konan_test_table, 'idx', 90)

--테이블내 모든 인덱스를 fillfactor 90으로 재생성
DBCC DBREINDEX (konan_test_table, '', 90)

 

DBCC DBREINDEX는 물리적으로 완전히 모든 행을 재배열 하며 인덱스까지 재구성

합니다. 비록 SQL2000부터는 이 작업이 빨라 지고 온라인 상에서 할 수 있다고

하지만.. 실제 대단한 부하가 걸리는 작업입니다.

이때 물리적으로 모든 행을 재배열 하지 않고 인덱스만 재구성하는 명령이 있으니

바로 DBCC INDEXDEFRAG 입니다. SQL2000만의 기능이기도 하지요.

 

--DBCC INDEXDEFRAG
DBCC INDEXDEFRAG (konanTestDB, konan_test_table, idx)

 

이런 식으로 사용 가능하며 비교적 좋은 성능을 보여 줍니다.

 

다음으로 통계데이터 입니다.

먼저 다음 질의를 봐 보도록 하지요.

 

--통계데이터 보기
DBCC SHOW_STATISTICS(konan_test_table,idx)

 

앞에서 잠깐 말 드린대로 선택도에 따라서 쿼리 최적화기가 인덱스를 타게할지

타게하지 않을지를 결정한다고 말씀 드렸지요.

이때 이 근거가 되는것이 바로 통계 데이터 입니다.

SQL서버2000은 자동으로 통계데이터가 해당하는 인덱스에 대해서 생성되며

자동으로 유지 됩니다.

 

--통계데이터 UPDATE
UPDATE STATISTICS konan_test_table(idx)

 

이런 식으로 update가 가능합니다.

튜닝적인 요소로 볼때 이는 수동으로 하지 않는것이 거의 대부분 좋으며 수동으로 할

경우는 대단히 많은 모니터링이 필요 합니다.

그럼 이정도로 색인 생성에 대한 부분은 마치도록 하지요.

다음으로 색인의 마지막 이야기인 성능과 관련된 부분을 조금더 보도록 하지요.

수고하셨습니다.

 

ex

DBCC SHOWCONTIG(tbl_report)


DBCC SHOW_STATISTICS(tbl_report,idx_report_01)


UPDATE STATISTICS tbl_report(idx_report_01)

'데이터베이스 > SQL Server' 카테고리의 다른 글

SQL Server DBA 가이드  (0) 2008.04.29
T-SQL로 테이블, 컬럼명 조회하기  (0) 2008.04.29
색인의 종류  (0) 2008.04.29
실행계획을 보는 순서  (0) 2008.04.29
VS.NET으로 개발하는 SQL 서버 2005  (0) 2008.04.28
728x90

자 이제 SQL서버의 색인을 풀어볼 차례 로군요.

약간 어려워도 조금만 참고 함께 이겨나가 보도록 하지요.

그만큼 중요합니다. ^_^

 

색인의 종류 SQL서버는 두가지 종류의 색인이 있습니다. 두가지 뿐이라니 뭔가 기분

좋지요? 그 두가지는 Clustered 인덱스와 Non-Clustered 인덱스 두가지 입니다.

각각 한글로는 클러스터된 인덱스 / 클러스터되지 않은 인덱스로 불립니다.

-_-;;  늘 그런것처럼 혼용해서 코난이는 쓸겁니다. 아시져? ^_^;;

 

인덱스는 분명 만들어야 하는 것입니다.

이 말은

1. 만드는 비용이 있다라는 것이지요.

다음 인덱스는 공간이 필요하다고 했습니다.

2. 디스크 비용이 필요하며 항상 공간 유지를 적절해야 한다는 겁니다.

만들면 끝인가요? 아니죠. 만들었으면?

3. 유지보수에 비용이 필요하겠지요.

인덱스를 만들면 데이터를 조회하는 속도를 높일 순 있지만..

만약 데이터가 삽입 / 수정 / 삭제 된다고 생각해 보세요.

앞에서 본 인덱스의 구조를 변경하면서 재생성 해야 겠지요?

4. 데이터 변경이 있을때 비용이 추가 된다는 겁니다.

앞에서 또한 말씀 드렸지만 인덱스를 사용하면 항상 빨라진다고 말씀 안드렸죠.

5. 인덱스는 항상 빠르지 않다.

인덱스를 사용하면 유용한 부분은?

5-1. WHERE절에서 참조되는 컬럼

5-2. 참조키가 설정되어 있는 컬럼

5-3. 참조키는 아니지만 JOIN에 사용되는 컬럼

5-4. 범위 검색이 일어나는 컬럼

5-5. ORDER BY로 정렬 되는 컬럼, GROUP BY로 그룹핑 되는 컬럼

5-6. TOP 구문과 함께 사용되는 SELECT절

등에서 사용하면 좋습니다.

그렇다면? 어디에 사용하면 바보 될까요?

5-7. WHERE절에서 사용되지 않는 컬럼에는 물론 효과 없음.

5-9. WHERE절에서 변환(함수등이 사용되는)되는 컬럼과 비교시 효과 없음.

5-10. 선택도(찾을 데이터 / 전체 데이터)가 클 경우 효과 적음.

잠시후 말씀 드리겠지만.. 예를들어 성별 컬럼과 같은 남 / 여 비율적으로 대략 50 : 50 의

구성비가 있는 컬럼이라면? 인덱스의 효과가 떨어 지겠죠.

이런 주의 사항이 필요합니다.

 

자 이제 인덱스 생성 구문을 실제로 봐 보도록 하지요.

 

CREATE [ UNIQUE ] [ CLUSTERED | NONCLUSTERED ] INDEX index_name
     ON { table | view } ( column [ ASC | DESC ] [ ,...n ] )[ WITH < index_option > [ ,...n] ]
[ ON filegroup ]

< index_option > ::=
     { PAD_INDEX |
        FILLFACTOR = fillfactor |
        IGNORE_DUP_KEY |
        DROP_EXISTING |
     STATISTICS_NORECOMPUTE |
    SORT_IN_TEMPDB
}

 

기분 좋게도 그다지 길지 않지요? 주요한 몇가지를 말씀 드리면..

UNIQUE를 지정해 고유 인덱스 또는 고유하지 않은 인덱스 생성이 가능합니다.

색인의 종류는 두가지로 CLUSTERED | NONCLUSTERED 로 할지 지정이 가능 합니다.

table이나 view에 생성이 가능합니다. SQL2000의 새로운 기능으로 View에 인덱스 생성이

가능 하지요. 뷰 강좌를 참고 해 보세요.

컬럼을 ASC 또는 DESC로 정렬해 생성 가능합니다. 특히 클러스터드 인덱스를 생성시

유용하며 ORDER BY 구문과도 밀접합니다. SQL2000의 새로운 기능입니다.

다음 인덱스 옵션에서

PAD_INDEX는 중간 레벨을 적절히 비워 데이터 삽입 등에 대비하기 위한 것이며

FILLFACTOR는 리프 레벨을 적절히 비워 역시 삽입 등에 대비하는 것입니다.

샘플에서 이야기를 해 드리도록 하지요.

IGNORE_DUP_KEY 는 중복되는 값을 무시한다는 의미 이구요.

DROP_EXISTING 은 이미 존재하는 인덱스가 있으면 제거하고 재생성하라는 의미 입니다.

STATISTICS_NORECOMPUTE 는 인덱스를 사용할지 안할지 쿼리 최적화기가 결정하는데

이 근거는 통계치 데이터라는 녀석으로 판단하게 됩니다. 이 통계 데이터는 기본적으로

자동적으로 항상 update 되는데 이 통계데이터를 자동 업데이트 하지 말라는 옵션 입니다.

SORT_IN_TEMPDB TEMPDB상에서 정렬하라는 옵션 입니다. 예를들어 데이터와 인덱스가

같은 물리적인 디스크에 있고 데이터가 한 1000만건 정도 된다면 인덱스 생성에 대단히

많은 시간이 소요 됩니다. 이때 TEMPDB에서 인덱스 생성시 필요한 정렬작업을 시키고

사용자 데이터베이스의 물리적인 디스크와 TEMPDB 쿨리적인 디스크가 틀리다면 인덱스

생성시 부하를 줄일 순 있지만 TEMPDB에 다른 불필요 공간이 생기니 주의 하셔야 하지요.

끝으로 ON filegroup은 인덱스 역시 데이터라고 말씀 드렸습니다. 데이터베이스 강좌에서

filegroup 을 적절히 분산시켜 생성해 속도를 높일 수 있다고 말씀 드린 것처럼 인덱스 역시

적절한 filegroup에 위치시켜 최적의 속도를 낼 수 있게 할 수 있지요.

 

백견이 불여일타라고 우선 한번 맹거 보도록 하지요.

 

CREATE DATABASE konanTestDB
GO

USE konanTestDB
GO

--테이블 생성
CREATE TABLE konan_test_table(
konan_id int IDENTITY (1, 1) NOT NULL
, konan_data char (50) NOT NULL
, konan_date char (50) NOT NULL
)
GO

--10000건의 샘플 데이터 삽입
set nocount on
GO

declare @i int
set @i = 0
while @i < 10000
begin
--WAITFOR DELAY '00:00:01'
insert into konan_test_table values
(
@i ,
convert(varchar, datepart(yy, getdate())) + '년 '
+ convert(varchar, datepart(mm, getdate())) + '월 '
+ convert(varchar, datepart(dd, getdate())) + '일 '
+ convert(varchar, datepart(hh, getdate())) + '시 '
+ convert(varchar, datepart(mi, getdate())) + '분 '
+ convert(varchar, datepart(ss, getdate())) + '초 '
+ convert(varchar, datepart(ms, getdate())) + '미리초 '
)
set @i = @i + 1
end
GO
--10초.

set nocount off
GO

--샘플 데이터 조회
SELECT TOP 100 * FROM konan_test_table
SELECT COUNT(konan_id) FROM konan_test_table

 

대략적인 샘플데이터 100건과 만건이 잘 들어간게 보일 겁니다.

여기서 어느정도의 IO 비용이 소요되는지 보도록 할까요?

물론 시간 / IO비용 모두가 중요하지만 우선 IO비용만 보도록 하지요.

 

--IO통계 보기
SET STATISTICS IO ON

SELECT * FROM konan_test_table WHERE konan_id = 5000

--IO통계?

SELECT * FROM konan_test_table WHERE konan_id < 10

--IO통계?

SELECT * FROM konan_test_table WHERE konan_id < 30

--IO통계?

SELECT * FROM konan_test_table WHERE konan_id < 1000

--IO통계?

--IO통계 끄기
SET STATISTICS IO OFF

 

메시지 부분을 보시면 IO통계를 확인하실 수 있을 겁니다.

이제 인덱스를 생성해 보도록 하지요.

 

 

--간단한 인덱스 생성
CREATE INDEX idx ON konan_test_table (konan_id)
GO

 

인덱스 생성은 잘 되셨을 거구요. 다시 데이터를 조회해 보도록 할까요?

 

--IO통계 보기
SET STATISTICS IO ON

SELECT * FROM konan_test_table WHERE konan_id = 5000

--IO통계?

SELECT * FROM konan_test_table WHERE konan_id < 10

--IO통계?

SELECT * FROM konan_test_table WHERE konan_id < 30

--IO통계?

SELECT * FROM konan_test_table WHERE konan_id < 1000

--IO통계?

--IO통계 끄기
SET STATISTICS IO OFF

 

통계를 봐 보세요. 어떠세요?

아마도 WHERE konan_id < 30 부터 인덱스가 없을때와 비슷한 수치가

나올 겁니다.

이것은 뭘 말하는 걸까요? 다음처럼 실행계획 표시를 하시거나 또는 컨트롤 + K를 눌러

실행계획을 봐 보도록 하지요.

바로 위의 같은 쿼리를 수행해 보면

 

위의 그림처럼 실행 계획을 보실 수 있습니다.

중요한건 Index Seek를 했다는 점이지요.

 

다음 쿼리에서 수행 계획을 보면?

이렇게 konan_id < 30을 봐 보시면 Table Scan을 하는 것을 알 수 있습니다.

분명 어떤것은 인덱스를 타고 어떤것은 인덱스를 타지 않지요?

앞에서 말씀드린 선택도 라고 해서 (찾을 데이터수 / 전체 데이터수)로 판단 되게 됩니다.

천천히 말씀 드리지요.

 

다음 인덱스의 정보를 보려면 어떻게 할까요?

 

--인덱스 정보 조회
sp_helpindex konan_test_table

 

그러면 인덱스의 정보를 확인할 수 있을 겁니다.

흥미있게 보실 부분으로 인덱스의 종류를 지정하지 않으면? 뒤에서 보시겠지만

넌클러스터드 인덱스로 잡히게 됩니다. 참고하세요.

 

인덱스의 변경 구문은 없으며 DROP INDEX 구문을 이용해 인덱스를 제거할 수 있습니다.

 

 

--인덱스 제거
DROP INDEX konan_test_table.idx

 

물론 앞에서 말씀드린 CREATE INDEX구문의 DROP EXISTING 구문으로 존재하는 인덱스를

지우고 생성할 수 도 있습니다.

 

우선 인덱스를 생성하고 돌려는 보셨네요.

이제 본격적인 SQL서버의 인덱스에 대해서 알아 보도록 할까요.

클러스터드 인덱스

클러스터드 인덱스는 간단히 인덱스의 리프레벨이 데이터 페이지와 같은 겁니다.

뭔소리냐구요? 천천히 설명 드리지요.

이런 화면을 생각해 보세요.

클러스터드 인덱스는 물리적으로 행을 재배열해 리프 레벨이 차곡차곡 오른쪽의 데이터

페이지처럼 쌓이게 합니다. 그럼 인덱스를 봐 볼까요?

인덱스가 설정된 컬럼의 값이 순차적으로 재배열 되게 되므로 왼쪽에 보시는

인덱스 페이지는 단지 키값과 페이지의 번호만이 지정되게 됩니다.

자 그럼 우리가 만약 5번 강해원을 찾으려 한다고 생각해 보도록 하지요.

이때는 먼저 클러스터드 인덱스의 값을 보니 4 < 5 < 7 사이가 되므로

4 - 2 인 2번 페이지로 가게 됩니다. 2번 페이지로 가서 바로 5번 강해원을 찾게 되지요.

대략 이러한 그림으로 이루어 지게 되는 겁니다.

만약 12번 김태영을 찾는다면? 이때는 역시 10 < 12 < 13 이므로 4페이지로 가서

12번 김태영을 찾을 수 있게 되겠지요.

 

이것이 가능한 이유가 뭘까요? 바로 물리적으로 행이 정렬되어 있기 때문에 가능한 거지요

만약 물리적으로 행이 재배열 되어 있지 않다면? 이런 작업은 불가해 지는 겁니다.

아울러 물리적으로 행들이 재배열 되어 있으므로 범위 검색에 대단히 유용합니다.

이 말은 선택도가 어느정도 높아도 - 클러스터드 인덱스의 경우 30%정도도 가능 - 인덱스

를 이용해 데이터를 조회할 수 있게 되지요.

아울러 클러스터드 인덱스가 저렇게 테이블에 하나 생성되어 있는데요.

만약 클러스터드 인덱스를 하나 더 테이블에 만들고 싶다면 어떨까요?

안타깝게도 다음번 클러스터드 인덱스는 테이블을 다시 재구성 하고 싶겠지만..

이미 한번 테이블이 정렬된 상태로 재구성 되어 있으므로 불가해 집니다.

오로지 테이블에 단 1개의 클러스터드 인덱스만 생성이 가능하니 주의 하셔야 합니다.

그럼 앞의 샘플쿼리를 클러스터드 인덱스로 생성하고 장난을 조금 쳐 볼까요?

 

--인덱스가 존재하면 지울것
DROP INDEX konan_test_table.idx

--클러스터드 인덱스 생성
CREATE CLUSTERED INDEX idx ON konan_test_table (konan_id)
GO

 

이렇게 클러스터드 인덱스를 생성할 수 있지요. 그런데 특이하게도

SQL서버는 클러스터드 인덱스에 대해서 대단히 높은 우선권을 부여 합니다.

예를들어 이럴땐.. 풀스캔을 해도 좋을것 같은데... 클러스터드 인덱스를 써서

검사할 경우가 있지요. 이는 말씀 드린대로 SQL 쿼리 최적화기가 판단하는데

테이블의 크기가 작거나(로우의 건수가 아닌 전체적인 크기) 또는 클러스터드 인덱스의

키값 컬럼이 작을 경우 종종 발생 합니다.

실제 현업에서는 범위검색에 종종 이 클러스터드 인덱스를 두게 되므로 정상적으로

잘 동작하게 되지요.

또한 클러스터드 인덱스는 비교적 넌클러스터드 인덱스보다 크기가 작습니다.

아울러 클러스터드 인덱스는 크기를 대략적으로 예측할 수 있는 인덱스이기도 하지요.

 

다음 넌 클러스터드 인덱스를 봐 보도록 할까요?

넌 클러스터드 인덱스

실제 테이블의 데이터가 항상 순차적으로 들어가 있는 것은 아닙니다.

관계형 데이터베이스에서 순차라는 것은 사실 의미가 없습니다. 관계형 데이터베이스의

순차 유지는 오로지 ORDER BY에 적절히 이용되는 컬럼을 잘 구성해야만 하는 것이지요.

예를들어 실제 진짜 SQL서버에 들어가 있는 테이블의 순서는 다음과 같을 겁니다.

이런 식의 데이터가 실제로 들어가 있게 되지요. 그렇다면 페이지 내부는 어떤 식일까요?

대략적으로 위의 그림과 같은 식의 데이터가 들어가 있게 됩니다.

실질적인 데이터 페이지 이지요. 이런 데이터페이지들의 번호에 클러스터드 인덱스를

만약 생성한다면 어떻게 될까요? 이럴 경우는 RID라는 녀석이 필요하게 됩니다.

간단히 RID는 로우의 구별하는 특정 값이라고 생각 하시면 됩니다.

넌 클러스터드 인덱스는 바로 이 RID를 가지고 데이터 페이지를 포인팅 하게 되지요.

RID 샘플을 보시면 다음과 같은 식입니다.

여기서 RID의 첫번째 1은 화일 그룸을 의미하게 됩니다.

그다음 숫자는 데이터 페이지 번호이며 마지막 세번째 숫자는 페이지 옵셋으로 정확히

페이지의 한 로우를 포인팅(Pointing)하게 되지요.

또한 이 인덱스 페이지는 다음과 같은 형식으로 분할 되겠지요.

 

이러한 형식이 넌클러스터드 인덱스에서 과연 어떻게 사용 될까요?

실제 구조를 그려 보도록 하지요.

이러한 식으로 생성이 되게 됩니다.

루트 레벨은 인덱스 페이지7이며 중간 레벨은 엔덱스 페이지 1,2,3,4 이고

실제 데이터페이지는 1,2,3,4,5 데이터 페이지가 되지요.

자 값을 네비게이션 해 보도록 합시다.

만약 제가 3번 이승용을 찾으려 한다고 생각해 보지요.

1 < 3 < 5 이므로 1페이지로 가야 겠지요? 같더니 3은 4페이지 2번째 로우에 있다고 합니다.

바로 4페이지로 가서 두번째 로우를 컨택하는 거지요.

또한 만약 8 차영인을 찾으려 한다면

5 < 8 < 9 이므로 인덱스페이지 2로 가서 8 차영인을 보니 1-3-2라고 되어 있습니다.

데이터 페이지 3의 2번째 로우를 포인팅 하게 되지요.

이것이 넌클러스터드 인덱스 입니다.

넌클러스터드 인덱스는 이렇게 포인팅 정보를 가지게 되므로 인덱스의 크기가 커지게

됩니다. 아울러 선택도가 높으면 바로 쿼리 최적화기는 이 넌클러스터드 인덱스를

사용하지 않게 되지요. 일반적으로 3% 이내 정도면 이 넌클러스터드 인덱스를 사용하지

않게 됩니다.

넌클러스터드 인덱스는 데이터페이지를 물리적으로 재배열하지 않으므로 여러개의

인덱스를 생성할 수 있습니다. 최대생성 가능 갯수는 249개 입니다.

두 차이를 비교한다면 다음과 같겠지요.

다음으로 클러스터드 인덱스가 있는 테이블의 넌클러스터드 인덱스를 알아 보도록 하지요.

 

물론 당연히 클러스터드 인덱스와 넌 클러스터드 인덱스가 있을때 넌클러스터드 인덱스를

조회할 경우 겠지요?

 

현재는 번호에 클러스터드 인덱스가 걸린 상태이며 이름에 넌클러스터드 인덱스가 설정된

상태 입니다.

이런 식으로 구성이 되게 됩니다. 먼저 알아 두셔야 할 것은!!

넌클러스터드 인덱스의 RID는 더이상 RID가 아닌 클러스터드 인덱스의 키값을 가지게 됩니다.

좀더 간단히 실제 조회를 해 보도록 하지요.

넌클러스터드 인덱스가 걸린 이름 컬럼의 유병수라는 이름을 조회해 보도록 합시다.

박훈 < 유병수 < 이수선 이므로 넌클러스터드 중간 레벨의 2페이지로 가게 될겁니다.

여기서 유병수를 찾으니 유병수는 클러스터드 인덱스 10번 키값을 가지고 있습니다.

10이라는 값을 가지고 클러스터드 인덱스 페이지로 가니 10은 데이터페이지 4에

있다고 합니다. 데이터페이지 4로 가니? 바로 10 유병수를 찾을 수 있게 되지요.

네 맞습니다. 바로 이런 그림이 되는 것이지요.

만약 한기환을 찾으려 한다면 어떻게 될까요?

한기환 < 한기환 이므로 넌클러스터드 중간레벨의 4페이지로 가게 되겠죠?

클러스터드 인덱스의 키값 2이니. 1 < 2 < 4 이므로 1 데이터 페이지로 가게 되며 1 데이터 페이지에서 2번 한기환을 발견하게 될 겁니다.

[출처] 색인의 종류|작성자 필립박

728x90
1. 기본 설정

먼저 실행계획과 페이지 입출력을 보기 위한 두 가지 설정을 ON하도록 하비다.

    SET STATISTICS IO ON

    SET STATISTICS PROFILE ON



그다음 기존글(인덱스와 실행계획)에서 생성했던, TBL_IDX테이블을 사용해서

TBL2_IDX란 테이블을 만들어 냅니다.

    SELECT  T1.noInt, (T2.a * 10000) + T1.noInt col1 INTO TBL2_IDX

    FROM    TBL_IDX T1 CROSS JOIN

        (SELECT 0 a

        UNION ALL

        SELECT 1

        UNION ALL

        SELECT 2) T2

    --30000개의 데이터

설명드렸듯이 SELECT ~ INTO는 바로 테이블을 만들 수 있는 유용한 방법입니다.

위 SQL은 1, 2, 3값을 가지는 T2라는 파생된 테이블(인라인뷰)과 CROSS JOIN을 함으로서

TBL_IDX의 3배에 크기를 가지는 테이블을 만들게 됩니다.

CROSS JOIN은 아무 조건 없는 조인으로서 카테션곱의 결과를 만들게 됩니다.

다음과 같이 TBL2_IDX에 인덱스를 생성합니다.



    CREATE INDEX tbl2_idx_idx1 ON TBL2_IDX(noInt)

    CREATE INDEX tbl2_idx_idx2 ON TBL2_IDX(col1)



2.쿼리 수행

다음쿼리를 수행하고 실행계획을 보도록 합니다.

    SELECT  *

    FROM    TBL_IDX T1 JOIN TBL2_IDX T2

        ON T1.noInt = T2.noInt

    WHERE   T2.col1 = 3



NO Rows Execute StmtText

1   1   1   SELECT *  FROM TBL_IDX T1 JOIN TBL2_IDX T2   ON T1.noInt = T2.noInt  WHERE T2.col1 = 3

2   1   1     |--Bookmark Lookup(BOOKMARK:([Bmk1000]), OBJECT:([PLANDB].[dbo].[TBL_IDX] AS [T1]))

3   1   1          |--Nested Loops(Inner Join, OUTER REFERENCES:([T2].[noInt]))

4   1   1               |--Bookmark Lookup(BOOKMARK:([Bmk1001]), OBJECT:([PLANDB].[dbo].[TBL2_IDX] AS
[T2]))

5   1   1               |    |--Index Seek(OBJECT:([PLANDB].[dbo].[TBL2_IDX].[tbl2_idx_idx2] AS [T2]), SEEK:
([T2].[col1]=3) ORDERED FORWARD)

6   1   1               |--Index Seek(OBJECT:([PLANDB].[dbo].[TBL_IDX].[tbl_idx_idx1] AS [T1]), SEEK:([T1].
[noInt]=[T2].[noInt]) ORDERED FORWARD)



*여기서 NO는 설명을 쉽게 하기 위해 제가 적어 놓은 것입니다.



실행계획은 보통 제일 안쪽중에서 위에것을 먼저 읽습니다.

하지만, 이것이 실행계획이 복잡해지면 쉽지 않습니다. 쉬운것부터 보면서 연습을 하는 것이 필요하죠.

저같은 경우는 위에서부터 보면서, 연산에 따라 분기하면 더 쉽게 볼 수 있다고 생각합니다.

물론, 이런 과정도 안쪽중에서 위에것이 먼저라는 순서는 변하지 않습니다.

먼저, 우리가 SELECT문장을 날린것이 제일 위에 있습니다.

SELECT문이 있는데, 해당 SELECT문의 결과를 얻기 위해 마지막으로 한 일은 Bookmark Lookup입니다.(2번과정)

그전에 한 일은 Nested Loops이고요, 여기서 이 연산이 중요합니다.

이 연산은 조인을 수행하는 연산인데, 조인을 수행하는 연산은 두 개의 자식 연산을 가지게 됩니다.

이 때 두개의 자식 연산중에 위쪽에 연산을 먼저 수행하게 됩니다.

여기서는 4번과 6번이 자식 연산인데, 4번이 먼저 수행됩니다. 4번은 하나의 자식 연산을 가지고 있으므로

이 자식 연산이 4번 연산보다 먼저 수행됩니다. 그러므로 5번이 제일 먼저 수행됩니다.

이런 과정을 통해, 역으로 생각해보면 실행 순서는

5번->4번->6번->3번(조인)->2번이 되어서 1번 문장을 만들어 내는 것입니다.

중요한 것은 자신이 자식 연산을 가지고 있으면 자식 연산이 먼저 수행되고, 자식 연산이 두 개 이상일 경우

제일 위에 연산부터 수행된다는 것입니다.

이 말이 곧, 제일 안쪽중에 제일 위에 실행계획부터 읽어야 된다는 말이 되죠.

조금만 습관이 되면, 쉽게 쉽게 볼 수 있습니다.

읽는 순서를 정했으니 순서대로 따라 가면서 분석을 해보도록 하죠.

먼저, 5번 연산은 Index Seek를 하고 있습니다.

5   1   1               |    |--Index Seek(OBJECT:([PLANDB].[dbo].[TBL2_IDX].[tbl2_idx_idx2] AS [T2]), SEEK:
([T2].[col1]=3) ORDERED FORWARD)

5번 연산의 StmtText를 보면 Index Seek라는 것은 쉽게 알 수 있습니다.

Index Seek라는 것은 인덱스를 인덱스 트리(BTree)를 통해 원하는 값을 찾는 것을 뜻합니다.

그렇다면 어떤 값을 찾고 있는지를 알아야 겠죠. StmtText의 뒷부분을 보시거나, 플랜의 Argument컬럼을

보시면 다음과 같은 부분이 있습니다.

OBJECT:([PLANDB].[dbo].[TBL2_IDX].[tbl2_idx_idx2] AS [T2]), SEEK:([T2].[col1]=3) ORDERED FORWARD

처음부터 해석을 해보면 OBJECT라는 것은 어떤 인덱스를 대상으로 하고 있는 것을 나타냅니다.

여기서는 tbl2_idx_idx2인덱스를 사용했습니다. 이 인덱스는 위에서 col1컬럼으로 만들어졌죠.

그다음을 보면 SEEK(T2.col1 = 3)이라고 있습니다. 이 것이 바로 인덱스로 검색(SEEK)하고 있는 값이죠.

이렇게 StmtText를 자세히 보면, 해당 연산에 대해서 더 자세히 알 수 있습니다.

그리고 인덱스를 통해 col1=3이란 값을 찾은 결과 행수는 1이란 것을 Rows컬럼을 통해 알 수 있습니다.

여기서 Plan중에 DefinedValues라는 컬럼의 값도 체크할 필요가 있습니다. 5번 연산의 DefinedValues의 값은

[Bmk1001]입니다. 이 값은 이 연산을 통해 정의된 값이 되겠죠. 이 값은 5번 연산 다음에 행해지는 4번 연산에서

사용되어 집니다.



5번의 다음 연산인 4번은 BookMark Lookup이라는 연산을 수행하고 있습니다.



4   1   1               |--Bookmark Lookup(BOOKMARK:([Bmk1001]), OBJECT:([PLANDB].[dbo].[TBL2_IDX] AS
[T2]))



Bookmark Lookup 연산은 인덱스 키값에 해당하는 실제 데이터를 테이블에 가서 찾아오는 과정을 말합니다.

5번과정에서 T2.col1=3인 데이터를 인덱스에서 찾아낸 후에, 해당 레코드의 나머지 데이터 값(noInt)을 찾아오기 위해

행하는 연산입니다.

StmtText를 보면 BOOKMARK([Bmk1001])이란 것이 있는 것을 알 수 있습니다. 이 Bmk1001은 5번 과정에서 정의된
값이었죠.

그러므로 5번 연산에 대한 Bookmark Lookup을 한다는 것이 증명됩니다. 사실은 자식 연산과 부모 연산이라는 관계만
으로도

어떤 Index에 대한 Bookmark Lookup이라는 것은 명확하게 알 수 있습니다.

다음 과정은 6번 과정입니다. 6번 과정역시 Index Seek를 하고 있습니다.

6번과정의 StmtText를 보면 다음과 같습니다.

OBJECT:([PLANDB].[dbo].[TBL_IDX].[tbl_idx_idx1] AS [T1]), SEEK:([T1].[noInt]=[T2].[noInt]) ORDERED
FORWARD)

인덱스는 TBL_IDX의 tbl_idx_idx1을 사용했습니다. 이 인덱스는 TBL_IDX의 noInt컬럼에 대해 정의된 인덱스입니다.

찾는 과정은 SEEK(T1.noInt = T2.noInt)입니다.

이 말은 곧, TBL_IDX2의 noInt값을 참조한다는 것입니다. 이 T2.noInt값은 5번과 4번을 과정을 거쳐서 가져온

값이 되겠죠.

그다음 연산인 3번을 보면 Nested Loops라는 연산을 볼 수 있는데 사실상, 6번 과정에서 T2.noInt와 동일한 noInt값을

가진 연산을 수행한 것이 이 연산을 수행한 것입니다. 사실상 이 연산은 두 개의 테이블이 이 방식으로

조인했다는 것을 알려주는 정도가 되겠죠.

이 Join연산에 대해서는 PLAN중에 PhysicalOp와 LogicalOp를 볼 필요가 있습니다. 여기서

Physical Op는 Nested Loops이고, LogicalOp는 Inner Join입니다.

논리적인 동작인 Inner Join은 해당 연산이 결과를 만족하는 값끼리 연결하는 Inner Join이라는 것을 뜻하고,

물리적인 동작인 Nested Loops는 해당 연산을 하기 위해 실제적으로 Nested Loops방식을 사용했다는 것입니다.

여기서 물리적인 동작 방법이 바낀다고 해도 결과는 변경되지 않습니다.

하지만, 논리적인 동작이 변경된다면 결과값은 바뀌게 됩니다.(Outer Join이 된다면 말이죠.)

마지막 과정인 2번을 보시면 Bookmark Lookup을 하고 있는 것을 알 수 있습니다. 이 과정은

TBL_IDX에 대한 실제 데이터를 찾아가는 과정이란 것을 알 수 있습니다.



단순하게 두 개의 테이블을 조인하는 실행계획을 설명하는데도 설명의 양이 너무 많은 거 같다는 생각이 듭니다.

하지만, 여러분들이 조금씩만 노력하다 보면 이런 설명이 필요없어지겠죠.^^

아직 설명이 다 끝난 것은 아닙니다. 실행계획을 보고 각 과정을 실제 쿼리와 연결시킬 수 있는 능력이 필요합니다.

이것또한 실행계획을 차례대로 보는 것만큼 쉽습니다.

단지 여러분들이, 실행계획과 실제쿼리를 자세히 관찰해서 증거를 모아서 판단하는 연습만이 필요할 뿐입니다.ㅁ

먼저 5번 과정에 Index Seek를 T2.col = 3에 대해 수행했습니다.

이것은 곧, 쿼리문의 T2.col = 3을 수행했다는 말이 되겠죠.

그리고, BookmarkLookup을 통해 T2.col=3인 레코드의 모든 데이터 정보를 가져 왔습니다.

우리는 여기서, 왜 이과정을 했을까를 생각해볼 필요가 있습니다. 조금만 생각하신다면

이 과정이 다음에 진행할 과정을 위한 것이라는 것을 알 수 있습니다. 4번의 다음과정은 6번입니다.

6번의 Seek연산은  다음과 같죠

SEEK:([T1].[noInt]=[T2].[noInt]

이 부분이 나온것은 쿼리에서 JOIN ON절에 해당합니다. ON T1.noInt = T2.noInt입니다.

우리는 T2의 인덱스를 통해 T2.col=3인 데이터를 가져 왔습니다. 해당 레코드를 T1과 JOIN을 해야 하는데

T1과 조인하기 위해서는 T2의 noInt컬럼이 필요합니다. 그런 이 컬럼은 실제 테이블에 저장되어 있으므로

해당 테이블을 찾아가는 4번 과정의 Bookmark Lookup이 진행되었던 거죠.

T1과 T2간에 noInt컬럼이 동일한 값을 찾는 Inner Join과정이 진행되었습니다.

그리고 나서 마지막으로 2번과정의 Bookmark Lookup이 수행되었습니다.

이 과정은 SELECT * 를 위해 수행된 것이죠. SELECT리스트에는 두 테이블의 모든 정보를 보여주어야 하므로

실제 테이블을 가서 데이터를 가져오는 작업을 수행한 것이죠.

만약에 2번 과정의 Bookmark Lookup이 제거된 PLAN을 보고 싶다면 다음과 같이 쿼리를 수행할 수 있습니다.



    SELECT  T1.noInt, T2.*

    FROM    TBL_IDX T1 JOIN TBL2_IDX T2

        ON T1.noInt = T2.noInt

    WHERE   T2.col1 = 3



실행계획을 보시면 2번과정에 있던 Bookmark Lookup이 제거된 것을 알 수 있습니다.

이것은 TBL_IDX에 있던 tbl_idx_idx1 인덱스만으로 쿼리가 커버됐기 때문입니다.

커버되었다는 것은 SQL문에 SELECT절과, WHERE절, ON절에 나온 컬럼이 모두 인덱스에 존재하는 컬럼이라는 것
입니다.



약간은 더 복잡한 쿼리를 통해, 실행계획을 보는 순서와, 더불어 실행계획을 보는 능력(?)을 계속

키워보도록 합시다.



이번 SQL문은 다음과 같습니다.





SQL문의 내용은 주문과 주문 상세중에 1998년 2월 26일 데이터에 대해서 고객별로 금액(Quantity * UnitPrice)을 구하


해당 고객들의 정보까지 Customers와 연결해서 보여주는 SQL입니다.

먼저 실행계획을 보기 전에 우리는 인덱스를 확인할 필요가 있습니다.

위 SQL에서 사용되는 테이블은 세개이고 각각의 인덱스를 살펴보면 다음과 같습니다.

테이블에 설정된 인덱스를 알아보기 위해서는 sp_helpindex 프로시져를 사용합니다.

이 프로시져를 사용하기 전에 SET STATISTICS PROFILE OFF를 실행하시기 바랍니다.

    USE Northwind

    SET STATISTICS PROFILE OFF

    sp_helpindex Orders

index_name  index_description   index_keys

--------------------------------------------------

CustomerID  nonclustered located on PRIMARY CustomerID

EmployeeID  nonclustered located on PRIMARY EmployeeID

OrderDate   nonclustered located on PRIMARY OrderDate

PK_Orders   clustered, unique, primary key located on PRIMARY   OrderID

ShippedDate nonclustered located on PRIMARY ShippedDate

ShippersOrders  nonclustered located on PRIMARY ShipVia

ShipPostalCode  nonclustered located on PRIMARY ShipPostalCode



    sp_helpindex [Order Details]

index_name  index_description   index_keys

--------------------------------------------------

OrderID         nonclustered located on PRIMARY OrderID

PK_Order_Details    clustered, unique, primary key located on PRIMARY   OrderID, ProductID

ProductID       nonclustered located on PRIMARY ProductID



    sp_helpindex [Customers]

index_name  index_description   index_keys

--------------------------------------------------

City    nonclustered located on PRIMARY City

CompanyName nonclustered located on PRIMARY CompanyName

PK_Customers    clustered, unique, primary key located on PRIMARY   CustomerID

PostalCode  nonclustered located on PRIMARY PostalCode

Region  nonclustered located on PRIMARY Region



여기서는 중복된 인덱스를 제거했습니다. 이 인덱스는 우리가 실행계획을 보기 전에

대충 실행계획이 어떻게 만들어질 것이다를 추측할 수 있게 해줍니다.





    SELECT  T1.*, T4.*

    FROM    Customers T1 JOIN

        (SELECT T2.CustomerID, SUM(T3.Quantity * T3.UnitPrice) amt

        FROM    Orders T2 JOIN [Order Details] T3

            ON T2.OrderID = T3.OrderID

        WHERE   T2.OrderDate = '1998-02-26 00:00:00.000'

        GROUP BY T2.CustomerID) T4

        ON T1.CustomerID = T4.CustomerID



위 SQL과 인덱스만을 보고 추측을 해보도록 하죠.

물론, 실제로는 비용기반 옵티마이져가 복잡한 과정을 통해 실행계획을 만들어 내므로 어느정도까지

추측이 가능할 뿐, 확실히 예측할 수는 없습니다.



제 생각에는 FROM절에 괄호안에 있는 파생된 테이블(또는 인라인 뷰라 함)이 먼저 실행되어질거 같습니다.

이 파생된 테이블 T4에는 Orders와 Order Details가 있습니다. WHERE조건에는 Orders의 OrderDate가 = 조건으로

있으므로 아마도 Orders의 OrderDate컬럼을 가지고 있는 OrderDate인덱스를 사용할 거 같습니다.

그 다음 Orders와 Order Details를 조인하기 위해 Orders에서 해당 OrderDate의 모든 데이터를 BookmarkLookup작
업을

통해 가져올 것입니다.(OrderID, CustomerID를 참조해야 하니까요)

그다음, Orders에서 가져온 OrderID를 Order Details에 공급해서 Join을 수행할 거 같습니다.

여기서는 여러가지 Join방법이 있으므로 어떤 방법을 사용할지는 잘 모르겠군요

JOIN을 수행하고, Order Details의 Quantity와 UnitPrice를 가져오기 위한 Bookmark Lookup이 필요할 거 같습니다.

그 다음, GROUP BY연산을 수행할거라 생각됩니다. GROUP BY를 위해, SORT가 수행될 수도 있습니다.

물론, SORT가 안 일어날 수도 있습니다. 이것에 관련된 기사를 어디선가 봤는데, 이 부분은 나중에 적어보도록 하겠
습니다.

그 다음, Customers와 JOIN을 수행하겠죠. 물론 조인의 방법은 예측을 쉽게 할 수는 없을 거 같습니다.

마지막으로 JOIN된 결과에 대해 Bookmark Lookup을 수행할 것입니다.



JOIN의 선택 방법에는 100%는 아니지만, 어느정도 근접한 룰이 있습니다.

이것에 대한 기사 역시 SQL Server Magazine에서 본적이 있었습니다. 하지만, 모든 정보를

제 머리에 넣고 있을수는 없으니까, 나중에 정리해보도록 하겠습니다.



여기까지는 제 예측잉었습니다. 저도 물론, 아직까지 위 쿼리를 수행해 보지 않았으므로 실행계획을 알 수 없습니다.

여러분들도 실행전에 실행계획을 나름대로 예측해 보셨으면 합니다. 좋은 경험이 되리라 생각되어 지니까요.

그럼 SQL문을 실행해 보도록 하겠습니다.



    SELECT  T1.*, T4.*

    FROM    Customers T1 JOIN

        (SELECT T2.CustomerID, SUM(T3.Quantity * T3.UnitPrice) amt

        FROM    Orders T2 JOIN [Order Details] T3

            ON T2.OrderID = T3.OrderID

        WHERE   T2.OrderDate = '1998-02-26 00:00:00.000'

        GROUP BY T2.CustomerID) T4

        ON T1.CustomerID = T4.CustomerID



1 6 1   SELECT T1.*, T4.*  FROM Customers T1 JOIN   (SELECT T2.CustomerID, SUM(T3.Quantity *
T3.UnitPrice) amt   FROM Orders T2 JOIN [Order Details] T3    ON T2.OrderID = T3.OrderID   WHERE
T2.OrderDate = '1998-02-26 00:00:00.000'   GROUP BY T2.CustomerID) T4   ON
2 6 1     |--Nested Loops(Inner Join, OUTER REFERENCES:([T2].[CustomerID]))
3 6 1          |--Stream Aggregate(GROUP BY:([T2].[CustomerID]) DEFINE:([Expr1002]=SUM(Convert([T3].
[Quantity])*[T3].[UnitPrice])))
4 16    1          |    |--Nested Loops(Inner Join, OUTER REFERENCES:([T2].[OrderID]))
5 6 1          |         |--Sort(ORDER BY:([T2].[CustomerID] ASC))
6 6 1          |         |    |--Bookmark Lookup(BOOKMARK:([Bmk1000]), OBJECT:([Northwind].[dbo].[Orders]
AS [T2]))
7 6 1          |         |         |--Index Seek(OBJECT:([Northwind].[dbo].[Orders].[OrderDate] AS [T2]), SEEK:
([T2].[OrderDate]='02 26 1998 12:00AM') ORDERED FORWARD)
8 16    6          |         |--Clustered Index Seek(OBJECT:([Northwind].[dbo].[Order Details].
[PK_Order_Details] AS [T3]), SEEK:([T3].[OrderID]=[T2].[OrderID]) ORDERED FORWARD)

9 6 6          |--Clustered Index Seek(OBJECT:([Northwind].[dbo].[Customers].[PK_Customers] AS [T1]),
SEEK:([T1].[CustomerID]=[T2].[CustomerID]) ORDERED FORWARD)



먼저 가장 먼저 일어난 작업은 7번에 있는 Index Seek입니다. OrderDate인덱스를 사용하고 있습니다.

여기서 결과가 6건이 나왔고, 이 6건에 대해 Bookmark Lookup이 수행되었습니다. 이것이 6번 과정이죠.

6번과정 다음에 Sort가 나왔군요, 저는 Join후에 GROUP BY가 나올거라 생각했는 데 SQL Server는 먼저 Sort를 했습
니다.

Sort는 CustomerID에 대해 이루어 졌습니다. 이는 CustomerID별로 GROUP BY를 해야 되기 때문에 이루어진거라 생
각되는 군요.

그 다음, 8번 과정이 이루어 집니다. Order Details 테이블에 대해 클러스터드 인덱스 SEEK를 하고 있습니다.

Order Details 테이블에는 OrderID가 클러스터드 인덱스의 컬럼인거 같습니다.

클러스터드 인덱스를 SEEK했으므로 JOIN후 UnitPrice와 Quantity를 가져오기 위한 Bookmark Lookup은 발생하지 않
습니다.

이 또한, 제가 틀린 부분이네요.^^ 클러스터드 인덱스는 리프노드에 실제 데이터를 담고 있기 때문에, Bookmark
Lookup이

필요 없습니다. 이 8번과정을 통해 4번 과정인 Nested Loops가 이루어지는 것이죠. 그 다음, 실제, GROUP BY를 수
행하기 위해

3번 과정인 Stream Aggregate가 일어나게 됩니다. 그 다음에 9번 과정인 Customers에 대한 클러스터드 인덱스 Seek
가 일어나게

9번 과정을 통해 2번과정인 JOIN이 실행되어 지구요.

이런 과정으로 진행이 되는 겁니다.

+ Recent posts