태터데스크 관리자

도움말
닫기
적용하기   첫페이지 만들기

태터데스크 메시지

저장하였습니다.

'dbms_xplan'에 해당되는 글 4건

  1. 2010.10.06 PLAN_HASH_VALUE
  2. 2009.02.04 DBMS_XPLAN에 대한 이해 (16)
  3. 2008.01.17 QB_NAME Hint의 편리함 - Oracle 10g
  4. 2008.01.15 The Power of Predicates in dbms_xplan

PLAN_HASH_VALUE

오라클/SQL 튜닝 2010.10.06 10:55
SQL A과 SQL B의 실행 계획이 같은지 다른지 비교할 수 있는 가장 쉬운 방법은 무엇일까요?

오라클이 제공하는 PLAN_HASH_VALUE가 정답이 아닐까합니다. 이 값은 말그대로 실행 계획에 대한 해시 값입니다. 해시 값이기 때문에 100% 유일성이 보장되지는 않지만, 거의 대부분의 경우 식별자로 사용할 수 있습니다.

오라클에서는 동일한 SQL 텍스트를 가지는 SQL 커서가 복수 개의 실행 계획을 가질 수 있도록 허용합니다. 이런 유연함 덕분에 여러 가지 성능 문제가 나타나지만요.

간단한 예제를 통해 논의해보겠습니다. 이 예제는 Oracle 11g의 Adaptive Cursor Sharing 기능을 이용하고 있습니다.

오라클 버전은 11gR2입니다.

TPACK@ukja1120> select * from v$version where rownum = 1;

BANNER
--------------------------------------------------------------------------------
Oracle Database 11g Enterprise Edition Release 11.2.0.1.0 - Production
편향된 데이터를 가지는 테이블 T1을 만듭니다. 컬럼 C1에 대해서 인덱스를 만들고, 히스토그램을 만듭니다.
TPACK@ukja1120> 
TPACK@ukja1120> create table t1
  2  as
  3  select level as c1 from dual connect by level <= 10000
  4  union all
  5  select 1 as c1 from dual connect by level <= 100000
  6  ;

Table created.

TPACK@ukja1120> 
TPACK@ukja1120> create index t1_n1 on t1(c1);

Index created.

TPACK@ukja1120> 
TPACK@ukja1120> exec dbms_stats.gather_table_stats(user, 't1', method_opt=>'for all columns size skewonly');

PL/SQL procedure successfully completed.
"100"의 값이 조건으로 사용될 때는 INDEX RANGE SCAN을 사용합니다.
TPACK@ukja1120> explain plan for
  2  select count(*) from t1 where c1 = 100;

Explained.

TPACK@ukja1120> @plan
TPACK@ukja1120> set line 200
TPACK@ukja1120> select * from table(dbms_xplan.display);
---------------------------------------------------------------------------
| Id  | Operation         | Name  | Rows  | Bytes | Cost (%CPU)| Time     |
---------------------------------------------------------------------------
|   0 | SELECT STATEMENT  |       |     1 |     3 |     1   (0)| 00:00:01 |
|   1 |  SORT AGGREGATE   |       |     1 |     3 |            |          |
|*  2 |   INDEX RANGE SCAN| T1_N1 |     1 |     3 |     1   (0)| 00:00:01 |
---------------------------------------------------------------------------
"1"의 값을 조건으로 사용할 때는 TABLE FULL SCAN을 사용합니다.
TPACK@ukja1120> explain plan for
  2  select count(*) from t1 where c1 = 1;

Explained.

TPACK@ukja1120> select * from table(dbms_xplan.display);
---------------------------------------------------------------------------
| Id  | Operation          | Name | Rows  | Bytes | Cost (%CPU)| Time     |
---------------------------------------------------------------------------
|   0 | SELECT STATEMENT   |      |     1 |     3 |    53   (4)| 00:00:01 |
|   1 |  SORT AGGREGATE    |      |     1 |     3 |            |          |
|*  2 |   TABLE ACCESS FULL| T1   | 99173 |   290K|    53   (4)| 00:00:01 |
---------------------------------------------------------------------------
이제 "100"의 값과 "1"의 값을 반복적으로 사용하면서 쿼리를 수행합니다. 단, 바인드 변수를 사용해서 Adaptive Cursor Sharing이 동작하도록 유도합니다.
TPACK@ukja1120> var b1 number;
TPACK@ukja1120> exec :b1 := 100;

PL/SQL procedure successfully completed.

TPACK@ukja1120> select count(*) from t1 where c1 = :b1;

  COUNT(*)
----------
         1

TPACK@ukja1120> 
TPACK@ukja1120> exec :b1 := 1;

PL/SQL procedure successfully completed.

TPACK@ukja1120> select count(*) from t1 where c1 = :b1;

  COUNT(*)
----------
    100001

TPACK@ukja1120> 
TPACK@ukja1120> exec :b1 := 100;

PL/SQL procedure successfully completed.

TPACK@ukja1120> select count(*) from t1 where c1 = :b1;

  COUNT(*)
----------
         1

TPACK@ukja1120> 
TPACK@ukja1120> exec :b1 := 1;

PL/SQL procedure successfully completed.

TPACK@ukja1120> select count(*) from t1 where c1 = :b1;

  COUNT(*)
----------
    100001

TPACK@ukja1120> 
TPACK@ukja1120> exec :b1 := 100;

PL/SQL procedure successfully completed.

TPACK@ukja1120> select count(*) from t1 where c1 = :b1;

  COUNT(*)
----------
         1

TPACK@ukja1120> 
TPACK@ukja1120> exec :b1 := 1;

PL/SQL procedure successfully completed.

TPACK@ukja1120> select count(*) from t1 where c1 = :b1;

  COUNT(*)
----------
    100001

TPACK@ukja1120> 
TPACK@ukja1120> exec :b1 := 100;

PL/SQL procedure successfully completed.

TPACK@ukja1120> select count(*) from t1 where c1 = :b1;

  COUNT(*)
----------
         1

TPACK@ukja1120> 
TPACK@ukja1120> exec :b1 := 1;

PL/SQL procedure successfully completed.

TPACK@ukja1120> select count(*) from t1 where c1 = :b1;

  COUNT(*)
----------
    100001

