부제: Cardinality Feed Back의 개념과 사용예제

이번 글은 난이도가 높으므로 익숙하지 않은 사람은 Cardinality Feedback의 개념 정도만 이해하기 바란다. 물론 이 블로그를 꾸준히 구독한 독자라면 어려움 없이 볼 수 있다.

 

현재 많은 시스템이 Oracle11g로 옮겨가고 있다. 11g는 새로운 기능이 많이 추가되었다. 하지만 새롭고 좋은 기능이라도 완벽하지 못하면 문제가 될 수 있다. 오늘은 11g의 새 기능 때문에 성능문제가 발생하는 경우를 소개한다.

시스템이 운영 중에 있을 때 가장 곤욕스러운 경우 중 하나는 SQL의 실행계획이 갑자기 바뀌어 성능이 나빠지는 것이다. SQL과 인덱스 그리고 통계정보가 모두 바뀌지 않아도 실행계획은 바뀔 수 있다. 예를 들면 Oracle11g의 기능인 Cardinality Feedback을 사용함으로 해서 얼마든지 실행계획이 바뀔 수 있는 것이다. 이번 시간에는 실행계획이 변경되는 원인 중 하나인 Cardinality Feedback 의 개념과 작동방식에 대해 알아보고 이것이 언제 문제가 되는지 분석해 보자. 이번에 소개할 예제는 종합적이다. Cardinality Feedback + Cost Based Query Transformation + Bloom Filter가 결합된 것이다. 이를 놓친다면 이들이 어떻게 결합되는지 알 수 없을 뿐만 아니라 성능이 악화된 원인을 파악할 수 없다.

 

예측, 실행, 비교, 그리고 전달

소 잃고 외양간 고친다는 말이 있다. 이미 늦었다는 이야기 이지만 좋은 말로 바꾸면 실수를 다시 하지 않겠다는 의지이다. cardinality feedback(이후 CF)도 이와 비슷한 개념이다. 예를 들어 col1 = ‘1’ 이라는 조건으로 filter되면 백만 건이 return된다고 옵티마이져가 예측해서 full table scan을 했다. 하지만 예측과 달리 실행결과가 100건이 나왔다면? 해당 SQL을 다시 실행할 때는 full table scan보다는 index scan이 유리할 것이다. 그런데 같은 SQL을 두 번째 실행할 때 "실제로는 백만 건이 아니라 100건 뿐이야"라는 정보를 옵티마이져에게 알려주는 전달자가 필요하다. 그 전달자가 바로 CF이다. CF가 없으면 결과가 100건 임에도 SQL을 실행 할 때마다 full table scan을 반복할 것이다. 결국 CF는 악성 실행계획을 올바로 수정하는 것이 목적이며 매우 유용한 기능임을 알 수 있다. CF의 단점은 최초에 한번은 full table scan이 필요하다는 것이다. 왜냐하면 실행해서 결과가 나와야만 실제 분포도(건수)를 알 수 있기 때문이다.

 

CF는 어떻게 실행되나?

CF는 같은 SQL을 두 번 이상 실행했을 때 적용된다. 그 이유는 아래의 CF 적용순서를 보면 알 수 있다.

1. 최초의 실행계획을 작성할 때(Hard Parsing 시에) 예측 분포도가 계산된다.

2. SQL이 실행된다. 한번은 실행 해봐야 예측 분포도와 실제 분포도를 비교할 수 있다.

3. 예측 분포도와 실제 분포도의 값이 차이가 크다면 실제 분포도를 저장한다.

4. 두 번째 실행될 때 CF에 의해 힌트의 형태로 옵티마이져에게 전달되어 실제 분포도가 적용된다. 이때 분포도뿐만 아니라 실행계획이 바뀔 수 있다. 두 번째 이후로 실행될 때는 CF가 계속 적용된다.

 

CF를 발생시켜보자

실행환경 :Oracle 11.2.0.1

 

ALTER SYSTEM FLUSH SHARED_POOL;

ALTER SESSION SET "_OPTIMIZER_USE_FEEDBACK" = TRUE; -- CF를 활성화 한다. default true이다.

 

SELECT /*+ GATHER_PLAN_STATISTICS LEADING(c)  */

       c.cust_id, c.cust_first_name, c.cust_last_name,

       s.prod_cnt, s.channel_cnt, s.tot_amt

  FROM customers c,

       (SELECT   s.cust_id,

                 COUNT (DISTINCT s.prod_id) AS prod_cnt,

                 COUNT (DISTINCT s.channel_id) AS channel_cnt,

                 SUM (s.amount_sold) AS tot_amt

            FROM sales s

        GROUP BY s.cust_id) s

 WHERE c.cust_year_of_birth = 1987

   AND s.cust_id = c.cust_id ;

 

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

| Id  | Operation                      | Name              | E-Rows | A-Rows |   A-Time   | Buffers | Used-Mem |

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

|   0 | SELECT STATEMENT               |                   |        |     23 |00:00:00.15 |    5075 |          |

|*  1 |  HASH JOIN                     |                   |    162 |     23 |00:00:00.15 |    5075 | 1215K (0)|

|   2 |   JOIN FILTER CREATE           | :BF0000           |    162 |    151 |00:00:00.01 |     148 |          |

|   3 |    TABLE ACCESS BY INDEX ROWID | CUSTOMERS         |    162 |    151 |00:00:00.01 |     148 |          |

|   4 |     BITMAP CONVERSION TO ROWIDS|                   |        |    151 |00:00:00.01 |       2 |          |

|*  5 |      BITMAP INDEX SINGLE VALUE | CUSTOMERS_YOB_BIX |        |      1 |00:00:00.01 |       2 |          |

|   6 |   VIEW                         |                   |   7059 |     55 |00:00:00.15 |    4927 |          |

|   7 |    SORT GROUP BY               |                   |   7059 |     55 |00:00:00.15 |    4927 |88064  (0)|

|   8 |     JOIN FILTER USE            | :BF0000           |    918K|   7979 |00:00:00.12 |    4927 |          |

|   9 |      PARTITION RANGE ALL       |                   |    918K|   7979 |00:00:00.11 |    4927 |          |

|* 10 |       TABLE ACCESS FULL        | SALES             |    918K|   7979 |00:00:00.09 |    4927 |          |

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

Predicate Information (identified by operation id):        

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

   1 - access("S"."CUST_ID"="C"."CUST_ID")               

   5 - access("C"."CUST_YEAR_OF_BIRTH"=1987)               

  10 - filter(SYS_OP_BLOOM_FILTER(:BF0000,"S"."CUST_ID")) --> Bloom Filter 적용     

                

SQL 실행결과 sales 테이블의 예측 분포도는 918K건이며 실제 분포도는 Bloom Filter가 적용되어 7979건이다. 그리고 group by operation(ID 7)의 예측 분포도는 7059건이며 실제 분포도는 55건이다. 예측과 실제의 분포도 차이는 두 경우 모두 100배 이상이다. 따라서 CF가 적용될 것이다. 이와는 반대로 customers 테이블의 예측 분포도와 실제 분포도는 162 152로 크게 다르지 않으므로 CF가 적용되지 않을 것이다. 이제 위의 SQL을 재 실행한다면 CF가 적용되어 실제 분포도가 적용될 것이다.

 

--> CF를 발생시키기 위해 위의 SQL 다시 실행               

                

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

| Id  | Operation                      | Name              | E-Rows | A-Rows |   A-Time   | Buffers | Used-Mem |

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

|   0 | SELECT STATEMENT               |                   |        |     23 |00:00:05.61 |    5075 |          |

|   1 |  SORT GROUP BY                 |                   |     55 |     23 |00:00:05.61 |    5075 |75776  (0)|

|*  2 |   HASH JOIN                    |                   |    270 |   3230 |00:00:05.60 |    5075 | 1201K (0)|

|   3 |    TABLE ACCESS BY INDEX ROWID | CUSTOMERS         |    162 |    151 |00:00:00.01 |     148 |          |

|   4 |     BITMAP CONVERSION TO ROWIDS|                   |        |    151 |00:00:00.01 |       2 |          |

|*  5 |      BITMAP INDEX SINGLE VALUE | CUSTOMERS_YOB_BIX |        |      1 |00:00:00.01 |       2 |          |

|   6 |    PARTITION RANGE ALL         |                   |   7979 |    918K|00:00:02.82 |    4927 |          |

