1. 인라인 뷰 활용

  • 대부분 조인은 1:M 관계인 테이블끼리의 조인이고 조인 결과는 M쪽 집합과 같은 단위가 되는데,
    이를 다시 1쪽 집합 단위로 그룹핑해야 한다면 M쪽 집합을 먼저 1쪽 단위로 그룹핑하고 나서 조인하는 것이 조인 횟수를 줄여주기 때문에 유리하다.
    그런 처리를 위해 인라인 뷰를 사용할 수 있다. 2009년도 상품별 판매수량과 판매금액을 집계하는 아래 쿼리를 예로 들어보자.
인라인 뷰 적용 전
{code}
SELECT min(t2.상품명) 상품명,
sum(t1.판매수량) 판매수량,
sum(t1.판매금액) 판매금액
FROM 일별상품판매 t1,
상품 t2
WHERE t1.판매일자 BETWEEN '20090101' AND '20091231'
AND t1.상품코드 = t2.상품코드
GROUP BY t2.상품코드

Call Count CPU Time Elapsed Time Disk Query Current Rows




---

-




Parse 1 0.000 0.000 0 0 0 0
Execute 1 0.000 0.000 0 0 0 0
Fetch 101 5.109 13.805 52744 782160 0 1000



---

-




Total 103 5.109 13.805 52744 782160 0 1000

Rows Row Source Operation


-











---
1000 SORT GROUP BY (cr=782160 pr=52744 pw=0 time=13804391 us)
365000 NESTED LOOPS (cr=782160 pr=52744 pw=0 time=2734163731004 us)
365000 TABLE ACCESS FULL 일별상품판매 (cr=52158 pr=51800 pw=0 time=456175026878 us)
365000 TABLE ACCESS BY INDEX ROWID 상품 (cr=730002 pr=944 pw=0 time=872397482545 us)
365000 INDEX UNIQUE SCAN 상품_PK (cr=365002 pr=4 pw=0 time=416615350685 us)

 |
Row Source Operation을 분석해 보면, 일별상품판매 테이블로부터 읽힌 365,000개 레코드마다 상품 테이블과 조인을 시도했다.
조인 과정에서 730,002개의 블록 I/O가 발생했고, 총 소요시간은 13.8초다.
아래 처럼 상품코드별로 먼저 집계하고서 조인하도록 바꾸고 다시 수행해 보자.
|| 인라인 뷰 적용 후 ||
| 

SELECT t2.상품명,
t1.판매수량,
t1.판매금액
FROM
(SELECT 상품코드,
sum(판매수량) 판매수량,
sum(판매금액) 판매금액
FROM 일별상품판매
WHERE 판매일자 BETWEEN '20090101' AND '20091231'
GROUP BY 상품코드
) t1,
상품 t2
WHERE t1.상품코드 = t2.상품코드

Call Count CPU Time Elapsed Time Disk Query Current Rows




---

-




Parse 1 0.000 0.000 0 0 0 0
Execute 1 0.000 0.000 0 0 0 0
Fetch 101 1.422 5.540 51339 54259 0 1000



---

-




Total 103 1.422 5.540 51339 54259 0 1000

Rows Row Source Operation


-











---
1000 NESTED LOOPS (cr=54259 pr=51339 pw=0 time=5540320 us)
1000 VIEW (cr=52158 pr=51339 pw=0 time=5531294 us)
1000 SORT GROUP BY (cr=52158 pr=51339 pw=0 time=5530293 us)
365000 TABLE ACCESS FULL 일별상품판매 (cr=52158 pr=51339 pw=0 time=2920041 us)
1000 TABLE ACCESS BY INDEX ROWID 상품 (cr=2101 pr=0 pw=0 time=8337 us)
1000 INDEX UNIQUE SCAN 상품_PK (cr=1101 pr=0 pw=0 time=3747 us)

 |
상품코드별로 먼저 집계한 결과건수가 1,000건이므로 상품 테이블과 조인도 1,000번만 발생했다.
조인 과정에서 발생한 블록 I/O는 2,101개에 불과하고 수행시간도 5.5초 밖에 걸리지 않았다.

h3. 2. 배타적 관계의 조인

엔터티가 두 개 이상의 다른 엔터티의 합집합과 관계(Relationship)를 갖는 것을 '상호배타적(Exclusive OR) 관계'라고 한다.
<그림 Ⅲ-4-31>에서 작업지시 테이블과 개통신청 및 장애접수 테이블과의 관계가 여기에 해당하며, 관계선에 표시된 아크(Arc)를 확인하기 바란다.

