쿼리 튜닝시 가끔 오라클이 똑똑하다고 느낄때가 있다
오늘은 그중에서 Query Transformation 이 되는 경우를 소개한다.
아래의 스크립트를 보자.

select a.empno, a.ename, b.dname
  from emp a,
       (select deptno, dname
          from dept
         where deptno = :v_deptno) b
 where a.deptno = b.deptno; 

----------------------------------------------------------------------------------------
| Id  | Operation                    | Name    | Rows  | Bytes | Cost (%CPU)| Time     |
----------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT             |         |   205 |  4715 |     4   (0)| 00:00:01 |
|   1 |  NESTED LOOPS                |         |   205 |  4715 |     4   (0)| 00:00:01 |
|   2 |   TABLE ACCESS BY INDEX ROWID| DEPT    |     1 |    13 |     1   (0)| 00:00:01 |
|*  3 |    INDEX UNIQUE SCAN         | PK_DEPT |     1 |       |     0   (0)| 00:00:01 |
|*  4 |   TABLE ACCESS FULL          | EMP     |   205 |  2050 |     3   (0)| 00:00:01 |
----------------------------------------------------------------------------------------

위스크립트를 보면  select 절에 b.dname 을 사용한다.
하지만 아래를 스크립트를 보면 인라인뷰 내에서는 dname 을 select 하지만 최종 select 절에서
b.dname 을 빼고 실행한 결과이다.
물론 아래상태에서 VIEW MERGE 가 진행 될것이다.

select a.empno, a.ename
   from emp a,
         (select deptno, dname
             from dept
          where deptno = :v_deptno) b
where a.deptno = b.deptno;  

------------------------------------------------------------------------------
| Id  | Operation          | Name    | Rows  | Bytes | Cost (%CPU)| Time     |
------------------------------------------------------------------------------
|   0 | SELECT STATEMENT   |         |   205 |  2665 |     3   (0)| 00:00:01 |
|   1 |  NESTED LOOPS      |         |   205 |  2665 |     3   (0)| 00:00:01 |
|*  2 |   INDEX UNIQUE SCAN| PK_DEPT |     1 |     3 |     0   (0)| 00:00:01 |
|*  3 |   TABLE ACCESS FULL| EMP     |   205 |  2050 |     3   (0)| 00:00:01 |
------------------------------------------------------------------------------


위 plan 을 보면 dept 에 rowid 로 테이블 엑세스 하는 operation 이 사라졌다.
신기하지 않은가?
오라클이 알아서 최종 select list 에서 d.dname 제거 한것을 알아채고 Query Transformation 을 하여
인덱스만 access 한것이다.
즉 아래의 스크립트 처럼 옵티마이져가 쿼리를 수정한것이다.

select a.empno, a.ename
   from emp a,
         (select deptno
             from dept
          where deptno = :v_deptno) b
where a.deptno = b.deptno;  


물론 옵티마이져가 사람이라면 아예 인라인뷰 b 를 빼고 아래처럼 재작성 할것이다.
아래처럼 해도 결과는 똑같기 때문이다.

 select a.empno, a.ename
    from emp a
 where a.deptno  = :v_deptno;
Posted by extremedb
,

Relational Database Index Design and the Optimizers

튜닝의 관점에서 가장중요하고 기본적인 것 3가지는 무엇일까?
DBMS의 구조의 이해? 아니면 Wait Event?
모두 중요하지만 정답이아니다.
사실 개발자나 설계자들이 모든 DBMS 의 구조나 Wait Event를 알기란 사실상 불가능하다.

필자가 바라보는 가장 중요한 세가지는 아래와 같다.
1.효율적인 스키마(테이블) 디자인  
2.고품질의 SQL 작성능력
3.전략적인 인덱스 설계

너무도 당연한 이야기 일까?
하지만 위의 3가지를 모두만족 하는 사이트는 거의 없다는 것이 현실이다.

위의 세가지중에 오늘은 RDB 인덱스 대하여 깊이있는 주제를 다루는 책에 대하여 이야기 할것이다.
RDB 튜닝에 있어서 인덱스의 중요성은 다른무엇보다도 크다고 하겠다.

