오라클이 제공하는 힌트가 너무 강력해서 가끔 기본적인 것을 지나칠 때가 있습니다. 가끔은 힌트가 없다면 어떻게 실행 계획을 제어할 수 있을까를 고민해보면 재미있는 발견을 할 때가 있습니다.

예를 들어 아래와 같은 5개의 테이블이 있습니다.

create table t1(c1, c2)
as 
select
	 level, level
from dual
connect by level <= 5000;

create table t2(c1, c2)
as 
select
	 level, level
from dual
connect by level <= 4000;

create table t3(c1, c2)
as 
select
	 level, level
from dual
connect by level <= 3000;

create table t4(c1, c2)
as 
select
	 level, level
from dual
connect by level <= 2000;

create table t5(c1, c2)
as 
select
	 level, level
from dual
connect by level <= 1000;

create index t1_n1 on t1(c1);
create index t2_n1 on t2(c1);
create index t3_n1 on t3(c1);
create index t4_n1 on t4(c1);
create index t5_n1 on t5(c1);

exec dbms_stats.gather_table_stats(user, 't1');
exec dbms_stats.gather_table_stats(user, 't2');
exec dbms_stats.gather_table_stats(user, 't3');
exec dbms_stats.gather_table_stats(user, 't4');
exec dbms_stats.gather_table_stats(user, 't5');

테이블 T1, T2, T3, T4, T4를 조인하는 쿼리에서 조인 순서는 어떻게 될까요? 테이블의 데이터 분포를 보면 T1(c1 between 1 and 10 조건 때문에)이 드라이빙 테이블이 되고 그 다음은 T5(1000건), T4(2000건), T3(3000건), T2(4000건)이 됨을 알 수 있습니다.

explain plan for
select 
	*
from
	t1, t2, t3, t5, t4
where
	t1.c1 = t2.c1
	and t1.c1 = t3.c1
	and t1.c1 = t4.c1
	and t1.c1 = t5.c1
	and t1.c2 between 1 and 10
;

--------------------------------------
| Id  | Operation             | Name |
--------------------------------------
|   0 | SELECT STATEMENT      |      |
|*  1 |  HASH JOIN            |      |
|*  2 |   HASH JOIN           |      |
|*  3 |    HASH JOIN          |      |
|*  4 |     HASH JOIN         |      |
|*  5 |      TABLE ACCESS FULL| T1   |
|   6 |      TABLE ACCESS FULL| T5   |
|   7 |     TABLE ACCESS FULL | T4   |
|   8 |    TABLE ACCESS FULL  | T3   |
|   9 |   TABLE ACCESS FULL   | T2   |
--------------------------------------

만일 조인 순서를 T1 -> T2 -> T3 -> T4 -> T5로 만들고 싶다면? 아래와 같이 ORDERED 힌트를 사용하면 되겠죠? 또는 LEADING(t1 t2 t3 t4 t5) 힌트를 사용해도 됩니다.

explain plan for
select /*+ ordered */
	*
from
	t1, t2, t3, t5, t4
where
	t1.c1 = t2.c1
	and t1.c1 = t3.c1
	and t1.c1 = t4.c1
	and t1.c1 = t5.c1
	and t1.c2 between 1 and 10
;

--------------------------------------
| Id  | Operation             | Name |
--------------------------------------
|   0 | SELECT STATEMENT      |      |
|*  1 |  HASH JOIN            |      |
|*  2 |   HASH JOIN           |      |
|*  3 |    HASH JOIN          |      |
|*  4 |     HASH JOIN         |      |
|*  5 |      TABLE ACCESS FULL| T1   |
|   6 |      TABLE ACCESS FULL| T2   |
|   7 |     TABLE ACCESS FULL | T3   |
|   8 |    TABLE ACCESS FULL  | T4   |
|   9 |   TABLE ACCESS FULL   | T5   |
--------------------------------------

만일 힌트를 사용하지 않고(그리고 RBO가 아닌 CBO라고 하면), 조인 순서가 항상 위와 같이 나오게 하고 싶다면 어떻게 해야될까요? 힌트에 익숙해지다보면 이런 간단한 질문에 대한 답이 선뜻 나오지 않는 경우가 많습니다.

가장 전형적인 방법은 아래와 같이 t1.c1 = t3.c1 조인 조건을 t1.c1 + 0*t2.c1 = t3.c1 같은 형태로 사용하는 것입니다. t3.c1에서 조인이 이루어지려면 t2.c1 값을 알아야 하므로 반드시 T2 -> T3 순서로 조인이 됩니다. 비슷한 원리로 조건절을 만들어가면 됩니다.

explain plan for
select 
	*
from
	t1, t2, t3, t4, t5
where
	t1.c1 = t2.c1
	and t1.c1 + 0*t2.c1 = t3.c1  -- t2.c1 값을 알아야 t3.c1 조인 가능
	and t1.c1 + 0*t3.c1= t4.c1  -- t3.c1 값을 알아야 t4.c1 조인 가능
	and t1.c1 + 0*t4.c1 = t5.c1  -- t4.c1 값을 알아야 t5.c1 조인 가능
	and t1.c2 between 1 and 10
;

--------------------------------------
| Id  | Operation             | Name |
--------------------------------------
|   0 | SELECT STATEMENT      |      |
|*  1 |  HASH JOIN            |      |
|*  2 |   HASH JOIN           |      |
|*  3 |    HASH JOIN          |      |
|*  4 |     HASH JOIN         |      |
|*  5 |      TABLE ACCESS FULL| T1   |
|   6 |      TABLE ACCESS FULL| T2   |
|   7 |     TABLE ACCESS FULL | T3   |
|   8 |    TABLE ACCESS FULL  | T4   |
|   9 |   TABLE ACCESS FULL   | T5   |
--------------------------------------

위의 방법을 응용하면 조인 순서를 자유롭게 제어할 수 있습니다. 예를 들어 조인 순서를 T1 -> T2 -> T4 -> T3 -> T5(T4가 T3보다 먼저 조인되게)로 만들려면 어떻게 해야할까요?

explain plan for
select 
	*
from
	t1, t2, t3, t4, t5
where
	t1.c1 = t2.c1
	{ 여기에 어떤 조건이 들어가야 아래의 실행 계획이 나올까요? }
	and t1.c2 between 1 and 10
;

--------------------------------------
| Id  | Operation             | Name |
--------------------------------------
|   0 | SELECT STATEMENT      |      |
|*  1 |  HASH JOIN            |      |
|*  2 |   HASH JOIN           |      |
|*  3 |    HASH JOIN          |      |
|*  4 |     HASH JOIN         |      |
|*  5 |      TABLE ACCESS FULL| T1   |
|   6 |      TABLE ACCESS FULL| T2   |
|   7 |     TABLE ACCESS FULL | T4   |
|   8 |    TABLE ACCESS FULL  | T3   |
|   9 |   TABLE ACCESS FULL   | T5   |
--------------------------------------

정답은 이미 공개된 것이나 마찬가지이기 때문에 여기에 적지는 않겠습니다. 혹시 잠깐 시간내서 도전해보고 싶은 분이 있으면 댓글로 남기셔도 좋겠습니다.

그래도 역시 힌트가 편하긴 편합니다!



출처: http://ukja.tistory.com/350 [오라클 성능 문제에 대한 통찰 - 조동욱]

1. Nested Loop Join

가. 기본 메커니즘

프로그래밍을 해 본 독자라면 누구나 아래 중첩 루프문(Nested Loop)의 수행 구조를 이해할 것이고, 그렇다면 Nested Loop Join(이하 NL Join)도 어렵지 않게 이해할 수 있다.