※ Exclusive OR
- 배타적 논리합
- Bool 연산자의 하나이며 2개의 피연산자 중 하나가 참이고 다른 하나가 거짓일 때에만 연산의 결과가 참이 된다.
'A XOR B'는 'A∀B' 등으로 표기하기도 한다.
- 출처 : IT용어사전, 두산동아, 한국정보통신기술협회

!4-31.jpg!

1. ERD 업무 설명
- 고객으로부터 개통이나 장애처리 요청을 받으면 작업기사에게 작업지시서를 발행
- 한 작업자에게만 작업지시를 내릴 때가 많지만, 작업 내용에 따라서는 여러 작업자가 필요할 수도 있다.
- 또한, 여러 작업자가 동시에 출동하는가 하면, 시간 간격을 두고 따로 출동하기도 한다.
- ERD에 다 표현하진 않았지만 개통신청과 장애접수는 관리하는 속성이 상당히 달라 별도의 테이블로 설계했다.
- 반면, 작업지시는 개통신청이든 장애접수든 거의 같은 속성을 관리하므로 한 테이블로 설계했다.
- 한 테이블로 통합하더라도 개통신청이나 장애접수 중 어느 것과 관계를 갖는지 구분할 수 있어야 한다.
- [그림 Ⅲ-4-31]과 같은 데이터 모델을 실제 데이터베이스로 구현할 때, 작업지시 테이블에는 아래 두 가지 방법 중 하나를 사용한다.

① 개통신청번호, 장애접수번호 두 칼럼을 따로 두고, 레코드별로 둘 중 하나의 칼럼에만 값을 입력한다.
② 작업구분과 접수번호 칼럼을 두고, 작업구분이 '1'일 때는 개통신청번호를 입력하고 '2'일 때는 장애접수번호를 입력한다.

①번처럼 설계할 때는 아래와 같이 Outer 조인으로 간단하게 쿼리를 작성할 수 있다.
|| Oracle || SQL Server ||
| 

SELECT
/*+ ordered use_nl(b) use_nl© */
a.작업일련번호,
a.작업자ID,
a.작업상태코드 ,
nvl(b.고객번호, c.고객번호) 고객번호 ,
nvl(b.주소, c.주소) 주소,
......
FROM 작업지시 a,
개통신청 b,
장애접수 c
WHERE a.방문예정일시 BETWEEN :방문예정일시1 AND :방문예정일시2
AND b.개통신청번호(+) = a.개통신청번호
AND c.장애접수번호(+) = a.장애접수번호

 | 

SELECT a.작업일련번호,
a.작업자ID,
a.작업상태코드 ,
isnull(b.고객번호, c.고객번호) 고객번호 ,
isnull(b.주소, c.주소) 주소,
......
FROM 작업지시 a
LEFT OUTER JOIN 개통신청 b ON b.개통신청번호 = a.개통신청번호
LEFT OUTER JOIN 장애접수 c ON c.장애접수번호 = a.장애접수번호
WHERE a.방문예정일시 BETWEEN :방문예정일시1 AND :방문예정일시2
OPTION(FORCE ORDER, loop JOIN)

 |
②번처럼 설계했을 경우는 UNION ALL을 이용한다.
|| Oracle ||
| 

SELECT x.작업일련번호,
x.작업자ID,
x.작업상태코드,
y.고객번호,
y.주소,
......
FROM 작업지시 x, 개통신청 y
WHERE x.방문예정일시 BETWEEN :방문예정일시1 AND :방문예정일시2
AND x.작업구분 = '1'
AND y.개통신청번호 = x.접수번호
UNION ALL
SELECT x.작업일련번호,
x.작업자ID,
x.작업상태코드,
y.고객번호,
y.주소,
......
FROM 작업지시 x, 장애접수 y
WHERE x.방문예정일시 BETWEEN :방문예정일시1 AND :방문예정일시2
AND x.작업구분 = '2'
AND y.장애접수번호 = x.접수번호

 |
- <작업구분+방문예정일시> 결합 인덱스 이용 시 중복 읽는 범위는 없음.
- <방문예정일시+작업구분> 결합 인덱스 이용 시 인덱스 스캔 범위 중복 발생.
- <방문예정일시> 인덱스 이용 시 작업 구문을 필터링하기 위한 테이블 RANDOM ACCESS까지 중복 발생.
이럴 경우 아래와 같이 수정함으로써 중복 액세스에 의한 비효율 해소