네 개의 차일드 커서가 생겼네요. (네 개가 생기는 이유는 무엇일까요? 추가적인 설명 필요!)
TPACK@ukja1120> col sql_id new_value sql_id
TPACK@ukja1120> select sql_id, version_count, plan_hash_value
  2  from v$sqlarea
  3  where sql_text = 'select count(*) from t1 where c1 = :b1';

SQL_ID        VERSION_COUNT PLAN_HASH_VALUE
------------- ------------- ---------------
7dwqb1wjmp5hm             4        73337487
차일드 커서를 나타내는 V$SQL 뷰를 보면 자식의 개수는 네 개이지만 PLAN_HASH_VALUE는 2개가 존재합니다. 즉, 실제 실행 계획은 두 개(INDEX RANGE SCAN + TABLE FULL SCAN) 뿐입니다.
TPACK@ukja1120> select sql_id, child_number, plan_hash_value
  2  from v$sql
  3  where sql_id = '&sql_id';
old   3: where sql_id = '&sql_id'
new   3: where sql_id = '7dwqb1wjmp5hm'

SQL_ID        CHILD_NUMBER PLAN_HASH_VALUE
------------- ------------ ---------------
7dwqb1wjmp5hm            0        73337487
7dwqb1wjmp5hm            1        73337487
7dwqb1wjmp5hm            2        73337487
7dwqb1wjmp5hm            3      3724264953
DBMS_XPLAN.DISPLAY_CURSOR 함수를 이용해서 실행 계획을 직접 조회해보면 더 상세한 정보를 얻을 수 있습니다.
TPACK@ukja1120> select * from table(dbms_xplan.display_cursor('&sql_id', null));
old   1: select * from table(dbms_xplan.display_cursor('&sql_id', null))
new   1: select * from table(dbms_xplan.display_cursor('7dwqb1wjmp5hm', null))

PLAN_TABLE_OUTPUT
--------------------------------------------------------------------------------
SQL_ID  7dwqb1wjmp5hm, child number 0
-------------------------------------
select count(*) from t1 where c1 = :b1

Plan hash value: 73337487

---------------------------------------------------------------------------
| Id  | Operation         | Name  | Rows  | Bytes | Cost (%CPU)| Time     |
---------------------------------------------------------------------------
|   0 | SELECT STATEMENT  |       |       |       |     1 (100)|          |
|   1 |  SORT AGGREGATE   |       |     1 |     3 |            |          |
|*  2 |   INDEX RANGE SCAN| T1_N1 |     1 |     3 |     1   (0)| 00:00:01 |
---------------------------------------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------

   2 - access("C1"=:B1)

SQL_ID  7dwqb1wjmp5hm, child number 1
-------------------------------------
select count(*) from t1 where c1 = :b1

Plan hash value: 73337487

---------------------------------------------------------------------------
| Id  | Operation         | Name  | Rows  | Bytes | Cost (%CPU)| Time     |
---------------------------------------------------------------------------
|   0 | SELECT STATEMENT  |       |       |       |     1 (100)|          |
|   1 |  SORT AGGREGATE   |       |     1 |     3 |            |          |
|*  2 |   INDEX RANGE SCAN| T1_N1 |     1 |     3 |     1   (0)| 00:00:01 |
---------------------------------------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------

   2 - access("C1"=:B1)

SQL_ID  7dwqb1wjmp5hm, child number 2
-------------------------------------
select count(*) from t1 where c1 = :b1

Plan hash value: 73337487

---------------------------------------------------------------------------
| Id  | Operation         | Name  | Rows  | Bytes | Cost (%CPU)| Time     |
---------------------------------------------------------------------------
|   0 | SELECT STATEMENT  |       |       |       |     1 (100)|          |
|   1 |  SORT AGGREGATE   |       |     1 |     3 |            |          |
|*  2 |   INDEX RANGE SCAN| T1_N1 |     1 |     3 |     1   (0)| 00:00:01 |
---------------------------------------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------

   2 - access("C1"=:B1)

SQL_ID  7dwqb1wjmp5hm, child number 3
-------------------------------------
select count(*) from t1 where c1 = :b1

Plan hash value: 3724264953

---------------------------------------------------------------------------
| Id  | Operation          | Name | Rows  | Bytes | Cost (%CPU)| Time     |
---------------------------------------------------------------------------
|   0 | SELECT STATEMENT   |      |       |       |    53 (100)|          |
|   1 |  SORT AGGREGATE    |      |     1 |     3 |            |          |
|*  2 |   TABLE ACCESS FULL| T1   | 99173 |   290K|    53   (4)| 00:00:01 |
---------------------------------------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------

   2 - filter("C1"=:B1)


76 rows selected.
PLAN_HASH_VALUE가 실행 계획을 구분하는데 사용될 수 있다는 특징을 잘 이용하면 실행 계획 변경을 추적하는데 사용할 수도 있습니다. 가령 AWR에 저장된 Top SQL과 SQL 텍스트는 동일하지만(SQL_ID) PLAN_HASH_VALUE은 다른 새로운 SQL 문이 등장했다면 실행 계획의 변경이 발생한 것으로 의심해볼 수 있습니다.

단, 이런 현상이 발생했다고 하더라도 하나의 SQL 텍스트가 여러 개의 실행 계획을 가질 수 있기 때문에 보다 정밀한 분석이 필요할 것입니다.

저작자 표시
신고

'오라클 > SQL 튜닝' 카테고리의 다른 글

조인 순서 제어하기 2  (2) 2010.11.04
조인 순서 제어하기  (3) 2010.10.25
비주얼 SQL 튜닝?  (6) 2010.10.21
Batching NLJ에 대한 오라클 매뉴얼의 설명  (0) 2010.10.11
PLAN_HASH_VALUE  (0) 2010.10.06
Trackback 0 : Comment 0

Write a comment


DBMS_XPLAN에 대한 이해

오라클 2009.02.04 14:34
dbms_xplan 패키지는 Oracle이 제공하는 모든 패키지들 중 가장 중요한 것이다.

왕 중의 왕이다.

적어도 Query를 튜닝하는 사람들에게는 이 명제가 진실일 것이다. dbms_xplan 패키지는 실행 계획을 분석하는 기능을 제공하기 때문이다. 이 패키지가 없이는 Query 튜닝은 불가능하다.

Oracle이 버전업되면서 실행 계획이라는 말의 범위가 점점 넓어지고 있다. 최신 버전인 11g 기준으로, 총 5 가지 종류의 실행 계획이 존재한다. 이제는 실행 계획이라는 말을 들으면 "어떤 실행 계획 말하는거야"라고 물어봐야 할 지경이다.

