글의 내용이 가슴에 와닫지 않는다는 독자가 있다. 필자의 잘못이다. 독자들을위하여 일부 내용을 댓글에 추가 하였으므로 반드시 확인하기 바란다. (2009.07.17)
대분분 튜닝을 해본 사람이면 USE_CONCAT 힌트를 잘 알고 있다고 생각할 것 이다. 하지만 문제는 얼마나 정확히 아는가가 중요하다. IN 이나 OR 조건이 있는 SQL에 USE_CONCAT 힌트를 사용하면 OR_Expansion(Union All 로 분리되는 쿼리변환) 이 발생한다는 것은 누구나 알것이다. 이것은 개발자들에 대한 일반적인 튜닝 가이드인것은 분명하다. 메뉴얼에는 분명히 이렇게 되어있다.
힌트 정확히 알기
이 힌트는 인자가 2개가 필요한데 튜닝을 전담하는 사람들까지도 이런 사실을 모르고 있으니 큰일이 아닐수 없다. 하지만 정확한 용법을 모르고 SQL을 튜닝을 하는 사람들을 비난할수는 없다. 그어떤 문서에도 USE_CONCAT의 용법이 자세히 나온 것이 없기 때문이다. 이럴경우 다양한 연구및 테스트를 진행 해보는수 밖에 없다. 아니면 SR 이라는 방법이 있기는 하다. 하지만 이러한 경우 SR 의 답변 성공률은 아주 낮다.
그럼 이제부터 용법을 하나씩 살펴보자.
환경 : 오라클 11.1.0.6
인덱스 상황 : employees(manager_id) , departments(department_id)
아래의 SQL 은 departments 테이블에서 department_id 컬럼에 대해서 IN 조건이 있고 employees 테이블에서 manager_id 컬럼에 대해서 IN 조건이 있다. 따라서 USE_CONCAT 힌트를 사용한다면 UNION ALL 로 구분될수 있는 SQL 이 4개(departments 2개, employees 2개)가 나올것이다.
SELECT /*+ QB_NAME(MAIN) */
e.employee_id, e.first_name, e.last_name, d.department_name
FROM employees e, departments d
WHERE e.department_id = d.department_id
AND ( d.department_id IN (:v_dept1, :v_dept2)
OR e.manager_id IN (:v_manager1, :v_manager2)
) ;
---------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)|
---------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 19 | 798 | 6 (17)|
| 1 | MERGE JOIN | | 19 | 798 | 6 (17)|
| 2 | TABLE ACCESS BY INDEX ROWID| DEPARTMENT | 27 | 432 | 2 (0)|
| 3 | INDEX FULL SCAN | DEPT_ID_PK1 | 27 | | 1 (0)|
|* 4 | FILTER | | | | |
|* 5 | SORT JOIN | | 107 | 2782 | 4 (25)|
| 6 | TABLE ACCESS FULL | EMPLOYEE | 107 | 2782 | 3 (0)|
---------------------------------------------------------------------------------
Plan 을 보면 OR_Expansion 이 발생하지 않았는데 옵티마이져는 Union All 로 분리하지 않는것이 가장 비용이 저렴했다는 것을 알수 있다.
그럼이제 USE_CONCAT 힌트를 사용해보자.
SELECT /*+ QB_NAME(MAIN) USE_CONCAT */
e.employee_id, e.first_name, e.last_name, d.department_name
FROM employees e, departments d
WHERE e.department_id = d.department_id
AND ( d.department_id IN (:v_dept1, :v_dept2)
OR e.manager_id IN (:v_manager1, :v_manager2)
) ;
------------------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)|
------------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 29 | 1218 | 9 (12)|
| 1 | CONCATENATION | | | | |
| 2 | MERGE JOIN | | 12 | 504 | 5 (20)|
| 3 | TABLE ACCESS BY INDEX ROWID | DEPARTMENT | 27 | 432 | 2 (0)|
| 4 | INDEX FULL SCAN | DEPT_ID_PK1 | 27 | | 1 (0)|
|* 5 | SORT JOIN | | 12 | 312 | 3 (34)|
| 6 | INLIST ITERATOR | | | | |
| 7 | TABLE ACCESS BY INDEX ROWID| EMPLOYEE | 12 | 312 | 2 (0)|
|* 8 | INDEX RANGE SCAN | EMP_MANAGER_IX | 12 | | 1 (0)|
| 9 | NESTED LOOPS | | | | |
| 10 | NESTED LOOPS | | 17 | 714 | 4 (0)|
| 11 | INLIST ITERATOR | | | | |
| 12 | TABLE ACCESS BY INDEX ROWID| DEPARTMENT | 2 | 32 | 2 (0)|
|* 13 | INDEX UNIQUE SCAN | DEPT_ID_PK1 | 2 | | 1 (0)|
|* 14 | INDEX RANGE SCAN | EMP_DEPARTMENT_IX | 10 | | 0 (0)|
|* 15 | TABLE ACCESS BY INDEX ROWID | EMPLOYEE | 9 | 234 | 1 (0)|
------------------------------------------------------------------------------------------
위의 Plan 을 보면 Id 기준으로 2번에 CONCATENATION 이 발생하였으므로 Union ALL 로 분리가 된것이다.
즉 옵티마이져는 SQL 을 아래와 같이 변형시킨것이다.
FROM employee e, department d
WHERE e.department_id = d.department_id
AND e.manager_id IN (:v_manager1, :v_manager2)
UNION ALL
SELECT e.employee_id, e.first_name, e.last_name, e.email, d.department_name
FROM employee e, department d
WHERE e.department_id = d.department_id
AND d.department_id IN (:v_dept1, :v_dept2)
AND ( LNNVL(e.manager_id=:v_manager1) AND LNNVL(e.manager_id=:v_manager2) ) ;
Union All 로 분리된 아래쪽의 SQL 에서 LNNVL을 사용한 이유는 윗쪽의 SQL이 이미 e.manager_id IN (:v_manager1, :v_manager2) 조건을 만족하는 데이터에 대하여 SELECT 를 하였으므로 아래쪽에서 또다시 SELECT 되는것을 막기위함이다.
Inlist 에 대해서는 Union All 로 분리되지 않았다.
위의 PLAN 을 자세히 보면 e.manager_id IN (:v_manager1, :v_manager2) 조건에 대해서는 Union All 로 분리되지 않았다. d.department_id IN (:v_dept1, :v_dept2) 조건 또한 마찬가지이다. 하지만 이것은 전통적인 OR_Expansion 변환이 아니다. USE_CONCAT 힌트 대신에 RULE 힌트를 사용할 경우 Plan은 아래와 같다.
------------------------------------------------------------
| Id | Operation | Name |
------------------------------------------------------------
| 0 | SELECT STATEMENT | |
| 1 | CONCATENATION | |
| 2 | NESTED LOOPS | |
| 3 | NESTED LOOPS | |
| 4 | TABLE ACCESS BY INDEX ROWID| EMPLOYEE |
|* 5 | INDEX RANGE SCAN | EMP_MANAGER_IX |
|* 6 | INDEX UNIQUE SCAN | DEPT_ID_PK1 |
| 7 | TABLE ACCESS BY INDEX ROWID | DEPARTMENT |
| 8 | NESTED LOOPS | |
| 9 | NESTED LOOPS | |
| 10 | TABLE ACCESS BY INDEX ROWID| EMPLOYEE |
|* 11 | INDEX RANGE SCAN | EMP_MANAGER_IX |
|* 12 | INDEX UNIQUE SCAN | DEPT_ID_PK1 |
| 13 | TABLE ACCESS BY INDEX ROWID | DEPARTMENT |
| 14 | NESTED LOOPS | |
| 15 | TABLE ACCESS BY INDEX ROWID | DEPARTMENT |
|* 16 | INDEX UNIQUE SCAN | DEPT_ID_PK1 |
|* 17 | TABLE ACCESS BY INDEX ROWID | EMPLOYEE |
|* 18 | INDEX RANGE SCAN | EMP_DEPARTMENT_IX |
| 19 | NESTED LOOPS | |
| 20 | TABLE ACCESS BY INDEX ROWID | DEPARTMENT |
|* 21 | INDEX UNIQUE SCAN | DEPT_ID_PK1 |
|* 22 | TABLE ACCESS BY INDEX ROWID | EMPLOYEE |
|* 23 | INDEX RANGE SCAN | EMP_DEPARTMENT_IX |
------------------------------------------------------------
RULE 힌트를 적용한 Plan은 우리가 예상한대로 UNION ALL 로 구분될수 있는 SQL이 4개(departments 2개, employees 2개)가 생성 되었다. 결국 CBO는 Inlist Plan을 사용할수 있는 경우에는 OR_Expansion 변환을 수행하지 않음을 알수 있다. 하지만 이렇게 결말을 내기에는 너무 싱겁다.
PLAN 고정 시키기
CBO 상황에서 조건절에 Inlist 가 있을 경우 항상 OR_Expansion 변환을 수행하지 않게 PLAN을 고정 하려면 어떻게 하면 될까?
그냥 /*+ USE_CONCAT*/ 이렇게만 사용하면 되는 걸까?
위의 질문들을 해결하려면 DBMS_XPLAN.DISPLAY_CURSOR 함수를 사용해서 Outline Data 정보를 보면 된다.
e.employee_id, e.first_name, e.last_name, d.department_name
FROM employees e, departments d
WHERE e.department_id = d.department_id
AND ( d.department_id IN (:v_dept1, :v_dept2)
OR e.manager_id IN (:v_manager1, :v_manager2)
) ;
위의 SQL 에 대한 PLAN 은 USE_CONCAT 을 사용하였으므로 이미 수행한 SQL의 PLAN 과 동일하므로 생략하고 Outline Data만 보기로 하자.
Outline Data
-------------
/*+
...중간생략
USE_CONCAT(@"MAIN" 8)
...중간생략
*/
숫자 8의 의미
오라클이 내부적으로 USE_CONCAT 힌트에 인자 2개를 사용한것을 알수 있다. 첫번째 인자는 쿼리블럭명이고 두번째 인자인 8의 의미는 Inlist 를 사용할수 있는 경우에는 Union All 로 분리하지 말것을 강제하는 힌트이다.
자존심이 허락지 않는다.
여기서 한단계 더 나아가 보자. 이번에는 거꾸로 Inlist 를 사용한 경우에도 무조건 Union All 로 분리되게 할수 있을까? RULE 힌트를 사용하라고? 그것은 언제 어떤버젼에서 없어질지 알수없는 아주 위험한 힌트이다.
또한 CBO 상황에서 이러한 힌트를 사용한다는 것은 자존심이 허락지 않는다.
아래의 SQL 을 보자.
e.employee_id, e.first_name, e.last_name, d.department_name
FROM employees e, departments d
WHERE e.department_id = d.department_id
AND ( d.department_id IN (:v_dept1, :v_dept2)
OR e.manager_id IN (:v_manager1, :v_manager2)
) ;
숫자 1의 의미
USE_CONCAT 힌트에 숫자 1을 사용하였다. 이것이 의미하는 바는 가능한 경우 모두 Union All 로 분리하라는 뜻이다. 이제 Plan 을 보자.
--------------------------------------------------------------------------------------------
| Id | Operation | Name | A-Rows | A-Time | Buffers |
--------------------------------------------------------------------------------------------
| 1 | CONCATENATION | | 3 |00:00:00.01 | 12 |
| 2 | NESTED LOOPS | | 0 |00:00:00.01 | 1 |
| 3 | NESTED LOOPS | | 0 |00:00:00.01 | 1 |
| 4 | TABLE ACCESS BY INDEX ROWID| EMPLOYEE | 0 |00:00:00.01 | 1 |
|* 5 | INDEX RANGE SCAN | EMP_MANAGER_IX | 0 |00:00:00.01 | 1 |
|* 6 | INDEX UNIQUE SCAN | DEPT_ID_PK1 | 0 |00:00:00.01 | 0 |
| 7 | TABLE ACCESS BY INDEX ROWID | DEPARTMENT | 0 |00:00:00.01 | 0 |
| 8 | NESTED LOOPS | | 0 |00:00:00.01 | 1 |
| 9 | NESTED LOOPS | | 0 |00:00:00.01 | 1 |
| 10 | TABLE ACCESS BY INDEX ROWID| EMPLOYEE | 0 |00:00:00.01 | 1 |
|* 11 | INDEX RANGE SCAN | EMP_MANAGER_IX | 0 |00:00:00.01 | 1 |
|* 12 | INDEX UNIQUE SCAN | DEPT_ID_PK1 | 0 |00:00:00.01 | 0 |
| 13 | TABLE ACCESS BY INDEX ROWID | DEPARTMENT | 0 |00:00:00.01 | 0 |
| 14 | NESTED LOOPS | | 2 |00:00:00.01 | 6 |
| 15 | TABLE ACCESS BY INDEX ROWID | DEPARTMENT | 1 |00:00:00.01 | 2 |
|* 16 | INDEX UNIQUE SCAN | DEPT_ID_PK1 | 1 |00:00:00.01 | 1 |
|* 17 | TABLE ACCESS BY INDEX ROWID | EMPLOYEE | 2 |00:00:00.01 | 4 |
|* 18 | INDEX RANGE SCAN | EMP_DEPARTMENT_IX | 2 |00:00:00.01 | 2 |
| 19 | NESTED LOOPS | | 1 |00:00:00.01 | 4 |
| 20 | TABLE ACCESS BY INDEX ROWID | DEPARTMENT | 1 |00:00:00.01 | 2 |
|* 21 | INDEX UNIQUE SCAN | DEPT_ID_PK1 | 1 |00:00:00.01 | 1 |
|* 22 | TABLE ACCESS BY INDEX ROWID | EMPLOYEE | 1 |00:00:00.01 | 2 |
|* 23 | INDEX RANGE SCAN | EMP_DEPARTMENT_IX | 1 |00:00:00.01 | 1 |
--------------------------------------------------------------------------------------------
인자를 1로 바꾸자 IN 조건 혹은 OR 조건에 대하여 모두 Union All 로 분리 되었다. 이제 모든 궁금증이 해소 되었다.
결론 :
인자없이 힌트를 USE_CONCAT(@MAIN)으로 사용 한다면 모든경우의 수를 다 고려하여 가장 비용이 적게드는 Plan 을 선택 할것이다. 심지어 USE_CONCAT 힌트를 사용 하였지만 분리되지 않는 경우가 있는데 이것은 힌트를 무시한 것이 아니라 옵티마이져 입장에서 비용계산을 기준으로 가장 저렴한 PLAN 을 선택한것이다. 만약 힌트를 사용하였지만 Union ALL 로 분리가 안되며 이것 때문에 성능이 문제가 된다면 USE_CONCAT 힌트의 숫자 인자(1혹은 8)를 활용하여 적절하게 튜닝할수 있어야 한다.
힌트를 제대로 아는 것이 얼마나 중요한지 가슴에 와 닿아야 할것이다. 생각해보라 의사들이 수술용 칼이나 마취용 주사 같은 것을 규정에 맞게 아주 정밀하고 세밀하게 사용하지 않고 대충 사용한다면 큰일이 날수 도 있을 것이다. 힌트도 마찬가지로 생각해야 한다.
'Oracle > SQL Tuning' 카테고리의 다른 글
오라클의 Update문은 적절한가? (15) | 2010.04.14 |
---|---|
Connect By VS ANSI SQL (7) | 2010.02.11 |
분석함수를 이용한 TOP SQL은 튜닝이 불가한가? (5) | 2009.05.22 |
Top SQL-( Rownum 의 정확한 사용법 ) (2) | 2009.03.26 |
인덱스없는 컬럼의 Order by 시 페이징 처리는 효율적인가? (2) | 2008.08.26 |