1. 인라인 뷰 활용
- 인라인 뷰 활용
- 1:M 조인 & 그룹핑 은 M을 그룹핑 후 조인이 유리
- Bad
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)
-- 일별상품판매 테이블의 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 건 수행
-- 2,101개의 블록 I/O 발생, 5.5초 소요
2. 배타적 관계의 조인
- 어떤 엔터티가 두 개 이상의 다른 엔터티의 합집합과 관계(Relationship)를 갖는 것 : 상호배타적(Exclusive OR)관계
- 작업지시 테이블 - (개통신청 및 장애접수 테이블)
- 작업지시 테이블 구현
- 개통신청번호, 장애접수번호 컬럼 별도 관리
[예제] Oracle
select /*+ ordered use_nl(b) use_nl(c) */ a.작업일련번호, a.작업자ID
, a.작업상태코드 , nvl(b.고객번호, c.고객번호) 고객번호 , nvl(b.주소, c.주소) 주소, ......
from 작업지시 a, 개통신청 b, 장애접수 c
where a.방문예정일시 = :방문예정일시
and b.개통신청번호(+) = a.개통신청번호
and c.장애접수번호(+) = a.장애접수번호
[예제] SQL Server
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.방문예정일시 = :방문예정일시
option(force order, loop join)
- 작업구분과 접수번호 칼럼을 두고, 작업구분('1', '2')에 따라 접수번호 컬럼에 해당 값 관리
select x.작업일련번호, x.작업자ID, x.작업상태코드, y.고객번호, y.주소, ......
from 작업지시 x, 개통신청 y
where x.방문예정일시 = :방문예정일시
and x.작업구분 = '1'
and y.개통신청번호 = x.접수번호
union all
select x.작업일련번호, x.작업자ID, x.작업상태코드, y.고객번호, y.주소, ......
from 작업지시 x, 장애접수 y
where x.방문예정일시 = :방문예정일시
and x.작업구분 = '2'
and y.장애접수번호 = x.접수번호
-- 인덱스[작업구분 + 방문예정일시] 사용시 GOOD
-- 인덱스[방문예정일시 + 작업구분] 사용시 인덱스 스캔범위 중복 발생
-- 인덱스[방문예정일시] 사용시 '작업구분' 필터링을 위한 Random 테이블 액세스 발생
[예제] Oracle
select /*+ ordered use_nl(b) use_nl(c) */ a.작업일련번호, a.작업자ID
, a.작업상태코드 , nvl(b.고객번호, c.고객번호) 고객번호 , nvl(b.주소, c.주소) 주소, ......
from 작업지시 a, 개통신청 b, 장애접수 c
where a.방문예정일시 = :방문예정일시
and b.개통신청번호(+) = decode(a.작업구분, '1', a.접수번호)
and c.장애접수번호(+) = decode(a.작업구분, '2', a.접수번호)
[예제] SQL Server
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.방문예정일시 = :방문예정일시
option(force order, loop join)
-- 비효율 해소
3. 부등호 조인
- 업무에 따라 between, like, 부등호 같은 연산자 조인 필요
- 누적 매출(running total) 구하기
-- 윈도우(분석) 함수 이용
select 지점, 판매월, 매출
, sum(매출) over (partition by 지점 order by 판매월 range between unbounded preceding and current row) 누적매출
from 월별지점매출
-- 부등호 조인 적용
select t1.지점, t1.판매월, min(t1.매출) 매출, sum(t2.매출) 누적매출
from 월별지점매출 t1, 월별지점매출 t2 where t2.지점 = t1.지점
and t2.판매월 <= t1.판매월
group by t1.지점, t1.판매월
order by t1.지점, t1.판매월;
4. Between 조인
가. 선분이력이란?
- 시작시점과 종료시점을 함께 관리하는 것, 가장 마지막 이력의 종료일자는 항상 '99991231' 이어야 함
select 고객번호, 연체금액, 연체개월수 from 고객별연체금액 where 고객번호 = '123' and '20040815' between b.시작일자 and b.종료일자 ;
select 고객번호, 연체금액, 연체개월수 from 고객별연체금액 a
where 고객번호 = '123' and 연체변경일자 = (select max(연체변경일자)
from 고객별연체금액
where 고객번호 = a.고객번호 and 변경일자 <= '20040815') ;
- 선분형태 단점
- 이력이 추가될 때 마다 기존 최종 이력의 종료일자도 같이 변경 필요 (DML 부하)
- 일반적인 PK [ 마스터 키 + 종료일자 + 시작일자 ] 가 이력 변경시 같이 변경 됨 (RDBMS 설계 사상)
- 개체 무결성을 완벽히 보장하기 어려움 (가장 큰 단점)
나. 선분이력 기본 조회 패턴
select 연체개월수, 연체금액 from 고객별연체금액 where 고객번호 = :cust_num and :dt between 시작일자 and 종료일자
select 연체개월수, 연체금액 from 고객별연체금액 where 고객번호 = :cust_num and 종료일자 = '99991231'
- 현재 시점 (선분이력 테이블에 정보를 미리 입력 허용시)
-- 고객별 연체변경이력을 지금 등록 하지만, 유효 시작일자가 내일 일때
Oracle
select 연체개월수, 연체금액 from 고객별연체금액
where 고객번호 = :cust_num and to_char(sysdate, 'yyyymmdd') between 시작일자 and 종료일자
SQL Server
select 연체개월수, 연체금액 from 고객별연체금액
where 고객번호 = :cust_num and convert(varchar(8), getdate(), 112) between 시작일자 and 종료일자
다. 선분이력 조인
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.종료일자
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'
- 현재 시점 (선분이력 테이블에 정보를 미리 입력 허용시)
Oracle
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.종료일자
SQL Server
and convert(varchar(8), getdate(), 112) between c1.시작일자 and c1.종료일자 and convert(varchar(8), getdate(), 112) between c2.시작일자 and c2.종료일자
라. Between 조인
- 일별종목거래및시세와 같은 일별 거래 테이블로부터 읽히는 미지의 거래일자 시점으로 선분이력을 조회
- 주식시장, 과거 20년, 당일 최고가로 장을 마친 종목 조회
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.종료일자
- 현재(=최종) 시점의 종목명이 아닌 거래가 일어난 그 시점의 종목명 조회
- 현재(=최종) 시점의 종목명과 상장주식수를 출력
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.종료일자
5. ROWID 활용
- '점이력' : 데이터 변경이 발생할 때마다 변경일자와 함께 새로운 이력 레코드를 쌓는 방식
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) (... )
-- 고객별연체이력 두번 액세스
-- INDEX RANGE SCAN(MIN/MAX) 발생
select /*+ ordered use_nl(b) rowid(b) */ a.고객명, a.거주지역, a.주소, a.연락처, b.연체금액, b.연체개월수
from 고객 a, 고객별연체이력 b
where a.가입회사 = 'C70' and b.rowid = (select /*+ index(c 고객별연체이력_idx01) */ rowid
from 고객별연체이력 c
where c.고객번호 = a.고객번호 and c.변경일자 <= a.서비스만료일 and rownum <= 1)
Eecution 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) OF '고객별연체이력_IDX01' (NON-UNIQUE) (Cost=2 Card=5K... )
-- 일반적인 NL Join과 같은 프로세스 진행 : Outer 인덱스 → Outer 테이블 → Inner 인덱스 → Inner 테이블
-- 고객별연체이력_IDX01 [ 고객번호 + 변경일자] 인덱스 필요
-- ROWID 튜닝은 성능이 아주 중요한 경우에 관리하에 적용 되어야 함
- SQL Server 는 Oracle 의 ROWID 를 이용한 테이블 액세스를 지원하지 않는다.