아래 Oracle Virus 동영상에 이것이 잘 설명되어 있다.




신고
tags : dbms_xplan
Trackback 0 : Comments 16
  1. 강정식 2009.02.06 17:38 신고 Modify/Delete Reply

    안녕하세요. 조동욱님
    오랜만에 뵙겠습니다. ^^ 요즘 스터디때문에 OOO책으로 열공 중입니다.

    하나 궁금한점이 있어서 리플로 남겨뵙니다.

    dbms_xplan 패키지에서 제공하는 함수 중 'display_cursor'가 매력적으로
    다가오는데요. /*+ gather_plan_statistics */ 힌트를 통해 트레이스를 뜨지
    않고 e-rows, a-rows의 값을 확인하여 explain plan과 runtime plan의 간극을
    확인할 수 있는게 좋은점인 것 같습니다.

    다만 제가 생각할 때 /*+ gather_plan_statistics */ 힌트를 통해 'display_cursor'
    함수를 이용하여 runtime plan을 확인하려면 실제 데이터를 흘려보아야 한다는
    단점이 있는 것 같습니다.

    display 함수는 현재 조동욱님이 여러 가이드를 해주셔서 참 편하기 쓰고 있습니다.
    display의 장점은 말 그대로 데이터를 흘려보지 않고 여러 각도에서 분석을 할 수
    있는 것인데 display_cursor 함수는 데이터를 실제 흘려봐야 하므로 제 경우에는
    차라리 10046 trace이 더 좋다고 판단되어 이를 사용하고 있습니다.

    제가 잘 못 이해하고 있는것인지요? 아니면 display_cursor를 이용하려면 실제
    데이터를 흘려봐야 하는 단점이 있는건지요?

    ps. outline hint에 대해 혹시 더 스터디하신 내용 없으신가요?
    제가 근무하는 사이트에서는 요즘 너무 plan이 복잡하여 outline hint의
    사용 비중이 2~3%에서 2~30%로 증가하였습니다.
    아마 향후에는 5~60%로 늘어나지 않을까 싶은데요. 이를 위해서는 자유롭게
    outline을 사용할 수 있도록 해야 하는데 이에 대한 기술문서가 없어서
    애를 먹고 있습니다.
    기술 선도를 하고 계시는 조동욱님께서 이에 대해 분석해 주시면 엔지니어들에게
    많은 도움이 될 것 같은데요. 혹시 시간이 되시면 스터디하신 내용에 대해
    공유 부탁드리겠습니다. ^^

    며칠전에 입춘이 지나갔습니니다. 2009년도 입춘대길 하시길 바랍니다.

    감사합니다.

  2. 강정식 2009.02.06 18:01 신고 Modify/Delete Reply

    동욱님 기왕 쓴 김에 하나 더 글 올려봅니다 ^^;
    OOO 및 조동욱님이 집필하신 책에 대해 독자들이 책을 보다가 궁금한점이 있을 때
    질문할 수 있는 게시판이있었으면 좋겠는데요. 여기 동욱님 블로그가 그 기능을
    하기에 좋을 거라 생각됩니다.

    혹시 그 기능을 하는 게시판 하나 개설해주실 수 있으신가요?

  3. 욱짜 2009.02.07 12:55 신고 Modify/Delete Reply

    1. 지금 엑셈에서 ask.ex-em.com 이라는 이름으로 Oracle 성능 관련 Q&A가 가능한 간단한 시스템을 개발하고 있습니다. 조만간 오픈될 것입니다. 이 사이트가 오픈되면 다양한 목적으로 활용 가능할 것입니다.

    만일 별도의 시스템이 필요하다고 판단되면 구글 그룹을 만드는 것도 방법이 될 거 같습니다.

    2. dbms_xplan.display_cursor 함수는 "현재 Shared Pool"에 등록되어 있는 실행 계획을 보는 기능이죠? 일단 한번 수행하고 나면 그 정보가 Shared Pool에 보관되어 있는 한은 거의 영구적으로 사용이 가능합니다. 한번 수행을 해보아야 한다는 사실에는 변함이 없지만, 결과를 보기 위해서 계속해서 수행할 필요는 없다는 것이지요. sql_id와 child_number만 알면 계속 조회할 수 있습니다.

    3. Outline 힌트라면 OUTLINE 옵션에 의해 나오는 힌트들의 목록을 말씀하시는건가요? 아니면 Stored Outline을 말씀하시는건가요?

  4. 욱짜 2009.02.08 00:10 신고 Modify/Delete Reply

    영문 블로그에 제 생각을 간단하게 정리했습니다.

    http://dioncho.wordpress.com/2009/02/07/dbms_statsdisplay_cursor-vs-sql-trace/

  5. 강정식 2009.02.09 08:59 신고 Modify/Delete Reply

    동욱님 답변 감사드립니다. 영문 블로그에 올리신 내용 참고하겠습니다.

    그리고 제가 추가적으로 문의한 Outline 힌트는 OUTLINE 옵션에 의해 나오는 힌트를 말씀드렸습니다.
    제가 지원하는 사이트가 View를 참조하는 프로그램들이 많다보니 phsh_pred, push_subq들을
    자유자재로 핸들링 하는것이 관건인데 이들을 기존 힌트들에서 Outline 힌트들을 참조하는 빈도수가
    많아져서요.

    더욱 스터디가 필요하다고 생각되는 부분은 아래의 내용인데요.

    NO_ACCESS(@SEL$5DAD4AD6 XRPSV@SEL$1)
    OUTLINE(@SEL$3)
    MERGE(@SEL$65)

    여기서 NO_ACCESS 부분에서 View Merging이 안되는 query_block과 Alias가 나오는데 이를
    ACCESS로 바꿔도 Merging이 안되는것들이 Outline 힌트 하단에 나오는 OUTLINE, MERGE
    부분을 일부 변경해보니 Merging이 되는것을 확인이 되서요.

    근데 이는 운이 좋게 된거라 아직 핸들링 할 수준은 아니어서 자유자재로 구사는 하지 못하고 있습니다.

    이에 대해 동욱님께서 스터디하신 내용이 있으시면 공유 부탁드리겠습니다.

    감사합니다 ^^

  6. 욱짜 2009.02.09 16:59 신고 Modify/Delete Reply

    Outline 옵션을 아주 잘 사용하고 계시는군요.
    가능한 명시적인 Block 이름(가령 QB_NAME Hint 사용)을 사용하는 것이 추후 유지보수를 위해 더 좋을 거 같다는 생각은 듭니다.

    Outline으로 사용된 Hint들에 대해서는 더 많은 정보를 얻게 되면 공유하도록 하겠습니다.

  7. 강정식 2009.03.03 14:16 신고 Modify/Delete Reply

    안녕하세요 욱짜님.

    다름이 아니라 display_cursor 사용할 때 sql_id를 찾지 못한다는 메세지와 함께 결과가 안나올때가 있어서요...
    그래서 'V$SQLAREA'에서 sql_id를 찾은 다음에 그 인자값으로 찾아야 하는데요.

    왜 이렇게 sql_id를 매칭시키지 못하는것에 대해 알려주실 수 있으신가요?

    아래는 테스트 스크립트입니다.

    -- create object
    DROP TABLE T1 PURGE;

    CREATE TABLE T1(C1 VARCHAR2(1), C2 INT);

    CREATE INDEX T1_N1 ON T1(C1);

    -- create data
    INSERT INTO T1
    SELECT 'A',
    LEVEL
    FROM DUAL
    CONNECT BY LEVEL <= 10000
    UNION ALL
    SELECT 'B', LEVEL
    FROM DUAL
    CONNECT BY LEVEL <= 10
    ;

    COMMIT;

    -- gather stats "without" histogram
    EXEC DBMS_STATS.GATHER_TABLE_STATS(USER, 'T1', METHOD_OPT => 'FOR ALL COLUMNS SIZE 1', NO_INVALIDATE => FALSE);

    SELECT /*+ GATHER_PLAN_STATISTICS */
    COUNT(*)
    FROM T1
    WHERE C1 = 'B'
    ;

    COUNT(*)
    ----------
    10

    1 row selected.

    -- not matched sql_id
    select * from table(dbms_xplan.display_cursor(null, null, 'iostats cost last'));

    PLAN_TABLE_OUTPUT
    --------------------------------------------------------------------------------------------------------------
    SQL_ID 9babjv8yq8ru3, child number 1

    BEGIN DBMS_OUTPUT.GET_LINES(:LINES, :NUMLINES); END;

    NOTE: cannot fetch plan for SQL_ID: 9babjv8yq8ru3, CHILD_NUMBER: 1
    Please verify value of SQL_ID and CHILD_NUMBER;
    It could also be that the plan is no longer in cursor cache (check v$sql_plan)
    ;

    -- get sql_id
    SELECT SUBSTR(SQL_TEXT, 1, 600) SQL_TEXT,
    SQL_ID
    FROM V$SQLAREA
    WHERE UPPER(SQL_TEXT) LIKE '%GATHER_PLAN_STATISTICS%'
    ORDER BY FIRST_LOAD_TIME DESC
    ;

    SQL_TEXT SQL_ID
    ------------------------------------------------------------------------------ -------------
    SELECT /*+ GATHER_PLAN_STATISTICS */ COUNT(*) FROM T1 WHERE C1 = 'B' 3ngqqmd0a1m3t
    ;

    -- re find display_cursor
    select * from table(dbms_xplan.display_cursor('3ngqqmd0a1m3t', null, 'iostats cost last'));

    PLAN_TABLE_OUTPUT
    --------------------------------------------------------------------------------------------------
    SQL_ID 3ngqqmd0a1m3t, child number 0
    -------------------------------------
    SELECT /*+ GATHER_PLAN_STATISTICS */ COUNT(*) FROM T1 WHERE C1 = 'B'

    Plan hash value: 3693069535

    --------------------------------------------------------------------------------------------------
    | Id | Operation | Name | Starts | E-Rows | Cost (%CPU)| A-Rows | A-Time | Buffers |
    --------------------------------------------------------------------------------------------------
    | 1 | SORT AGGREGATE | | 1 | 1 | | 1 |00:00:00.01 | 31 |
    |* 2 | TABLE ACCESS FULL| T1 | 1 | 5005 | 9 (0)| 10 |00:00:00.01 | 31 |
    --------------------------------------------------------------------------------------------------

    Predicate Information (identified by operation id):
    ---------------------------------------------------

    2 - filter("C1"='B')

  8. 욱짜 2009.03.03 15:02 신고 Modify/Delete Reply

    1. Windows 환경에서 SQL*Plus 사용
    2. set serveroutput on 상태인 경우
    에 발생하는 문제입니다.

    set serveroutput off 를 지정하시면 해결될겁니다.

  9. 강정식 2009.03.03 15:34 신고 Modify/Delete Reply

    감사합니다 ^^

  10. 강정식 2009.04.09 00:05 신고 Modify/Delete Reply

    안녕하세요 욱짜님 ^^ 강정식입니다.
    XPLAN에서 여쭤볼것이 있어서 이렇게 질문을 남깁니다.

    제가 'XPLAN과 10046 event를 이용한 튜닝방법' 세미나를 준비중이라
    DBMS_XPLAN.DISPLAY(이하 DISPLAY), DBMS_XPLAN.DISPLAY_CURSOR(이하 CURSOR)
    2개를 테스트 해 보고 있습니다.

    그런데 ADVANCED Format을 통해 나온 Query Block Name(이하 블럭)을 이용하여
    인덱스를 변경하는 건을 테스트 하다가 생각지도 못한 Case가 발견되어 질문을 드립니다.

    이야기로 풀어 정리를 하자면 DISPLAY를 통해 나온 블럭을 이용하여 힌트를 교정한 뒤
    CURSOR로 수행을 해보니 그 힌트가 적용되지 않았습니다.

    하여 그 원인을 확인해본 결과 Predicate Information에서 바인드 변수가 TO_NUMBER(:B1)로
    바뀌어서 들어가는데 이로 인해 DISPLAY 할 때와 CURSOR 할 때가 PLAN이 바뀌어져 버렸고
    이로 인해 블럭또한 바뀌어서 적용이 안되었습니다.

    하여 DISPLAY시 바인드 변수의 데이터타입을 지정한 뒤 PLAN을 확인해보니 여전히
    TO_NUMBER(:B1)로 적용이 되고 있었습니다. 결국 PLAN을 확인할 때는 바인드 변수의
    데이터타입을 핸들링 할 수 없었는데 혹시 욱짜님께서 이를 핸들링하는 방법에 대해
    아시고 계신게 있으신지요?

    아래에 간단하게 데이터타입에 대한 내용을 확인할 수 있는 스크립트를 올려드립니다.

    -- create table
    DROP TABLE TEST_X PURGE;

    CREATE TABLE TEST_X AS
    SELECT LEVEL EMPNO
    FROM DUAL
    CONNECT BY LEVEL <= 100
    ;

    -- create index
    CREATE UNIQUE INDEX EMP_U1 ON TEST_X (EMPNO) COMPUTE STATISTICS;

    -- gather statistics
    EXEC DBMS_STATS.GATHER_TABLE_STATS(USER, 'TEST_X', CASCADE => TRUE);

    -- 테스트
    VAR B1 NUMBER;
    :B1 := 1;

    EXPLAIN PLAN FOR
    SELECT *
    FROM TEST_X
    WHERE EMPNO = :B1
    ;

    SELECT * FROM TABLE(DBMS_XPLAN.DISPLAY);

    --------------------------------------------------------------------------------------
    | Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
    --------------------------------------------------------------------------------------
    | 0 | SELECT STATEMENT | | 1 | 16 | 1 (0)| 00:00:01 |
    | 1 | TABLE ACCESS BY INDEX ROWID| TEST_X | 1 | 16 | 1 (0)| 00:00:01 |
    |* 2 | INDEX UNIQUE SCAN | EMP_U1 | 1 | | 0 (0)| 00:00:01 |
    --------------------------------------------------------------------------------------

    Predicate Information (identified by operation id):
    ---------------------------------------------------

    2 - access("EMPNO"=TO_NUMBER(:B1)) <--- var B1 number로 선언해도 to_number()를 함
    ;

    SET SERVEROUTPUT OFF;

    VAR B1 NUMBER;
    :B1 := 1;

    SELECT /*+ GATHER_PLAN_STATISTICS */
    *
    FROM TEST_X
    WHERE EMPNO = :B1
    ;

    SELECT * FROM TABLE(DBMS_XPLAN.DISPLAY_CURSOR);

    --------------------------------------------------------------------------------------
    | Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
    --------------------------------------------------------------------------------------
    | 0 | SELECT STATEMENT | | | | 1 (100)| |
    | 1 | TABLE ACCESS BY INDEX ROWID| TEST_X | 1 | 16 | 1 (0)| 00:00:01 |
    |* 2 | INDEX UNIQUE SCAN | EMP_U1 | 1 | | 0 (0)| |
    --------------------------------------------------------------------------------------

    Predicate Information (identified by operation id):
    ---------------------------------------------------

    2 - access("EMPNO"=:B1) <--- var B1 number한것이 적용됨

  11. 욱짜 2009.04.09 06:45 신고 Modify/Delete Reply

    번거로우시겠지만 이 내용을 다른 분들도 공유할 수 있게 Ask Exem에 올려주시겠습니까?

  12. 강정식 2009.04.09 09:21 신고 Modify/Delete Reply

    네 올려드렸습니다. ^^

  13. 욱짜 2009.04.09 10:52 신고 Modify/Delete Reply

    답변을 간단하게 올렸습니다. 원래 제가 답변을 올리면 질문자에게 이메일이 발송되게 되어 있는데 이상하게 입력하신 이메일로는 반송(서버에서 거부 당함)이 되더라구요. 다음 번에 이용하시게 되면 다른 이메일 주소를 사용해보시면 좋겠습니다.

  14. 엑셥 2009.04.09 13:48 신고 Modify/Delete Reply

    욱짜님 답변 감사드립니다.
    그런데 실 사례를 공유해 드리지 못한것 같아 다시한번 사례를 올려드렸습니다.
    해당 스크립트도 같이 올려드렸으니 local에서 테스트 해보실 수 있으실겁니다.

    여기에도 url를 올려드리겠습니다.

    http://blog.naver.com/xsoft/150045721858

    감사합니다.

  15. 욱짜 2009.04.09 15:28 신고 Modify/Delete Reply

    혹시 Ask Exem에서 메일이 발송되었나요? 되었다면 여기에 제가 답글을 남기지 않아도 될텐데요.

    Ask Exem에 답글을 남겼습니다. 확인해보세요.

  16. 양용성 2010.05.27 18:18 신고 Modify/Delete Reply

    욱짜님의 글을 보고 공부해서 아래와 같이 프로그램을 만들어 봤습니다.
    늘 자료 감사히 보고 있습니다.
    http://sqlgate.wordpress.com/2010/05/18/%EC%83%88%EB%A1%9C%EC%9A%B4-dbms_xplan-display-%EA%B8%B0%EB%8A%A5/

