NL 조인, 소트 머지 조인, 해시 조인, 스칼라 서브 쿼리, 고급 조인 기법에 대해 알아보자
중첩 루프문(Nested Loop)
의 수행구조와 동일
begin
for outer in (select deptno, empno, rpad(ename,10) ename from emp)
loop -- outer 루프
for inner in (select dname from dept where deptno=outer.deptno)
loop -- inner 루프
dbms_output.put_line(outer.empno||' : '||outer.ename||' : '||inner.dname);
end loop;
end loop;
end;
위의 PL/SQL 문
은 아래 쿼리와 100% 같은 순서
로 데이터를 액세스하고, 데이터 출력 순서도 같다.
=> 내부적으로 쿼리를 Recursive
하게 반복 수행하지 않는다는 점만 다름
--oracle
select /*+ ordered use_nl(d) */ e.empno, e.ename, d.dname
from emp e, dept d
where d.deptno=e.deptno;
select /*+ leading(e) use_nl(d) */ e.empno, e.ename, d.dname
from emp e, dept d
where d.deptno=e.deptno;
--sql server
select e.empno, e.ename, d.dname
from emp e inner loop join dept d on d.deptno=e.deptno
option (force order)
select e.empno, e.ename, d.dname
from emp e, dept d
where d.deptno=e.deptno
option (force order, loop join)
소트 머지 조인
과 해시 조인
도 각각 소트 영역
과 해시 영역
에 가공해 둔 데이터
를 이용한다는 점만 다르고, 기본적인 조인 프로세싱
은 동일
select /*+ ordered use_nl(d) */
e.empno, e.ename, d.dname, e.job, e.sal
from dept d, emp e
where d.deptno=e.deptno --1
and d.loc='SEOUL' --2
and d.gb='2' --3
and e.sal>=1500 --4
order by sal desc;
--인덱스
#pk_dept : dept.deptno
#dept_loc_idx : dept.loc
#pk_emp : emp.empno
#emp_deptno_idx : emp.deptno
#emp_sal_idx : emp.sal
Execution Plan
---------------------------------------------------
0 SELECT STATEMENT
1 0 SORT ORDER BY
2 1 NESTED LOOPS
3 2 TABLE ACCESS BY INDEX ROWID DEPT
4 3 INDEX RANGE SCAN DEPT_LOC_IDX
5 2 TABLE ACCESS BY INDEX ROWID EMP
6 5 INDEX RANGE SCAN EMP_DEPTNO_IDX
사용된 인덱스
: dept_loc_idx
와 emp_deptno_idx
조건비교 순서
: 2->3->1->4
dept_loc_idx
인덱스 범위 스캔 (ID=4)dept 테이블 액세스
(ID=3)emp_deptno_idx
인덱스 범위 스캔 (ID=6)emp 테이블 액세스
(ID=5)내림차순
정렬 (ID=1)각 단계를 완료하고 나서 다음 단계로 넘어가는 것이 아니라 한 레코드씩 순차적으로 진행
=> order by
는 전체 집합
을 대상으로 정렬
하기 때문에 작업을 모두 완료하고서 다음 오퍼레이션 진행
StmtText
-------------------------------------------------------------
|--Sort(ORDER BY:([e].[sal] DESC))
|--Filter(WHERE:([emp].[sal] as [e].[sal]>=(1500)))
|--Nested Loops(Inner Join, OUTER REFERENCES:([Bmk1003]))
|--Nested Loops(Inner Join, OUTER REFERENCES:([d].[deptno]))
| |--Filter(WHERE:([dept].[gb] as [d].[gb]='2'))
| | |--Nested Loops(Inner Join, OUTER REFERENCES:([Bmk1000]))
| | |--Index Seek(OBJECT:([dept].[dept_loc_idx] AS [d]), SEEK:([loc]='CHICAGO') )
| | |--RID Lookup(OBJECT:([dept] AS [d]), SEEK:([Bmk1000]=[Bmk1000]) )
| |--Index Seek(OBJECT:([emp].[emp_deptno_idx]), SEEK:([e].[deptno]=[dept].[deptno]))
|--RID Lookup(OBJECT:([emp] AS [e]), SEEK:([Bmk1003]=[Bmk1003]) LOOKUP ORDERED FORWARD)
스캔할 데이터
가 더 있는지 확인하는 one-plus 스캔
표시dept_loc_idx
를 스캔하는 양
에 따라 전체 일량 좌우
dept 테이블 랜덤 액세스
emp_deptno_idx 인덱스
탐색하는 부분 (조인 액세스
), 랜덤 액세스
emp 테이블 랜덤 액세스
랜덤 액세스 위주
의 조인 방식대량의 데이터
조인시 매우 비효율적한 레코드씩 순차적
으로 진행대용량 집합
이라도 부분범위처리
가 가능한 상황에서 매우 극적인 응답 속도
낼 수 있음먼저 액세스
되는 테이블의 처리 범위에 의해 전체 일량
이 결정인덱스 구성 전략
이 특히 중요NL 조인
은 소량의 데이터
를 주로 처리하거나 부분범위처리
가 가능한 온라인 트랜잭션 환경
에 적합
NL 조인의 성능
을 높이기 위해 테이블 Prefetch
, 배치 I/O
기능 도입
=> 테이블 Prefetch
: 인덱스를 이용해 테이블을 액세스하다가 디스크 I/O
가 필요해지면, 이어서 곧 읽게 될 블록까지 미리
읽어서 버퍼 캐시에 적재
하는 기능
=> 배치 I/O
: 디스크 I/O Call
을 미뤘다가 읽을 블록이 일정량 쌓이면 한꺼번에 처리
하는 기능
두 기능 모두 건건이 I/O Call
을 발생시키는 비효율
을 줄이기 위해 고안
Rows Row Source Operation
---------------------------------
5 NESTED LOOPS
3 TABLE ACCESS BY INDEX ROWID OF 사원
5 INDEX RANGE SCAN OF 사원_x1
5 TABLE ACCESS BY INDEX ROWID OF 고객
8 INDEX RANGE SCAN OF 고객_X1
Rows Row Source Operation
---------------------------------
5 TABLE ACCESS BY INDEX ROWID OF 고객
12 NESTED LOOPS
3 TABLE ACCESS BY INDEX ROWID OF 사원
3 INDEX RANGE SCAN OF 사원X_1
8 INDEX RANGE SCAN OF 고객_X1
Rows Row Source Operation
---------------------------------
5 NESTED LOOPS
8 NESTED LOOPS
3 TABLE ACCESS BY INDEX ROWID OF 사원
3 INDEX RANGE SCAN OF 사원_x1
8 INDEX RANGE SCAN OF 고객_X1
5 TABLE ACCESS BY INDEX ROWID OF 고객
NL 조인
은 조인 칼럼
을 선두
로 갖는 인덱스
가 있는지가 매우 중요
=> 없으면 Outer 테이블
에서 읽히는 건마다 Inner 테이블 전체
를 스캔하기 때문
소트 머지 조인(Sort Merge Join)
: 두 테이블을 각각 정렬
한 다음 두 집합을 머지(Merge)
하면서 조인 수행
소트 단계
: 양쪽 집합을 조인 칼럼 기준
으로 정렬
머지 단계
: 정렬된 양쪽 집합을 서로 머지(merge)
조인 칼럼
에 인덱스
가 있으면(Oracle은 Outer 테이블만 해당
), 1번 소트 단계
를 거치지 않고 곧바로 조인 가능
SQL Server
는 조인 연산자
가 =
일 때만 소트 머지 조인
수행
--oracle
select /*+ ordered use_merge(e) */ d.deptno, d.dname, e.empno, e.ename
from dept d, emp e
where d.deptno = e.deptno;
--sql server
select d.deptno, d.dname, e.empno, e.ename
from dept d, emp e
where d.deptno = e.deptno
option (force order, merge join)
StmtText
-------------------------------------------------------------
|--Merge Join(Inner Join, MANY-TO-MANY MERGE:([d].[deptno])=([e].[deptno]))
|--Sort(ORDER BY:([d].[deptno] ASC))
| |--Table Scan(OBJECT:([SQLPRO].[dbo].[dept] AS [d]))
|--Sort(ORDER BY:([e].[deptno] ASC))
|--Table Scan(OBJECT:([SQLPRO].[dbo].[emp] AS [e]))
Inner 집합
인 emp 테이블
이 정렬
돼 있기 때문에 조인에 실패하는 레코드
를 만나는 순간 멈출
수 있음
=> deptno=10
인 레코드를 찾기 위해 1번 스캔을 하다가 20을 만나는 순간 멈춤
=> 스캔 시작점
을 찾으려고 매번 탐색X
, deptno=20
인 레코드를 찾는 2번 스캔은 1번 스캔
에서 멈춘 지점
을 기억했다가 거기서 부터 시작하면 됨
=> Outer 집합
인 dept 테이블
도 같은 순서로 정렬돼 있으므로 가능
Outer 집합(정렬된 dept)에서 첫 번째 로우 o를 가져온다.
Inner 집합(정렬된 emp)에서 첫 번째 로우 i를 가져온다.
loop
양쪽 집합 중 어느 것이든 끝에 도달하면 loop를 빠져나간다.
if o = i 이면
조인에 성공한 로우를 리턴한다.
inner 집합에서 다음 로우 i를 가져온다.
else if o < i 이면
outer 집합에서 다음 로우 o를 가져온다.
else (즉, o > i 이면)
inner 집합에서 다음 로우 i를 가져온다.
end if
end loop
조인 하기 전에 양쪽 집합
을 정렬
=> NL 조인
은 정렬 없이 Outer 집합
을 한 건씩 차례대로 조인을 진행하지만, 소트 머지 조인
은 양쪽 집합을 조인 칼럼 기준으로 정렬
한 후에 조인 시작
부분적
으로 부분범위처리
가 가능
=> Outer 집합
이 조인 칼럼 순
으로 미리 정렬된 상태
에서 사용자가 일부 로우
만 fetch하다가 멈춘
다면 Outer 집합
은 끝까지 읽지 않아도 됨
테이블별 검색 조건
에 의해 전체 일량
이 좌우
=> NL 조인
은 Outer 집합
의 건마다 Inner 집합
을 탐색하지만, 소트 머지 조인
은 두 집합을각각 정렬한 후에 조인함으로 각 집합의 크기
, 즉 테이블별 검색 조건
에 의해 전체 일량 좌우
스캔 위주
의 조인 방식
=> NL 조인
이 랜덤 액세스 위주
의 조인 방식이라면, 소트 머지 조인
은 스캔 위주의 조인 방식
이다. Inner 테이블
을 반복 액세스
하지 않으므로 머지 과정
에서 랜덤 액세스 발생X
=> 각 테이블 검색 조건
에 해당하는 대상 집합
을 찾을 때 인덱스
를 이용한 랜덤 액세스 방식
으로 처리될 수 있고, 이때 발생량이 많다면 소트 머지 조인 이점 사라짐
해시 조인(Hash Join)
: NL 조인이나 소트 머지 조인이 효과적이지 못한 상황 해결하고자 나온 방식
--oracle
select /*+ ordered use_hash(e) */ d.deptno, d.dname, e.empno, e.ename
from dept d, emp e
where d.deptno = e.deptno
--SQL Server
select d.deptno, d.dname, e.empno, e.ename
from dept d, emp e
where d.deptno = e.deptno
option (force order, hash join)
StmtText
-------------------------------------------------------------
|--Hash Match(Inner Join, HASH:([d].[deptno])=([e].[deptno]))
|--Table Scan(OBJECT:([SQLPRO].[dbo].[dept] AS [d]))
|--Table Scan(OBJECT:([SQLPRO].[dbo].[emp] AS [e]))
해시 조인
은 둘 중 작은 집합(Build Input)
을 읽어 해시 영역(Hash Area)
에 해시 테이블(=해시 맵)
을 생성하고, 반대쪽 큰 집합(Probe Input)
을 읽어 해시 테이블 탐색
하며 조인
해시 테이블
생성. 집합
중 작다
고 판단되는 집합
을 읽어 해시 테이블
만듦. 해시 테이블
만들 때 해시 함수
사용. 해시 함수
에서 리턴받은 해시 값
이 같은 데이터
를 같은 해시 버킷
에 체인(연결 리스트)
으로 연결Probe Input
을 스캔. 해시 테이블 생성을 위해 선택되지 않은 나머지 데이터 집합(Probe Input)
을 스캔해시 테이블
탐색. Probe Input
에서 읽은 데이터로 해시 테이블 탐색 시에도 해시 함수
사용. 해시 함수
에서 리턴받은 버킷 주소
로 찾아가 해시 체인
을 스캔하면서 데이터 찾음해시 조인
은 NL조인
처럼 조인 과정에서 발생하는 랜덤 액세스 부하
가 없고, 소트 머지 조인
처럼 조인 전에 미리 양쪽 집합을 정렬
하는 부담 X
=> 해시 테이블
생성하는 비용 수반
=> Build Input
이 작을 때
효과적
=> Hash Build
를 위해 가용 한 메모리 공간
을 초과
할 정도로 Build Input
이 대용량 테이블이면 디스크
에 썼다가 다시 읽어들이는 과정을 거쳐 성능 저하
해시 키 값
으로 사용되는 칼럼
에 중복 값
이 거의 없을 때 효과적
해시 테이블
을 만드는 단계는 전체범위처리
가 불가피하지만, 반대쪽 Probe Input
을 스캔하는 단계는 NL조인
처럼 부분범위처리
가능
만약 In-Memory 해시 조인
이 불가능할 때 DBMS는 Grace 해시 조인
이라고 알려진 조인 알고리즘 사용
=> 두 단계로 나눠 진행
조인되는 양쪽 집합(조인 이외 조건절 만족하는 레코드)
모두 조인 칼럼
에 해시 함수
를 적용하고, 반환된 해시 값
에 따라 동적 파티셔닝
실시.
독립적
으로 처리할 수 있는 여러 개의 작은 서브 집합
으로 분할함으로써 파티션 짝(Pair)
을 생성하는 단계
파티션 단계에서 양쪽 집합
을 모두
읽어 디스크 상의 Temp 공간
에 저장해야 하므로 In-Memory 해시 조인
보다 성능 크게 떨어짐
각 파티션 짝(Pari)
에 대해 하나씩 조인 수행
. 이때 각각에 대한 Build Input
과 Probe Input
은 독립적으로 결정, 즉 파티션하기 전
어느 쪽이 작은 테이블이었는지에 상관없이
각 파티션 짝(pair)별
로 작은 쪽 파티션을 Build Input
으로 선택해 해시 테이블
생성
해시 테이블
생성 후, 반대 쪽 파티션 로우
를 하나씩 읽으며 해시 테이블 탐색
, 모든 파티션 짝에 대한 처리가 완료될 때까지 반복
Grace 해시 조인
은 한마디로 분할/정복(Divide & Conquer) 방식
Recursive 해시 조인
: 디스크에 기록된 파티션 짝
끼리 조인을 수행하려고 작은 파티션
을 메모리에 로드하는 과정에서 또다시 가용 메모리
를 초과하는 경우, 추가적인 파티셔닝 단계
를 거치는 것
해시 알고리즘
의 성능은 해시 충돌(다른 입력값에 대한 출력값이 같은 것)
을 얼마나 최소화
할 수 있느냐에 달림
=> 방지하기 위해선 많은 해시 버킷
을 할당해야 함
=> DBMS는 가능하면 충분히 많은 개수
의 버킷
을 할당함으로써 버킷 하나당 하나의 키 값
만 갖게 하려고 노력
해시 버킷
을 아무리 많이 할당하더라도 해시 테이블
에 저장할 키 칼럼
에 중복 값
이 많다면 하나의 버킷
에 많은 엔트리
가 달릴 수 밖에 없음
=> 해시 버킷을 아무리 빨리 찾아도 해시 버킷
을 스캔
하는 단계에서 많은 시간 허비하므로 탐색 속도 저하
해시 조인 성능
을 좌우하는 두 가지 포인트
한 쪽 테이블
이 가용 메모리에 담길 정도로 충분히 작아야 함
Build Input 해시 키 칼럼
에 중복 값
이 거의 없어야 함해시 조인
을 언제 사용
하는 것이 효과적인가
조인 칼럼
에 적당한 인덱스가 없어
NL 조인이 비효율적
일 때NL 조인 드라이빙 집합
에서 Inner 쪽 집합
으로의 조인 액세스량
이 많아 랜덤 액세스 부하
가 심할 때소트 머지 조인
하기에는 두 테이블이 너무 커
소트 부하가 심할 때수행빈도
가 낮
고 쿼리 수행 시간
이 오래 걸리는
대용량 테이블
을 조인할 때OLAP
, DW
, 배치 프로그램
수행시간이 짧으
면서 수행빈도
가 매우 높은 쿼리
(OLTP
)를 해시 조인으로 처리한다면, NL 조인
에 사용되는 인덱스
는 영구적
으로 유지되면서 다양한 쿼리
를 위해 공유
및 재사용
되지만, 해시 테이블
은 단 하나의 쿼리
를 위해 생성
하고 조인이 끝나면 바로 소멸
함
=> CPU
와 메모리 사용률
을 크게 증가시킴은 물론, 메모리 자원 확보를 위해 각종 래치 경합
이 발생해 시스템 동시성
저하 가능
서브쿼리
: 쿼리에 내장된 또 다른 쿼리 블록
스칼라 서브 쿼리(Scalar Subquery)
: 함수처럼 한 레코드당
정확히 하나의 값
만을 리턴하는 서브쿼리
=> 주로 Select 절
에 사용
=> 칼럼
이 올 수 있는 대부분의 위치에서 사용 가능
select empno, ename, sal, hiredate ,
(select d.dname from dept d where d.deptno = e.deptno) dname
from emp e
where sal >= 2000
--동일
-- dept와 조인에 실패하는 emp 레코드가 있을 경우 dname으로 null값이 출력
select /*+ ordered use_nl(d) */ e.empno, e.ename, e.sal, e.hiredate, d.dname
from emp e right outer join dept d
on d.deptno = e.deptno
where e.sal >= 2000
스칼라 서브 쿼리
를 사용하면 내부적으로 캐시
를 생성하고, 여기에 서브쿼리
에 대한 입력 값
과 출력 값
을 저장
=> 메인쿼리로부터 같은 입력 값
이 들어오면 서브쿼리를 실행하는 대신 캐시된 출력 값
을 리턴
=> 캐시
에서 찾지 못할 때만 쿼리 수행
, 결과는 버리지 않고 캐시에 저장
select empno, ename, sal, hiredate
, (select d.dname --출력 값 : d.dname
from dept d
where d.deptno=e.empno --입력 값 : e.empno)
from emp e
where sal >= 2000;
해싱 알고리즘
을 사용하므로 입력 값 종류
가 소수
여서 해시 충돌 가능성
이 적은 때
여야 캐싱 효과
얻을 수 있음
=> 반대면 캐시 확인 비용
때문에 오히려 성능 저하
-- 사원 테이블 전체를 읽어야 하는 비효율이 있음 (11g 이후로 조인 조건 Pushdown 기능 작동시 괜찮음)
select d.deptno, d.dname, avg_sal, min_sal, max_sal
from dept d right outer join
(select deptno, avg(sal) avg_sal, min(sal) min_sal, max(sal) max_sal
from emp group by deptno) e
on e.deptno=d.deptno
where d.loc='CHICAGO';
구하고자 하는 값들을 모두 결합
하고, 바깥에서 substr 함수
로 분리
--oracle
select deptno, dname
, to_number(substr(sal,1,7)) as avg_sal
, to_number(substr(sal,8,7)) as min_sal
, to_number(substr(sal,15)) as max_sal
from (
select d.deptno, d.dname
,(select lpad(avg(sal),7) || lpad(min(sal),7) || max(sal)
from emp where deptno=d.deptno) sal
from dept d
where d.loc='CHICAGO'
)
--sql server
select deptno, dname
, cast(substring(sal, 1, 7) as float) avg_sal
, cast(substring(sal, 8, 7) as int) min_sal
, cast(substring(sal, 15, 7) as int) max_sal
from ( select d.deptno, d.dname
,(select str(avg(sal), 7, 2) + str(min(sal), 7) + str(max(sal), 7)
from emp where deptno = d.deptno) sal
from dept d
where d.loc = 'CHICAGO'
) x
오라클 12c부터 스칼라 서브 쿼리
도 Unnesting
가능
=> 옵티마이저가 사용자 대신 자동
으로 쿼리 변환
=> NL 조인
이 아닌 해시 조인
으로 실행될 수 있는 이유는 Unnesting
됐기 때문
select c.고객번호, c.고객명
,(select /*+unnest*/ round(avg(거래금액),2) 평균거래금액
from 거래
where 거래일시 >= trunc(sysdate,'mm')
and 고객번호 = c.고객번호)
from 고객 x
where c.가입일시 >= trunc(add_months(sysdate,-1),'mm');
------------------------------------
Execution Plan
------------------------------------
0 SELECT STATEMENT Optimizer=ALL_ROWS
1 0 HASH JOIN (OUTER)
2 1 TABLE ACCESS(FULL) OF '고객' (TABLE)
3 1 VIEW OF 'SYS.VW_SSQ_1' (VIEW) (Cost=4)
4 3 HASH (GROUP BY)
5 4 TABLE ACCESS(FULL) OF '거래' (TABLE)
-- unnest와 merge 힌트를 같이 사용했을 때
------------------------------------
Execution Plan
------------------------------------
0 SELECT STATEMENT Optimizer=ALL_ROWS (Cost=7 Card=15 Bytes=405)
1 0 HASH (GROUP BY) (Cost=7 Card=15 Bytes=405)
2 1 HASH JOIN (OUTER) (Cost=6 Card=15 Bytes=405)
3 2 TABLE ACCESS(FULL) OF '고객' (TABLE) (Cost=3 Card=4 Bytes=80)
4 2 TABLE ACCESS(FULL) OF '거래' (TABLE) (Cost=3 Card=14 Bytes=98)
1:M 관계
인 테이블 끼리 조인하면, 조인 결과
는 M쪽 집합
과 같은 단위
=> 1쪽 집합 단위
로 그룹핑해야 한다면, M쪽 집합
을 먼저 1쪽 단위
로 그룹핑
하고 나서 조인
하는 것이 유리
=> 조인 횟수
를 줄여
주기 때문인데, 이 처리를 위해 인라인 뷰
사용
-- 상품별 판매수량과 판매금액 집계(2009)
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)
일별상품판매 테이블로부터 읽힌 36만5천개 레코드 마다 상품테이블과 조인
시도
=> 조인 과정에서 73만개의 블록 I/O 가 발생했고 총 소요시간은 13초
-- 상품코드별로 먼저 집계하고서 조인
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 SORT GROUP BY (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=5531293 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)
상품코드별
로 집계한 결과건수가 1000건
이므로 상품 테이블과의 조인도 1000번만 발생했으며, 발생한 블록I/O는 2101개이며 수행 시간도 5초이다.
상호배타적(Exclusive OR) 관계
: 엔터티가 두 개 이상
의 다른 엔터티의 합집합
과 관계를 갖는 것
작업지시
테이블은 두 가지 방법 중 하나를 사용
개통신청번호
와 장애접수번호
두 칼럼을 따로 두고, 레코드별로 둘 중 하나의 칼럼에만 값 입력작업구분
과 접수번호
칼럼을 두고 작업구분이 1일때는 개통신청번호를 입력하고, 2일 때는 장애접수번호를 입력--1번 방법
select /*+ orderd use_nl(b) use_nl(c) */ *
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.장애접수번호;
--2번 방법
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.접수번호
작업구분+방문예정일시
로 인덱스를 구성하면 범위에 중복은 없다. 그러나 방문예정일시+작업구분
으로 인덱스를 구성한다면, 스캔 범위
에 중복
이 생긴다.
--비효율 제거
select /*+ordered use_nl(b) use_nl(c)*/
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.접수번호);
누적매출(running total)
을 구할 땐 between, like, 부등호
같은 연산자로 조인
--윈도우 함수 이용
select 지점, 판매월, 매출,
sum(매출) over (partition by 지점 order by 판매월
range between unbounded preceding and current row)매출
from 월별지점매출;
--윈도우 함수 지원X시
select t1.지점, t1.판매월, min(t1.매출) 매출, sum(t2.매출) 누적매출
from 월별지점매출 t1, 월별지점매출 t2
where t2.지점 = t1.지점
and t2.판매월 <= t1.판매월
group by t1.지점, t1.판매월
order by t1.지점, t1.판매월;
점이력
: 고객별연체금액 변경이력을 관리할 때 이력의 시작시점
만 관리하는 것
선분이력
: 시작시점
과 종료시점
을 함께 관리
=> 가장 마지막 이력
의 종료일자
는 항상 99991231
로 입력해두어야 함
이력을 선분형태
로 관리하면 쿼리
가 간단해지는 것이 장점
-- 123번의 2004년 8월 15일 시점 이력 조회
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
를 마스터 키+종료일자+시작일자
로 구성하는데, 이력을 변경할 때마다 PK 값
을 변경하는 셈이어서 설계상 맞지 않는다는 지적 받음
개체 무결성
을 완벽히 보장하기 어려움
과거
, 현재
, 미래
임의 시점을 모두 조회
select 연체개월수, 연체금액
from 고객별연체금액
where 고객번호 = :cust_num
and :dt between 시작일자 and 종료일자
현재 시점
조회
select 연체개월수, 연체금액
from 고객별연체금액
where 고객번호 = :cust_num
and 종료일자 = '99991231'
선분이력 테이블
에 정보를 미리 입력
해 두는 경우가 종종 있고, 그럴 땐 현재 시점
을 위처럼 조회해선 안된다.
=> ex. 고객별 연체변경이력을 지금 등록하지만, 그 정보의 유효 시작일자가 내일일 수 있음
select 연체개월수, 연체금액
from 고객별연체금액
where 고객번호 = :cust_num
and to_char(sysdate,'yyyymmdd') between 시작일자 and 종료일자
--sql server
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'
-- 미래 시점 데이터를 미리 입력하는 예약 기능이 있다면..
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.종료일자
지금까진 선분이력 조건
이 상수
였다. 즉, 조회 시점
이 정해져 있었다.
일별종목거래및시세
와 같은 일별 거래 테이블
로부터 읽히는 미지의 거래일자 시점
으로 선분이력(종목이력)
을 조회할 경우 between
조인 이용
-- 주식시장에서 과거 20년동안 당일 최고가로 장을 마친 종목 조회
select a.거래일자, a.종목코드, b.종목한글명, b.종목영문명, b.상장주식수
,a.시가, a.종가, a.체결건수, a.체결수량, a.거래대금
from 일별종목거래및시세 a, 종목이력 b
where a.거래일자 between to_char(add_month(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_month(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.종료일자;
점이력
: 선분이력과 대비해, 데이터 변경이 발생할 때마다 변경일자
와 함께 새로운 이력 레코드
쌓는 방식
-- 점이력에선 찾고자 하는 시점보다 앞선 변경일자 중 가장 마지막 레코드 찾음
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) (Cost=1 Bytes=13)
7 6 FIRST ROW (Cost=2 Card=5K Bytes=63K)
8 7 INDEX (RANGE SCAN (MIN/MAX)) OF '고객별연체이력_IDX01' (NON-UNIQUE)(..)
위 쿼리는 고객별연체이력
을 2 번
액세스 하고 있다. 다행히 옵티마이저가 서브쿼리 내 서비스 만료일보다 작은 레코드를 모두 스캔하지 않고, 인덱스를 거꾸로 스캔
하면서 가장 큰 값 하나
만을 찾았다.
위 쿼리가 빈번하게 수행되어 액세스를 줄여야 하는 상황이라면 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(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) OF '고객별연체이력_IDX01' (NON-UNIQUE)(..)
위 쿼리가 제대로 작동하려면 고객번호+변경일자
순으로 구성돼야 한다.