|   7 |     TABLE ACCESS FULL          | SALES             |   7979 |    918K|00:00:00.98 |    4927 |          |

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

Predicate Information (identified by operation id):               

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

   2 - access("S"."CUST_ID"="C"."CUST_ID")

   5 - access("C"."CUST_YEAR_OF_BIRTH"=1987)

 

Note

-----

   - cardinality feedback used for this statement --> CF가 발생되었음을 나타냄.

 

두 번째 실행 할 때 CF가 적용되어 예측 분포도가 7979로 바뀌었고 group by 분포도는 55건으로 바뀌었다. 이에 따라 실행계획도 바뀌었다. CF에 의해서 쿼리변환(Complex View Merging)이 발생된 것이다. 그리고 note CF가 적용되었다고 친절히 설명된다.

 

이제 더 자세한 분석을 위하여 10053 Trace의 내용을 보자. 두 번째 실행된 SQL 10053 Trace에 따르면 쿼리변환전의 SQL은 다음과 같다.

 

SELECT /*+ LEADING (C) */

       c.cust_id, c.cust_first_name, c.cust_last_name,

       s.prod_cnt, s.channel_cnt, s.tot_amt tot_amt

  FROM tlo.customers c,

       (SELECT   /*+ OPT_ESTIMATE (GROUP_BY ROWS=55.000000 ) OPT_ESTIMATE (TABLE S ROWS=7979.000000 ) */

                 s.cust_id cust_id, COUNT (DISTINCT s.prod_id) prod_cnt,

                 COUNT (DISTINCT s.channel_id) channel_cnt, SUM (s.amount_sold) tot_amt

            FROM tlo.sales s

        GROUP BY s.cust_id) s

 WHERE c.cust_year_of_birth = 1987

AND s.cust_id = c.cust_id ;

 

CF에 의해서 OPT_ESTIMATE 힌트가 적용되었다. 실제 건수로 적용하는 것이므로 일견 문제가 없어 보인다. 하지만 쿼리변환과정(Complex View Merging)을 거치면 문제가 생긴다. 10053 Trace에서 나타난 쿼리변환 후의 SQL은 다음과 같다.

 

SELECT   /*+ OPT_ESTIMATE (GROUP_BY ROWS=55.000000 ) LEADING (C) OPT_ESTIMATE (TABLE S ROWS=7979.000000 ) */

         c.cust_id cust_id, c.cust_first_name cust_first_name,

         c.cust_last_name cust_last_name, COUNT (DISTINCT s.prod_id) prod_cnt,

         COUNT (DISTINCT s.channel_id) channel_cnt, SUM (s.amount_sold) tot_amt

    FROM tlo.customers c, tlo.sales s

   WHERE c.cust_year_of_birth = 1987

AND s.cust_id = c.cust_id

GROUP BY s.cust_id, c.ROWID, c.cust_last_name, c.cust_first_name, c.cust_id ;

 

CF의 문제점은?

위의 SQL은 두 가지 문제점이 있다. 두 문제 모두 쿼리변환에 의해 발생된다. 첫 번째 문제는 Bloom Filter와 관련된 것이다. CF의 영향으로 원본 SQL에 존재했던 Group By (Complex View)가 사라졌다. 뷰가 없어짐으로써 Bloom Filter가 적용되지 않는다. Filter가 사라졌음에도 불구하고 Filter가 존재했던 Cardinality 7979를 적용해 버렸다. 이에 따라 CF를 적용했음에도 7979건과 실제건수인 91 8천 건과는 엄청난 차이가 나고 말았다. Bloom Filter가 사라질 때는 CF를 적용하면 안 된다는 이야기이다. 비유하자면 Filter가 없는데도 불구하고 Filter가 존재할 때의 건수를 적용시킨 것이다.

 

두 번째 문제는 쿼리변환 후 힌트의 상속과 관련된다. 쿼리변환전의 CF의 의한 힌트를 보면 Group By된 뷰의 건수는 55건이다. 그런데 이 힌트는 오직 sales 테이블에 대한 것이다. 그런데 쿼리변환후의 힌트를 보면 그대로 55건이 적용되어 되어버렸다. Group by가 외부로 빠져 나옴으로 해서 GROUP_BY ROWS는 전체건수와 마찬가지가 되어버렸다. sales 테이블의 Group By건수는 55건이 맞다. 하지만 쿼리변환 때문에 조인 후에 Group By 하게 된다면 cardinality를 다시 계산해야 한다. 조인이 없는 테이블의 Group By건수와 조인후의 Group By건수가 어떻게 같을 수 있나?

 

두 가지의 문제점은 Cost를 계산할 때 그대로 적용되어 버린다. 10053 trace를 보자.

 

Access path analysis for SALES

***************************************

SINGLE TABLE ACCESS PATH

  Single Table Cardinality Estimation for SALES[S]

  Table: SALES  Alias: S

    Card: Original: 918843.000000    >> Single Tab Card adjusted from:918843.000000 to:7979.000000

  Rounded: 7979  Computed: 7979.00  Non Adjusted: 918843.00

  Access Path: TableScan

    Cost:  1328.68  Resp: 1328.68  Degree: 0

      Cost_io: 1321.00  Cost_cpu: 155262306

      Resp_io: 1321.00  Resp_cpu: 155262306

 

Bloom Filter가 없음에도 불구하고 Sales 테이블의 건수(Cardinality) 7979로 적용되어 버렸다. 이제 Group By가 적용된 건수를 보자.

 

GROUP BY cardinality:  270.000000, TABLE cardinality:  270.000000

>> Query Blk Card adjusted from 270.000000  to: 55.000000

    SORT ressource         Sort statistics

      Sort width:         583 Area size:      510976 Max Area size:   102340608

      Degree:               1

      Blocks to Sort: 3 Row size:     69 Total Rows:            270

      Initial runs:   1 Merge passes:  0 IO Cost / pass:          0

      Total IO sort cost: 0      Total CPU sort cost: 20302068

      Total Temp space used: 0


Group By Cardinality와 관련된 Trace 내용이다. 여기서도 잘못된 Group By건수인 55를 적용시키고 있다. 조인 후에 Group By할 때는 Cardinality를 다시 계산해야 옳다. 이래서는 제대로 된 Cost가 나올 수 없다. 여기에 밝혀진 문제점은 SQL 하나에서 나온 것이므로 실전에서는 두 가지 문제뿐만 아니라 더 많을 것이다. 물론 옵티마이져가 모든 경우에 완벽할 수는 없다.


해결책
CF
문제의 해결방법을 생각해보자. 갑자기 실행계획이 바뀌어 성능문제가 발생했을 때 dbms_xplan.display_cursor의 note나 10053 Trace의 실행계획 부분을 보면 CF가 적용되었는지 아닌지 알 수 있다. 만약 CF가 적용되었다면 일단 의심해보아야 한다. 아래는 10053 trace의 실행계획 부분이다.

-----------------------------------------------------------+------------------------

| Id  | Operation                       | Name             | Rows  | Bytes | Cost  |

-----------------------------------------------------------+------------------------

| 0   | SELECT STATEMENT                |                  |       |       |  1368 |

| 1   |  SORT GROUP BY                  |                  |    55 |  2915 |  1368 |

| 2   |   HASH JOIN                     |                  |   270 |   14K |  1367 |

| 3   |    TABLE ACCESS BY INDEX ROWID  | CUSTOMERS        |   162 |  5832 |    38 |

| 4   |     BITMAP CONVERSION TO ROWIDS |                  |       |       |       |

| 5   |      BITMAP INDEX SINGLE VALUE  | CUSTOMERS_YOB_BIX|       |       |       |

| 6   |    PARTITION RANGE ALL          |                  |  7979 |  132K |  1329 |

| 7   |     TABLE ACCESS FULL           | SALES            |  7979 |  132K |  1329 |

-----------------------------------------------------------+------------------------

Predicate Information:

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

2 - access("S"."CUST_ID"="C"."CUST_ID")

5 - access("C"."CUST_YEAR_OF_BIRTH"=1987)

 

Content of other_xml column

===========================

nodeid/pflags: 7 17nodeid/pflags: 6 17  cardinality_feedback: yes --> CF가 적용됨

...이후 생략