|| Oracle || SQL Server ||
| 

SELECT
/*+ ordered use_nl(b) use_nl© */
a.작업일련번호,
a.작업자ID,
a.작업상태코드 ,
nvl(b.고객번호, c.고객번호) 고객번호 ,
nvl(b.주소, c.주소) 주소,
......
FROM 작업지시 a,
개통신청 b,
장애접수 c
WHERE a.방문예정일시 BETWEEN :방문예정일시1 AND :방문예정일시2
AND b.개통신청번호(+) = decode(a.작업구분, '1', a.접수번호)
AND c.장애접수번호(+) = decode(a.작업구분, '2', a.접수번호)

 | 

SELECT a.작업일련번호, a.작업자ID, a.작업상태코드 ,
isnull(b.고객번호, c.고객번호) 고객번호 ,
isnull(b.주소, c.주소) 주소,
......
FROM 작업지시 a
LEFT OUTER JOIN 개통신청 b
ON b.개통신청번호 =
(
CASE WHEN a.작업구분 = '1' THEN a.접수번호 END
)
LEFT OUTER JOIN 장애접수 c
ON c.장애접수번호 =
(
CASE WHEN a.작업구분 = '2' THEN a.접수번호 END
)
WHERE a.방문예정일시 BETWEEN :방문예정일시1 AND :방문예정일시2
OPTION(FORCE ORDER, loop JOIN)

 |

h3. 3. 부등호 조인

'=' 연산자 조인 외에 BETWEEN, LIKE, 부등호 같은 연산자로 조인 가능.

아래 형태의 테이블에서 우측과 같은 형태의 누적매출을 구해보자.
각 지점별로 판매월과 함께 증가하는 누적매출(running total)을 구하려는 것이다.

!4-32.jpg!

윈도우 함수(Oracle에서는 분석 함수(Analytic Function)라고 함)를 이용하면 아래와 같이 간단히 원하는 결과를 얻을 수 있다.
| 

SELECT 지점,
판매월,
매출 ,
sum(매출) over (partition BY 지점 ORDER BY 판매월 range BETWEEN unbounded preceding AND CURRENT ROW) 누적매출
FROM 월별지점매출

 |
윈도우 함수가 지원되지 않는 DBMS를 사용하고 있다면 아래와 같이 부등호 조인을 이용해 같은 결과를 얻을 수 있다
| 

SELECT t1.지점, t1.판매월, min(t1.매출) 매출, sum(t2.매출) 누적매출
FROM 월별지점매출 t1, 월별지점매출 t2
WHERE t2.지점 = t1.지점
AND t2.판매월 <= t1.판매월
GROUP BY t1.지점, t1.판매월
ORDER BY t1.지점, t1.판매월;

 |

h3. 4. Between 조인


h5. 가. 선분이력이란?

  - 고객별연체금액 변경이력을 관리할 때 이력의 시작시점만을 관리하는 것을 '점이력' 모델이라함.
  - 시작시점과 종료시점을 함께 관리하는 것을 '선분이력' 모델이라고 함. 
  - 선분이력 모델에서 가장 마지막 이력의 종료일자는 항상 '99991231'(시간까지 관리할 때는 '99991231235959')로 입력해 두어야 함.

!표_4-3.jpg!

<그림 Ⅲ-4-33>은 <표 Ⅲ-4-3>에 있는 123번 고객에 대한 3개의 선분이력 레코드를 일직선 상에 펼쳐서 그려본 것이다. 

!4-33.jpg!

  - 이력을 선분형태로 관리하면 쿼리가 간단해지는 장점이 있음.
    EX)123번 고객의 2004년 8월 15일 시점 이력을 조회하고자 할 때 아래 처럼 between 조인을 이용해 간편하게 조회할 수 있다.
!표_4-3.jpg!

SELECT 고객번호,
연체금액,
연체개월수
FROM 고객별연체금액
WHERE 고객번호 = '123'
AND '20040815' BETWEEN b.시작일자 AND b.종료일자;



데이터를 점이력으로 관리할 때는 아래 처럼 서브쿼리를 이용해 복잡하게 쿼리를 해야한다.