< C, JAVA > for(i=0; i<100; i++){ -- outer loop for(j=0; j<100; j++){ -- inner loop // Do Anything ... } }

위 중첩 루프문과 같은 수행 구조를 사용하는 NL Join이 실제 어떤 순서로 데이터를 액세스하는지 아래 PL/SQL문이 잘 설명해 준다.

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 dept d, emp e 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)

사실 뒤에서 설명할 Sort Merge Join과 Hash Join도 각각 소트 영역(Sort Area)과 해시 영역(Hash Area)에 가공해 둔 데이터를 이용한다는 점만 다를 뿐 기본적인 조인 프로세싱은 다르지 않다.

나. NL Join 수행 과정 분석

이제 NL Join의 기본 메커니즘을 이해했으므로 아래 조인문에서 조건절 비교 순서가 어떻게 되는지 분석해 보자.

select /*+ ordered use_nl(e) */ e.empno, e.ename, d.dname, e.job, e.sal from dept d, emp e where e.deptno = d.deptno …………… ① and d.loc = 'SEOUL' …………… ② and d.gb = '2' …………… ③ and e.sal >= 1500 …………… ④ 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

조건절 비교 순서, 그리고 위 5개 인덱스 중 어떤 것이 사용될지도 함께 고민해 보기 바란다.

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 인 것을 위 실행계획을 보고 알 수 있다. 그럼 조건비교 순서는? SQL 조건절에 표시한 번호로 ② → ③ → ① → ④ 순이다. 실행계획을 해석할 때, 형제(Sibling) 노드 간에는 위에서 아래로 읽는다. 부모-자식(Parent-Child) 노드 간에는 안쪽에서 바깥쪽으로, 즉 자식 노드부터 읽는다. 위 실행계획의 실행 순서를 나열하면 다음과 같다.

1. dept_loc_idx 인덱스 범위 스캔(ID = 4) 2. 인덱스 rowid로 dept 테이블 액세스(ID = 3) 3. emp_deptno_idx 인덱스 범위 스캔(ID = 6) 4. 인덱스 rowid로 emp 테이블 액세스(ID = 5) 5. sal 기준 내림차순(desc) 정렬(ID = 1)

위 실행계획을 그림으로써 표현해 보면 [그림 Ⅲ-4-26]과 같다.

인덱스

[그림 Ⅲ-4-26]을 해석할 때는, 형제 노드 간에는 좌에서 우로 읽고, 부모-자식 노드 간에는 아래에서 위쪽으로, 즉 자식 노드부터 읽는다.

1. dept.loc = ‘SEOUL’ 조건을 만족하는 레코드를 찾으려고 dept_loc_idx 인덱스를 범위 스캔한다. 2. dept_loc_idx 인덱스에서 읽은 rowid를 가지고 dept 테이블을 액세스해 dept.gb = ‘2’ 필터 조건을 만족하는 레코드를 찾는다. 3. dept 테이블에서 읽은 deptno 값을 가지고 조인 조건을 만족하는 emp 쪽 레코드를 찾으려고 emp_deptno_idx 인덱스를 범위 스캔한다. 4. emp_deptno_idx 인덱스에서 읽은 rowid를 가지고 emp 테이블을 액세스해 sal >= 1500 필터 조건을 만족하는 레코드를 찾는다. 5. 1~4 과정을 통과한 레코드들을 sal 칼럼 기준 내림차순(desc)으로 정렬한 후 결과를 리턴한다.

여기서 기억할 것은, 각 단계를 완료하고 나서 다음 단계로 넘어가는 게 아니라 한 레코드씩 순차적으로 진행한다는 사실이다. 단, order by는 전체 집합을 대상으로 정렬해야 하므로 작업을 모두 완료하고서 다음 오퍼레이션을 진행한다. 아래는 SQL Server에서의 실행계획이다.

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)

SQL Server에서 제공하는 그래픽 모드 실행계획은 [그림 Ⅲ-4-27]과 같다.

인덱스

[그림 Ⅲ-4-28]을 보면 지금까지 설명한 NL Join의 수행 절차를 좀 더 명확히 이해할 수 있다.

인덱스

11, 19, 31, 32는 스캔할 데이터가 더 있는지 확인하는 one-plus 스캔을 표시한 것이다. (O)는 테이블 필터 조건에 의해 레코드가 걸러지지 않은 것을 의미하고, 반대로 (X)는 테이블 필터 조건에 의해 걸러진 것을 의미한다. [그림 Ⅲ-4-28]을 보면서, dept_loc_idx 인덱스를 스캔하는 양에 따라 전체 일량이 좌우됨을 이해하기 바란다. 여기서는 단일 칼럼 인덱스를 ‘=’ 조건으로 스캔했으므로 비효율 없이 6(=5+1)건을 읽었고, 그만큼 테이블 Random 액세스가 발생했다. 우선 이 부분이 NL Join의 첫 번째 부하지점이다. 만약 dept 테이블로 많은 양의 Random 액세스가 있었는데 gb = ‘2’ 조건에 의해 필터링되는 비율이 높다면 어떻게 해야 할까? 이미 1장에서 배웠듯이 dept_loc_idx에 gb 칼럼을 추가하는 방안을 고려해야 한다. 두 번째 부하지점은 emp_deptno_idx 인덱스를 탐색하는 부분이며, Outer 테이블인 dept를 읽고 나서 조인 액세스가 얼만큼 발생하느냐에 의해 결정된다. 이것 역시 Random 액세스에 해당하며, [그림 Ⅲ-4-28]에서는 gb = ‘2’ 조건을 만족하는 건수만큼 3번의 조인시도가 있었다. 만약 emp_deptno_idx의 높이(height)가 3이면 매 건마다 그만큼의 블록 I/O가 발생하고, 리프 블록을 스캔하면서 추가적인 블록 I/O가 더해진다. 세 번째 부하지점은 emp_deptno_idx를 읽고 나서 emp 테이블을 액세스하는 부분이다. 여기서도 sal >= 1500 조건에 의해 필터링되는 비율이 높다면 emp_deptno_idx 인덱스에 sal 칼럼을 추가하는 방안을 고려해야 한다. OLTP 시스템에서 조인을 튜닝할 때는 일차적으로 NL Join부터 고려하는 것이 올바른 순서다. 우선, NL Join 메커니즘을 따라 각 단계의 수행 일량을 분석해 과도한 Random 액세스가 발생하는 지점을 파악한다. 조인 순서를 변경해 Random 액세스 발생량을 줄일 수 있는 경우가 있지만, 그렇지 못할 때는 인덱스 칼럼 구성을 변경하거나 다른 인덱스의 사용을 고려해야 한다. 여러 가지 방안을 검토한 결과 NL Join이 효과적이지 못하다고 판단될 때 Hash Join이나 Sort Merge Join을 검토한다.

다. NL Join의 특징

대부분 DBMS가 블록(또는 페이지) 단위로 I/O를 수행하는데, 하나의 레코드를 읽으려고 블록을 통째로 읽는 Random 액세스 방식은 설령 메모리 버퍼에서 빠르게 읽더라도 비효율이 존재한다. 그런데 NL Join의 첫 번째 특징이 Random 액세스 위주의 조인 방식이라는 점이다. 따라서 인덱스 구성이 아무리 완벽하더라도 대량의 데이터를 조인할 때 매우 비효율적이다. 두 번째 특징은, 조인을 한 레코드씩 순차적으로 진행한다는 점이다. 첫 번째 특징 때문에 대용량 데이터 처리 시 매우 치명적인 한계를 드러내지만, 반대로 이 두 번째 특징 때문에 아무리 대용량 집합이더라도 매우 극적인 응답 속도를 낼 수 있다. 부분범위처리가 가능한 상황에서 그렇다. 그리고 순차적으로 진행하는 특징 때문에 먼저 액세스되는 테이블의 처리 범위에 의해 전체 일량이 결정된다. 다른 조인 방식과 비교했을 때 인덱스 구성 전략이 특히 중요하다는 것도 NL Join의 중요한 특징이다. 조인 칼럼에 대한 인덱스가 있느냐 없느냐, 있다면 칼럼이 어떻게 구성됐느냐에 따라 조인 효율이 크게 달라진다. 이런 여러 가지 특징을 종합할 때, NL Join은 소량의 데이터를 주로 처리하거나 부분범위처리가 가능한 온라인 트랜잭션 환경에 적합한 조인 방식이라고 할 수 있다.

2. Sort Merge Join

NL Join은 조인 칼럼을 선두로 갖는 인덱스가 있는지가 매우 중요하다. 만약 조인 칼럼을 선두로 갖는 인덱스가 없으면 Outer 테이블에서 읽히는 건마다 Inner 테이블 전체를 스캔하기 때문이다. 그럴 때 옵티마이저는 Sort Merge Join이나 다음 절에서 설명할 Hash Join을 고려한다. Sort Merge Join은 이름이 의미하는 것처럼 두 테이블을 각각 정렬한 다음에 두 집합을 머지(Merge)하면서 조인을 수행한다. Sort Merge Join은 아래 두 단계로 진행된다.

① 소트 단계 : 양쪽 집합을 조인 칼럼 기준으로 정렬한다. ② 머지 단계 : 정렬된 양쪽 집합을 서로 머지(merge)한다.

만약 조인 칼럼에 인덱스가 있으면(Oracle의 경우 Outer 테이블에만 해당) ①번 소트 단계를 거치지 않고 곧바로 조인할 수도 있다. Oracle은 조인 연산자가 부등호이거나 아예 조인 조건이 없어도 Sort Merge Join으로 처리할 수 있지만, SQL Server는 조인 연산자가 ‘=’ 일 때만 Sort Merge Join을 수행한다는 사실에도 유념하기 바란다.

가. 기본 메커니즘

아래 SQL은 dept 테이블을 기준으로 emp 테이블과 조인할 때 Sort Merge Join 방식을 사용하라고 힌트로 지시하고 있다.

[예제] Oracle select /*+ ordered use_merge(e) */ d.deptno, d.dname, e.empno, e.ename from dept d, emp e where d.deptno = e.deptno Execution Plan ------------------------------------------------------------- 0 SELECT STATEMENT Optimizer=CHOOSE (Cost=11 Card=654 Bytes=35K) 1 0 MERGE JOIN (Cost=11 Card=654 Bytes=35K) 2 1 SORT (JOIN) (Cost=6 Card=654 Bytes=14K) 3 2 TABLE ACCESS (FULL) OF 'DEPT' (Cost=2 Card=654 Bytes=14K) 4 1 SORT (JOIN) (Cost=5 Card=327 Bytes=11K) 5 4 TABLE ACCESS (FULL) OF 'EMP' (Cost=2 Card=327 Bytes=11K) [예제] 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]))