만약 CF가 문제가 된다면 해당 SQL을 시작하기 전에 세션단위로 _optimizer_use_feedback = false를 적용하거나 opt_param 힌트를 사용하면 된다. 이렇게 하면 CF가 방지되어 쿼리변환의 원인이 제거된다. 따라서 Bloom Filter도 보존할 수 있다. 또 다른 방법은 인라인뷰에 no_merge 힌트를 적용하여 쿼리변환을 방지하면 문제는 해결된다. 이 두 가지 방법은 결국 쿼리변환을 방지하는 것이다.

 

결론

CF란 건수를 예측하고, 실행해서 실제건수와 예측건수를 비교하여 차이가 많다면 다음 번에 실행할 때 옵티마이져에게 실제건수를 전달해주는 역할을 한다. CF의 개념을 정리 했으므로 이제 큰 그림을 그려보자. 위의 예제에서 성능이 악화된 직접적인 이유는 Bloom Filter가 사라졌기 때문이다. 하지만 그렇게 된 이유는 쿼리변환 때문이며 쿼리변환의 이유는 CF 때문이다. 직접적인 원인을 찾았다고 해도 포기해선 안 된다. 꼬리에 꼬리를 무는 원인이 있을 수 있기 때문이다. 이를 도식화 하면 다음과 같다.

사용자 삽입 이미지


옵티마이져의 설계관점에서 개선해야 될 사항을 논의 해보자. 옵티마이져가 CBQT를 고려할 때는 두 가지의 경우로 판단한다. 쿼리변환을 적용하기 전(Iteration 1) Cost와 적용 후(Iteration 2) Cost를 비교해야 되기 때문이다. 쿼리변환전의 Cost를 구할 때는 CF를 적용시키고 반대로 쿼리변환 후에는 CF를 적용하지 않는 것이 더 좋은 Cost를 구할 수 있다. 왜냐하면 비록 답이 같다고 하더라도 형태가 전혀 다른 SQL에 대해 CF를 적용시킬 이유는 없기 때문이다. 물론 이렇게 해도 여전히 문제가 될 수는 있다. 하지만 문제의 발생확률은 많이 줄어들지 않겠는가?

 


신고
Posted by extremedb

댓글을 달아 주세요

  1. 윤상원 2010.10.25 16:15 신고  댓글주소  수정/삭제  댓글쓰기

    항상 좋은 정보 감사합니다.
    글을 읽고 궁금한 점이 있어 이렇게 질문을 남깁니다.
    bind 변수를 사용할 경우는 Cardinality Feedback이 적용되지 않나요?
    bind 변수를 사용할 경우는 실행할 때마다 결과가 달라지므로 Cardinality Feedback이 적용되기 어려울꺼 같은데요.
    bind 변수를 사용할 경우는 Adaptive Cursor Sharing가 적용되는 것이고 상수를 사용할 경우는
    Cardinality Feedback이 적용되는 것인가요?
    답변 부탁드립니다~

    • Favicon of http://scidb.tistory.com BlogIcon extremedb 2010.10.25 18:09 신고  댓글주소  수정/삭제

      상수를 변수로 바꾸어 테스트 해도 똑같이 cf가 발생합니다.
      하지만 변수값이 바뀌었을 때는 좀더 깊은 연구가 필요합니다.
      감사합니다.

  2. salvation 2010.10.25 16:15 신고  댓글주소  수정/삭제  댓글쓰기

    글 시작할때 어렵다고 하셨는데 전혀 어렵지않으면 어느정도 수준인가요? 자랑이 아니라 정말궁금...

    • Favicon of http://scidb.tistory.com BlogIcon extremedb 2010.10.26 00:05 신고  댓글주소  수정/삭제

      1.CF
      2.Query Transformation
      3.Bloom Filter

      1,2,3 을 다 안다면 어렵지 않습니다.
      만약 컨설턴트라면 수준이 높다고 할 수 없지만
      DBA라면 옵티마이져를 어느정도 아시는 분이라고 할 수 있습니다. 즉 개발자, DBA, 컨설턴트는 기대치가 다르므로 다르게 판단되어야 할 것 같습니다.

    • 혈기린 2010.10.26 15:24 신고  댓글주소  수정/삭제

      컨설턴트가 될려면 어느정도의 수준이 되어야 할까요?
      멀고먼 길이네요 -.-;;

    • Favicon of http://scidb.tistory.com BlogIcon extremedb 2010.10.26 19:16 신고  댓글주소  수정/삭제

      어려운 질문입니다.
      기술은 기본만 되어 있다면 가능합니다. 제가 컨설팅을 시작할 당시(지금도 마찬가지 이지만)는 실력이 좋지 않았습니다. 실력이란 목표가 있으면 따라 옵니다.
      구체적인 목표가 있어야 훌륭한 컨설턴트가 될 것 같습니다.

      많은 분들이 열정을 이야기 하시는데 열정은 불꽃 같은 것이라 사그라 들 수 있습니다. 큰 목표하나를 세우시고 세부적인 목표를 세우신다면 좋은 성과가 있을것 입니다. 큰 목표가 40 이나 50세에 뭘 하고 있을건지 에 대한 대답이라면 세부적인 목표는 큰꿈을 이루기 위해 이번주에 할일이 무엇인지를 묻는것 입니다. 목표가 세워지면 열정은 따라 옵니다. 도움 되셨나요?

    • 혈기린 2010.10.27 16:02 신고  댓글주소  수정/삭제

      조언 감사 드립니다
      목표를 세워서 열씨미 노력해야겠네요 ^^

  3. salvation 2010.10.25 20:49 신고  댓글주소  수정/삭제  댓글쓰기

    답변 감사합니다. 위의 댓글은 갤럭시S로 쓴겁니다. ㅎㅎ
    저는 개발자도, DBA도, 컨설턴트도 아닌... 어중이 떠중이 입니다.
    저의 수준에 의심이 들어 질문을 던진 것 이였습니다.

    상수를 변수로 바꾸었을때에 CF가 발생한다 말씀 하셨는데..
    바인드 피킹이 off인 상태에서는 발생하지 않을것으로 보이는데..
    바인드 피킹이 on 상태라면 결국 CF라는게 결국 어느 한범주에 속할거 같다는 생각이 듭니다.
    Adaptive Cursor Sharing, CF 이런게 결국 어느 한 범주에 속하는 것이 아닐까요?
    말 그대로 logical optimizer라 하면 너무 큰 범주구요..

    • Favicon of http://scidb.tistory.com BlogIcon extremedb 2010.10.26 01:01 신고  댓글주소  수정/삭제

      Adaptive Cursor Sharing, CF, Dynamic Sampling 등은 Physical 옵티마이져를 보완합니다. SQl Plan Baseline, SQL Profile, Stored OutLine 이놈들은 실행계획을 고정시키는 놈들입니다.

      옵티마이져의 기능이라고 볼 수도 있겠지만 옵티마이져의 실수를 막아주는 모듈이라고 보는 것이 더 정확합니다. 이 모든 것은 옵티마이져 서포터즈들 입니다. 즉 옵티마이져는 Logicl + Physical 이 존재하는 상태이지만 옵티마이져가 완벽하지 못하므로 이를 보완하는 것들 입니다. 하지만 아이러니 하게도 서포터즈들이 문제를 일으킬 수 도 있다는 겁니다.^^

      위의 놈들 중에서 11g의 SQl Plan Baseline이 가장 강력합니다. Adaptive Cursor Sharing, CF, Dynamic Sampling 등이 실행계획을 변경시킬 수 있지만 SQl Plan Baseline이라는 최후의 보루를 두어서 안심할 수 있을것입니다.

  4. ExtraOdinary 2010.11.03 15:52 신고  댓글주소  수정/삭제  댓글쓰기

    아직 11g로 운영중인 사이트가 별로 없었는데, 동규님 블로그를 통해 11g의 좋은 기능들과 함정(?)들을 미리 이해할 수 있어 항상 감사합니다.

블로그가 일주일에 한번만 업데이트 되기 때문에 많은 분들이 어떤 내용이 블로그에 올라올지 궁금해 하시는것 같습니다. 그래서 시간이 허락한다면 블로그에 올라갈 내용을 미리 공지 하겠습니다.
 
제목
: Cardinality Feed Back
이 위험할 때

부제목: Cardinality Feed Back의 개념과 사용예제

문서의 목적
1. Oracle11
의 새 기능인 Cardinality Feedback의 개념을 알아보고 실행예제를 분석해본다.
2. Cardinality Feedback
이 문제가 되는 경우를 살펴보고 해결방법을 제시한다
.