사용자 삽입 이미지

이책의 주제는 인덱스이다.
인덱스 가지고 책한권이나 읽어야 하나?
그런 생각이 날법도 하지만 일단 한장 한장 넘기다 보면
그런 생각이 싹 사라진다.
이책은 거의 모든 메이져 RDBMS 사의 인덱스를 커버하며
이책에서 제공하는 공식 또한 모든 RDB 에서 적용이 가능하다.
참고로 이책은 공식으로 시작하고 공식으로 끝난다.
물론 어려운 수학공식이 아닌 쉬운 계산식이다.

한가지 아쉬운점은 IBM DB2 의 기준으로 글이 쓰여졌다는 점이다.
다시말하면 DB2 엔지니어의 관점으로 Oracle 이나 SQL 서버를 바로보고 있다는 점이다.
하지만 인덱스의 특성상 DBMS 마다 방식이 다를수 없는 것 또한 인덱스의 특징이다.
물론  각 밴더사의 특수한 인덱스는 특성이 서로다를수 있지만 이책의 범위 밖이다.
인덱스관련 책이라고 하지만 인덱스와 옵티마이져의 관계, SQL 작성법 및 조인, 역정규화 등등 인덱스와 직/간접적으로 관련된 분야 까지도 언급하고 있다.

가장 재미있는 점은 저자는 모든 인덱스를 3가지 종류로 나누고 있다.
1 Star 인덱스
2 Star 인덱스
3 Star 인덱스
각각의 특성및 적용방법에 대해서는 언급하지 않고 여러분들의 몫으로 돌린다.

한가지 옥의티 라면 페이지수(300 페이지) 에 비해서 양장본(Hard Cover)이므로 너무 비싸다는 점이다.
하지만 내가 여러분이라면 그리고 튜닝에 관심이 있다면 꼭 구입 할것이다.
한국뿐만 아니라 전세계적으로 인덱스를 주제로 하는 책이 전무한 실정에서 이책은 가뭄에 단비와 같다고 하겠다.
특히 인덱스 설계나 인덱스 디자인 차트를 작성하는 설계자및 튜너에게 필요한 책이라고 할수 있다.
여러분 각자 사내 스터디 그룹을 만들고 이책을 마스터 하는것은 어떨까?

필자는 이책에 별 5개를 주고싶다.
평가 : : ★★★★★

Amazon 평가 : Relational Database Index Design and the Optimizers

RELATIONAL DATABASE INDEX DESIGN AND THE Optimizers 상세보기
Lahdenmaki, Tapio/ Leach, Michael 지음 | Wiley 펴냄


PS :
필자는 책장사가 아니다.
가끔씩 고품질의 RDB 책을 소개하는 사람일 뿐이다.(오해하지 마시길)

Posted by extremedb
,

몇주전에 필자는  DBA 로부터 _optimizer_push_pred_cost_based 값을 True 에서 False 로 바꾸겠다는 보고를 받았다.
즉 Push Predicate 에 대해서는 CBQT(Cost Based Query Transformation) 을 사용하지 않고 Heuristic 변환 이나 Rule 변환 (Costing 을 하지않고 무조건 조건절이 push predicate 됨)방법을 사용하겠다는 뜻이다.
이유는 ora-600 에러와 Update, Delete 시에 where 절에 서브쿼리를 사용할때 서브쿼리내에 Union all 을 사용할경우 Wrong Result 가 나온다는 것이었다.
섬뜩한 결정이다.
일단 필자는 DBA 들에게 기다려보라고 이야기 하였다.
시스템이 OLTP 이니 Heuristic 변환 이 더맞을 수 있을거 같지만 필자가 프로젝트를 진행하는 곳은 False 로 바꿀경우에 더욱 무서운 일이 기다리고 있었다.
아래의 스크립트를 보자.