Sort Merge Join의 수행 과정을 그림으로 도식화하면 [그림 Ⅲ-4-29]와 같다.

인덱스

[그림 Ⅲ-4-29]에서 주목할 점은, Inner 집합인 emp 테이블이 정렬돼 있기 때문에 조인에 실패하는 레코드를 만나는 순간 멈출 수 있다는 사실이다. 예를 들어, deptno=10인 레코드를 찾기 위해 ①번 스캔을 진행하다가 20을 만나는 순간 멈춘다. 또 한 가지는, 정렬된 emp에서 스캔 시작점을 찾으려고 매번 탐색하지 않아도 된다는 점이다. 예를 들어, deptno=20인 레코드를 찾는 ②번 스캔은 ①번에서 스캔하다가 멈춘 지점을 기억했다가 거기서부터 시작하면 된다. Outer 집합인 dept 테이블도 같은 순서로 정렬돼 있기 때문에 가능한 일이다. 아래는 Sort Merge Join이 머지하는 방식을 pseudo 코드로 작성한 것이다.

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

[그림 Ⅲ-4-29]와 위 pseudo 코드를 잘 살펴보면, 실제 조인 수행 과정이 NL Join과 크게 다르지 않다. outer 집합과 inner 집합을 미리 정렬해 둔다는 점만 다르다. 다시 말하지만, 양쪽 집합을 먼저 정렬해 두었기 때문에 위와 같은 처리 로직이 가능하다.

나. Sort Merge Join의 특징

Sort Merge Join은 다음과 같은 특징을 가진다.

  • 조인 하기 전에 양쪽 집합을 정렬한다.

NL Join은 정렬 없이 Outer 집합을 한 건씩 차례대로 조인을 진행하지만, Sort Merge Join은 양쪽 집합을 조인 칼럼 기준으로 정렬한 후에 조인을 시작한다. 대량 집합 조인은 Random 액세스 위주의 NL Join의 경우 비효율이 있고, 이 비효율을 줄이고자 나온 조인 방식이 Sort Merge Join이다. 만약 정렬해야 할 집합이 초대용량 테이블이면 정렬 자체가 큰 비용을 수반하기 때문에 성능 개선 효과를 얻지 못할 수도 있다. 하지만, 일반 인덱스나 클러스터형 인덱스처럼 미리 정렬된 오브젝트를 이용하면 정렬작업을 하지 않고 바로 조인을 수행할 수 있어 Sort Merge Join이 좋은 대안이 될 수 있다.

  • 부분적으로, 부분범위처리가 가능하다.

Sort Merge Join은 양쪽 집합을 정렬해야 함으로 부분범위처리가 불가능할 거 같지만, 부분적으로는 가능하다. Outer 집합이 조인 칼럼 순으로 미리 정렬된 상태에서 사용자가 일부 로우만 Fetch 하다가 멈춘다면 Outer 집합은 끝까지 읽지 않아도 되기 때문이다.

  • 테이블별 검색 조건에 의해 전체 일량이 좌우된다.

NL Join은 Outer 집합의 매 건마다 Inner 집합을 탐색한다. Outer 집합에서 조인 대상이 되는 건수에 의해 전체 일량이 좌우되는 이유다. 그러나 Sort Merge Join은 두 집합을 각각 정렬한 후에 조인함으로 각 집합의 크기, 즉 테이블별 검색 조건에 의해 전체 일량이 좌우된다.

  • 스캔(Scan) 위주의 조인 방식이다.

NL Join이 Random 액세스 위주의 조인 방식이라면 Sort Merge Join은 스캔 위주의 조인 방식이다. Inner 테이블을 반복 액세스하지 않으므로 머지 과정에서 Random 액세스가 발생하지 않는 것이다. 하지만, Random 액세스가 전혀 없는 것은 아니다. 각 테이블 검색 조건에 해당하는 대상 집합을 찾을 때 인덱스를 이용한 Random 액세스 방식으로 처리될 수 있고, 이때 발생하는 Random 액세스량이 많다면 Sort Merge Join의 이점이 사라질 수 있다.

3. Hash Join

가. 기본 메커니즘

Hash Join은 NL Join이나 Sort Merge Join이 효과적이지 못한 상황을 해결하고자 나온 조인 방식이다. 아래는 Oracle과 SQL Server 각각에서 Hash Join으로 유도했을 때의 실행계획이다.

[예제] Oracle select /*+ ordered use_hash(e) */ d.deptno, d.dname, e.empno, e.ename from dept d, emp e where d.deptno = e.deptno Execution Plan ------------------------------------------------------------- 0 SELECT STATEMENT Optimizer=CHOOSE (Cost=5 Card=654 Bytes=35K) 1 0 HASH JOIN (Cost=5 Card=654 Bytes=35K) 2 1 TABLE ACCESS (FULL) OF 'DEPT' (Cost=2 Card=654 Bytes=14K) 3 1 TABLE ACCESS (FULL) OF 'EMP' (Cost=2 Card=327 Bytes=11K) [예제] 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]))

Hash Join은 둘 중 작은 집합(Build Input)을 읽어 해시 영역(Hash Area)에 해시 테이블(= 해시 맵)을 생성하고, 반대쪽 큰 집합(Probe Input)을 읽어 해시 테이블을 탐색하면서 조인하는 방식이다.([그림 Ⅲ-4-30] 참조)

인덱스

해시 함수는, 출력값을 미리 알 순 없지만, 같은 입력값에 대해 같은 출력값을 보장하는 함수다. 다른 입력값에 대한 출력값이 같을 수는 있는데, 이를 ‘해시 충돌’이라고 한다. 해시 테이블을 만들 때 해시 충돌이 발생하면, 입력값이 다른 엔트리가 한 해시 버킷에 담길 수 있다. 이런 원리를 바탕으로 Hash Join 과정을 좀 더 자세히 살펴보자.

  • 1단계 : 해시 테이블 생성두 집합 중 작다고 판단되는 집합을 읽어 해시 테이블을 만든다. 해시 테이블을 만들 때 해시 함수를 사용한다. 해시 테이블은 해시 버킷으로 구성된 배열이라고 생각하면 된다. 해시 함수에서 리턴받은 해시 값이 같은 데이터를 같은 해시 버킷에 체인(연결 리스트)으로 연결한다.
  • 2단계 : Probe Input을 스캔해시 테이블 생성을 위해 선택되지 않은 나머지 데이터 집합(Probe Input)을 스캔한다.
  • 3단계 : 해시 테이블 탐색Probe Input에서 읽은 데이터로 해시 테이블을 탐색할 때도 해시 함수를 사용한다. 즉, 해시 함수에서 리턴받은 버킷 주소로 찾아가 해시 체인을 스캔하면서 데이터를 찾는다.

Hash Join은, NL Join처럼 조인 과정에서 발생하는 Random 액세스 부하가 없고 Sort Merge Join처럼 조인 전에 미리 양쪽 집합을 정렬하는 부담도 없다. 다만, 해시 테이블을 생성하는 비용이 수반된다. 따라서 Build Input이 작을 때라야 효과적이다. 만약 Hash Build를 위해 가용한 메모리 공간을 초과할 정도로 Build Input이 대용량 테이블이면 디스크에 썼다가 다시 읽어 들이는 과정을 거치기 때문에 성능이 많이 저하된다. Build Input으로 선택된 테이블이 작은 것도 중요하지만 해시 키 값으로 사용되는 칼럼에 중복 값이 거의 없을 때라야 효과적이다. 이유는 잠시 후 자세히 설명한다. 해시 테이블을 만드는 단계는 전체범위처리가 불가피하지만, 반대쪽 Probe Input을 스캔하는 단계는 NL Join처럼 부분범위처리가 가능하다는 사실도 기억하자.

나. Build Input이 가용 메모리 공간을 초과할 때 처리 방식

Hash Join은 Hash Build를 위한 가용한 메모리 공간에 담길 정도로 Build Input이 충분히 작아야 효과적이라고 했다. 만약 In-Memory Hash Join이 불가능할 때 DBMS는 ‘Grace Hash Join’이라고 알려진 조인 알고리즘을 사용하는데, 이는 아래 두 단계로 나누어 진행된다.

1) 파티션 단계

조인되는 양쪽 집합(→ 조인 이외 조건절을 만족하는 레코드) 모두 조인 칼럼에 해시 함수를 적용하고, 반환된 해시 값에 따라 동적으로 파티셔닝을 실시한다. 독립적으로 처리할 수 있는 여러 개의 작은 서브 집합으로 분할함으로써 파티션 짝(pair)을 생성하는 단계다. 파티션 단계에서 양쪽 집합을 모두 읽어 디스크 상의 Temp 공간에 일단 저장해야 하므로 In-Memory Hash Join보다 성능이 크게 떨어지게 된다.

2) 조인 단계