SELECT 고객번호,
연체금액,
연체개월수
FROM 고객별연체금액 a
WHERE 고객번호 = '123'
AND 연체변경일자 =
(SELECT max(연체변경일자)
FROM 고객별연체금액
WHERE 고객번호 = a.고객번호
AND 변경일자 <= '20040815'
) ;



  - 위와 같은 설계 시 PK를 일반적으로 <마스터 키 + 종료일자 + 시작일자> 순으로 결합 인덱스 구성
  - 이 경우 이력을 변경 시마다 PK 값을 변경하여 RDBMS 설계 사상에 맞지 않음
  - 개체 무결성을 완벽히 보장하기 어렵다는 것이 가장 큰 단점이다.

h5. 나. 선분이력 기본 조회 패턴

기본적인 패턴으로 과거, 현재, 미래 임의 시점을 모두 조회할 수 있게 하려면 아래 처럼 쿼리를 작성하면 된다.

SELECT 연체개월수,
연체금액
FROM 고객별연체금액
WHERE 고객번호 = :cust_num
AND :dt BETWEEN 시작일자 AND 종료일자



현시점을 조회 시 '99991231' 상수 조건을 이용해 아래와 같이 '=' 조건으로 검색하는 것이 성능상 유리


SELECT 연체개월수,
연체금액
FROM 고객별연체금액
WHERE 고객번호 = :cust_num
AND 종료일자 = '99991231'




h5. 다. 선분이력 조인

1) 과거/현재/미래의 임의 시점 조회

!4-34.jpg!

고객과 두 선분이력 테이블을 조인해서 2004년8월 15일 시점 데이터를 조회할 때는 아래와 같이 쿼리하면 된다. 
물론 :dt 변수에는 '20040815'(시작일자, 종료일자가 문자열 칼럼일 때)를 입력한다. 


SELECT c.고객번호,
c.고객명,
c1.고객등급,
c2.전화번호
FROM 고객 c,
고객등급변경이력 c1,
전화번호변경이력 c2
WHERE c.고객번호 = :cust_num
AND c1.고객번호 = c.고객번호
AND c2.고객번호 = c.고객번호
AND :dt BETWEEN c1.시작일자 AND c1.종료일자
AND :dt BETWEEN c2.시작일자 AND c2.종료일자



2) 현재 시점 조회
위 쿼리를 이용해 과거, 현재, 미래 어느 시점이든 조회할 수 있지만, 만약 미래 시점 데이터를 미리 입력하는 예약 기능이 없다면 
"현재 시점(즉, 현재 유효한 시점)" 조회는 아래와 같이 '=' 조건으로 만들어 주는 것이 효과적이다.


SELECT c.고객번호,
c.고객명,
c1.고객등급,
c2.전화번호
FROM 고객 c,
고객등급변경이력 c1,
전화번호변경이력 c2
WHERE c.고객번호 = :cust_num
AND c1.고객번호 = c.고객번호
AND c2.고객번호 = c.고객번호
AND c1.종료일자 = '99991231'
AND c2.종료일자 = '99991231'



미래 시점 데이터를 미리 입력하는 예약 기능이 있다면, 현재 시점을 조회할 때 아래와 같이 조회해야 한다. 


SELECT c.고객번호,
c.고객명,
c1.고객등급,
c2.전화번호
FROM 고객 c,
고객등급변경이력 c1,
전화번호변경이력 c2
WHERE c.고객번호 = :cust_num
AND c1.고객번호 = c.고객번호
AND c2.고객번호 = c.고객번호
AND to_char(sysdate, 'yyyymmdd') BETWEEN c1.시작일자 AND c1.종료일자
AND to_char(sysdate, 'yyyymmdd') BETWEEN c2.시작일자 AND c2.종료일자



......
and convert(varchar(8), getdate(), 112) between c1.시작일자 and c1.종료일자
and convert(varchar(8), getdate(), 112) between c2.시작일자 and c2.종료일자



h5. 라. Between 조인

지금까지는 선분이력 조회 시점이 정해져 있었다. [그림 Ⅲ-4-35]에서 만약 우측(일별종목거래및시세)과 같은 
일별 거래 테이블로부터 읽히는 미지의 거래일자 시점으로 선분이력(종목이력)을 조회할 때는 between 조인을 이용한다.

!4-35.jpg!