Write a comment


QB_NAME Hint의 편리함 - Oracle 10g

오라클 2008.01.17 14:51
Oracle 10g에서 QB_NAME 이라는 유용한 힌트가 추가되었다. QB_NAME은 Query Block에 사용자가 직접 별명(이름)을 부여하는 것을 가능하게 해주는 힌트이다.

오라클은 SQL Text를 여러 개의 Query Block 으로 나누어 관리한다. 가령 하나의 SQL Text에 Inline View가 하나 포함되어 있다면 내부적으로 두 개의 Query Block이 존재한다.

아주 간단한 예제만으로도 이 힌트가 얼마나 유용한지 알 수 있다.

아래와 같은 쿼리의 실행 계획을 보자.

explain plan for select t1.id1, t2.name2, x.id4, x.name5,
    (select count(*) from t1 s where s.id1 = t1.id1) as id1_1
from t1, t2, t3, t5,
    (select t4.id4, t5.name5
        from t4, t5
        where t4.id4 = t5.id5 and t5.name5 like '%c%') x
where t1.id1 = t2.id2
    and t2.id2 in (select id3 from t3 where name3 like '%b%')
    and t2.id2 = x.id4
    and t3.id3 = t1.id1
    and t5.name5 = t1.name1;
   
select * from table(dbms_xplan.display(null,null));