파티션 단계가 완료되면 각 파티션 짝(pair)에 대해 하나씩 조인을 수행한다. 이때, 각각에 대한 Build Input과 Probe Input은 독립적으로 결정된다. 즉, 파티션하기 전 어느 쪽이 작은 테이블이었는지에 상관없이 각 파티션 짝(pair)별로 작은 쪽 파티션을 Build Input으로 선택해 해시 테이블을 생성한다. 해시 테이블이 생성되고 나면 반대 쪽 파티션 로우를 하나씩 읽으면서 해시 테이블을 탐색하며, 모든 파티션 짝에 대한 처리가 완료될 때까지 이런 과정을 반복한다. Grace Hash Join은 한마디로, 분할 정복(Divide & Conquer) 방식이라고 말할 수 있다. 실제로는 DBMS 벤더마다 조금씩 변형된 형태의 하이브리드(Hybrid) 방식을 사용하지만 두 개의 큰 테이블을 Hash Join하는 기본 알고리즘은 Grace Hash Join에 바탕을 두고 있다.

  • Recursive Hash Join(=Nested-loops Hash Join)

디스크에 기록된 파티션 짝(pair)끼리 조인을 수행하려고 ‘작은 파티션’을 메모리에 로드하는 과정에서 또다시 가용 메모리를 초과하는 경우가 발생할 수 있다. 그럴 때는 추가적인 파티셔닝 단계를 거치게 되는데, 이를 ‘Recursive Hash Join’이라고 한다.

다. Build Input 해시 키 값에 중복이 많을 때 발생하는 비효율

잘 알다시피 해시 알고리즘의 성능은 해시 충돌(collision)을 얼마나 최소화할 수 있느냐에 달렸으며, 이를 방지하려면 그만큼 많은 해시 버킷을 할당해야만 한다. [그림 Ⅲ-4-30]에는 개념적으로 설명하기 위해 하나의 버킷에 여러 키 값이 달리는 구조로 표현하였지만, DBMS는 가능하면 충분히 많은 개수의 버킷을 할당함으로써 버킷 하나당 하나의 키 값만 갖게 하려고 노력한다. 그런데 해시 버킷을 아무리 많이 할당하더라도 해시 테이블에 저장할 키 칼럼에 중복 값이 많다면 하나의 버킷에 많은 엔트리가 달릴 수 밖에 없다. 그러면 해시 버킷을 아무리 빨리 찾더라도 해시 버킷을 스캔하는 단계에서 많은 시간을 허비하기 때문에 탐색 속도가 현저히 저하된다. Build Input의 해시 키 칼럼에는 중복 값이 (거의) 없어야 Hash Join이 빠르게 수행될 수 있음을 이해할 것이다.

라. Hash Join 사용기준

Hash Join 성능을 좌우하는 두 가지 키 포인트는 다음과 같다.

  • 한 쪽 테이블이 가용 메모리에 담길 정도로 충분히 작아야 함
  • Build Input 해시 키 칼럼에 중복 값이 거의 없어야 함

위 두 가지 조건을 만족할 때라야 Hash Join이 가장 극적인 성능 효과를 낼 수 있음을 앞에서 살펴보았다. 그러면 Hash Join을 언제 사용하는 것이 효과적인지 그 선택 기준을 살펴보자.

  • 조인 칼럼에 적당한 인덱스가 없어 NL Join이 비효율적일 때
  • 조인 칼럼에 인덱스가 있더라도 NL Join 드라이빙 집합에서 Inner 쪽 집합으로의 조인 액세스량이 많아 Random 액세스 부하가 심할 때
  • Sort Merge Join 하기에는 두 테이블이 너무 커 소트 부하가 심할 때
  • 수행빈도가 낮고 조인할 때

앞쪽 세 가지 사항은 앞에서 이미 설명한 내용이므로 생략하기로 하고, 마지막 항목을 강조하면서 Hash Join에 대한 설명을 마치려고 한다. Hash Join이 등장하면서 Sort Merge Join의 인기가 많이 떨어졌다고 했는데, 그만큼 Hash Join이 빠르기 때문이다. Hash Join이 워낙 빠르다 보니 모든 조인을 Hash Join으로 처리하려는 유혹에 빠지기 쉬운데, 이는 매우 위험한 생각이 아닐 수 없다. 수행시간이 짧으면서 수행빈도가 매우 높은 쿼리(→ OLTP성 쿼리의 특징이기도 함)를 Hash Join으로 처리한다면 어떤 일이 발생할까? NL Join에 사용되는 인덱스는 (Drop하지 않는 한) 영구적으로 유지되면서 다양한 쿼리를 위해 공유 및 재사용되는 자료구조다. 반면, 해시 테이블은 단 하나의 쿼리를 위해 생성하고 조인이 끝나면 곧바로 소멸하는 자료구조다. 따라서 수행빈도가 높은 쿼리에 Hash Join을 사용하면 CPU와 메모리 사용률을 크게 증가시킴은 물론, 메모리 자원을 확보하기 위한 각종 래치 경합이 발생해 시스템 동시성을 떨어뜨릴 수 있다. 따라서 Hash Join은 ①수행 빈도가 낮고 ②쿼리 수행 시간이 오래 걸리는 ③대용량 테이블을 조인할 때(→ 배치 프로그램, DW, OLAP성 쿼리의 특징이기도 함) 주로 사용해야 한다. OLTP 환경이라고 Hash Join을 쓰지 못할 이유는 없지만 이 세 가지 기준(①~③)을 만족하는지 체크해 봐야 한다

4. Scalar Subquery

쿼리에 내장된 또다른 쿼리 블록을 서브쿼리라고 하는데, 그 중에서 함수처럼 한 레코드당 정확히 하나의 값만을 리턴하는 서브쿼리를 ‘Scalar Subquery’라고 한다. Scalar Subquery는 주로 select-list에서 사용되지만 몇 가지 예외사항을 뺀다면 칼럼이 올 수 있는 대부분 위치에서 사용 가능하다.

select empno, ename, sal, hiredate ,(select d.dname from dept d where d.deptno = e.deptno) dname from emp e where sal >= 2000

Scalar Subquery를 사용한 위 쿼리 문장은 아래 Outer 조인문과 100% 같은 결과를 낸다. 즉, 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

위에서 예시한 쿼리는 결과만 같은 것이 아니라 조인을 수행하는 처리 경로도 동일한데, NL 방식으로 수행되도록 힌트를 사용했기 때문이다. 다만 Scalar Subquery에는 내부적으로 캐싱 기법이 작용된다는 점이 다르고, 이를 이용한 튜닝이 자주 행해진다.

가. Scalar Subquery의 캐싱 효과

아래 쿼리는 위치가 ‘CHICAGO’인 부서(dept)만 대상으로 급여 수준을 집계하려는 것인데, 사원(emp) 테이블 전체를 다 읽어야 하는 비효율이 있다.

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'

아래와 같이 바꿀 수 있으면 좋겠지만 스칼라 서브쿼리는 한 레코드당 하나의 값만 리턴한다는 특징 때문에 그럴 수가 없다.

select d.deptno, d.dname ,(select avg(sal), min(sal), max(sal) from emp where deptno = d.deptno) from dept d where d.loc = 'CHICAGO'

그렇다고 아래와 같이 쿼리한다면 emp에서 같은 범위를 반복적으로 액세스하는 비효율이 생긴다.

select d.deptno, d.dname ,(select avg(sal) from emp where deptno = d.deptno) avg_sal ,(select min(sal) from emp where deptno = d.deptno) min_sal ,(select max(sal) from emp where deptno = d.deptno) max_sal from dept d where d.loc = 'CHICAGO'

이럴 때, 아래 처럼 구하고자 하는 값들을 모두 결합하고서 바깥쪽 액세스 쿼리에서 substr 함수로 분리하는 방법이 유용하게 쓰인다.

[예제] Oracle select deptno, dname , to_number(substr(sal, 1, 7)) avg_sal , to_number(substr(sal, 8, 7)) min_sal , to_number(substr(sal, 15)) 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


ROWNUM DATA 를 추출한 후 조회 하자


ex) rownum 의 잘못된 사용 사례

WHERE ROWNUM = N ( N > 1 )

WHERE ROWNUM > N ( N > 1 )


추출하려면

select 

    rnum,

    t.*

from

(

select

    e1.*,

    rownum rnum

from

(select * from emp order by empno desc) e1 ) t

where rnum >= 6 and rnum <= 12


와 같이 추출 해야한다.




ORDER BY 와 ROWNUM 을 같은 위치에 두지 말자

sql 구성요소들은 아래와 같은 순서로 실행 된다


1) FROM, WHERE 절을 처리

2) ROWNUM 조건 적용

3) SELECT COLUMN LIST 절을 적용

4) GROUP BY 절을 적용

5) HAVING 절을 적용

6) ORDER BY 절을 적용




ROWNUM=1 은 ROWNUM<=1 로 사용하자


CURSOR_SHARING 을 FORCE나 SIMILAR로 적용 하였을 경우

ROWNUM=1을 ROWNUM=:"SYS_B_0" 


1이라는 상수값을 바인드 변수값으로 재해석하여 실행한다


고로 전체 DATA를 다 수행한 뒤 1개 값을 가져오므로 성능악화 된다