아래는 주식시장에서 과거 20년 동안 당일 최고가로 장을 마친(종가=최고가) 종목을 조회하는 쿼리다. 
<그림 Ⅲ-4-35>의 일별종목거래및시세 테이블로부터 시가, 종가, 거래 데이터를 읽고 그 당시 종목명과 상장주식수는 종목이력으로부터 가져오는데, 
조인 연산자가 '='이 아니라 between이라는 점이 특징적이다.


SELECT a.거래일자, a.종목코드, b.종목한글명, b.종목영문명, b.상장주식수 ,
a.시가, a.종가, a.체결건수, a.체결수량, a.거래대금
FROM 일별종목거래및시세 a,
종목이력 b
WHERE a.거래일자 BETWEEN to_char(add_months(sysdate, -20*12), 'yyyymmdd') AND to_char(sysdate-1, 'yyyymmdd')
AND a.종가 = a.최고가
AND b.종목코드 = a.종목코드
AND a.거래일자 BETWEEN b.시작일자 AND b.종료일자



이런 식으로 조회하면 현재(=최종) 시점의 종목명을 가져오는 것이 아니라 
<그림 Ⅲ-4-36>에서 보는 것처럼 거래가 일어난 바로 그 시점의 종목명을 읽게 된다.

!4-36.jpg!

거래 시점이 아니라 현재(=최종) 시점의 종목명과 상장주식수를 출력하려면 between 조인 대신 아래와 같이 상수 조건으로 입력해야 한다.(<그림 Ⅲ-4-37> 참조)


SELECT a.거래일자, a.종목코드, b.종목한글명, b.종목영문명, b.상장주식수 ,
a.시가, a.종가, a.체결건수, a.체결수량, a.거래대금
FROM 일별종목거래및시세 a,
종목이력 b
WHERE a.거래일자 BETWEEN to_char(add_months(sysdate, -20*12), 'yyyymmdd') AND to_char(sysdate-1, 'yyyymmdd')
AND a.종가 = a.최고가
AND b.종목코드 = a.종목코드
AND to_char(sysdate, 'yyyymmdd') BETWEEN b.시작일자 AND b.종료일자



!4-37.jpg!

위 쿼리는 종목 테이블을 종목이력과 통합해 하나로 설계했을 때 사용하는 방식이며 [그림 Ⅲ-4-35]처럼 종목과 종목이력을 
따로 설계했을 때는 최종 시점을 위해 종목 테이블과 조인하면 된다. 

h3. 5. ROWID 활용

선분이력과 대비해, 데이터 변경이 발생할 때마다 변경일자와 함께 새로운 이력 레코드를 쌓는 방식을 '점이력'이라고 한다.

!4-38.jpg!

이력 모델에선 일반적으로 서브쿼리를 이용해 찾고자 하는 시점(서비스만료일)보다 앞선 변경일자 중
가장 마지막 레코드를 찾기 위해 아래와 같이 조회한다.
| 

SELECT a.고객명,
a.거주지역,
a.주소,
a.연락처,
b.연체금액,
b.연체개월수
FROM 고객 a,
고객별연체이력 b
WHERE a.가입회사 = 'C70'
AND b.고객번호 = a.고객번호
AND b.변경일자 =
(SELECT max(변경일자)
FROM 고객별연체이력
WHERE 고객번호 = a.고객번호
AND 변경일자 <= a.서비스만료일
)

Execution Plan
















-
0 SELECT STATEMENT Optimizer=CHOOSE (Cost=845 Card=10 Bytes=600)
1 0 TABLE ACCESS (BY INDEX ROWID) OF '고객별연체이력' (Cost=2 Card=1 Bytes=19)
2 1 NESTED LOOPS (Cost=845 Card=10 Bytes=600)
3 2 TABLE ACCESS (BY INDEX ROWID) OF '고객' (Cost=825 Card=10 Bytes=410)
4 3 INDEX (RANGE SCAN) OF '고객_IDX01' (NON-UNIQUE) (Cost=25 Card=10)
5 2 INDEX (RANGE SCAN) OF '고객별연체이력_IDX01' (NON-UNIQUE) (Cost=1 Card=1)
6 5 SORT (AGGREGATE) (Card=1 Bytes=13)
7 6 FIRST ROW (Cost=2 Card=5K Bytes=63K)
8 7 INDEX (RANGE SCAN (MIN/MAX)) OF '고객별연체이력_IDX01' (NON-UNIQUE) (... )

 |

 - 고객별연체이력 테이블 두 번 액세스
 - 서브쿼리 내에서 서비스만료일보다 작은 레코드를 모두 스캔하지 않고 인덱스를 거꾸로 스캔하면서 가장 큰 값 하나만을 찾는
   실행계획 수립
 - 한 블록이라도 액세스를 줄여야 하는 상황이라면 ROWID를 이용해 아래와 같이 튜닝할 수 있다.