----------------------------------------------------------------------------------
| Id  | Operation               | Name   | Rows  | Bytes | Cost (%CPU)| Time     |
----------------------------------------------------------------------------------
|   0 | SELECT STATEMENT        |        |     1 |   113 |    14  (15)| 00:00:01 |
|   1 |  SORT AGGREGATE         |        |     1 |    13 |            |          |
|*  2 |   INDEX RANGE SCAN      | T1_IDX |     1 |    13 |     1   (0)| 00:00:01 |
|*  3 |  HASH JOIN              |        |     1 |   113 |    14  (15)| 00:00:01 |
|*  4 |   HASH JOIN SEMI        |        |     1 |   106 |    12  (17)| 00:00:01 |
|   5 |    NESTED LOOPS         |        |     1 |    86 |     9  (12)| 00:00:01 |
|*  6 |     HASH JOIN           |        |     1 |    73 |     8  (13)| 00:00:01 |
|*  7 |      HASH JOIN          |        |     1 |    53 |     6  (17)| 00:00:01 |
|   8 |       NESTED LOOPS      |        |     1 |    33 |     3   (0)| 00:00:01 |
|*  9 |        TABLE ACCESS FULL| T5     |     1 |    20 |     2   (0)| 00:00:01 |
|* 10 |        INDEX RANGE SCAN | T4_IDX |     1 |    13 |     1   (0)| 00:00:01 |
|  11 |       TABLE ACCESS FULL | T2     |     1 |    20 |     2   (0)| 00:00:01 |
|  12 |      TABLE ACCESS FULL  | T1     |     1 |    20 |     2   (0)| 00:00:01 |
|* 13 |     INDEX RANGE SCAN    | T3_IDX |     1 |    13 |     1   (0)| 00:00:01 |
|* 14 |    TABLE ACCESS FULL    | T3     |     1 |    20 |     2   (0)| 00:00:01 |
|  15 |   TABLE ACCESS FULL     | T5     |     1 |     7 |     2   (0)| 00:00:01 |
----------------------------------------------------------------------------------