하지만 ROWNUM<=:"SYS_B_0" 는 값보다 작거나 같은 값이라는 범위

(STOPKEY)가 정해지기 때문에 변수의 값을 확인하여

작거나 같은 데이터를 추출하므로 전체 데이터를 처리하지 않고, 1건만 추출하고

SQL의 수행은 종료된다




INDEX_DESC와 ROWNUM<=1을 함께 사용하지 말자


완벽해 보이는 방법일수도 있겠으나 index가 unusabled이거나 없는 경우라면

데이터의 정합성이 훼손될 수 있으므로 이 방법 대신


max() 혹은 order by desc 후 ronum <= 1 값을 가져오는 쿼리를 쓰는 방법이 좋다.




ROWNUM<=1 은 항상 빠르지 않다


ROWNUM<=1이 빠른경우는 INDEX RANGE SCAN 하여 COLUMN 의 NUM_DISTINCT (종류가 적은)

최종 추출 건수가 적은 경우에만 빠르다.


즉 INDEX SCAN 하지 않고 FULL TABLE ACCESS 하여 NUM_DISTINCT가 많은 경우

모든 것을 실행뒤 COUNT STOPKEY 하므로 이러한 상황에선 느릴 수도 있다.




인라인 뷰에 ROWNUM을 추가할 때는 주의하자


select t3.*, t2.*

  from rownum_t2 t2,

( select rownum rnum, rownum_t3.*

from rownum_t3 ) t3

where t2.c1 = t3.c1

   and t2.c2 = :b1

   and t2.c3 = :b2;


>> rownum_t3 테이블을 table access full 하여 view 한 cost 비용이 높다


select

rownum, 

t3.*,

t2.*

  from rownum_t2 t2,

( select *

from rownum_t3 ) t3

where t2.c1 = t3.c1

   and t2.c2 = :b1

   and t2.c3 = :b2;


>> rownum_t3과 t2모두 index range scan 하여 join 후 count를 먹여

     cost 비용이 적게 든다

SQL 중 최종 추출 결과가 적은 경우에는, Function의 수행위치를 변경하여 과다 수행되는 성능 문제를 해결 할 수 있다.

예를들면,


select z.*

(select

empCnt (e.title) empCnt ,

deptCnt (d.title) deptCnt,

...

from emp e inner join dept d ON e.deptno = d.deptno

where ... ) z

order by rownum <= 3


만약 emp 와 dept 의 inner join rows가 4만건이라면 해당 함수도 4만횟수를 수행하여 쿼리 I/O 상에서는 문제가 없겟지만

함수 I/O 상에서 문제가 크게 발생된다 왜냐하면 함수를 굳이 4만번 수행하지 않고 rownum 3회만 수행하게 할 수 있기 때문이다.



select x.*

, empCnt(x.eTitle) empCnt

, deptCnt(x.dTitle) deptCnt

from (

select z.*

from (

select

e.title eTitle ,

d.title dTitle ,

...

from emp e inner join dept d ON e.deptno = d.deptno

where ... ) z

order by rownum <= 3

) x


위와 같이 쿼리를 수행하면 과다 수행 되지않고 3row를 뽑은뒤 함수가 3번만 수행 되게 되어진다.

하지만 SQL의 최종 추출 결과가 많은 경우에는, Function 의 수행 위치를 바꾸는 것만으로 성능을 개선하기는 힘들다.


Main SQL 에서 추출한 데이터 중 Function을 수행하는 입력값의 종류가 적은 경우 (Distinct rows가 적은경우) 스칼라 서브쿼리에서 Function을 수행하도록 변경하면, Multi Buffer 를 이용해 성능을 개선 시킬 수 있다.


select

jisaname( gr. jisacode1, '00', sysdate ) jisaname1 , 

jisaname( gr.jisacode1, gr.jisacode2, sysdate ) jisaname2,

groupname( gr.parent_groupno ) parent_groupname , 

groupname ( gr.groupno ) groupname ,

...

from t_member mem inner join t_group gr ON gr.groupno = mem.groupno

where ...


해당 쿼리의 함수별 추출 데이터 건수가 4,708 * 4 = 18,832 건이라 하면


select

(SELECT jisaname( gr. jisacode1, '00', sysdate ) FROM DUAL) jisaname1 , 

(SELECT jisaname( gr.jisacode1, gr.jisacode2, sysdate ) FROM DUAL) jisaname2,

(SELECT groupname( gr.parent_groupno ) FROM DUAL) parent_groupname , 

(SELECT groupname ( gr.groupno ) FROM DUAL) groupname ,

...

from t_member mem inner join t_group gr ON gr.groupno = mem.groupno

where ...


jisaname() 은 2번, groupname()은 총 2,213번으로 수행건이 줄어들었다.

gr.jisacode1, 과 gr.jisacode2가 모두 동일한 값을 가지고 있어 스칼라 서브쿼리에서 수행되도록 변경한 경우,

최초 1회씩만 수행하고 나머지는 Cache에서 결과 값을 가져 올 수 있었기 때문이다.



이와같이 입력 값에 대응하는 값의 종류가 다양하지 않아, 같은 입력 값에 대한 결과 값이 항상 동일하다면,

Function 은 스칼라 서브쿼리에서 수행하도록 변경하여 Function 수행에 의한 부하를 감소시켜야 한다.




Hash Join 방식

먼저 수행되는 테이블 (선행 테이블)에 대해 상수 조건으로 데이터 걸러낸 후 join key column 으로 Hash map을 만든다.

그 다음 Probe Table(후행 테이블)을 수행하는데 이 때 상수 조건이 있다면, 먼저 상수 조건으로 대상을 

줄인다. 그 이후 선행 테이블이 생성해 놓은 Hash Map 에 있는 데이터 중에서 조인에 성공한 데이터를 추출한다.


선행테이블의 상수 조건으로 데이터를 줄일 수 있더라도, 후행테이블의 상수 조건 없음으로 인해, 후행 테이블의 전체 데이터 건수만큼 Function 이 수행된다.

(I/O 처리량이 급격히 높아져 성능이 안좋게 된다.)



ex)

FN_C1_CODENM := c1 코드명을 가져오는 함수

FN_C2_CODENM := c2 코드명을 가져오는 함수


SELECT /*+ LEADING(T1) USE_HASH(T1 T2) */

T1.*,

T2.*

FROM FUNCTION_TABLE T1, C1_CODE_NM T2

WHERE

T1.C2 = 0

AND T1.C3 = 'A'

AND T1.C1 = T2.C1

AND T2.C3 = FN_C2_CODENM(T2.C4)


t1의 상수를 검색한 table 값을 hash로 만들어 후행테이블인 t2와 데이터를 추출한다

(t2의 total rows 가 10000만건이라면 성능이 그다지 좋지가 않다)


SELECT /*+ LEADING(T1) USE_HASH(T1 T2) */

T1.*,

T2.*

FROM FUNCTION_TABLE T1, C1_CODE_NM T2

WHERE

T1.C2 = 0

AND T1.C3 = 'A'

AND T1.C1 = T2.C1

AND T2.C4 IN (2, 4)

AND T2.C3 = FN_C2_CODENM(T2.C4)


하지만 위 쿼리와같이 t2의 where 상수 조건이 있다면 그만큼 줄여서 데이터를 추출한다

(t2의 c4값이 2와 4인 row가 4만건이라면 해당 수만큼 hash 조인한다)





Nested Loops Join 방식

선행 테이블을 액세스한 후 join 조건으로 후행 테이블을 반복 탐색하며 조인을 수행 한다.

이 때 선행 테이블에서 추출된 건수만큼 반복 수행하게 된다. 따라서 선행 테이블을 상수조건으로

추출한 건수만큼 조인을 시도하여 Function 이 수행된다.


(후행 테이블의 수가 많다 하더라도 선행테이블을 상수조건 만큼 줄여 반복 탐색 하기 때문에 I/O처리량이

hash join 방식 보다 낮아져 성능이 좋게 된다.)



ex)

SELECT /*+ LEADING(T1) USE_NL(T1 T2) */

T1.*,

T2.*

FROM FUNCTION_TABLE T1, C1_CODE_NM T2

WHERE

T1.C2 = 0

AND T1.C3 = 'A'

AND T1.C1 = T2.C1

AND T2.C3 = FN_C2_CODENM(T2.C4)


선행 테이블의 t1의 상수값을 검색한 row 에 반복적으로 t2 후행테이블 값을 조인 시킨다.

(t2가 10000만건이라도 t1의 상수검색한 row 수가 3800건이라면 3800번 조인한다)



SELECT /*+ LEADING(T1) USE_NL(T1 T2) */

T1.*,

T2.*

FROM FUNCTION_TABLE T1, C1_CODE_NM T2

WHERE

T1.C2 = 0

AND T1.C3 = 'A'

AND T1.C1 = T2.C1

AND T2.C4 IN (2, 4)

AND T2.C3 = FN_C2_CODENM(T2.C4)


후행테이블인 t2의 상수조건까지 들어간다면 t2의 상수검색된 row와 join 하기때문에 t2의 상수검색되지 않은 테이블보다