| 

SELECT
/*+ ordered use_nl(b) rowid(b) */
a.고객명,
a.거주지역,
a.주소,
a.연락처,
b.연체금액,
b.연체개월수
FROM 고객 a, 고객별연체이력 b
WHERE a.가입회사 = 'C70'
AND b.rowid =
(SELECT
/*+ index_desc(c 고객별연체이력_idx01) */
rowid
FROM 고객별연체이력 c
WHERE c.고객번호 = a.고객번호
AND c.변경일자 <= a.서비스만료일
AND ROWNUM <= 1
)

Execution Plan
















-
0 SELECT STATEMENT Optimizer=CHOOSE (Cost=835 Card=100K Bytes=5M)
1 0 NESTED LOOPS (Cost=835 Card=100K Bytes=5M)
2 1 TABLE ACCESS (BY INDEX ROWID) OF '고객' (Cost=825 Card=10 Bytes=410)
3 2 INDEX (RANGE SCAN) OF '고객_IDX01' (NON-UNIQUE) (Cost=25 Card=10)
4 1 TABLE ACCESS (BY USER ROWID) OF '고객별연체이력' (Cost=1 Card=10K Bytes=137K)
5 4 COUNT (STOPKEY)
6 5 INDEX (RANGE SCAN DESCENDING) OF '고객별연체이력_IDX01' (NON-UNIQUE) (Cost=2 Card=5K... )

 |

  - 고객(a)에서 읽은 고객번호로 서브쿼리 쪽 고객별연체이력(c)과 조인하여 얻은 rowid 값으로 고객별연체이력(b) 액세스
  - 고객별연체이력을 두 번 참조했지만, 실행계획 상에는 한 번만 조인한 것과 일량이 같다. 
  - 일반적인 NL Join과 같은 프로세스(Outer 인덱스 → Outer 테이블 → Inner 인덱스 → Inner 테이블)로 진행되는 것에 주목하기 바란다. 
  - 위 쿼리가 제대로 작동하려면 고객별연체이력_idx01 인덱스가 반드시 [ 고객번호 + 변경일자 ] 순으로 구성돼 있어야 하며 
    인덱스 구성이 변경되면 쿼리 결과가 틀려진다.
  - first row(min/max) 알고리즘이 작동한다면 일반적으로 그것만으로도 충분한 성능을 내므로 굳이 위와 같은 기법을 적용하지 않는 것이 좋다.
  - SQL Server는 Oracle처럼 사용자가 직접 ROWID를 이용해 테이블을 액세스(Table Access By User Rowid)하는 방식을 지원하지 않는다. 










h3. 연습문제

|h4. 문제 1. 다음 설명 중 틀린 것은?
1) Oracle에서 인덱스 구성 칼럼이 모두 null인 레코드는 인덱스에 저장하지 않는다.
2) Oracle에서 인덱스 구성 칼럼 중 하나라도 null이 아닌 레코드는 인덱스에 저장한다.
3) SQL Server는 인덱스 구성 칼럼이 모두 null인 레코드도 인덱스에 저장한다.
4) SQL Server는 null 값을 인덱스 맨 뒤에 저장한다.|
|h4. 문제 2. 다음 중 인덱스 클러스터링 팩터(Clustering Factor)를 잘못 설명한 것은?
1) 데이터가 모여 있는 정도를 말한다.
2) 인덱스를 Rebuild 하면 클러스터링 팩터가 좋아진다.
3) 인덱스 클러스터링 팩터가 좋을수록 물리적인 I/O가 적게 발생한다.
4) 인덱스 클러스터링 팩터가 좋을수록 논리적인 I/O가 적게 발생한다.|
|h4. 문제 3. 100명의 영업사원이 있고, 각 사원의 1년치 영업실적은 평균 10만 건에 달한다. 이런 상황에서 가장 수행 빈도가 높은 아래 쿼리를 최적화해 달라는 요청을 받았다. 가장 좋은 방안은?