목차
1.
서론
2. Cardinality Feedback의 개념:
소제목 예측, 실행, 비교, 그리고 전달 부분
3. Cardinality Feedback의 작동방법: 소제목 CF는 어떻게 실행되나? 부분
4.
Cardinality Feedback 실행예제: 소제목 CF를 발생시켜보자 부분
5.
Cardinality Feedback 문제점: 소제목 CF의 문제점은? 부분
6.
문제의 해결방법: 소제목 해결책 부분
7.
결론

분석도구
1. 10053 Trace
2. DBMS_XPLAN.display_cursor

참조문서
Closing the Query Processing Loop in Oracle 11g - Allison Lee, Mohamed Zait


예상발행일자
2010.10.25 일


주의사항: 블로그 내용은 예고없이 변경될 수 있습니다.

많이 기대해주세요.

신고
Posted by extremedb

댓글을 달아 주세요

  1. Favicon of http://1ststreet.tistory.com BlogIcon SITD 2012.06.25 14:47 신고  댓글주소  수정/삭제  댓글쓰기

    와...
    진짜 글쓰는 법에 대한 모범을 보여주시네요.
    먼저 전체적인 윤곽을 보여주시니..

부제목: view에서 union, minus, intersect를 제거하라

많은 사람들이 union union all의 차이점에 대해 알고 있다. union Sort와 중복제거라는 기능으로 인해 UNION ALL에 비하여 성능이 떨어진다는 것이다. 옳은 말이다. 하지만 union대신에 union all을 써야 하는 또 다른 이유가 있다는 것을 아는 사람은 얼마나 될까? 이 사실을 알지 못하면 불필요한 엑세스가 추가될 수 있으므로 성능이 저하된다.


먼저 고객테이블을 이용하여 뷰를 하나 만들고 그것을 이용한 SQL문을 만들어 보자
.
환경 : Oracle 11.2.0.1

create or replace view vw_cust5 as
select *
  from customers
union           --> union을 사용함
select *
  from customers;


select a.cust_id,
       b.prod_id,
       b.time_id,
       b.channel_id,
       b.quantity_sold
  from vw_cust5 a,
       sales b
 where a.cust_id = b.cust_id
   and a.cust_id = 14865  ; 

 

뷰에서 사용하는 컬럼은 a.cust_id 하나 뿐이다. 따라서 고객 테이블에 PK인덱스를 사용한다면 customers 테이블로 엑세스 하지 않아도 된다. 하지만 아래의 실행계획을 본다면 문제를 발견할 수 있다.


-------------------------------------------------------------+----------------
| Id  | Operation                            | Name          | Rows  | Cost  |
-------------------------------------------------------------+----------------
| 0   | SELECT STATEMENT                     |               |       |    62 |
| 1   |  HASH JOIN                           |               |   260 |    62 |
| 2   |   VIEW                               | VW_CUST5      |     2 |     6 |
| 3   |    SORT UNIQUE                       |               |     2 |     6 |
| 4   |     UNION-ALL                        |               |       |       |
| 5   |      TABLE ACCESS BY INDEX ROWID     | CUSTOMERS     |     1 |     2 |
| 6   |       INDEX UNIQUE SCAN              | CUSTOMERS_PK  |     1 |     1 |
| 7   |      TABLE ACCESS BY INDEX ROWID     | CUSTOMERS     |     1 |     2 |
| 8   |       INDEX UNIQUE SCAN              | CUSTOMERS_PK  |     1 |     1 |
| 9   |   PARTITION RANGE ALL                |               |   130 |    56 |
| 10  |    TABLE ACCESS BY LOCAL INDEX ROWID | SALES         |   130 |    56 |
| 11  |     BITMAP CONVERSION TO ROWIDS      |               |       |       |
| 12  |      BITMAP INDEX SINGLE VALUE       | SALES_CUST_BIX|       |       |
-------------------------------------------------------------+----------------


PK
인덱스를 사용하였지만 customers 테이블로 불필요한 엑세스를 하였다. 이유가 무엇일까? 10053 trace를 보자.

Final query after transformations:******* UNPARSED QUERY IS *******

SELECT "A"."CUST_ID" "CUST_ID","B"."PROD_ID" "PROD_ID","B"."TIME_ID" "TIME_ID","B"."CHANNEL_ID" "CHANNEL_ID","B"."QUANTITY_SOLD" "QUANTITY_SOLD" FROM  ( (SELECT "CUSTOMERS"."CUST_ID" "CUST_ID","CUSTOMERS"."CUST_FIRST_NAME" "CUST_FIRST_NAME","CUSTOMERS"."CUST_LAST_NAME"
중간생략
"CUST_TOTAL_ID","CUSTOMERS"."CUST_SRC_ID" "CUST_SRC_ID","CUSTOMERS"."CUST_EFF_FROM" "CUST_EFF_FROM","CUSTOMERS"."CUST_EFF_TO" "CUST_EFF_TO" FROM "TLO"."CUSTOMERS" "CUSTOMERS" WHERE "CUSTOMERS"."CUST_ID"=14865 AND "CUSTOMERS"."CUST_ID"=14865)UNION (SELECT "CUSTOMERS"."CUST_ID" "CUST_ID","CUSTOMERS"."CUST_FIRST_NAME" "CUST_FIRST_NAME","CUSTOMERS"."CUST_LAST_NAME"
중간생략
"CUST_TOTAL_ID","CUSTOMERS"."CUST_SRC_ID" "CUST_SRC_ID","CUSTOMERS"."CUST_EFF_FROM" "CUST_EFF_FROM","CUSTOMERS"."CUST_EFF_TO" "CUST_EFF_TO" FROM "TLO"."CUSTOMERS" "CUSTOMERS" WHERE "CUSTOMERS"."CUST_ID"=14865 AND "CUSTOMERS"."CUST_ID"=14865)) "A","TLO"."SALES" "B" WHERE "A"."CUST_ID"="B"."CUST_ID" AND "B"."CUST_ID"=14865


위의 trace는 쿼리변환을 끝낸 상태의 SQL이다. 그런데 SQL을 자세히 보면 뷰 내부의 모든 컬럼을 select 하고 있다. 다시 말해 뷰 내부의 컬럼중에 cust_id만 존재하면 되는데, 나머지 컬럼이 삭제되지 않고 남아있다. 이것 때문에 불필요한 테이블 엑세스가 나타난 것이다.


union
대신에 union all을 사용해보자

테스트를 위하여 100건 짜리 고객테이블을 만들고 unique 인덱스를 만들어 보자.

create table customers_100 as 
select *
  from customers
 where rownum < 101;
 
create unique index PK_CUSTOMERS_100  on customers_100 (cust_id);


이제 union 대신에 union all로 뷰를 생성하여 테스트한다.

create or replace view vw_cust2 as
select *
 from customers
UNION ALL
select *
 from customers_100 ;
 
SELECT a.cust_id,
       b.prod_id,
       b.time_id,
       b.channel_id,
       b.quantity_sold
  FROM vw_cust2 a,
       sales b
 WHERE a.cust_id = b.cust_id
   AND a.cust_id = 14865  ;


---------------------------------------------------------------+----------------
| Id  | Operation                            | Name            | Rows  | Cost  |
---------------------------------------------------------------+----------------
| 0   | SELECT STATEMENT                     |                 |       |    57 |
| 1   |  HASH JOIN                           |                 |   260 |    57 |
| 2   |   VIEW                               | VW_CUST2        |     2 |     1 |
| 3   |    UNION-ALL                         |                 |       |       |
| 4   |     INDEX UNIQUE SCAN                | CUSTOMERS_PK    |     1 |     1 |
| 5   |     INDEX UNIQUE SCAN                | PK_CUSTOMERS_100|     1 |     0 |
| 6   |   PARTITION RANGE ALL                |                 |   130 |    56 |
| 7   |    TABLE ACCESS BY LOCAL INDEX ROWID | SALES           |   130 |    56 |
| 8   |     BITMAP CONVERSION TO ROWIDS      |                 |       |       |
| 9   |      BITMAP INDEX SINGLE VALUE       | SALES_CUST_BIX  |       |       |
---------------------------------------------------------------+----------------


SLP
란 무엇인가?