더 조인문이 줄어든다 (예 : 1800건) --> 왜 1800건일까


SELECT count(*)

FROM FUNCTION_TABLE T1, C1_CODE_NM T2

WHERE

T1.C2 = 0

AND T1.C3 = 'A'

AND T1.C1 = T2.C1

AND T2.C4 IN (2, 4)

--AND T2.C3 = FN_C2_CODENM(T2.C4)

위와같이 function으로 검색한 부분을 지우면 rows가 1800건이 나온다.

이러한 이유로, Nested Loops Join 이 성공하자 마자 Function이 수행되지 않는다.


조인 조건으로 조인 성공이 이루어지더라도 후행 테이블에 상수 조건이 잇다면, 조건을 수행하고 만족하는 데이터 건에 대해서만

Function 을 수행하여 데이터를 추출하게 된다는 것을 확인할 수 있다.



NL ( NASTED LOOPS JOIN )


요약 하자면

CASE    조인순서    조인 방법    T1 데이터건수    T2 데이터건수    조인후추출건수    Function수행 횟수

1        T1 -> T2    HASH JOIN    3,846            100,000            768            100,000

2        T1 -> T2    HASH JOIN    3,846            40,000             384            40,000

3        T1 -> T2    NL    JOIN    3,846            100,000            768              3,846

4        T1 -> T2    NL    JOIN    3,846            40,000              384            1,538

5        T2 -> T1    NL    JOIN    3,846            100,000            768            100,000

6        T2 -> T1    NL    JOIN    3,846            40,000              384            40,000



위 표를 참고하여 조인 방법 및 조인 순서를 고려해 Select 절 Function 수행횟수를 최적화 하여 SQL 성능 문제 해결에

도움이 되길 바란다.


  • Return 받을 행을 받는 Object Type을 생성
CREATE OR REPLACE TYPE obj_type AS object
( c1 INT,
  c2 INT
);
/


  • Collection Type 생성
CREATE OR REPLACE TYPE table_type
AS TABLE OF obj_type;
/


  • Table Function을 생성 - 원하는 만큼의 Row를 출력하는 Function
CREATE OR REPLACE FUNCTION table_func (p_start int, p_end int)
  RETURN table_type
  IS
    v_type TABLE_TYPE := table_type();
  BEGIN
   
    FOR i IN p_start..p_end LOOP
      v_type.extend;
      v_type(i) := obj_type(i,i);
    END LOOP;
     
    RETURN v_type;
 END;
 /


  • FROM 절에 'TABLE'이라는 Keyword를 이용해 아래와 같은 결과 추출
SELECT * FROM TABLE(table_func(1,3));
 
 
   C1         C2
----- ----------
    1          1
    2          2
    3          3



Pipelined Table Function은 하나의 Row를 받아서 바로 처리하므로 수행 속도가 빠르다. 이에 비해 Table Function은 전체 Row가 처리된 이후에 동작되므로 Pipelined Table Function에 비해 이전 처리된 Row를 Cache할 Memory를 더 요구하게 된다.


Table Function은 전체 데이터 처리를 수행하지만 Pipelined Table Function은 부분 범위 처리를 수행한다

Oracle 10g부터 사용하는 dbms_xplan Package의 Function들도 Pipelined Table Function으로 구현되어 있다.





출처 : http://www.gurubee.net/lecture/2238

merge 문을 사용 할 때 

on절에 온 Unique 컬럼을 update 인자로 쓸 수 없다.


하지만 그 unique 컬럼을 merge 문으로 사용하기 위해 아래와 같이 

rowid 로 join을 걸어 해당 unique 컬럼을 update 로 사용 할 수 있다.




SELECT e.empno, e.ename

  FROM emp e INNER JOIN dept d ON e.deptno = d.deptno




SELECT a.empno, a.ename

  FROM emp a,

       (SELECT b.*, aa.ROWID rid

          FROM dept b, emp aa

         WHERE b.deptno = aa.deptno(+)) c

 WHERE a.ROWID = c.rid




아 참고로 Merge 문의 on 절에 오는 a.coulmn = b.column 들은

unique 값이 어야 한다.


unique 값이 아니라면 distinct 나 group by 하여 1:1 매칭이 되게 하여야 merge 문 에러가 나지 않는다.



여러 개의 Table이 Join된 View를 Update할 수 있다는 것을 잘 알고 있을 것이다. Oracle은 이것을 Updatable Join View라고 부른다.

View(단, Subquery까지 포함)를 Update할 수 있는 방법은 내가 알기로는 세 가지 정도가 있다. 

- Updatable Join View
- Merge Statement(정확하게 말하면 View는 아니지만 그런 효과가 있다는 의미)
- Instead of Trigger

역시 하나의 문제에 대해 여러 가지 솔루션이 있을 수 있다는 것을 아는 것이 중요하다는 것의 예가 되겠다.

간단한 예를 통해 각 방법들을 살펴 보면...

우선 다음과 같이 Parent/Child 관계를 가지는 Table을 만든다.

drop table t1 purge;
drop table t2 purge;

create table t1(c1 int, c2 int);
create table t2(c1 int, c2 int);

insert into t1
select level, level
from dual
connect by level <= 10
;

insert into t2
select mod(level, 10)+1, level
from dual
connect by level <= 100
;

commit;



Data만으로 보면 Parent(T1)/Child(T2) 관계이지만 Oracle이 이 관계를 파악할 수 있는 방법은 없다.

다음과 같이 두 개의 View를 만든다. View의 "Column"에 유의하자.

create or replace view v1 
as
select 
  t1.c1, t2.c2
from 
  t1, t2
where
  t1.c1 = t2.c1
  and t1.c1 between 1 and 2
;



이 View는 Updatable한가? user_updatable_columns 뷰를 보면 알 수 있다.

select 
  column_name, updatable, insertable, deletable
from 
  user_updatable_columns
where
  table_name = 'V1'
;

COLUMN_NAME          UPDATABLE INSERTABL DELETABLE 
-------------------- --------- --------- --------- 
C1                   NO        NO        NO        
C2                   NO        NO        NO   

     

전혀 Updatable하지 않다. Oracle은 Parent/Child 관계를 알 수 없기 때문이다. 
View의 특정 Column이 Updatable하려면 Key-Preserved해야 하는데(Oracle의 용어이다), Key-Preserved한지를 Oracle이 판단할 수 없기 때문이다.

같은 이유로 다음과 같은 View도 전혀 Updatable하지 않다. 

create or replace view v2
as
select 
  t2.c1, t2.c2
from 
  t1, t2
where
  t1.c1 = t2.c1
  and t1.c1 between 1 and 2
;

select 
  column_name, updatable, insertable, deletable
from 
  user_updatable_columns
where
  table_name = 'V2'
;
COLUMN_NAME          UPDATABLE INSERTABL DELETABLE 
-------------------- --------- --------- --------- 
C1                   NO        NO        NO        
C2                   NO        NO        NO    

    

Updatable하지 않기 때문에 다음과 같이 Subquery(View)에 대한 Update는 실패한다.

update (
  select 
    t1.c1, t2.c2
  from
    t1, t2
  where
    t1.c1 = t2.c1
    and t1.c1 between 1 and 2
  ) x
set x.c2 = x.c2 + 1000
;

ERROR at line 10:
ORA-01779: cannot modify a column which maps to a non key-preserved table



update (
  select 
    t2.c1, t2.c2
  from
    t1, t2
  where
    t1.c1 = t2.c1
    and t1.c1 between 1 and 2
  ) x
set x.c2 = x.c2 + 1000
;

ERROR at line 10:
ORA-01779: cannot modify a column which maps to a non key-preserved table



이 문제를 해결하는 한가지 안좋은 방법은 /*+ BYPASS_UJVC */ Hint를 사용하는 것이다. 말 그대로 Updatable Join View Constraint(제약 조건)을 Bypass한다는 것이다.

update /*+ bypass_ujvc */ (
  select 
    t1.c1, t2.c2
  from
    t1, t2
  where
    t1.c1 = t2.c1
    and t1.c1 between 1 and 2
  ) x
set x.c2 = x.c2 + 1000
;

20 rows updated.




궁극적인 해결책은 Oracle에게 Parent/Child 관계를 알려 주는 것이다.

더 정확하게 이야기하면 t1:t2 = 1:N이며 t1.c1이 Unique하기 때문에 t2와 관련된 Column들은 Updatable하다는 것을 알려 주어야 한다.

다음과 같이 t1.c1에 primary key를 추가함으로써 이를 달성할 수 있다. 

alter table t1 add constraint t1_pk primary key (c1);



다음과 같이 일부 혹은 전체 Column들이 Updateable하다는 것을 확인할 수 있다. 

alter view v1 compile;
alter view v2 compile;

select 
  column_name, updatable, insertable, deletable
from 
  user_updatable_columns
where
  table_name = 'V1'
;
COLUMN_NAME          UPDATABLE INSERTABL DELETABLE 
-------------------- --------- --------- --------- 
C1                   NO        NO        NO        
C2                   YES       YES       YES       


select 
  column_name, updatable, insertable, deletable