현상 :파라미터를 FALSE로 바꾸고 조인을 사용하는 뷰를 메인쿼리에서 아우터조인 하는경우 Push Predicate 적용안됨.
필자가 있는프로젝트의 경우 대부분의 쿼리가 경우 공통코드뷰를 사용하며 아우터조인 하는 경우도 30% 나 되었다.(갯수로 따지면 1500 개 정도의 쿼리임)
따라서  FALSE 로 바꿀경우 VW_공통코드를 아우터조인하는 쿼리를 전부 Push Predicate 되게 쿼리를 고쳐야 하는 상황 이었다.
오라클 버젼 10.2.0.3 이며 FIRST_ROWS_1 환경에서 테스트 되었다.

1.문제가 되는 쿼리 패턴

SELECT  A.고객번호   
        A.주민번호                                           
        A.고객명                                 
        A.고객분류코드                               
        B.고객분류코드명                               
FROM   TB_고객기본  A,
            VW_공통코드  B           
WHERE  A.주민번호        = :V_주민번호          
AND    B.공통대분류코드   (+)  = '고객분류코드'          
AND    B.공통소분류코드   (+)  = A.고객분류코드;



여기서 VW_공통코드는 두개의 테이블로 되어 있으며
공통대분류코드와 공통소분류코드가 조인이 되어있는 상태이다.

2. VW_공통코드 의 스크립트
--대부분의 사이트에서 사용하는 전형적인 공통코드 뷰이다.

CREATE VIEW VW_공통코드 AS
SELECT A.공통대분류코드,
             A.공통대분류코드사용여부,
             B.공통소분류코드,
             B.공통소분류코드명
    FROM TB_공통대분류코드 A,
              TB_공통소분류코드 B
  WHERE A.공통대분류코드 = B.공통대분류코드;  



3.인덱스 현황
TB_고객기본  : (주민번호) --> UNIQUE 인덱스
TB_공통대분류코드 : (공통대분류코드) --> PK 인덱스
TB_공통소분류코드 : (공통대분류코드, 공통소분류코드) --> PK 인덱스

4.PLAN

--  _optimizer_push_pred_cost_based  = true 일경우의 plan  

Execution Plan
--------------------------------------------------------------------------------
SELECT STATEMENT Optimizer=FIRST_ROWS_1 (Cost=3 Card=1 Bytes=119)
  NESTED LOOPS (OUTER) (Cost=3 Card=1 Bytes=119)
    TABLE ACCESS (BY GLOBAL INDEX ROWID) OF 'TB_고객기본' (TABLE) (Cost=1 Card=1 Bytes=38)
      INDEX (UNIQUE SCAN) OF 'IX_고객기본_주민번호' (INDEX (UNIQUE)) (Cost=1 Card=1)
    VIEW PUSHED PREDICATE OF 'VW_공통코드' (VIEW) (Cost=2 Card=1 Bytes=81) --> PUSHED PREDICATE 작동함.
      NESTED LOOPS (Cost=2 Card=1 Bytes=54)
        TABLE ACCESS (BY INDEX ROWID) OF 'TB_공통소분류코드' (TABLE) (Cost=1 Card=1 Bytes=39)
          INDEX (UNIQUE SCAN) OF 'PK_공통소분류코드' (INDEX (UNIQUE)) (Cost=1 Card=1)  ---> unique scan
        INDEX (UNIQUE SCAN) OF 'PK_공통대분코드' (INDEX (UNIQUE)) (Cost=1 Card=1 Bytes=15)



-- --  _optimizer_push_pred_cost_based  =  false 일경우의 plan

Execution Plan
--------------------------------------------------------------------------------
SELECT STATEMENT Optimizer=FIRST_ROWS_1 (Cost=13 Card=1 Bytes=77)
  NESTED LOOPS (OUTER) (Cost=13 Card=1 Bytes=77)
    TABLE ACCESS (BY GLOBAL INDEX ROWID) OF 'TB_고객기본' (TABLE) (Cost=1 Card=1 Bytes=35)
      INDEX (UNIQUE SCAN) OF 'IX_고객기본_주민번호' (INDEX (UNIQUE)) (Cost=1 Card=1)
    VIEW OF 'VW_공통코드' (VIEW) (Cost=12 Card=1 Bytes=42)         --> PUSHED PREDICATE 작동안함.   
      NESTED LOOPS (Cost=2 Card=20 Bytes=980)
        INDEX (UNIQUE SCAN) OF 'PK_공통대분코드' (INDEX (UNIQUE)) (Cost=1 Card=1 Bytes=15)
        TABLE ACCESS (BY INDEX ROWID) OF 'TB_공통소분류코드' (TABLE) (Cost=1 Card=20 Bytes=680)
          INDEX (RANGE SCAN) OF 'PK_공통소분류코드' (INDEX (UNIQUE)) (Cost=1 Card=20) ---> range scan



