탑크리에듀(http://www.topcredu.co.kr), 오라클자바커뮤니티(http://ojc.asia)에서 제공하는 온라인 무료 화상교육, 오라클 SQL힌트/튜닝 강의자료 3회차 입니다. 12월22일 진행되는 교육에서는 중첩루프조인 소개 및 USE_NL, USE_NL_WITH_INDEX, ORDERED 힌트 구문에 대해 살펴볼 예정 입니다.
1. 온라인 화상(양방향) 교육 3회차
(2016.12.22 목요일 20:30~21:00)
- 중첩루프 조인이란?
- ORDERED, USE_NL, USE_NL_WITH_INDEX 힌트구문
2. 중첩루프조인(Nested Loop Join)
부분 범위처리에 유리한 조인 방식으로 두 개 이상의 테이블에서 하나의
테이블(Driving/Outer Table)을 기준으로 순차적으로 다른 테이블(Driven/Inner Table)의
ROW(RECORD, 행)를 조인하여 원하는 데이터를 추출하는 방식의 조인이다. 부분범위
처리시에 사용하며 확실히 범위를 줄여 줄 조건들이 있을 때 사용하는 것이 좋다.
추출되는 ROW 가 많아 질수록 성능이 현저히 떨어지며 성능을 높이기 위해서는 Driving
Table 의 크기가 작을수록 유리하며 Inner Table 의 조인되는 칼럼은 인덱스가 생성 되어
있어야 한다. (대부분 조인되는 칼럼은 외래키 칼럼이다.)
Inner Table 에서 인덱스를 사용한다면 검색 알고리즘을 적용할 수도 있으며 한번 읽혀진
블록은 DataBase Buffer Cache 에 남겨져 있기에 반복적인 I/O 양은 최소화 될 수 있다. 만약
Inner Table 의 크기가 작다면 메모리에 올려서 검색하는 것이 좋지만 현실적으로 대용량의
데이터 처리에서는 어렵다.
Nested Loop join 에서는 Inner Table 을 대체로 인덱스에 의한 랜덤 액세스에 의해
접근하므로 랜덤 액세스 되는 ROW 가 많다면 수행속도가 저하된다. 만약 Inner Table Join
칼럼에 인덱스가 없다면 Hash or Merge Join 을 이용해야 한다. 또한 Inner Table 에서 조인
컬럼은 Unique Column 일수록 성능이 좋다.
Driving Table 인덱스 액세스의 경우 첫 번째 매칭되는 데이터를 제외하면 나머지 데이터는
한 건씩 스캔 한다.
조인방향에 다라 인덱스가 선택적으로 사용된다.
주어진 상수 값에 의해 범위가 줄어드는 것은 아니며 값을 받아 처리 범위를 결정하며 조인
후 마지막 체크되는 조건은 수행속도에 영향을 준다.
Driving Table 이 대량이거나 Inner Table 에서 조인되는 데이터가 대량인 경우 Sort Merge
또는 Hash Join 을 검토해야 한다.
3. Hints for Join Orders(ORDERED)
힌트는 FROM 뒤에 기술되는 테이블의 순서대로 조인이 일어나도록 해주는 구문이며 대부분
단독으로는 사용되지 않고 USE_NL(중첩 루프 조인을 유도), USE_MERGE(머지 소트 조인을
유도), USE_HASH(HASH 조인을 유도)등과 같이 사용된다.
USE_NL/USE_MERGE/USE_HASH 등의 인자로 사용되는 테이블은 FROM 절에서 두 번째로
나타나는 테이블 이어야 하는데 FROM 절에서 처음 나타나는 테이블이 드라이빙
테이블(OUTER/DRIVING TABLE)이 되고 나중에 나타나는 테이블이 PROBED TABLE(INNER
TABLE)이 된다. 보통 드라이빙 테이블의 경우 데이터 건수가 적거나 조인 칼럼에 대해
인덱스가 없는 테이블이 주로 사용된다.
중첩 루프 조인(Nested Loop Join)이나 해시조인(Hash Join)의 경우 성능이 FROM 절 뒤에
나타나는 테이블의 순서에 영향을 받지만 MERGE 조인은 영향을 받지 않는다. 머지 조인은
메모리에 올려서 정렬 시킨 후 병합하니까 실행 계획은 다르게 나올지 모르지만 성능에는
관계 없다.
[형식]
/*+ ORDERED */
SQL> select /*+ORDERED USE_NL(e) */
e.ename,
d.dname
from dept d, emp e
where e.deptno = d.deptno
------------------------------------------------------------
Operation Object Name Rows Bytes Cost
---------------------------------------------------------------
SELECT STATEMENT Optimizer Mode=ALL_ROWS 14 4
TABLE ACCESS BY INDEX ROWID EMP 4 32 1
NESTED LOOPS 14 266 4
TABLE ACCESS FULL DEPT 4 44 3
INDEX RANGE SCAN IDX_EMP_DEPTNO 5 0
FROM절에서 처음 나타나는 테이블이 드라이빙 테이블(DRIVING/OUTER TABLE)이며 비드라이
빙 테이블(PROBE/INNER TABLE)이 USE_NL의 인자로 들어간다!!
SQL> select /*+ORDERED USE_NL(D) */
4. e.ename,
d.dname
from emp e, dept d
where e.deptno = d.deptno
이번에는 EMP 테이블이 드라이빙 테이블이 된다.
--------------------------------------------------------------
Operation Object Name Rows Bytes Cost
--------------------------------------------------------------
SELECT STATEMENT Optimizer Mode=ALL_ROWS 14 3
NESTED LOOPS 14 266 3
TABLE ACCESS BY INDEX ROWID EMP 14 112 2
INDEX FULL SCAN IDX_EMP_DEPTNO 13 1
TABLE ACCESS BY INDEX ROWID DEPT 1 11 1
INDEX UNIQUE SCAN PK_DEPT 1 0
Hints for Join Orders(USE_NL)
USE_NL 힌트는 테이블을 조인 하는 경우 중첩 루프 조인(Nested Loop Join)이 일어나도록
하는 힌트 문장이다. 중첩 루프 조인은 중첩 반복이라고도 하는데 하나의
테이블(outer/driving table)에서 추출된 ROW 를 가지고 일일이 다른 테이블(inner/probed
table)을 반복해서 조회하여 찾아지는 레코드를 최종 데이터로 간주하는 방법 이다.
EMP 테이블과 DEPT 테이블을 조인하는 경우 DEPT 테이블이 건수가 작다면 우선 이
테이블을 외부 루프로 해서 하나씩 읽으면서 이에 대응하는 EMP 테이블의 데이터를 추출
하는 경우가 중첩 루프 조인에 해당 한다. 이때 EMP 테이블의 경우 대부분 조인 칼럼에
인덱스를 이용하는 것이 성능상 좋다. 그러므로 중첩 루프 조인은 테이블 중 적어도 하나의
조인 컬럼에 대해 인덱스(or Hash Index)가 존재할 때 연관되는 방식으로 이 중첩 루프
조인에서 테이블 중 하나의 테이블 또는 중간 결과 셋을 대상으로 FULL SCAN 이 일어나게
된다.
보통 USE_NL 힌트 구문은 ORDERED 힌트 구문과 같이 사용되는데 USE_NL 이 취하는 인자는
FROM 절에서 두 번째 나오는 테이블(비드라이빙 테이블, inner/probed table)을 명시해
주어야 한다. 인수로 사용되지 않은 첫 번째 테이블이 드라이빙 테이블(outer table)이 된다.
5. [형식]
/*+ USE_NL ( table [table]... ) */
-- ordered, use_nl 힌트를 이용한 조인시 드라이빙 테이블의 선정을 힌트로...
-- use_nl : 중첩루프조인 하라는 힌트
-- ORDERED 힌트는 FROM절에 나타나는 순서대로 조인을 하라는 힌트이며
-- FROM절의 처음 나타나는 테이블이 드라이빙 테이블이 되며
-- LEADING 힌트는 해당 테이블을 먼저 드라이빙 하라는 의미의 힌트이다.
-- 실습환경 오라클11g, MYEMP1은 1000만건, MYDEPT1은 7건
-- CBO, OPTIMIZER_MODE = ALL_ROWS
select count(*) from myemp1;
select count(*) from mydept1;
show parameter optimizer_mode
-- MYEMP1, MYDEPT1 테이블의 DEPTNO 칼럼에 생성된 인덱스 확인
select a.index_name, a.column_name, b.visibility
from USER_IND_COLUMNS a, USER_INDEXES b
where a.table_name in ('MYEMP1', 'MYDEPT1')
and a.index_name = b.index_name
and a.column_name = 'DEPTNO'
order by a.index_name, a.column_name;
select count(d.dname)
from myemp1 e, mydept1 d
where e.deptno = d.deptno;
--mydemp1(deptno인덱스 Fast Full Scan)이 드라이빙 테이블
select /*+ ordered use_nl(d) */
6. count(d.dname)
from myemp1 e, mydept1 d
where e.deptno = d.deptno;
--ordered 와 같이 써야함, use_nl 힌트는 무시됨
select /*+ use_nl(d) */
count(d.dname)
from myemp1 e, mydept1 d
where e.deptno = d.deptno;
--use_nl의 인자로 비드라이빙 테이블(d)이 와야하는데 잘못 옴, 힌트는 무시된다.
select /*+ ordered use_nl(e) */
count(d.dname)
from myemp1 e, mydept1 d
where e.deptno = d.deptno;
--use_nl의 인자를 두개 걸면 중첩루프걸림, 드라이빙 테이블은 알아서 작은 테이블(MYDEPT1)로
select /*+ use_nl(d e) */
count(d.dname)
from myemp1 e, mydept1 d
where e.deptno = d.deptno;
--use_nl의 인자를 두개 걸면 중첩루프걸림, 드라이빙 테이블은 알아서 작은 테이블(MYDEPT1)로
select /*+ use_nl(e d) */
count(d.dname)
from mydept1 d, myemp1 e
where e.deptno = d.deptno;
--아래는 ordered에 의해 myemp1 테이블이 드라이빙 테이블
7. --use_nl인자가 2개인것은 use_nl(d) and use_nl(e)의 shortcut,둘중 맞는것으로 해석함
select /*+ ordered use_nl(d e) */
count(d.dname)
from myemp1 e, mydept1 d
where e.deptno = d.deptno;
--아래는 ordered에 의해 myemp1 테이블이 드라이빙 테이블
--use_nl인자가 2개인것은 use_nl(d) and use_nl(e)의 shortcut, 둘중 맞는것으로 해석함
select /*+ ordered use_nl(e d) */
count(d.dname)
from myemp1 e, mydept1 d
where e.deptno = d.deptno;
--아래는 ordered에 의해 mydept1 테이블이 드라이빙 테이블
--use_nl인자가 2개인것은 use_nl(d) and use_nl(d)의 shortcut, 둘중 맞는것으로 해석함
select /*+ ordered use_nl(e d) */
count(d.dname)
from mydept1 d, myemp1 e
where e.deptno = d.deptno;
--mydept1이 드라이빙 테이블
--form절에 처음 나타나는 mydept1이 드라이빙 테이블, myemp1은 비드라이빙 테이블이며
-- use_nl의 인자로 들어가야 한다.
select /*+ ordered use_nl(e) */
count(d.dname)
from mydept1 d, myemp1 e
where e.deptno = d.deptno;
8. Hints for Join Orders(USE_NL_WITH_INDEX)
중첩 루프조인(Nested Loop Join)에서 USE_NL_WITH_INDEX 에 기술한 내부(inner)테이블과
조인시 기술한 인덱스를 사용하여 조인하도록 명시하는 힌트이다.
[형식]
/*+ USE_NL_WITH_INDEX ( table [Index] [Index]... ) */
MYEMP1, MYEMP1_OLD 테이블에는 EMPNO가 PK이므로 인덱스가 생성되어 있다. ENAME 칼럼
의 인덱스를 확인하고 없다면 생성하자.
SQL> create index idx_myemp1_ename on myemp1(ename);
SQL> create index idx_myemp1_old_ename on myemp1_old(ename);
-- MYEMP1과 MYEMP1_OLD 테이블 전체 칼럼에 대한 인덱스는 아래 쿼리에서 확인하자. 인덱
스는 반드시 VISIBLE 상태로 되어 있어야 옵티마이저가 접근 가능하다.
SQL> SELECT A.INDEX_NAME, A.COLUMN_NAME, B.VISIBILITY
FROM USER_IND_COLUMNS A, USER_INDEXES B
WHERE A.TABLE_NAME IN ('MYEMP1', ‘MYEMP1_OLD’)
AND A.INDEX_NAME = B.INDEX_NAME;
-- USE_NL 힌트를 사용하니 비드라이빙 테이블인 MYEMP1과 조인시 EMPNO, ENAME 인덱스중
EMPNO PK인덱스를 사용함을 알수있다.(선택도,분포도가 UNIQUE 인덱스인 PK_MYEMP1이 좋
다.)
SQL> SELECT /*+ ordered use_nl(e1) */
COUNT(E1.ENAME)
FROM MYEMP1_OLD E2, MYEMP1 E1
WHERE E1.EMPNO = E2.EMPNO
AND E1.ENAME = E2.ENAME ;
COUNT(E1.ENAME)
---------------
1666667
10. select count(*) from myemp1;
select * from myemp1;
drop table myemp1_bak;
create table myemp1_bak (emppno, ename, sal, job, deptno)
as
select empno, ename, sal, job, deptno from myemp1
where rownum < 500000;
select count(*) from myemp1_bak;
exec dbms_stats.gather_table_stats(USER, 'myemp1');
exec dbms_stats.gather_table_stats(USER, 'myemp1_bak');
SELECT A.INDEX_NAME, A.COLUMN_NAME, B.VISIBILITY
FROM USER_IND_COLUMNS A, USER_INDEXES B
WHERE A.TABLE_NAME IN ('MYEMP1', 'MYEMP1_BAK')
AND A.INDEX_NAME = B.INDEX_NAME;
-- 힌트없이 오라클에서는 해시조인 한다. 3.4초
SELECT
COUNT(E1.ENAME)
FROM MYEMP1_BAK E2, MYEMP1 E1
WHERE E1.SAL = E2.SAL
AND E1.JOB = E2.JOB
AND E1.DEPTNO = E2.DEPTNO;
-- 중첩루프조인하는데 조인 인덱스는 알아서 선택하라. 1.2초
SELECT /*+ ordered use_nl(e1)*/
COUNT(E1.ENAME)
FROM MYEMP1_BAK E2, MYEMP1 E1
WHERE E1.SAL = E2.SAL
AND E1.JOB = E2.JOB
AND E1.DEPTNO = E2.DEPTNO;
-- 중첩루프조인 하는데 내부테이블의 job 칼럼 인덱스 사용하라.
SELECT /*+ ordered use_nl_with_index(e1 idx_myemp1_job) */
COUNT(*)
11. FROM MYEMP1_BAK E2, MYEMP1 E1
WHERE E1.SAL = E2.SAL
AND E1.JOB = E2.JOB
AND E1.DEPTNO = E2.DEPTNO;
-- 중첩루프조인 하는데 내부테이블의 deptno 칼럼 인덱스 사용하라
SELECT /*+ ordered use_nl_with_index(e1 idx_myemp1_deptno)*/
COUNT(*)
FROM MYEMP1_BAK E2, MYEMP1 E1
WHERE E1.SAL = E2.SAL
AND E1.JOB = E2.JOB
AND E1.DEPTNO = E2.DEPTNO;
-- 모든 인덱스를 경유하여 인덱스끼리 조인하여 데이터 추출, 느리다.
SELECT /*+ index_join(e1) */
COUNT(*)
FROM MYEMP1_BAK E2, MYEMP1 E1
WHERE E1.SAL = E2.SAL
AND E1.JOB = E2.JOB
AND E1.DEPTNO = E2.DEPTNO;