실행 계획을 해석하는데 숙달된 전문가가 아니라면 아마 지레 겁이 날 것이다. 서너 개의 인라인 뷰와 서브 쿼리만으로 실행 계획이 금방 매우 복잡해진다. 가장 큰 문제는 실행 계획의 특정 오퍼레이션(1,2,...,15)이 SQL Text의 어느 부분과 매칭되는지 직관적으로 파악하기가 어렵다는 것이다.

QB_NAME 힌트는 이런 복잡한 쿼리의 실행 계획을 해석하는데 아주 유용한 도구가 된다. QB_NAME 힌트를 이용한 예는 다음과 같다.

-- Statistics Level 을 All로 변경한다.
alter session set statistics_level = all;

-- QB_NAME 힌트 부여
explain plan for select /*+ qb_name(main) */ t1.id1, t2.name2, x.id4, x.name5,
    (select /*+ qb_name(scalar) */ count(*) from t1 s where s.id1 = t1.id1) as id1_1
from t1, t2, t3, t5,
    (select /*+ qb_name(inline) */ t4.id4, t5.name5
        from t4, t5
        where t4.id4 = t5.id5 and t5.name5 like '%c%') x
where t1.id1 = t2.id2
    and t2.id2 in (select /*+ qb_name(subquery) */ id3 from t3 where name3 like '%b%')
    and t2.id2 = x.id4
    and t3.id3 = t1.id1
    and t5.name5 = t1.name1;

-- Cursor에서 ALL stats를 조회한다.
select * from table(dbms_xplan.display(null,null, 'ALL'));

----------------------------------------------------------------------------------
| Id  | Operation               | Name   | Rows  | Bytes | Cost (%CPU)| Time     |
----------------------------------------------------------------------------------
|   0 | SELECT STATEMENT        |        |     1 |   113 |    14  (15)| 00:00:01 |
|   1 |  SORT AGGREGATE         |        |     1 |    13 |            |          |
|*  2 |   INDEX RANGE SCAN      | T1_IDX |     1 |    13 |     1   (0)| 00:00:01 |
|*  3 |  HASH JOIN              |        |     1 |   113 |    14  (15)| 00:00:01 |
|*  4 |   HASH JOIN SEMI        |        |     1 |   106 |    12  (17)| 00:00:01 |
|   5 |    NESTED LOOPS         |        |     1 |    86 |     9  (12)| 00:00:01 |
|*  6 |     HASH JOIN           |        |     1 |    73 |     8  (13)| 00:00:01 |
|*  7 |      HASH JOIN          |        |     1 |    53 |     6  (17)| 00:00:01 |
|   8 |       NESTED LOOPS      |        |     1 |    33 |     3   (0)| 00:00:01 |
|*  9 |        TABLE ACCESS FULL| T5     |     1 |    20 |     2   (0)| 00:00:01 |
|* 10 |        INDEX RANGE SCAN | T4_IDX |     1 |    13 |     1   (0)| 00:00:01 |
|  11 |       TABLE ACCESS FULL | T2     |     1 |    20 |     2   (0)| 00:00:01 |
|  12 |      TABLE ACCESS FULL  | T1     |     1 |    20 |     2   (0)| 00:00:01 |
|* 13 |     INDEX RANGE SCAN    | T3_IDX |     1 |    13 |     1   (0)| 00:00:01 |
|* 14 |    TABLE ACCESS FULL    | T3     |     1 |    20 |     2   (0)| 00:00:01 |
|  15 |   TABLE ACCESS FULL     | T5     |     1 |     7 |     2   (0)| 00:00:01 |
----------------------------------------------------------------------------------

Query Block Name / Object Alias (identified by operation id):
-------------------------------------------------------------

   1 - SCALAR
   2 - SCALAR       / S@SCALAR
   3 - SEL$EA1A1EE6
   9 - SEL$EA1A1EE6 / T5@INLINE
  10 - SEL$EA1A1EE6 / T4@INLINE
  11 - SEL$EA1A1EE6 / T2@MAIN
  12 - SEL$EA1A1EE6 / T1@MAIN
  13 - SEL$EA1A1EE6 / T3@MAIN
  14 - SEL$EA1A1EE6 / T3@SUBQUERY
  15 - SEL$EA1A1EE6 / T5@MAIN

QB_NAME 힌트를 통해 부여한 별명을 통해 정확하게 어떤 Operation이 SQL Text의 어느 부분과 연결되어 있는지 매우 직관적으로 파악할 수 있다. 이 기능을 이용하면 매우 길고 복잡한 쿼리에서도 문제가 되는 부분을 좀 더 쉽게 파악할 수 있다.

가령 매우 복잡한 쿼리에서 특정 인라인 뷰가 실행 계획에서 어느 부분에 해당하는지 파악하려면 다음과 같이 QB_NAME 힌트를 부여하면 된다.