실행계획에서 보듯이 union all 을 사용하니 테이블 엑세스가 사라졌다. 이것은 SLP(Select List Pruning)라는 쿼리변환의 기능 때문에 가능한 것이다. SLP union all이 들어있는 뷰를 사용할 때 발생하며 뷰의 컬럼중에 사용하지 않는 것을 제거한다. 이제 10053 trace 내용 중에서 SLP에 대해 분석해보자. 특히 SLP 변환 전 SQL SLP 변환 후 SQL을 비교해보라. 아래의 10053 trace 내용이 복잡해 보이지만 구조는 단순하며 다음과 같다.

1) 쿼리변환 전 SQL
2) 쿼리변환(SLP)
3) 쿼리변환 후 SQL

 

SQL:******* UNPARSED QUERY IS *******
SELECT "A"."CUST_ID" "CUST_ID","B"."PROD_ID" "PROD_ID","B"."TIME_ID" "TIME_ID","B"."CHANNEL_ID" "CHANNEL_ID","B"."QUANTITY_SOLD" "QUANTITY_SOLD" FROM  ( (SELECT "CUSTOMERS"."CUST_ID" "CUST_ID","CUSTOMERS"."CUST_FIRST_NAME" "CUST_FIRST_NAME","CUSTOMERS"."CUST_LAST_NAME" "CUST_LAST_NAME","CUSTOMERS"."CUST_GENDER"
중간생략
"CUST_SRC_ID","CUSTOMERS"."CUST_EFF_FROM" "CUST_EFF_FROM","CUSTOMERS"."CUST_EFF_TO" "CUST_EFF_TO" FROM "TLO"."CUSTOMERS" "CUSTOMERS") UNION ALL  (SELECT "CUSTOMERS_100"."CUST_ID" "CUST_ID","CUSTOMERS_100"."CUST_FIRST_NAME""CUST_FIRST_NAME",
"CUSTOMERS_100"."CUST_LAST_NAME" "CUST_LAST_NAME","CUSTOMERS_100"."CUST_GENDER"
중간생략
"CUST_SRC_ID","CUSTOMERS_100"."CUST_EFF_FROM""CUST_EFF_FROM","CUSTOMERS_100"."CUST_EFF_TO"
"CUST_EFF_TO" FROM "TLO"."CUSTOMERS_100" "CUSTOMERS_100")) "A","TLO"."SALES" "B" WHERE "A"."CUST_ID"="B"."CUST_ID" AND "A"."CUST_ID"=14865
Query block SEL$1 (#0) unchanged
SLP: Removed select list item CUST_FIRST_NAME from query block SEL$3
SLP: Removed select list item CUST_FIRST_NAME from query block SEL$2
SLP: Removed select list item CUST_FIRST_NAME from query block SET$1
...
중간생략
SLP: Removed select list item CUST_EFF_TO from query block SEL$3
SLP: Removed select list item CUST_EFF_TO from query block SEL$2
SLP: Removed select list item CUST_EFF_TO from query block SET$1
JE:   Considering Join Elimination on query block SEL$1 (#0)
*************************
Join Elimination (JE)   
*************************
SQL:******* UNPARSED QUERY IS *******
SELECT "A"."CUST_ID" "CUST_ID","B"."PROD_ID" "PROD_ID","B"."TIME_ID" "TIME_ID","B"."CHANNEL_ID" "CHANNEL_ID","B"."QUANTITY_SOLD" "QUANTITY_SOLD" FROM  ( (SELECT "CUSTOMERS"."CUST_ID" "CUST_ID" FROM "TLO"."CUSTOMERS" "CUSTOMERS") UNION ALL  (SELECT "CUSTOMERS_100"."CUST_ID" "CUST_ID" FROM "TLO"."CUSTOMERS_100" "CUSTOMERS_100")) "A","TLO"."SALES" "B" WHERE "A"."CUST_ID"="B"."CUST_ID" AND "A"."CUST_ID"=14865


필요한 컬럼만 살아남다
SLP(Select List Pruning)
가 발생하여 사용하지 않는 모든 컬럼을 삭제하였다. 쿼리변환 후의 SQL을 보면 뷰 내부의 컬럼은 모두 제거되고 cust_id만 남아있다. SLP 기능에 의해 테이블 엑세스가 없어진 것이다.
뷰 내부에는 union뿐만 아니라 minus,intersect 등의 집합 연산의 사용을 자제해야 한다. SLP가 발생하지 않기 때문이다.


그렇다면 뷰내부에 union all과 minus를 동시에 사용하면 어떻게 될까? 이제 union all
minus를 동시에 사용한 뷰를 생성하고 SLP가 발생하는지 테스트 해보자.

CREATE OR REPLACE VIEW vw_cust33 AS
SELECT *
  FROM customers
UNION ALL
SELECT *
  FROM customers
MINUS
SELECT *
  FROM customers_100;


이제 위의 뷰를 사용하여 select문을 실행해보자.

SELECT a.cust_id,
       b.prod_id,
       b.time_id,
       b.channel_id,
       b.quantity_sold
  FROM vw_cust33 a, sales b
 WHERE a.cust_id = b.cust_id
   AND a.cust_id = 14865 ;


---------------------------------------------------------------+----------------
| Id  | Operation                            | Name            | Rows  | Cost  |
---------------------------------------------------------------+----------------
| 0   | SELECT STATEMENT                     |                 |       |    63 |
| 1   |  HASH JOIN                           |                 |   260 |    63 |
| 2   |   VIEW                               | VW_CUST33       |     2 |     7 |
| 3   |    MINUS                             |                 |       |       |
| 4   |     SORT UNIQUE                      |                 |     2 |       |
| 5   |      VIEW                            |                 |     2 |     4 |
| 6   |       UNION-ALL                      |                 |       |       |
| 7   |        TABLE ACCESS BY INDEX ROWID   | CUSTOMERS       |     1 |     2 |
| 8   |         INDEX UNIQUE SCAN            | CUSTOMERS_PK    |     1 |     1 |
| 9   |        TABLE ACCESS BY INDEX ROWID   | CUSTOMERS       |     1 |     2 |
| 10  |         INDEX UNIQUE SCAN            | CUSTOMERS_PK    |     1 |     1 |
| 11  |     TABLE ACCESS BY INDEX ROWID      | CUSTOMERS_100   |     1 |     1 |
| 12  |      INDEX UNIQUE SCAN               | PK_CUSTOMERS_100|     1 |     0 |
| 13  |   PARTITION RANGE ALL                |                 |   130 |    56 |
| 14  |    TABLE ACCESS BY LOCAL INDEX ROWID | SALES           |   130 |    56 |
| 15  |     BITMAP CONVERSION TO ROWIDS      |                 |       |       |
| 16  |      BITMAP INDEX SINGLE VALUE       | SALES_CUST_BIX  |       |       |
---------------------------------------------------------------+----------------


minus
가 존재하여 SLP가 발생되지 않았다. Minus 때문에 불필요한 테이블 엑세스가 세 번이나 발생되었다. 어떻게 하면 이 문제를 해결할 수 있을까? 물론 부정형 서브쿼리(not exists)를 사용하면 minus 를 대신할 수 있으므로 불필요한 테이블 엑세스는 없어질 것이다.


minus
를 사용하면서 SLP가 가능한가?

문제는 minus를 사용하면서 불필요한 엑세스를 방지할 수 있는 방법이 있냐는 것이다. 가장 쉬운 방법은 뷰에서 minus 부분을 제거하는 것이다. 즉 아래의 SQL처럼 minus 대신에 테이블을 직접 사용하면 된다.

create or replace view vw_cust as
select * from customers
UNION ALL
select * from customers ;
 
SELECT a.cust_id,
       b.prod_id,
       b.time_id,
       b.channel_id,
       b.quantity_sold
  FROM VW_CUST a,        --
minus가 빠진 뷰를 사용함

       sales b
 WHERE a.cust_id = b.cust_id
   AND a.cust_id = 14865
MINUS  
SELECT a.cust_id,
       b.prod_id,
       b.time_id,
       b.channel_id,
       b.quantity_sold
  FROM CUSTOMERS_100 a, --
테이블을 직접 사용함
       sales b
 WHERE a.cust_id = b.cust_id
   AND a.cust_id = 14865 ;

-----------------------------------------------------------------+----------------
| Id  | Operation                              | Name            | Rows  | Cost  |
-----------------------------------------------------------------+----------------
| 0   | SELECT STATEMENT                       |                 |       |   115 |
| 1   |  MINUS                                 |                 |       |       |
| 2   |   SORT UNIQUE                          |                 |   260 |    59 |
| 3   |    HASH JOIN                           |                 |   260 |    58 |
| 4   |     VIEW                               | VW_CUST         |     2 |     2 |
| 5   |      UNION-ALL                         |                 |       |       |
| 6   |       INDEX UNIQUE SCAN                | CUSTOMERS_PK    |     1 |     1 |
| 7   |       INDEX UNIQUE SCAN                | CUSTOMERS_PK    |     1 |     1 |
| 8   |     PARTITION RANGE ALL                |                 |   130 |    56 |
| 9   |      TABLE ACCESS BY LOCAL INDEX ROWID | SALES           |   130 |    56 |
| 10  |       BITMAP CONVERSION TO ROWIDS      |                 |       |       |
| 11  |        BITMAP INDEX SINGLE VALUE       | SALES_CUST_BIX  |       |       |
| 12  |   SORT UNIQUE                          |                 |   130 |    56 |
| 13  |    NESTED LOOPS                        |                 |   130 |    55 |
| 14  |     INDEX UNIQUE SCAN                  | PK_CUSTOMERS_100|     1 |     0 |
| 15  |     PARTITION RANGE ALL                |                 |   130 |    55 |
| 16  |      TABLE ACCESS BY LOCAL INDEX ROWID | SALES           |   130 |    55 |
| 17  |       BITMAP CONVERSION TO ROWIDS      |                 |       |       |
| 18  |        BITMAP INDEX SINGLE VALUE       | SALES_CUST_BIX  |       |       |
-----------------------------------------------------------------+----------------
예상대로 뷰에서 minus를 삭제하니 성공적으로 SLP가 발생되었고 테이블 엑세스가 모두 사라졌다.

결론
 
이번 시간에는 union과 union all의 또 다른 차이점에 대해 알아보았다. union all을 사용하면 SLP가 발생되어 뷰에서 사용되지 않는 컬럼을 제거한다. 이때 인덱스만으로 scan을 끝낼수 있는 경우 불필요한 테이블스캔이 방지된다. 따라서 뷰 내부에서는 union, minus, intersect를 빼는 것이 유리하다.
뷰 내부의 minus는 not exists로 바꾸면 된다. 대부분의 경우 뷰 내부에 Intersect도 필요치 않다. Intersect란 교집합이며 이것은 조인으로 해결할 수 있다. 왜냐하면 조인이란 두 집합에서 조인된 컬럼을 기준으로 값이 같은 것만 추출하는 기능이기 때문이다. 그렇지 않은가?

신고
Posted by extremedb

댓글을 달아 주세요

  1. 2010.10.18 11:28 신고  댓글주소  수정/삭제  댓글쓰기

    좋은 내용 잘 보고 갑니다.

    잘 지내시죠?
    언제 함 뵈야 하는데 시간적 여유, 심리적 여유가 잘 안나네요..

    조만간 함 연락드릴께요,....^^

  2. feelie 2010.10.18 12:54 신고  댓글주소  수정/삭제  댓글쓰기

    Union, Union all 의 차이에 이런부분도 있다니...
    넓은 지식도 중요하지만..
    깊이있는 지식의 중요성을 한번더 일깨워주시네요.
    내용감사합니다..

    • Favicon of http://scidb.tistory.com BlogIcon extremedb 2010.10.18 13:14 신고  댓글주소  수정/삭제

      승필님 반갑습니다.
      말씀하신대로 상식이 중요하다는 의미에서 넓고 얇게 파는것도 중요하지만, 전공분야의 하나를 깊게 파는 것도 중요한것 같습니다.

  3. Favicon of http://ukja.tistory.com BlogIcon 욱짜 2010.10.18 15:15 신고  댓글주소  수정/삭제  댓글쓰기

    10053 트레이스의 활용 예제로 사용하기 좋군요!

부제목: 논리적인 생각과 글쓰기의 도구- 논증모형(논리모델)

 

The War는 예술성이 부족한 영화인가?

2007년 여름 우리는 심형래 감독의 영화 <The war>는 두 가지 이유로 이슈가 되었다. 화려한 컴퓨터그래픽과는 대조적으로 스토리가 엉망이라는 것이다. 일부 인문학계에서는 스토리에 예술성이 더해졌다면 좀더 완벽한 영화가 되었을 것이라고 주장한다. 하지만 예술성이 문제가 아니라 논리적이지 않은 스토리전개가 문제이다. 가장 문제가 되었던 영화의 세 장면을 분석해보자.

1. 조선의 남녀가 아무런 암시도 없이 몇 백 년이 지난 후 미국에서 환생하는 장면

2. 갑자기 하늘에서 떨어진 착한 용의 도움으로 나쁜 용이 제거되는 장면

3. 사랑하는 사이가 아닌 남녀 주인공이 아무런 계기도 없이 바로 키스를 하는 장면

위의 세가지 문제는 예술성의 부재가 아니라 스토리전개의 논리가 부족하다는 점을 잘 보여준다. 문화평론가 진중권 교수는 특히 2번에 대해서 '데우스 엑스 마키나'라고 하였다. 굳이 이런 용어를 사용하지 않더라도 장면과 장면 사이에 인과관계를 찾아볼 없기 때문에 스토리가 엉성한 것을 있다. 예술성은 다음 문제이다.

 

영화를 볼 때 스토리전개의 논리가 약하면 엉성한 영화라는 느낌을 받는다. 그런데 영화가 아닌 사람에게서 논리가 빠지면 어떻게 될까? 이 질문은 답하기가 비교적 쉽다.

1. 논리적인 사고를 하지 못하면 문제를 해결하기 어렵다.

2. 논리적인 사고를 하지 못하면 논리적인 말을 할 수 없고, 논리적인 글도 쓸 수 없다.

3. 1 2를 할 수 없으면 신뢰받는 사람이 될 수 없다.

하지만, 위의 주장이 옳다고 여기는 사람들도 시큰둥한 반응을 보인다. 대부분 다음과 같은 말을 한다. ‘누가 그걸 모르나?' 중요한 것은 논리적인 사람이 되는 방법이나 도구는 무엇인가?' 이며 그 질문에 제대로 대답하는 사람은 거의 없다는 것이다. 공감한다. 이런 질문에는 한가지 정답만을 말하기는 어렵다. 하지만, 필자는 감히 한가지 정답만 이야기 해야겠다. 논리적인 사람이 되려면 논증모형을 활용해야 한다.  

 

논증모형이란 무엇인가?

사용자 삽입 이미지

그림의 파랑 색 부분이 논증모형이며 중간중간의 형광색 부분이 설명이다.

 

논증의 대표선수-주장+이유+근거

먼저 논리모형의 요소 중에서 가장 중요한 주장-이유-근거에 대해 알아보자. ‘문제해결을 위해 무엇을 하라라고 주장하려면 이유가 있어야 한다. 이유는 근거(증거)를 기초로 하면 더욱 힘이 실린다. 이것이 논증모형의 핵심인 주장-이유-근거이다. 다음 예문을 보면 논증모형을 더 쉽게 이해할 수 있다.


폭력적인 영화를 자주 보는것은 어린이들의 정서에 악영향을 미친다.(주장) 왜냐하면 영화라는 허구의 세계에 깊이 빠지면 허구와 현실을 구분하는 능력이 서서히 떨어지기 때문이다.(이유) 최근 연구결과에 의하면, 폭력적인 영화에 노출된 12세 미만 어린이들은 이후 생략.(근거)

 

논증은 주장과 이유만으로도 구성될 수 있다. 예를 들어 암에 걸렸기 때문에(이유) 수술을 받아야 한다(주장)”은 근거를 필요로 하지 않는다. 하지만 이런 단순한 주제는 사실상 논증이 필요 없다. 이와 반대로 복잡한 문제를 해결하는 논증은 근거가 반드시 필요하다. 근거에 기초한 이유일 때 주장에 힘이 실리기 때문이다.

 

갑론을박의 주인공 - 반론수용 / 반박

반론수용이란 자신의 주장에 대해 누군가 ~라고 반대하거나 대안을 제시할 때 발생한다. 반론이나 대안이 적절하다면 수용해야 한다는 뜻이다. 이와는 반대로 반대의견이 옳지 않을 때는 또다시 반대논증을 해야 하는데 이를 반박이라 한다. 반론수용/반박은 논증모형의 모든 요소(주장-이유-근거)에서 발생할 수 있다. 즉 주장을 반박할 수도, 이유를 반박할 수도, 근거를 반박할 수도, 세 가지 모두를 반박할 수도 있다.

 

논증모형에 반론수용/반박이 존재하는 이유는 홀로 생각하거나 글을 쓰는 경우 때문이다. 주장에 대해 상대와 대화할 때는 갑론을박이 자유로이 일어난다. 하지만 혼자 생각하거나 글을 쓸 때는 상대방과 대화를 할 수 없다. 따라서 어떤 대안이 있는지, 어떤 반대가 있는지, 그 주장이 어떤 문제를 일으키는지에 대한 상대방의 의견을 들을 수 없다. 논증모형에 반론수용/반박을 강제로 추가함으로써 혼자일지라도 여러 가지 반론과 대안 그리고 주장이 일으킬 수 있는 문제점을 생각해 보는 시간을 갖자는 것이다. 그렇게 함으로써 자기의견을 부정하거나 반박할 수 있는 관점을 미리 계산하고 이에 대응할 때 타당하고 공정한 논증을 할 수 있다.

 

주장과 이유를 연결시키는 접착제 - 전제

주장과 이유를 연결시키지 못할 때 전제가 필요하다. 아래의 주장과 이유를 보자.

감독이 자리에 없으므로(이유) 선수들은 대충 훈련할 것이다.(주장)” 그런데 이 주장과 이유가 어떤 관계가 있는지 모르는 사람이 있을 수 있다. 감독이 자리에 없다고 해서 선수들이 훈련을 하지 않는다고 말할 수 있는가?”라고 질문할 수 있다. 이럴 때 전제를 활용하면 이유와 주장을 연결시킬 수 있다.

고양이가 없으면 쥐들이 날뛴다.”(전제)

마찬가지로 선수들은 대충훈련할 것이다.”(주장) “감독이 없기 때문이다.”(이유)

위의 예제와 마찬가지의 방법으로 전제는 이유와 근거를 연결시키는데 사용할 수도 있다.

 

이제 논증모형에 대한 설명이 마무리 되었으므로 논증모형이 필요한 세가지 이유와 활용방안을 알아보자.

 

사람들은 얼마나 논리적일까?

디나 쿤의 실험: 이 실험의 목적은 보통 사람들이 얼마나 논리적인가를 알아보는 것이다.

실험방법: 사람들에게 주요 사회문제가 되고 있는 ‘실업’이나 ‘자퇴’와 같은 주제에 대해 어떻게 생각하는지 질문함.

실험대상자: 고등학생, 대학생, 그 분야의 전문가에 이르기까지 다양한 사람들을 대상으로 160명에게 질문함.

질문내용 :

1. 어떻게 그런 생각을 하기 되었습니까? 어떤 근거를 제시할 수 있습니까?

2. 당신의 의견에 반대하는 사람은 어떻게 이야기할까요? 어떤 근거를 제시할까요?
3.
그 사람이 틀렸다는 것을 증명하기 위해서 어떤 이야기를 하겠습니까?

4. 어떤 근거가 나오면 당신의 관점이 틀렸다는 것을 인정하겠습니까?
5.
다른 관점도 옳을 수 있지 않을까요? 

 

실험의 결과 :

질문을 받은 대부분의 사람들은 논리적인 추론에 기초하여 또 다른 견해를 떠올리지 못했거나 자신의 견해를 뒷받침하는 타당한 근거도 떠올리지 못했다! 즉 대부분의 사람들은 논증 능력이 부족하여 합리성 결여된 것이다.

실험의 출처: 논증의 기술(앤서니 웨스턴)

 

논리적인 사람은 단순히 타당한 이유만 제시한다고 자신의 주장을 받아들여줄 것이라 생각하지 않는다. 자신의 주장과 이유뿐만 아니라 신뢰할 수 있는 근거를 제시하고 여러 가지 대안과 반론에 대해 적절히 대응할 때만 자신의 주장이 받아들여 질 것이라고 생각해야 한다. 위의 실험을 하기 전에 논증모형을 설명해주고 이를 활용했다면 결과는 달라질 것이다. 논증모형이라는 엄격한 절차를 거치면 주장과 이유뿐만 아니라 근거를 제시하게 되며 대안이나 반론 등을 생각할 수 밖에 없기 때문이다.

 

논증은 학술집단과 전문가 집단의 핵심기술이다

산업시대 우리선조들이 자동차를 비롯한 여러 도구와 제품의 질을 판단하는 법을 배웠다. 하지만 21세기는 산업사회가 아니라 지식과 정보의 사회이다. 정보를 판단 할 때 논증은 믿음과 행동의 근거가 된다. 다시 말해 정보의 질을 판단하기 위해 논증이라는 도구를 활용하는 법을 배워야 한다. 논증은 학술집단과 전문가 집단이 살아남기 위한 생존도구이며 핵심기술이다. 왜냐하면 우리가 어떤 중요한 정보를 얻었을 때 그 정보를 신뢰할 것인지, 그 정보의 내용대로 실행할 것인지, 정보의 근거는 무엇인지 판단하는 방법이 논증이기 때문이다.

 

논증모형은 글쓰기뿐만 아니라 생각을 정리하는 도구이다

내 주위의 저자들은 한결같이 말한다. “생각이 나지 않거나 정리되지 않으면 일단 써라.” 내 생각도 다르지 않다. 논증모형은 생각의 도구는 아니며 글쓰기의 도구로만 생각하는 사람이 다음과 같은 말을 하였다. “논증모형은 논리적인 글쓰기의 도구일 뿐 그 이상도 그 이하도 아니다.” 일견 옳은 의견인 듯 보인다. 하지만 50% 부족한 생각이다. 왜냐하면, 사고란 글쓰기를 통해서 생겨나며, 글쓰기 또한 사고의 과정에서 생겨나기 때문이다. 생각은 눈으로 볼 수 없다. 하지만 생각을 글로 정리하는 순간에 시각화 과정을 거치면서 지식이 자신의 것이 된다. , 자신의 생각을 글로 바꿈으로써 생각이 표상화(이미지로 바뀜) 되는 것이다. 지식의 이미지화는 뇌에 또 다른 자극을 주기 때문에 새로운 생각이 들 수도, 생각이 정리될 수도 있다.

 

필자가 책 쓸 때 놀라웠던 점은 마지막 원고에서 초고의 내용이 거의 남아있지 않았다는 것이다. 물론 단순히 문체만 바꾼 글도 있었지만 많은 경우는 내용 자체가 바뀌었다. 즉 글쓰기를 통해 생각이 바뀌거나 정리된 것이다. 필자의 이런 경험이 의심스러우면 내일이라도 당장 실험해보기 바란다. 회의에 들어가기 전에 30분을 투자하여 글을 써보라. 회의의 문제가 무엇인지, 어떻게 하면 해결할 수 있을 것인지(주장), 또 그 방법대로 해야 하는 이유는 무엇인지(이유), 그 방법 대로해서 성공한 사례가 있는지(근거), 더 좋은 대안은 없는지, 누가 어떤 이유로 반대하지는 않을지(반론), 그러면 나는 어떻게 대응할 것인지(반박) 말이다. 30분간의 글쓰기가 회의에서 효과를 본다면 논증모형은 단순히 글쓰기의 도구일 뿐만 아니라 생각의 도구임을 깨닫게 될 것이다.

 

논리적인 글에서 논증모형은 단락이론보다 우월하다

논증모형은 아직 많이 활용되고 있지 않다. 예를 들어 대입논술에서는 아직까지도 논증모형을 활용하지 않고 단락이론을 따르고 있다. 그 결과 글을 작성할 때 많은 문제점이 발생되고 있다. 현재 많은 고등학생들이 논술의 모범이라고 믿고 있는 신문의 글은 단락이론을 따르고 있을까? 신문 글의 구성과 단락 전개에 관한 연구(신향식) 따르면 4 신문의 사설과 컬럼의 단락을 분석한 결과 놀랍게도 평균적으로 단락 중에 단락이 단락이론에서 벗어난 오류라고 한다. 심지어 4대 일간지의 논술위원까지 실수를 저지르다니…… 내 생각에 단락의 전개이론이라는 것이 매우 추상적이어서 많은 이들이 실수를 저지르는 것 같다. 단락의 전개이론은 수사학의 3대 요소라 불리며 다음과 같다.

 

통일성 원리: 단락의 뒷받침문장들은 소주제와 내용적으로 일치하고 연관하는 것으로만 선택했는가?

연결성 원리: 단락의 뒷받침문장들을 순리적으로, 조리 있게 연결하여 소주제가 효과적으로 드러났는가?

강조성 원리: 단락은 독자들이 납득할 수 있도록 설명, 논증 또는 구체적 예시 등을 통하여 소주제를 충분히 뒷받침하여 강조하고 있는가?

 

위의 글에서 보듯이 단락의 전개이론은 매우 추상적이고 어려워서 쉽게 이해가 가지 않는다. 위의 간단한 설명으로는 모자라 추가적인 설명이 필요하다. 학생들은 추가적인 설명으로도 이해가 어려워 실제 단락의 예시를 보기도 한다. 하지만 논증모형을 활용한다면 이런 어려움을 아주 쉽게 해결할 수 있다. 주장 + 이유 + 근거를 한 단락으로 한다면 어려울 것이 무엇인가? 이슈가 되는 글이라면 주장 + 이유 + 근거 + 반론수용/반박을 한 단락으로 하면 된다. 도대체 대입 논술에서 단락이론이 필요한 이유가 무엇이란 말인가? 심지어 단락이론에서 제시하는 도입단락이나 종결단락도 논증모형으로 구현된다. 자세한 내용은 첨부파일을 참조하라. 물론 논증이 아닌 설명문(exposition)이나 기술문(description), 서사문(narration) 등에서는 단락이론을 따라야 한다. 하지만 논리적인 글에서는 더 쉽고 과학적인 논증모형이 나왔기 때문에 단락이론은 효용가치가 없어졌다. 이제는 논술뿐만 아니라 이공계열 과학자들과 변호사및 판검사들은 논증모형을 활용해야 한다.

 

결론

당신이 논증모형을 활용한다면 더 깊은 생각과 더 좋은 글을 쓰는데 필수적인 도구가 될 것이다. 논증모형은 사고방법과 글쓰기방법 등의 기술적인 요소에 영향을 끼칠 뿐 아니라 당신을 대외적으로 신뢰받는 사람으로 변화시킨다. 더 효율적인 방법으로 문제를 해결할 것이며 논리적인 사고에서 나오는 말과 글은 많은 이들이 믿고 따를 것이다. 지난 반세기 동안 스티븐 툴민이 최초로 제시한 논리모델(툴민모델)의 문제점이 많이 검증되고 수정되어 실용적으로 바뀌었다. 이제는 더 이상 논증모형을 의심의 눈초리로 볼 것이 아니라 이 모형을 어디에 활용할 수 있을지를 생각해야 할 때이다.

사용자 삽입 이미지
사진설명(스티븐 툴민):
논증모형을 최초로 제시했다.


참조서적:
논증의 탄생(조셉 윌리엄스, 그레고리 콜럼)
논변의 사용(스티븐 툴민)

아래의 파일은 논증의 탄생중 Part1의 논증모형 부분을 요약한 것이다. 10MB가 넘으므로 분할압축하여 올린다.



논증의 탄생_Part1 요약.7z.001

논증의 탄생_Part1 요약.7z.002


압축을 푸는 프로그램

신고
Posted by extremedb

댓글을 달아 주세요

  1. feelie 2010.10.13 19:47 신고  댓글주소  수정/삭제  댓글쓰기

    모델링을 책으로만 공부하고 있는데. 논리적인 사고를 키우는것이 정말 힘든것 같습니다.
    좋은 내용 감사합니다

  2. Favicon of http://1ststreet.tistory.com BlogIcon SITD 2012.02.28 18:01 신고  댓글주소  수정/삭제  댓글쓰기

    토플 공부를 하고 있는데요,
    영어 라이팅이나 스피킹 공부를 하면서 비슷한 걸 느꼈습니다.

    <주장이 있으면 이유와 근거를 제시하라.
    이유에는 내 의견일 수도 있고, 타인의 의견일수도 있다.
    근거에는 내 의견의 장점일수도 있고, 반대의견의 문제점을 쓸수도 있다.>

    참 쉽고 당연한건데, 그걸 쓰기가 무척 어렵다는 걸 깨닭았습니다.
    그동안 이건 당연한거 아냐? 라고 생각만 했었기 때문인지 당연한 주장도 이유 생각하기가 힘들더라구요.
    거기다 토플에서 제시하는 2가지 이상의 이유를 생각하려고 하면 저도 모르게 모범 답안을 생각하게 되던 기억이 납니다.
    남 따라 하는거 싫어하는데, 막상 시간에 쫓기니 그거밖에 생각이 안나더라구요.

    논리적으로 사고한다고 생각했었는데, 주장만 있는 논리였던거 같아요.

  3. jangsas21@hanmail.net 2015.02.15 21:22 신고  댓글주소  수정/삭제  댓글쓰기

    헐.. 저런 도표는 어디서 구하시나여??


책 (The Logical Optimizer)의 Part 4에 대한 PPT가 완성되었다. 이제 본문의 모든 내용이 PDF로 요약 되었다. 책을 쓴 저자의 의무를 어느 정도 한것 같다.

Part 4는 CBQT (Cost Based Query Transformation)의 내부원리에 대한 내용이다. 즉 쿼리변환(Query Transformation)에 대한 내용이 아니라 옵티마이져의 원리에 대한 내용이다. 본문 내용중에서 가장 난위도가 있는 부분이기도 하다.

사용자 삽입 이미지
사용자 삽입 이미지


Tstory의 용량제한 때문에 할 수 없이 파일을 2개로 나눠(분할압축) 올린다.

압축  프로그램 7zip

THE LOGICAL OPTIMIZER (양장)
국내도서>컴퓨터/인터넷
저자 : 오동규
출판 : 오픈메이드 2010.04.05
상세보기



저작자 표시 비영리 동일 조건 변경 허락
신고
Posted by extremedb

댓글을 달아 주세요

  1. 리베 2010.10.04 10:35 신고  댓글주소  수정/삭제  댓글쓰기

    항상 좋은 자료 감사합니다. 오동규님 덕분에 실력이 쑤~~~욱 올라가고 있는듯... ^^

    • Favicon of http://scidb.tistory.com BlogIcon extremedb 2010.10.04 12:38 신고  댓글주소  수정/삭제

      안녕하세요. 리베님
      실력이 향상되었다면 참으로 다행스런 일 입니다.
      제가 이제 좀 쉬었으니 슬슬 다음 주제를 준비해야 할 단계가 온것 같습니다.^^

  2. feelie 2010.10.07 12:39 신고  댓글주소  수정/삭제  댓글쓰기

    좋은 자료 감사합니다

  3. 김시연 2010.10.26 14:15 신고  댓글주소  수정/삭제  댓글쓰기

    오늘 컨설팅 복귀하고, 자료 다운받아서 쭉 보고 있습니다. PPT 만드는게 보통일이 아닌데, 수고 많으셨습니다.
    그리고 혹시 Logical Optimizer에 대한 세미나나 교육 계획이 있으신가요?
    그럼 갑자기 추워진 날씨에 감기 조심하세요~!

    • Favicon of http://scidb.tistory.com BlogIcon extremedb 2010.10.26 14:53 신고  댓글주소  수정/삭제

      시연님 오랜만 입니다. 복귀하셨군요. 고생하셨습니다. 교육에 관하여 말씀 드리겠습니다.
      올해부터 HP 교육센터를 오픈메이드가 운영하게 됨에 따라 logical optimizer 교육은 준비중입니다. 아마도 주말(토, 일)을 이용한 4일 과정이 될것 같습니다. 혹시 짧은 세미나나 출장교육은 수고스럽더라도 저에게 메일로 문의해 주시기 바랍니다.
      감사합니다.

  4. 2010.11.30 09:55 신고  댓글주소  수정/삭제  댓글쓰기

    귀한 자료네요... 책도 읽었는데 이렇게 또 볼수 있어서 좋습니다. 감사합니다.

  5. Favicon of http://blog.naver.com/genisu BlogIcon 김승욱 2013.01.07 10:55 신고  댓글주소  수정/삭제  댓글쓰기

    책을 읽다 놀란것이 의무감에 대한 말씀을 하신거에 대해 참 감동받았는데
    PPT까지 올려주시다니...정말...대단하신것 같습니다.감사합니다!!!