select substring(일자, 1, 6) 월도
, sum(판매금액) 총판매금액
, avg(판매금액) 평균판매금액
, max(판매금액) 최대판매금액
, min(판매금액) 최소판매금액
from 영업실적
where 사번 = 'S1234'
and 일자 between '20090101' and '20091231'
group by substring(일자, 1, 6)


1) \[사번 + 일자\]로 구성된 인덱스를 생성한다.
2) Oracle 이라면 \[사번 + 일자\] 기준으로 IOT 를 구성하고, SQL Server 라면 클러스터형 인덱스를 생성한다.
3) Oracle 이라면 클러스터를 생성하고, 클러스터에 테이블을 할당한다. 같은 사번끼리 데이터가 모이도록 사번을 클러스터 키(Key)는 선정한다.
4) 테이블을 일자 칼럼 기준으로 Range 파티셔닝하고, Full Table Scan으로 처리한다.|
|h4. 문제 4. 다음 중 인덱스 튜닝에 대해 잘못 설명하고 있는 것은?
1) 인덱스 튜닝의 핵심요소 중 하나는 불필요한 테이블 Random 액세스가 발생하지 않도록 하는 데에 있다. 이를 위해 인덱스 칼럼 순서를 바꿔주는것도 큰 효과가 있다.
2) 인덱스를 경유한 테이블 액세스 횟수가 같더라도 인덱스 구성에 따라 스캔 효율이 달라진다. 따라서 인덱스 스캔 효율을 높이기 위해 인덱스 칼럼 순서를 바꿔야 할 때가 종종 있다.
3) 조건절이 아래와 같을 때 인덱스를 \[deptno + ename\] 순으로 구성하나 \[ename + deptno\] 순으로 구성하나 인덱스 스캔 효율에 차이가 없다. << where deptno = 10 and ename = 'SCOTT' >>
4) 인덱스를 경유한 테이블 Random 액세스 부하가 심할 때, 클러스터 테이블이나 IOT를 활용하는 방안을 고려할 수 있다.|
|h4. 문제 5. 조인에 대한 설명 중 올바른 것은?
1) 소량의 데이터를 조인할 때는 NL Join, 중간 규모의 데이터를 조인할 때는 Hash Join, 대량의 데이터를 조인할 때는 Sort Merge Join 이 효과적이다.
2) 인덱스 스캔 상 비효율이 발생하지 않도록 인덱스를 최적으로 구성해 주기만 하면, 대용량 데이터 처리에도 NL Join 이 효과적이다.
3) Hash Join은 빠른 Hashing 알고리즘을 사용하기 때문에 최상의 속도를 보장한다. 뿐만 아니라 오브젝트를 영구적으로 저장하지 않기 때문에 저장 효율을 높이는 데도 도움이 된다. 따라서 DW, OLTP를 불문하고 적극적으로 활용하는 것이 좋다.
4) OLTP 시스템에서 조인을 튜닝할 때는 일차적으로 NL 조인부터 고려하는 것이 올바른 순서다.|
|h4. 문제 6. SQL 트레이스를 수집한 결과 Row Source Operation이 다음과 같았다. 가장 우선적으로 검토할 사항으로 올바른 것은? 단, 한 달간 주문 건수는 평균 50만 건이다.

select c.고객명, c.연령, c.전화번호, o.주문일자, o.주문총금액, o.배송지주소
from 고객 c, 주문 o
where o.고객번호 = c.주문번호
and o.고객등급 = 'A'
and c.연령 between 51 and 60
and o.주문일자 between '20101201' and '20101231'

Rows Row Source Operation


-








10 NESTED LOOPS
23 TABLE ACCESS BY INDEX ROWID 고객
2978 INDEX RANGE SCAN 고객_IDX
10 TABLE ACCESS BY INDEX ROWID 주문
28 INDEX RANGE SCAN 주문_IDX


1) 고객_IDX 인덱스 칼럼 순서를 조정한다.
2) 고객_IDX 인덱스에 칼럼을 추가한다.
3) 주문_IDX 인덱스 칼럼 순서를 조정한다.
4) 주문_IDX 인덱스에 칼럼을 추가한다.|
{color:white}

문제 1. 4 - 널 값은 SQL Server 는 맨 앞, Oracle 은 맨 뒤 저장
문제 2. 2
문제 3. 2 - 인덱스는 Random 액세스, 클러스터 구성은 원치 않는 일자 액세스, 파티셔닝은 다른 사원 액세스 비효율
문제 4. 1
문제 5. 4
문제 6. 2


{color}