FALSE 일경우 이런스타일의 쿼리에서는 PUSH_PRED 힌트도 통하지 않는다.
NESTED LOOP 방식에서 LOOP 내에서 건건이 해당하는 대분류코드 전체를 RANGE 스캔하므로 전체적인 시스템관점에서 심각한 성능저하 현상을 발생시킬수 있다는 것이다.
이런스타일의 쿼리에는 Heuristic 이나 Rule 변환의 사각지대인거 같다.
이러한 패턴의 SQL 문이 1500 개 이상이 되는 상황에서 디폴트 값인 TRUE 를 FALSE 로 바꿀수 없었으며
TRUE 로 놓고 문제가 되는 쿼리를 역으로 잡기로 하였다.
즉 문제가 되는 쿼리만 SESSION 단위로 FALSE 로 하기로 하였다.
다행인것은 ALERT LOG 를 분석한결과 ORA-600 에러는 발생하지 않았으며
Update, Delete 시에 where 절에 서브쿼리를 사용할때 서브쿼리내에 Union all 을 사용할경우 Wrong Result 가 나오는 버그를 유발하는 쿼리는 없었으며 이또한 만약을 위하여 Patch를 Request 한 상태이다.

결론 :
옵티마이져의 특성과 관련된 히든 파라미터의 수정은 매우 신중해야 하며 SIDE EFFECT 에 의한 피해를 최소화 하기 위하여 노력해야한다.
버그를 피할 목적으로 무조건 DEFAULT 값을 무시하고 버그없는 세팅으로 바꾸는 경우가 있다.
이런경우 거의 예외없이 재앙이 따른다.
필자의 권고안은 제일 좋은 것은 패치미며 그것이 안될때는 DEFAULT 값으로 놓고 문제가 있는 SESSION 단위로 파라미터를 변경하는것을 권장한다.

Posted by extremedb
,

히스토그램의 가상(virtual) 컬럼이용

개념 :
FBI (Function Based Index) 를 사용하고 히스토그램을 생성하면 오라클은 유져의 의지와는 상관없이 히스토그램에 가상컬럼을 사용한다.
가상컬럼은 11g 에서는 아예 컬럼값으로 인정하고 파티션및 인덱스도 생성할수 있게 되었다.
예를 들면 upper(컬럼1) 로 해서 컬럼을 생성할수 있는것이다.
하지만 11g 에서도 가상컬럼을 인덱스로 만들면 FBI 로 생성된다.
FBI 사용시 히스토그램에서 가상(virtual) 컬럼이용의 개념과 주의사항을 살펴본다.
아래예제는 오라클 10gR2 에서 테스트 하였다.

--테이블 생성
create table HIDDEN_COL_TEST (A varchar2(20) , B varchar2(100));

-- 데이터 생성
--컬럼 A 에는 분포도가 'a' 가 50%, 'A' 가 50% 로 생성한다.
insert into HIDDEN_COL_TEST

select 'a' , rpad('b',100) from all_objects
union all
select 'A' , rpad('b',100) from all_objects;

commit;

--통계정보생성
EXEC dbms_stats.gather_table_stats(user,'HIDDEN_COL_TEST',cascade=>true);

--인덱스 생성
create index HIDDEN_COL_IDX on HIDDEN_COL_TEST ( upper(A) ) ;

--인덱스 통계생성
EXEC dbms_stats.gather_index_stats(user,'HIDDEN_COL_IDX');

이제 준비가 다되었다.
explain plan for select * from HIDDEN_COL_TEST where upper(A) = :v_bind;
-->변수에 'a' 사용함.