from 
  user_updatable_columns
where
  table_name = 'V2'
;
COLUMN_NAME          UPDATABLE INSERTABL DELETABLE
-------------------- --------- --------- ---------
C1                   YES       YES       YES      
C2                   YES       YES       YES    

  

따라서 다음과 같이 Join View를 Update할 수 있다.
더 정확하게 말하면 user_updatable_columns 뷰에서 'YES'로 나온 Column에 대해서는 update/insert/delete가 가능하다.

update (
  select 
    t1.c1, t2.c2
  from
    t1, t2
  where
    t1.c1 = t2.c1
    and t1.c1 between 1 and 2
  ) x
set x.c2 = x.c2 + 1000
;
20 rows updated.



다음과 같은 Insert 구문은 어떨까? 성공할까?

insert into (
    select 
    t1.c1, t2.c2
  from
    t1, t2
  where
    t1.c1 = t2.c1
    and t1.c1 between 1 and 2
  ) 
values(1, 11)
;



Column t1.c1은 Updatable하지 않기 때문에 다음과 같이 에러가 난다.


ERROR at line 1:
ORA-01779: cannot modify a column which maps to a non key-preserved table



반면 다음과 같은 Insert는 성공한다. 
Key-Preserved인 Table에 대해서만 Insert가 이루어지기 때문이다.

insert into (
    select 
    t2.c1, t2.c2
  from
    t1, t2
  where
    t1.c1 = t2.c1
    and t1.c1 between 1 and 2
  ) 
values(1, 11)
;

1 row created.




다음과 같이 Delete도 가능하다.

delete from (
  select 
    t1.c1, t2.c2
  from
    t1, t2
  where
    t1.c1 = t2.c1
    and t1.c1 between 1 and 2
  ) x
;

20 rows deleted.



View에 대한 Update는 어떤 실행 계획을 보일까? 아래에 그 결과가 있다.

explain plan for
update (
  select
    t1.c1, t2.c2
  from
    t1, t2
  where
    t1.c1 = t2.c1
    and t1.c1 between 1 and 2
  ) x
set x.c2 = x.c2 + 1000
;



매우 논리적인 실행 계획을 보인다.

-----------------------------------------------------------------------------
| Id  | Operation           | Name  | Rows  | Bytes | Cost (%CPU)| Time     |
-----------------------------------------------------------------------------
|   0 | UPDATE STATEMENT    |       |     4 |   156 |     4   (0)| 00:00:01 |
|   1 |  UPDATE             | T2    |       |       |            |          |
|   2 |   NESTED LOOPS      |       |     4 |   156 |     4   (0)| 00:00:01 |
|*  3 |    TABLE ACCESS FULL| T2    |    20 |   520 |     4   (0)| 00:00:01 |
|*  4 |    INDEX UNIQUE SCAN| T1_PK |     1 |    13 |     0   (0)| 00:00:01 |
-----------------------------------------------------------------------------

Predicate Information (identified by operation id):   
---------------------------------------------------   
                                                      
   3 - filter("T2"."C1">=1 AND "T2"."C1"<=2)          
   4 - access("T1"."C1"="T2"."C1")                    
       filter("T1"."C1">=1 AND "T1"."C1"<=2)         




논의를 더 확장해보자. 
이런 Query에서 Parallel Execution은 가능한가? 
안될 이유가 없다.

explain plan for
update (
  select /*+ parallel(t2) parallel(t1) no_index(t2) no_index(t1) */ 
    t1.c1, t2.c2
  from
    t1, t2
  where
    t1.c1 = t2.c1
    and t1.c1 between 1 and 2
  ) x
set x.c2 = x.c2 + 1000
;



실행 계획은 다음과 같다.

-------------------------------------------------------------
| Id  | Operation                | Name     | Rows  | Bytes |
-------------------------------------------------------------
|   0 | UPDATE STATEMENT         |          |     4 |   156 |
|   1 |  UPDATE                  | T2       |       |       |
|   2 |   PX COORDINATOR         |          |       |       |
|   3 |    PX SEND QC (RANDOM)   | :TQ10001 |     4 |   156 |
|*  4 |     HASH JOIN            |          |     4 |   156 |
|   5 |      PX RECEIVE          |          |     2 |    26 |
|   6 |       PX SEND BROADCAST  | :TQ10000 |     2 |    26 |
|   7 |        PX BLOCK ITERATOR |          |     2 |    26 |
|*  8 |         TABLE ACCESS FULL| T1       |     2 |    26 |
|   9 |      PX BLOCK ITERATOR   |          |    20 |   520 |
|* 10 |       TABLE ACCESS FULL  | T2       |    20 |   520 |
-------------------------------------------------------------



View 부분(Select ...)은 명백하게 병렬 실행이 된다.
하지만 Update 부분은 병렬 실행이 되지 않는 것을 알 수 있다.

Parallel DML은 다음과 같이 명시적으로 활성화를 해야만 실행이 된다.

alter session enable parallel dml;

explain plan for
update (
  select /*+ parallel(t2) parallel(t1) no_index(t2) no_index(t1) */ 
    t1.c1, t2.c2
  from
    t1, t2
  where
    t1.c1 = t2.c1
    and t1.c1 between 1 and 2
  ) x
set x.c2 = x.c2 + 1000
;

---------------------------------------------------------------------
| Id  | Operation                        | Name     | Rows  | Bytes |
---------------------------------------------------------------------
|   0 | UPDATE STATEMENT                 |          |     4 |   156 |
|   1 |  PX COORDINATOR                  |          |       |       |
|   2 |   PX SEND QC (RANDOM)            | :TQ10002 |     4 |   156 |
|   3 |    UPDATE                        | T2       |       |       |
|   4 |     PX RECEIVE                   |          |     4 |   156 |
|   5 |      PX SEND HASH (BLOCK ADDRESS)| :TQ10001 |     4 |   156 |
|*  6 |       HASH JOIN                  |          |     4 |   156 |
|   7 |        PX RECEIVE                |          |     2 |    26 |
|   8 |         PX SEND BROADCAST        | :TQ10000 |     2 |    26 |
|   9 |          PX BLOCK ITERATOR       |          |     2 |    26 |
|* 10 |           TABLE ACCESS FULL      | T1       |     2 |    26 |
|  11 |        PX BLOCK ITERATOR         |          |    20 |   520 |
|* 12 |         TABLE ACCESS FULL        | T2       |    20 |   520 |
---------------------------------------------------------------------




Merge 구문이 등장함으로써 Updatable Join View의 기능을 어느 정도 대신할 수 있게 되었다. 
아래와 같이 Insert가 없는 Merge 구문을 사용하면 똑같은 효과를 얻을 수 있다.
(더 직관적이기도 하다)

merge into t2
using (select c1, c2 from t1 
          where c1 between 1 and 2) x
on (x.c1 = t2.c1)
when matched 
  then update set t2.c2 = t2.c2 + 1000
;

20 rows merged.



Updatable Join View나 Merge를 이용해서 하나의 Query로 View에 대해 DML이 불가능하다면?
개별 Table에 대한 여러 번의 DML로 나누어서 수행할 수 밖에 없는가?
만일 권한 관리 때문에 View에 대한 권한밖에 줄 수 없다면?

이런 상황을 해결할 수 있는 마지막 수단이 Instead of Trigger이다. 

가령 다음과 같은 View v1에 대한 Insert 문은 (당연하게도) 모두 실패한다.
(Column t1.c1은 Updatable하지 않기 때문이다)

insert into v1
values(1, 10)
;
ERROR at line 1:
ORA-01779: cannot modify a column which maps to a non key-preserved table

insert into v1
select values(100, 100);
ERROR at line 1:
ORA-01779: cannot modify a column which maps to a non key-preserved table


/*+ bypass_ujvc */ Hint를 사용하면 해결될까?

insert /*+ bypass_ujvc */ into v1 values(1, 10)
*
ERROR at line 1:
ORA-01776: cannot modify more than one base table through a join view


View v1은 Table t1, t2의 Column을 모두 참조하고 있기 때문에 두 개의 Table에 대해 Insert하는 것이 불가능하다.

이 문제를 해결할 수 있는 한가지 방법은 View에 대한 Trigger(Instead of Trigger)를 만드는 것이다.

create or replace trigger trg1
instead of insert on v1
for each row 
declare

begin
  insert into t1 values(:new.c1, 100);
  insert into t2 values(:new.c1, 100);
  
end;
/


다음과 같이 Insert가 모두 성공한다. 

insert into v1
values(1, 10)
;
ERROR at line 1:
ORA-00001: unique constraint (UKJA.T1_PK) violated
ORA-06512: at "UKJA.TRG1", line 4
ORA-04088: error during execution of trigger 'UKJA.TRG1'

insert into v1
values(100, 100);

1 row created.



역시 가장 중요한 것은 하나의 문제에 대해 여러 가지 솔루션이 있을 수 있다는 가정을 잊지 않는 것이다.



출처 : http://ukja.tistory.com/187

where절에 있는 서브쿼리 결과가 1개 이상인 경우

filter 방식 / join 방식 옵티마이저 