select
 ... { very complex query here } ...
from ( ... ( select /*+ QB_NAME(problematic_query) */ ... ) ) a, ( ... ( ... ) ) b,
    c, d, ...


QB_NAME 힌트의 또 하나의 위대한 점은 우리가 부여한 별칭을 조회 뿐만 아니라 "사용"도 가능하다는 것이다. 아래 예를 보면...

-- 우리가 부여한 subquery 이름을 이용해 no_unnest 힌트를 부여한다.
explain plan for select /*+ qb_name(main) no_unnest(@subquery) */ t1.id1, t2.name2, x.id4, x.name5,
    (select /*+ qb_name(scalar) */ count(*) from t1 s where s.id1 = t1.id1) as id1_1
from t1, t2, t3, t5,
    (select /*+ qb_name(inline) */ t4.id4, t5.name5
        from t4, t5
        where t4.id4 = t5.id5 and t5.name5 like '%c%') x
where t1.id1 = t2.id2
    and t2.id2 in (select /*+ qb_name(subquery) */ id3 from t3 where name3 like '%b%')
    and t2.id2 = x.id4
    and t3.id3 = t1.id1
    and t5.name5 = t1.name1;

select * from table(dbms_xplan.display(null,null, 'ALL'));

-- Subquery Unnesting이 Disable 됨으로써 Filter 조건이 사용되었다.
---------------------------------------------------------------------------------------
| Id  | Operation                    | Name   | Rows  | Bytes | Cost (%CPU)| Time     |
---------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT             |        |     1 |    93 |    13  (16)| 00:00:01 |
|   1 |  SORT AGGREGATE              |        |     1 |    13 |            |          |
|*  2 |   INDEX RANGE SCAN           | T1_IDX |     1 |    13 |     1   (0)| 00:00:01 |
|*  3 |  FILTER                      |        |       |       |            |          |
|*  4 |   HASH JOIN                  |        |     1 |    93 |    12  (17)| 00:00:01 |
|   5 |    NESTED LOOPS              |        |     1 |    86 |     9  (12)| 00:00:01 |
|*  6 |     HASH JOIN                |        |     1 |    73 |     8  (13)| 00:00:01 |
|*  7 |      HASH JOIN               |        |     1 |    53 |     6  (17)| 00:00:01 |
|   8 |       NESTED LOOPS           |        |     1 |    33 |     3   (0)| 00:00:01 |
|*  9 |        TABLE ACCESS FULL     | T5     |     1 |    20 |     2   (0)| 00:00:01 |
|* 10 |        INDEX RANGE SCAN      | T4_IDX |     1 |    13 |     1   (0)| 00:00:01 |
|  11 |       TABLE ACCESS FULL      | T2     |     1 |    20 |     2   (0)| 00:00:01 |
|  12 |      TABLE ACCESS FULL       | T1     |     1 |    20 |     2   (0)| 00:00:01 |
|* 13 |     INDEX RANGE SCAN         | T3_IDX |     1 |    13 |     1   (0)| 00:00:01 |
|  14 |    TABLE ACCESS FULL         | T5     |     1 |     7 |     2   (0)| 00:00:01 |
|* 15 |   TABLE ACCESS BY INDEX ROWID| T3     |     1 |    20 |     1   (0)| 00:00:01 |
|* 16 |    INDEX RANGE SCAN          | T3_IDX |     1 |       |     1   (0)| 00:00:01 |
---------------------------------------------------------------------------------------

이 유용한 힌트를 복잡한 쿼리에서 활용할 수 있는 기회를 갖기를 바래본다.


신고
Trackback 0 : Comment 0

Write a comment


The Power of Predicates in dbms_xplan

오라클 2008.01.15 22:39
9i에서 dbms_xplan 패키지가 소개되면서 실행 계획을 사용하는 방식에 일대 혁명이 일어났다. 단순히 실행 계획을 추측하기 위해 사용하던 것에서 벗어나 이제는 SQL Trace를 대신하거나 혹은 SQL Trace의 부족한 부분을 보완할 정도의 툴로 발전했다.

dbms_xplan 패키지가 제공하는 정보들은 하나 하나가 대단히 중요한 것들이다. 오늘은 그 중 Predicate 정보에 대해 재밌는 예를 살펴보고자 한다.

dbms_xplan 패키지를 사용해 본 사람이라면 아래와 같은 정보에 익숙할 것이다.

explain plan for select count(*) from t_const1 where name = '0987654321';
select * from table(dbms_xplan.display);

PLAN_TABLE_OUTPUT
Plan hash value: 4135594318

-------------------------------------------------------------------------------
| Id  | Operation          | Name     | Rows  | Bytes | Cost (%CPU)| Time     |
-------------------------------------------------------------------------------
|   0 | SELECT STATEMENT   |          |     1 |     7 |     2   (0)| 00:00:01 |
|   1 |  SORT AGGREGATE    |          |     1 |     7 |            |          |
|*  2 |   TABLE ACCESS FULL| T_CONST1 |     1 |     7 |     2   (0)| 00:00:01 |
-------------------------------------------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------
   2 - filter("NAME"='0987654321')

위의 정보에서 Predicate Information를 해석해 보면 2번째 Operation, 즉 TABLE ACCESS FULL Operation에서 NAME = '0987654321' 조건이 Filter로 사용되었다는 의미이다. 이것만으로도 Plan을 해석하는데 있어서 매우 유용한 정보가 된다.

Predicate의 타입은 Filter나 Access로 나뉘는데 Access는 Optimizer가 Access Path를 결정하는데 사용했다는 의미이고, Filter는 Filtering 조건으로만 사용했다는 의미이다.

Predicate 정보가 매우 중요한 역할을 제공하는 예를 한가지 만들어 보자. 우선 아래의 SQL Trace 결과를 보자.

alter session set sql_trace = true;                                            
select /*+ index(t_const1) */ count(*) from t_const1 where name = '0987654321';                       
alter session set sql_trace = false;                                           
                                                                               
call     count       cpu    elapsed       disk      query    current        rows
------- ------  -------- ---------- ---------- ---------- ----------  ----------
Parse        1      0.00       0.00          0          0          0           0
Execute      1      0.00       0.00          0          0          0           0
Fetch        1      0.96       0.94          0       8633          0           1
------- ------  -------- ---------- ---------- ---------- ----------  ----------
total        3      0.96       0.94          0       8633          0           1