위의쿼리의 경우 전체건이 조회된다.
Plan은 당연히 FTS( Full Table Scan) 으로 풀려야함에도 불구하고 옵티마이져는 우리의 기대를 져버린다.


select * from table(dbms_xplan.display);

---------------------------------------------------------------
Id Operation Name Rows
---------------------------------------------------------------
0 SELECT STATEMENT 792
1 TABLE ACCESS BY INDEX ROWID HIDDEN_COL_TEST 792
* 2 INDEX RANGE SCAN HIDDEN_COL_IDX 320
---------------------------------------------------------------

전체건수의 100% 에 해당하는 데이터를 오라클은 인덱스를 사용하였다.왜그럴까?
아래는 dynamic_sampling 을 최고수준으로 주었지만 별소용이 없었다.


explain plan for
select /*+ dynamic_sampling(HIDDEN_COL_TEST 10) */ *
from HIDDEN_COL_TEST where upper(A) = :v_bind;

select * from table(dbms_xplan.display);

---------------------------------------------------------------
Id Operation Name Rows
---------------------------------------------------------------
0 SELECT STATEMENT 792
1 TABLE ACCESS BY INDEX ROWID HIDDEN_COL_TEST 792
* 2 INDEX RANGE SCAN HIDDEN_COL_IDX 320
---------------------------------------------------------------

오라클이 비정상적으로 인덱스를 사용한 이유는 히스토그램에 가상컬럼을 생성시켜 주지 않았기 때문이다.

히스토그램을 조회해보자.
SELECT TABLE_NAME, COLUMN_NAME, ENDPOINT_NUMBER, ENDPOINT_VALUE FROM USER_TAB_HISTOGRAMS WHERE table_name = 'HIDDEN_COL_TEST';

TABLE_NAME COLUMN_NAME ENDPOINT_NUMBER ENDPOINT_VALUE
---------------------- ------------------- ------------------------ ------------------------
HIDDEN_COL_TEST A 0 3.37499295804764E35
HIDDEN_COL_TEST B 0 5.09496674487288E35
HIDDEN_COL_TEST A 1 5.03652795277878E35
HIDDEN_COL_TEST B 1 5.09496674487288E35

아직 가상컬럼이 나타나지 않았다.

--테이블 통계정보를 다시생성한다.
EXEC dbms_stats.gather_table_stats(user,'HIDDEN_COL_TEST',cascade=>true);

히스토그램을 다시 조회해보자.
SELECT TABLE_NAME, COLUMN_NAME, ENDPOINT_NUMBER, ENDPOINT_VALUE FROM USER_TAB_HISTOGRAMS WHERE table_name = 'HIDDEN_COL_TEST';

TABLE_NAME COLUMN_NAME ENDPOINT_NUMBER ENDPOINT_VALUE
---------------------- ------------------- ------------------------ ------------------------
HIDDEN_COL_TEST A 0 3.37499295804764E35
HIDDEN_COL_TEST B 0 5.09496674487288E35
HIDDEN_COL_TEST A 1 5.03652795277878E35
HIDDEN_COL_TEST B 1 5.09496674487288E35

HIDDEN_COL_TEST SYS_NC00003$ 5585 3.37499295804764E35

히스토그램에 가상컬럼이 생성됬다.
이제 실행계획을 다시 생성시켜보자.
explain plan for select * from HIDDEN_COL_TEST where upper(A) = :v_bind;

select * from table(dbms_xplan.display);

-----------------------------------------------------
Id Operation Name Rows
-----------------------------------------------------
0 SELECT STATEMENT 79061
* 1 TABLE ACCESS FULL HIDDEN_COL_TEST 79061
-----------------------------------------------------


예상대로 실행계획이 정상으로 돌아왔다.

결론 :
FBI 생성시 반드시 테이블 통계정보를 다시생성해야 한다는걸 알수 있다.
그렇지않으면 가상컬럼이 히스토그램에 생성되지 않을 뿐만아니라 성능도 저하될수 있다는걸 반드시 기억하여야 한다.

참조 URL:
http://www.oracledba.co.uk/tips/collect_stats_subtle.htm

Posted by extremedb
,