filter 방식 - 메인 sql 데이터 만큼 서브쿼리가 돈다.

즉, 메인 sql 100만건 돈다면 일일이 서브쿼리에게 100만건 input 한다

(서브 쿼리의 결과가 1개라고 한다면 성능저하 되지는 않지만 그런 경우는 거의 없으므로

성능이 안좋은 쿼리라고 할 수 있다)


join 방식 - 메인 sql 양이 많고 서브 sql 의 양이 적을때

선 서브 sql 선 scan 하여 join 할수 있다.


하지만 nested loops join 이외 다른 join은 filter 방식에 대한 효과를 얻지 못한다.

(join 하고자하는 input 값이 적을땐 join 방식보다 filter 방식이 더 성능상 이점을 가질

수 있다)


스칼라 서브쿼리를 사용할 때 결과 추출값이 많을 때는

서브쿼리에서 조인으로 변경한 후 hash join 유도 하는게 좋다 (no index scan)


결과 추출값이 적을 때는 조인에서 부분 범위처리 (rownum >=10)를 선행 후

스칼라 서브쿼리로 바꾸는것이 I/O 처리량을 줄일 수 있다


관련 내용은 여기에 자세히 

http://oracle10g.tistory.com/871



오라클 9i 에서부터


Bind 변수를 가진 SQL 에 대해서 최초파싱할 때, 실제 Bind 값을 먼저 확인하고, 히스토그램을 참조하여

최적화 하는 것을 말한다. 

(Bind 변수를 사용하는 경우, Query가 최초로 실행되는 시점의 실제 Bind 값을 이용해서(Peeking해서) 실행 계획을 세우는 것을 의미한다.)


Bind 변수를 사용하는 경우 실제로 실행되는 시점에 어떤 값이 들어오는지 알 수 없기 때문에 컬럼의 평균적인 분포만을 가지고 비용을 계산할 수 밖에 없다. 대부분의 경우에는 큰 문제가 안되지만, 다음과 같은 경우에는 치명적인 단점을 가지고 있다. 


가령 status 컬럼의 분포가 다음과 같다고 하면...

 - status = 1  : 99%

 - status = 99 : 1%


이 경우

- Where status = '1' 이면 인덱스를 경유하지 않고 Full Table Scan을 하는 것이 유리하다

- Where status = '99' 이면 인덱스를 경유하는 것이 유리하다.


하지만, 

- Where status = :b1 과 같이 Bind 변수를 사용하면 어떤 값이 올지 모르기 때문에 평균적인 분포를 따르게 된다. 따라서 이 경우 반드시 Full Table Scan을 선택한다. 


Bind Peeking은 이러한 단점을 피하기 위해 쿼리가 실행되는 최초 시점에 Bind 변수에 들어온 값을 이용하게 된다. 즉, 쿼리가 최초로 실행되는 시점에 


exec :b1 := '1';

... Where status = :b1 


과 같이 실행되면 Full Table Scan을,


exec :b2 := '99';

... Where status = :b1


과 같이 실행되면 Index Range scan을 선택하게 된다. 

단, 위와 같이 작동하려면 컬럼 값이 "1" 인 경우와 "99"인 경우의 분포도의 차이를 Oracle이 명확하게 이해하고 있어야 되므로 Histogram이 반드시 존재해야 한다. 가령 


exec dbms_stats.gather_table_stats(user, 'TABLE_NAME', method_opt=>'for columns size 2 status');


와 같이 컬럼에 대해 적당한 크기의 Histogram이 생성되어 있어야 된다.


위의 설명을 이해했다면 Bind Peeking에는 기본적인 딜레마가 있음을 알 수 있다. 위의 예를 보면 쿼리가 최초로 실행되는 시점에 "1"의 값이 오느냐, "99"의 값이 오느냐에 따라 실행 계획이 결정된다. 어떤 쪽이든 한 쪽은 손해를 볼 수 밖에 없는 결정적인 구조적 한계를 가지고 있다.


이런 구조적 한계를 가진 기능을 제공한다는 것은 기본적인 설계의 결함이라고 할 수 있다. 덕분에 많은 시스템에서 Bind Peeking 기능을 비활성화시켜 버리고 있다. _optim_peek_user_binds 파라미터 값을 false로 지정해 버리는 것이다. 


이런 경향은 10g에서 특히 심한데, 그 이유는 10g에서 dbms_stats의 method_opt 파라미터의 기본값이 for all columns size auto로 바뀌는 바람에 Histogram의 생성 여부를 Oracle이 자동으로 판단해버리는데 있다. 이전 버전에서는 for all columns size 1 이 기본값으로 Histogram이 생성되지 않았다.

Histogram이 없으면 Bind Peeking에 의한 부작용은 거의 없다. 10g에서 Histogram의 생성 여부를 Oracle이 판단함으로써 이전 버전에서는 존재하지 않던 Histogram이 갑자기 생성되고 이로 인해 Bind Peeking의 결함이 더욱 두드러지게 나타나게 되어 버린 것이다. 

이 때문에 Oracle 10g에서는 _optim_peek_user_binds 파라미터를 False로 바꾸는 것이 기본 권고안처럼 되어 버릴 정도가 되었다.


하지만, Oracle 11g에서 아주 큰 희소식이 전달되었다. 이른바 Adaptive Cursor Sharing이라는 기능이 소개된 것인데, 이 기능으로 인해 Bind Peeking과 Histogram의 조합에 의한 부작용이 말끔히 해소될 수 있다. 


Adaptive Cursor Sharing이란 말 그대로 상황에 맞게 유연하게 Cursor를 Share하겠다는 의미이다. 이 개념을 구현하기 위해 Oracle은 Bind Sensitive Cursor, Bind Aware Cursor라는 새로운 개념을 도입했다. 

Bind Sensitive Cursor란, 말 그대로 Bind 값에 민감한 Cursor라는 의미이다. 즉, Bind 값이 바뀌면 그것을 민감하게 처리하겠다는 의미이다. 1) Equal 조건에서는 조건절에 쓰인 컬럼에 Histogram이 있고 2) Range 조건인 경우 Oracle은 이것을 Bind Senstive Cursor라고 부른다.

Bind Aware Cursor란, Bind Sensitive Cursor에 입력된 Bind 값에 따라 실행 계획이 분화된 Cursor를 의미한다. 즉, Bind Aware Cursor가 생성되었다는 것은 Bind 변수의 값에 따라 Oracle이 적절한 Child Cursor를 생성했다는 것을 의미한다.


Adaptive Cursor Sharing 기법을 사용하면 Bind Peeking에 의해 최초 실행 시점에 실행 계획이 결정되더라도 이후 새로운 Bind 변수 값이 사용되면 이에 맞게 실행 계획을 분화(새로운 Child Cursor 생성)시킨다. 따라서 Bind Peeking에 의한 부작용이 사실상 없어지게 된다.

단, 조건절에 쓰인 컬럼에 Histogram이 있고, Histogram의 분포도에 따라 실행 계획에 큰 차이가 있을 수 있다고 판단된다는 조건이 중요하다. 즉, 적절한 Histogram 없이는 의미가 없다는 것이다. Histogram은 이제 우리가 반드시 이해하고 활용해야 하는 존재가 되었다.

(dbms_stats의 method_opt 파라미터의 기본값이 for all columns size auto로 바뀌는 순간 이미 Histogram은 피할 수 없는 존재가 되어버렸다는 사실을 기억할 필요가 있겠다)


아래 샘플 코드를 이용하면 Oracle 11g의 Adaptive Cursor Sharing 기법을 이해할 수 있다. 


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

-- Oracle 11g Bind Aware Cursor

-- Author: 조동욱

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


-- create objects

drop table acs_table;


create table acs_table(id int, name char(10));


create index acs_table_idx on acs_table(id);


insert into acs_table select 1, 'name' from all_objects where rownum <= 100000


insert into acs_table values(99, 'name');


commit;


-- gather statistics with histogram

exec dbms_stats.gather_table_stats(user, 'ACS_TABLE', estimate_percent=>100, method_opt=>'FOR COLUMNS SIZE 2 ID', cascade=>true);


-- check histogram

select * from dba_tab_histograms where table_name = 'ACS_TABLE';


-- Bind query

alter system flush shared_pool


var id number;


-- id == 1

-- 각 단계마다 아래 쿼리 결과 확인

select sql_id, sql_text,is_bind_sensitive,is_bind_aware

from v$sql where sql_text like 'select count(name) from acs_table%';


exec :id := 1;


select count(name) from acs_table where id = :id;


-- id == 99

exec :id := 99;


select count(name) from acs_table where id = :id;


select count(name) from acs_table where id = :id;


-- id == 1 again

exec :id := 1;


select count(name) from acs_table where id = :id;


-- check mismatch

select * from v$sql_shared_cursor where sql_id = '<sql_id>';





출처 : http://ukja.tistory.com/entry/Bind-Peeking%EC%9D%98-%EB%94%9C%EB%A0%88%EB%A7%88%EC%99%80-Oracle-11g%EC%9D%98-Adaptive-Cursor-Sharing

+ Recent posts