SQL Trace 결과를 많이 본 사람이면 "으잉?"하고 의문을 품을 것이다. 8633블록을 Logical Reads로 읽는데 무려 1초에 가까운 시간이 걸린 것이다. 대개의 경우 이 정도의 일량이면 0.1 초 내에 끝나는 것이 일반적이다. 왜 이런 현상이 발생하는 것일까? 보통 이런 경우 Function의 과도한 사용같은 것을 의심해 보지만, 위의 Query와 SQL Trace 결과를 보면 전혀 무관하다는 것을 알 수 있다.

안타깝게도 SQL Trace 만으로는 위의 결과를 해석할 수 없다. 위의 쿼리를 dbms_xplan 패키지를 사용해서 다시 분석해 보자.

select /*+ gather_plan_statistics index(t_const1) */  count(*) from t_const1
where name = '0987654321';

select * from table(dbms_xplan.display(null, null, 'ALLSTATS LAST'));

PLAN_TABLE_OUTPUT
SQL_ID  8pjh0vyqxqzny, child number 0
-------------------------------------
select /*+ gather_plan_statistics index(t_const1) */  count(*) from t_const1 where
name = '0987654321'

Plan hash value: 225932663

--------------------------------------------------------------------------------------------
| Id  | Operation         | Name         | Starts | E-Rows | A-Rows |   A-Time   | Buffers |
--------------------------------------------------------------------------------------------
|   1 |  SORT AGGREGATE   |              |      1 |      1 |      1 |00:00:01.25 |    4961 |
|*  2 |   INDEX RANGE SCAN| T_CONST1_IDX |      1 |   9938 |   1000K|00:00:00.01 |    4961 |
--------------------------------------------------------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------

   2 - access("NAME"='0987654321')
       filter(TRIM("NAME")='0987654321')

(gather_plan_statistics 힌트에 대해서는 여기서 상세히 논의한 바 있다)

아하!! 위의 결과를 보면 전혀 원하지 않았던 filter 조건이 추가된 것을 확인할 수 있다. 이런 원하지 않은 Filtering 조건에 의해 TRIM("NAME")='0987654321' 계산이 반복적으로 발생하고 이로 인해 원하지 않는 성능 저하가 발생한 것이다. 이런 원하지 않는 Filtering이 발생하는 주된 이유 중 하나가 CHECK Constraint 조건이다. 실제로 dba_constraints 뷰를 조회해보면 다음과 같은 결과를 얻을 수 있다.

select constraint_name, constraint_type, search_condition
from dba_constraints where table_name = 'T_CONST1';

CONSTRAINT_NAME    CONSTRAINT_TYPE    SEARCH_CONDITION
C1    C    name = trim(name)

즉, 테이블을 생성할 때 Blank 문자를 없애기 위해 다음과 같은 Constraint가 추가된 것이다.

alter table t_const1 add constraint c1 check (name = trim(name));

Filtering 조건에 Constraint가 있는 경우, Oracle의 Optimizer는 이를 염두에 둔 실행 계획을 생성하며, 실행 과정에서 데이터의 범위를 줄이기 위해 필요하다면 Constraint 조건을 Filtering Predicate로 삽입하는 일을 수행한다. 이것은 많은 경우 성능에 유리하지만 위의 샘플에서는 오히려 불필요한 Filtering을 유발해 성능 저하를 가져오는 것이다.

이제 Constraint를 제거하면 어떤 변화가 생기는지 관찰해 보자.

alter table t_const1 drop constraint c1;

select /*+ gather_plan_statistics index(t_const1) */  count(*) from t_const1
where name = '0987654321';

select * from table(dbms_xplan.display_cursor(null,null,'ALLSTATS LAST'));

PLAN_TABLE_OUTPUT
SQL_ID  8pjh0vyqxqzny, child number 0
-------------------------------------
select /*+ gather_plan_statistics index(t_const1) */  count(*) from t_const1 where name =
'0987654321'

Plan hash value: 225932663

-----------------------------------------------------------------------------------------------------
| Id  | Operation         | Name         | Starts | E-Rows | A-Rows |   A-Time   | Buffers | Reads  |
-----------------------------------------------------------------------------------------------------
|   1 |  SORT AGGREGATE   |              |      1 |      1 |      1 |00:00:00.22 |    4961 |     20 |
|*  2 |   INDEX RANGE SCAN| T_CONST1_IDX |      1 |    993K|   1000K|00:00:00.01 |    4961 |     20 |
-----------------------------------------------------------------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------

   2 - access("NAME"='0987654321')

Constraint를 제거한 후 불필요한 Filtering 조건이 제거된 것을 확인할 수 있다. 그만큼 성능은 개선된다.  또 하나의 중요한 변화는 E-Rows(Estimated Rows, Cardinality)의 변화이다. Constraint에 의해 Filtering 조건이 추가된 경우에는 E-Rows 값이 9938 인데 반해, Constraint가 없는 경우에는 E-Rows의 값이 993K로 정확하게 100배의 값이다. Oracle은 Constraint 조건에 의해 Filtering이 추가되는 경우 여기서 99%가 걸러진다(Filter)고 가정하고 실제 예상 로우수의 1%만을 Cardinality로 사용한다. 이런 Cardinality의 변화는 실행 계획의 변화를 유발할 수도 있다.

하지만, 기껏 데이터 정합성을 위해 추가한 Constraint를 이런 이유로 제거하는 것은 용납하기 힘들 것이다. 다행히 10195 Event를 이용하면 Constraint를 제거하지 않고도 동일한 효과를 얻을 수 있다.


alter session set events '10195 trace name context forever, level 1';

위의 간단한 예제는 dbms_xplan 패키지가 제공하는 Predicate 정보가 특정 문제를 분석하는 데 있어서 없어서는 안되는 중요한 정보를 제공한다는 것을 잘 보여준다. dbms_xplan 패키지가 제공하는 정보는 매우 다양하며, 잘 활용하면 특정 상황에서는 다른 툴로는 도저히 해석할 수 없는 몇몇 현상들에 대한 분석을 가능하게 해주기도 한다. 앞으로 기회가 닿는 대로 dbms_xplan 패키지의 중요한 기능들에 대해서 논의하는 시간을 가졌으면 하는 바램이다.


신고
Trackback 1 : Comment 0

Write a comment

티스토리 툴바