SQL 튜닝책을 세 권정도 읽은 신입사원이 SQL 튜닝방법론을 요청하였다. 이유는 튜닝책에 방법론이 없다는 것이다. 튜닝 방법론이란 “SQL을 튜닝 해달라고 요청 받았을 때 내가 무엇 무엇을 해야 하나?” 이다. SQL 튜닝시의 To-Do 리스트(체크리스트)를 요구한 것이다.

 

SQL 튜닝을 자주 하면서도, 그 안에 몇 가지 작업이 있는지 생각하지 못했다. 누가 그랬던가? 일상을 낯설게 느껴보라고… SQL 튜닝요청을 받았을 때 내가 어떤 일을 하는지 가르쳐 주면 되겠구나 하는 생각이 들었다. 그 결과 7가지 방법이 결론으로 도출되었다. 만약 7가지 방법을 모두 적용할 수 있는 경우임에도 불구하고 하나라도 빠진다면 최적화된 SQL을 만들 수 없다.

 

아래는 필자와 신입사원의 대화이다.

 

신입사원 : SQL 튜닝의 원칙 몇 가지를 저에게 일러 주실 수 있나요? 튜닝책도 몇 가지 보았고, 강의도 많이 들었지만 이 원칙만 지키면 100점 만점에 90점은 맞는다.” 는 원칙 같은 것은 없더군요. 저는 이제 입문하는 단계이므로 모든 경우에 100점을 맞을 필요는 없습니다.

 

필자 : 온라인 SQL이냐 대용량 배치 SQL이냐에 따라 튜닝방법이 달라지므로 설명하기가 힘들군요.

 

신입사원 : 걱정 하실 것 없습니다. 대용량 배치는 프로그램이 많지 않으므로 제외하고, 온라인 SQL 튜닝 원칙을 몇 가지 일러주세요.

 

필자 : 온라인 SQL이라 하더라도 관점에 따라 튜닝방법이 다릅니다. 예를 들어 Peak Time Insert 문이나 Update , Select문이 집중적으로 몰릴 때의 튜닝방법이 있고, 단순히 SQL 하나에 에 집중해서 응답시간을 최소화 하는 튜닝방법이 있습니다.

 

신입사원 : 그런 것을 지금 모두 알아야 할 필요는 없습니다. 제가 튜닝 프로젝트에 투입되었다고 가정하고, 성능이 느린 Select문 하나를 받았을 때 튜닝을 어떻게 해야 하는지에 대해서만 설명해주시면 됩니다.

 

고단수 신입사원

이렇게 해서 신입사원에게 말려들게 되었다. , 초보라도 몇 가지 원칙만 지키면 온라인 Select문에 대한 튜닝을 100점 만점에 90점을 맞을 수 있는 방법을 요구하는 것이다. 사실 이런 질문에 가장 적합한 답변은 “SQL 튜닝책을 읽어보라는 것이다. 그런데 신입사원이 필자와 대화과정(튜닝책도 몇 가지 보았고 ~)에서 이런 답변을 못하도록 교묘히 막고 있다. 고단수이다. 몇 가지 방법만 알게 된다면 90점을 받는다고? 처음부터 그런 방법은 없다고 할 걸 그랬나? 후회가 된다. 어찌되었든 약속처럼 되어버렸으므로 이 글을 쓰게 되었다. …..머리가 아파온다.

 

온라인 Select문 튜닝 방법론

온라인 SQL의 튜닝방법은 여러 가지가 있을 수 있다. 하지만 그 중에서 가장 기초적이고, 기본적인 방법을 공개한다. 아래의 7가지 항목을 점검하고 약한 곳을 보강하면 된다. 이 글은 SQL 튜닝책을 두 권 정도 본 사람들을 위한 것이다. 튜닝에 자신있는 사람들은 볼 필요가 없다.

 

1. 적절한 인덱스를 사용하여 Block I/O를 최소화 하라

조인이 없는 경우는 적절한 인덱스를 사용하는 것 만으로도 상당한 효과를 볼 수 있다. 조인이 있는 경우는 특히 Driving(선행) 집합에 신경을 써야 한다. 왜냐하면 Nested Loop 조인을 사용했고, 선행집합의 건수가 많다면, 후행집합의 조인의 시도횟수가 증가하므로 성능이 느려진다. 따라서 적절한 인덱스를 이용하여 선행집합의 건수를 줄인다면, 혹은 가장 적은 집합을 선행으로 놓는다면, 후행집합으로의 조인건수는 줄어든다. 물론 이때에도 후행집합의 적절한 인덱스는 필수 조건이다. Driving 집합의 Block I/O를 줄이기 위하여 최적화된 인덱스가 없다면 생성하고, 있다면 그것을 사용하라. 다시 말해 최적의 Access Path를 만들어라.

 

운영중인 시스템이라면 최적의 Access Path를 위해 인덱스를 변경하거나 생성할 때는 주의해야 한다. 현재 튜닝하고 있는 SQL에 최적화된 인덱스를 생성하더라도 다른 SQL에 악영향을 줄 수 있기 때문이다. 인덱스를 생성하거나 변경할 때는 그 테이블을 사용하는 다른 SQL의 실행계획이 변경되지 않는지 각별히 신경을 써야 한다. 이런 이유 때문에 개발과정에서 효율적인 인덱스 설계가 중요시 된다.

 

2. 조인방법과 조인순서를 최적화 하라

온라인에서 사용하는 Select문은 좁은 범위를 검색하는 경우가 많다. 이럴 때는 대부분 Nested Loop Join이 유리하다. 그러므로 조인건수가 소량인 SQL Hash Join이나 Sort Merge Join이 발견되면 Nested Loop Join으로 변경하는 것이 더 유리한지 검토해야 한다. 물론 여기서도 Nested Loop 조인에 관해서만 다룬다.

 

Nested Loop 조인에서 가장 중요한 것은 조인순서이다. From절에 테이블(집합)이 두 개라면 후행집합의 관점에서는 적절한 인덱스만 존재한다면 그것으로 족하다. 만약 From절에 테이블(집합)이 세 개 이상이라면 조인순서를 변경할 수 있는지에 대한 두 가지 원리를 사용하라. 두 가지 원리는 아래의 단락에서 소개된다. 아무리 조인할 집합이 많다고 하더라도 이 두 가지의 원리는 동일하게 적용될 수 있다. 두 가지 원리를 이용할 때 필요하다면 Leading 힌트를 사용해야 한다.

 

첫 번째, 후행집합에 적절한 인덱스가 없는 경우에 조인순서를 바꾸면, 최적의 인덱스를 사용할 수 있는 경우가 많다. 예컨대, 튜닝전의 조인순서가 Aà B à C 라고 하면, 중간집합인 B에 적절한 인덱스가 없고 오히려 C에 적절한 인덱스가 존재하는 경우가 있다. 이럴 때는 B에 인덱스를 무작정 생성하지 말고, 조인순서를 A à C à B로 바꿀 수 있는지, 바꾸는 것이 더 효율적인지 검증하라. 조인순서만 바꾸어 주어도 일량이 획기적으로 줄어드는 경우가 많다. 만약 조인순서를 바꿀 수 없거나, C를 중간집합으로 하는 것이 비효율적이라면, B를 중간집합으로 유지하고 적절한 인덱스를 사용해야 한다.

 

두 번째, 조인되는 집합 중 특정 인덱스에서 Block I/O가 증가하는 경우에 조인순서의 변경을 검토하라. 이때 10046 Trace DBMS_XPLAN.Display_Corsor를 이용하면 조인집합들의 Block I/O량을 관찰할 수 있다. 예를 들어, 튜닝전의 조인순서가 Aà B à C 라고 하고, 집합 B에서 Block I/O량이 증가하면 A à C à B로 바꾸면 일량이 줄어드는 경우가 많다. C를 먼저 조인(Filter)하여 선행집합(B의 입장에서는 C가 선행이다)의 건수를 줄이고 B에 조인하면 성능이 향상된다.

 

3. Table Access(Random Access)를 최소화 하라

Random Access rowid로 테이블을 엑세스하는 것을 말한다. 1번과 2번을 최적화 했다면 Random Access도 자동으로 많이 줄어들었을 것이다. 하지만 그것이 끝은 아니다. 여전히 성능이 만족스럽지 못하다면 Random Access 횟수를 줄이는 것을 간과해서는 안 된다.

 

인덱스를 사용하면 rowid가 자동으로 획득된다. 만약 인덱스에 없는 컬럼을 Select 해야 한다면 rowid로 테이블을 엑세스 해야 한다. 이때 테이블로 엑세스 해야 할 건수가 많고, 인덱스의 컬럼순으로 테이블이 sort되어있지 않다면 성능이 매우 저하된다. 왜냐하면 테이블이 인덱스 기준으로 sort되어 있지 않기 때문에 테이블을 방문할 때마다 서로 다른 블럭을 읽어야 하기 때문이다.

 

비유적으로 설명해보자. 우리가 심부름을 할 때 세 군대의 상점(A,B,C)을 들러야 한다고 치자. 그 상점들이 모두 한 건물 내부에 존재한다면 얼마나 좋겠는가? 그 심부름은 매우 빠른 시간에 끝날 것이다. 하지만 반대로 상점 A는 부산에 있고 상점 B는 대구에 있고, 상점 C는 서울에 있다면? 만약 당신의 성격이 매우 좋아서 그 심부름을 한다고 해도 시간이 많이 걸릴 것이다. Random Access도 마찬가지이다. 인덱스의 rowid로 테이블을 방문할 때, 테이블이 인덱스기준으로 sort되어 상점처럼 다닥다닥 붙어있다면 성능은 매우 빠르고, 흩어져 있을수록 성능이 느려진다. (오라클에서는 테이블이 인덱스 기준으로 sort 되어 있는 정도를 Clustering Factor라고 한다.) 바로 이런 이유 때문에 index scan보다는 Table Scan이 느린 것이다. 따라서 우리는 Random Access의 부하를 최소화 해야 한다.

 

Random Access의 부하를 줄이는 방법은 네 가지이다. 첫 번째, 테이블의 종류를 변경하는 방법이다. IOT나 클러스터를 이용하면 Clustering Factor가 극단적으로 좋아진다. 또한 파티션을 이용하면 같은 범위의 데이터를 밀집시킬 수 있다. 두 번째, 효율적인 인덱스를 사용하거나 조인방법과 순서를 조정하여 Table Access를 최소화 하는 방법이다. 이 방법은 1번과 2번에서 이미 설명 되었다. 세 번째, 인덱스에 컬럼을 추가하여 Table Access를 방지하는 방법이다. 예를 들어 Select절의 특정 컬럼 때문에 테이블이 엑세스 된다면, 인덱스의 마지막에 그 컬럼을 추가하면 된다. 네 번째, 인덱스만 엑세스 하고 테이블로의 엑세스는 모든 조인을 끝내고 마지막에 시도하여 Random Access의 횟수를 줄이는 방법이다. 해당 을 참조하라. 

 

4. Sort Hash 작업을 최소화 하라

1,2,3번을 통하여 최적의 Access Path Join을 사용했다면, Block I/O의 관점에서는 튜닝이 끝난 것이다. 하지만 1,2,3번이 모두 해결되었다 해도 Order by Group By 때문에 성능이 저하 될 수 있다. 특히 결과가 많은 경우, sort는 치명적이다.

 

인덱스가 sort 되어있다는 특성을 이용하면 order by 작업을 대신할 수 있다.  Group By sort 가 발생하는데 group by 단위와 인덱스의 컬럼이 동일 하다면 sort는 발생하지 않는다. 최적의 인덱스를 사용하면 Access Path를 개선하는 효과뿐만 아니라 Sort의 부하도 없어진다.

Union All
을 제외한 집합연산(Union, Minus, Intersect)를 사용하면 Sort Unique 혹은 Hash Unique가 발생한다. Union Union All로 바꿀 수 없는지 검토해야 하고, Minus Not Exists 서브쿼리를 이용하여 Anti Join으로 바꿀 수 없는지 고려해야 한다. Intersect는 교집합이므로 조인으로 바꿀 수 있는지 검토해야 한다. 아주 가끔 Distinct를 사용한 SQL이 눈에 뛰는데 이 또한 Sort Unique 혹은 Hash Unique를 발생시킨다. 모델러나 설계자에게 문의하여 Distinct를 제거할 방법이 없는지 문의해야 한다.

 

Oracle 10g부터는 Hash Group By가 발생할 수 있는데, 이미 적절한 인덱스를 사용하는 경우라면 Hash Group By를 사용할 필요는 없다. 이런 경우 NO_USE_HASH_AGGREGATION 힌트를 사용하면 Sort Group By로 바꿀 수 있다. 이렇게 해주면 실행계획에 “SORT GROUP BY NOSORT” Operation이 발생하며, Sort Hashing 작업이 전혀 발생하지 않는다. Group By의 부하를 해결하는 또 하나의 방법은 스칼라 서브쿼리를 사용하는 것이다. 조인을 사용하면 Sum 값을 구하기 위해 Group By가 필수적이다. 하지만 스칼라 서브쿼리를 사용하면 Group By를 사용하지 않고도 sum 이나 Min/Max 값을 구할 수 있다. 또한 분석함수의 Ranking Family(rank, dens_rank, row_number)를 최적화된 인덱스와 같이 사용하면 Group By Sort를 하지 않고도 Min/Max 값을 구할 수 있다. 이때는 실행계획에 “WINDOW NOSORT” Operation이 발생한다. 관련 글을 참조하기 바란다.

 

5. 한 블록은 한번만 Scan하고 끝내라

같은 데이터를 반복적으로 Scan하는 SQL이 의외로 많다. 대표적인 경우가 Union All로 분리되었지만 실제로는 그럴 필요가 없는 경우이다. 예를 들어 Where 절에 구분코드가 1일 때 , 2일 때, 3일 때 별로 SQL이 나누어져 있는 경우이다. Where 절을 구분코드 in (1,2,3) 으로 처리하고, Select절에서 Decode Case 문을 사용하여 구분코드별로 처리해준다면 Union All은 필요 없다. Union All을 사용하는 또 한가지의 경우는 Sub Total(소계) Grand Total(총계)를 구해야 하는 경우이다. 이 경우도 Rollup/Cube Grouping Sets Group By절에 사용한다면 소계나 총계를 위한 별도의 Select문을 실행 시킬 필요는 없다. 1~4번의 과정은 SQL문의 변경이 없거나 최소화 된다. 하지만 5번의 경우는 SQL을 통합시켜야 하기 때문에 시간이 많이 소모되며, 많은 사고가 요구되는 창조적인 과정이다. 여기까지 했다면 진행되었다면 원본 SQL 자체의 튜닝은 완료 된 셈이다.

 

6. 온라인의 조회화면이라면 페이징처리는 필수이다

부분범위 처리를 해야 한다. 물론 전체 건을 처리해야 하는 경우는 있을 것이다. 하지만 조회화면이라면 몇 십만 건 혹은 몇 만 건이나 되는 결과를 모두 볼 수 없다. 따라서 볼 수 있는 단위로 끊어서 출력해야 한다. 예를 들어 결과 건수가 10만 건이라고 해도 최초의 50건을 화면에 먼저 뿌린다면 1,2,3,4 번에서 설명했던 모든 부하(Block I/O의 부하, 조인의 부하, Random Access의 부하, Sort의 부하)를 한꺼번에 감소시킬 수 있다. 따라서 가능하면 개발자를 설득하거나 책임자를 설득하여 페이징 처리를 하는 것이 바람직하다.

 

페이징 처리를 해도 효과를 볼 수 없는 몇 가지 예외가 있다. 분석함수를 사용하거나, Connect By + Start With를 사용한다면 페이징 처리의 효과는 없다. 분석함수의 경우 인라인뷰의 외부로 뺄 수 있다면 부분범위 처리가 가능하다. 이에 관해서는 해당 을 참조하기 바란다. Connect By + Start With를 사용한 경우는 부분범위처리가 불가능하다. 하지만 11g R2의 신기능인 Recursive With절을 사용한다면 페이징 처리의 효과를 볼 수 있다. 이때, Recursive With절에 Search(Order By절과 같은 기능)을 사용한다면 Connect By와 마찬가지로 페이징 처리의 효과가 없으니 주의해야 한다. 즉 인덱스의 구성을 적절히 하여 Sort를 대신해야 한다. Recursive With가 무엇인지 궁금한 사람은 관련 을 참조하기 바란다.

 

7. 답이 틀리면 안 된다. SQL을 검증하라

7번은 SQL 자체를 튜닝하는 것은 아니다. 하지만 7번을 튜닝 방법에 추가한 이유는 있다. 튜닝을 하였음에도 답이 틀린다면, 튜닝을 하지 않은 것 보다 못하다. 그러므로 튜닝 후에 답이 옳은지 항상 검증해야 한다. 1~ 7번 중에 가장 중요한 것이 7번이다.

 

방법론 정리

1.     적절한 인덱스를 사용하여 Block I/O를 최소화 하라.

2.     조인방법과 조인순서를 최적화 하라.

3.     Table Access(Random Access)를 최소화 하라

4.     Sort Hash 작업을 최소화 하라

5.     한 블록은 한번만 Scan하고 끝내라

6.     온라인의 조회화면이라면 페이징처리는 필수이다

7.     답이 틀리면 안 된다. SQL을 검증하라

 

방법론의 의미

만약 1~7번을 모두 적용할 수 있는 경우임에도 불구하고 하나라도 빠진다면 그것은 최적화된 SQL이 아니다. 물론 튜닝을 할 때 위의 1~6번을 항상 적용할 수 있는 것은 아니다. 경우에 따라서는 하나만 적용될 수도 있고, 두 개만 적용할 수 있는 SQL도 있다. 하지만 1~6번을 모두 적용할 수 있는지 꼼꼼히 살펴야 한다.

 

이 글은 튜닝 입문하여 관련 책들을 몇 권 본 사람들을 위한 기본적인 튜닝방법에 관한 것이다. 1번부터 7번까지의 방법은 기본 중에 기본이다. 이것들만 알아도 온라인 조회화면에서 사용하는 SQL을 튜닝하는데 어려움이 없을 것이다. 다시 말해 90%는 해결 할 수 있다. 그렇다면 나머지 10%? 그것들은 그때 그때 마다 다르게(On the fly 모드) 처리된다. 또한 그것들은 책이나 매뉴얼에 나와있지 않기 때문에 경험치 이거나 실험과 연구의 결과로 알아내는 것들이다.

 

일상을 낯설게 느껴보니 좋은 점이 많다. 언제 필자의 다른 일상(모델링, 시스템분석/진단)에 대한 방법론도 만들어 보려고 한다.


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

'Oracle > SQL Tuning' 카테고리의 다른 글

COPY_T 테이블 필요한가?  (6) 2011.04.04
Sort 부하를 좌우하는 두 가지 원리  (9) 2011.03.29
SQL튜닝 방법론  (17) 2011.01.27
Pagination과 분석함수의 위험한 조합  (26) 2010.12.23
오라클의 Update문은 적절한가?  (15) 2010.04.14
Connect By VS ANSI SQL  (6) 2010.02.11
Posted by extremedb

댓글을 달아 주세요

  1. 타락천사.. 2011.01.28 09:45 신고  댓글주소  수정/삭제  댓글쓰기

    항상 좋은 글 감사합니다.
    새해 복 많이 받으시길 ㅇ_ㅇ

  2. Ejql 2011.01.28 13:57 신고  댓글주소  수정/삭제  댓글쓰기

    이런게 신입튜너한테는 아주 좋은 가이드라인 같습니다. 이것을 달고 살면서 누락된 부분이 있는지 체크포인트도 될 것이고,
    자주오면 올수록 배워가는게 있는 내용 감사합니다.

  3. 2011.01.28 14:07 신고  댓글주소  수정/삭제  댓글쓰기

    1,2,3,4,5,6,7 을 모두 할 수 있다면 이미 초보는 아닌거같습니다 ㅎㅎ

  4. 2011.01.28 18:34 신고  댓글주소  수정/삭제  댓글쓰기

    방법론 3.Table Access(Random Access)를 최소화 하라
    4번째 라인에
    "만약 인덱스에 없는 컬럼을 Select 해야 한다면 rowid로 테이블을 엑세스 해야 한다"라고 하셨는데,
    인덱스를 통한 테이블 엑세스가 아닌 경우에도(Full Table Scan) rowid를 이용하여 테이블 엑세스를 한다는 말씀이신가요?

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

      반갑습니다.
      인덱스를 사용한 경우를 나타낸 겁니다. FTS는 Random Acess를 사용하지 않습니다. 예를 들어 인덱스가 A + B 로 되어있을때 where A = '1' and B = '2' 라면 인덱스는 조회조건을 완벽하게 만족합니다. 하지만 select 절에 C 컬럼이 있다면 어쩔 수 없이 인덱스에서 획득한 rowid 로 테이블을 Random하게 엑세스 해야합니다. 이 경우를 나타낸 겁니다.
      감사합니다.

  5. 매컬리 2011.02.01 09:42 신고  댓글주소  수정/삭제  댓글쓰기

    정말 기다리고 기다리던 글이었습니다.

    감사합니다. ^^

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

      매컬리님 블로그를 이용해 주셔서 감사합니다.
      이런 글을 기다리고 있었다니 놀랍습니다.
      매번 주관적인 입장으로 글을 쓰다보니 신입 튜너들을 위한 글은 별로 없었나 봅니다.^^ 소통이 중요하다는 것 새삼 느낍니다.

      설 연휴 잘보내시기 바랍니다.

  6. 정재열 2011.03.24 17:03 신고  댓글주소  수정/삭제  댓글쓰기

    지난 번에 본 글인데 오늘 다시 보니 또 새롭습니다.

    저는 신입사원분의 마음에 심히 공감하게 됩니다 ㅎ

    좋은 글에 감사를 드립니다. :]

  7. 초보자 2011.05.26 15:12 신고  댓글주소  수정/삭제  댓글쓰기

    우와.. 좋은글 감사합니다...

    많은 도움이 되네요...

  8. Favicon of http://www.perfectreplicawatch.co.uk/replica-tag-heuer-c-152.html BlogIcon replica tag heuer 2011.08.06 16:34 신고  댓글주소  수정/삭제  댓글쓰기

    오오미 환영합니다 +_+

  9. Favicon of http://ohnu.tistory.com BlogIcon 오뉴 2015.02.01 16:45 신고  댓글주소  수정/삭제  댓글쓰기

    도움이 되는 글이라 퍼갈게요. 감사합니다. ㅎ

  10. 꽃사슴트윈스 2015.05.18 01:48 신고  댓글주소  수정/삭제  댓글쓰기

    필요한 내용이 포함되어 있어서 공부하고 갑니다. 감사해요!^_^ 출처 밝히고 퍼가겠습니다!

  11. Favicon of http://tlstjscjswo.tistory.com BlogIcon 리틀홍콩 2015.09.16 13:51 신고  댓글주소  수정/삭제  댓글쓰기

    좋은 정보 감사합니다!!

    출처 남기고 담아갈게요 : )


select /*+ full(a) full(b)  leading(a) use_hash(b) */

a.col1, b.col2

  from tab1 a,

       tab1 b

  where a.col1 = b.col2 ;

 

오해와 현실

위의 SQL을 보면 from 절의 두 테이블은 동일하다. 그리고 건수가 많아서 힌트를 주었으므로, 둘 다 full table scan을 할 것이다. 따라서 위의 SQL을 실행하고 결과를 본다면, a b의 일량(block I/O)은 동일하다.”라고 알고 있는 사람이 많이 있다. a를 읽었더니 block I/O 량이 1000 블럭이라면 b를 읽을 때도 1000 블럭이 나올 것이라는 이야기다. 이런 주장이 사실일까? 결론부터 말하자면 사실이 아니다. b쪽이 더 많은 블럭을 scan 해야 한다. 그래서 b쪽을 scan할 때 더 느리다. b쪽에 더 많은 일량이 나온다면 버그라고 생각하는 사람도 있지만, 버그가 아니라 정상적인 결과이다.

 

이 글의 목적

위의 결론에 따르면 후행테이블을 scan 할 때 심각한 성능저하가 발생 할 수 있다. 이런 현상을 주위의 지인들에게 질문한 결과 적절한 이유나 원인을 말하는 사람은 거의 없었다. 성능문제의 원인을 모르면 튜닝을 할 수 없다. 그러므로 이 글에서는 성능이 저하되는 이유를 독자에게 제시하고, 비효율을 해결 할 수 있는 방법을 설명한다. 또한 이런 문제가 발생하지 않는 예외적인 경우도 살펴본다.

 

이제 테스트를 진행하기 위해 테이블을 하나 만들자.

 

create table test1 as

select lpad(level, 5, '0') as num,

       lpad(level, 60, '0') as num_txt

  from dual

connect by level <= 50000 ;

 

인덱스가 없음으로 앞으로 모든 실행계획은 full table scan이 될 것이다. 정확한 분석을 위해 test1 테이블의 full table scan 일량(logical reads)을 알아보자.

 

select count(*)

  from test1;

 

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

| Id  | Operation          | Name  | Starts | A-Rows |   A-Time   | Buffers |

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

|   0 | SELECT STATEMENT   |       |      1 |      1 |00:00:00.01 |     504 |

|   1 |  SORT AGGREGATE    |       |      1 |      1 |00:00:00.01 |     504 |

|   2 |   TABLE ACCESS FULL| TEST1 |      1 |  50000 |00:00:00.06 |     504 |

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

 

full table scan의 결과 일량은 504 블럭이다. 따라서 test1 테이블의 데이터가 변경되지 않는다면 항상 504 블럭이 나와야 한다. 정말 그렇게 될까?

 

아래 SQL의 조인 순서는 a--> b 이다.

 

select /*+ leading(a b) */ a.num

  from test1 a,

       test1 b

  where a.num = b.num

    and a.num > '00100'

    and substr(b.num_txt,  -5) > '00100'; --> substr의 인자 -5는 마지막 다섯 자리라는 뜻이다.

 

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

| Id  | Operation          | Name  | Starts | A-Rows |   A-Time   | Buffers |

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

|   0 | SELECT STATEMENT   |       |      1 |  49900 |00:00:00.45 |    5998 |

|*  1 |  HASH JOIN         |       |      1 |  49900 |00:00:00.45 |    5998 |

|*  2 |   TABLE ACCESS FULL| TEST1 |      1 |  49900 |00:00:00.06 |     504 |

|*  3 |   TABLE ACCESS FULL| TEST1 |      1 |  49900 |00:00:00.15 |    5494 |

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

 

Predicate Information (identified by operation id):

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

   1 - access("A"."NUM"="B"."NUM")

   2 - filter("A"."NUM">'00100')

   3 - filter((SUBSTR("B"."NUM_TXT",(-5))>'00100' AND "B"."NUM">'00100'))

 

무려 11배나 차이가 난다

선행테이블은 정상적으로 504블록이 나왔다. 하지만 이상하게도 선행테이블과 동일한 테이블인 후행테이블( b )의 일량이 약 11배나 많다. 수행시간도 후행테이블이 더 느리다. 같은 테이블을 동일한 방법으로 scan 했는데 왜 Block I/O 수가 11배나 차이가 날까?

 

힌트를 주어 조인 순서를 바꿔보자.

 

select /*+ leading(b a) */ a.num

  from test1 a,

       test1 b

  where a.num = b.num

    and a.num > '00100'

    and substr(b.num_txt,  -5) > '00100';

 

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

| Id  | Operation          | Name  | Starts | A-Rows |   A-Time   | Buffers |

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

|   0 | SELECT STATEMENT   |       |      1 |  49900 |00:00:00.34 |    5998 |

|*  1 |  HASH JOIN         |       |      1 |  49900 |00:00:00.34 |    5998 |

|*  2 |   TABLE ACCESS FULL| TEST1 |      1 |  49900 |00:00:00.11 |     504 |

|*  3 |   TABLE ACCESS FULL| TEST1 |      1 |  49900 |00:00:00.06 |    5494 |

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

 

Predicate Information (identified by operation id):

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

   1 - access("A"."NUM"="B"."NUM")

   2 - filter((SUBSTR("B"."NUM_TXT",(-5))>'00100' AND "B"."NUM">'00100'))

   3 - filter("A"."NUM">'00100')

  

array size가 원인이다

이번에는 반대로 a의 일량이 b보다 11배 많게 나왔다. 즉 일관성 있게 후행테이블의 일량이 11배가 많다. 그 이유는 툴(오렌지) array size 10 으로 되어있었기 때문이다. 다른 말로 바꾸면 array size 10 이기 때문에 49900건을 모두 출력하려면 4990 fetch 해야 한다. 즉 위의 일량 5494는 원래의 블록 수인 504 fetch 회수(4990 블럭)을 더한 것이다. 여기까지는 이해가 될 것인데 문제는 fetch 할 때마다 한 블록을 더 읽어야 하는가?이다.

 

Fetch 할 때마다 이전에 읽었던 1블럭을 더 읽어야 한다

한 블록에 20건이 들어있다고 가정하고, Array size 10 이라고 치자. 그러면 한 블럭의 데이터(20)를 모두 출력 하려면 동일한 블럭을 반복적으로 두 번 fetch 해야 한다. 바로 이것이 fetch 할 때마다 이미 읽었던 블럭(직전에 fetch 했던 block중 마지막 block)을 다시 Scan 할 수 밖에 없는 이유이다.

 

비효율을 없애려면 array size를 적정 수준으로 늘려라

 

set arraysize 100 --array size 100으로 변경

 

select /*+ leading(a b) */ a.num

  from test1 a,

       test1 b

  where a.num = b.num

    and a.num > '00100'

    and substr(b.num_txt,  -5) > '00100';

 

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

| Id  | Operation          | Name  | Starts | A-Rows |   A-Time   | Buffers |

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

|   0 | SELECT STATEMENT   |       |      1 |  49900 |00:00:00.38 |    1507 |

|*  1 |  HASH JOIN         |       |      1 |  49900 |00:00:00.38 |    1507 |

|*  2 |   TABLE ACCESS FULL| TEST1 |      1 |  49900 |00:00:00.06 |     504 |

|*  3 |   TABLE ACCESS FULL| TEST1 |      1 |  49900 |00:00:00.11 |    1003 |

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

 

Predicate Information (identified by operation id):

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

   1 - access("A"."NUM"="B"."NUM")

   2 - filter("A"."NUM">'00100')

   3 - filter((SUBSTR("B"."NUM_TXT",(-5))>'00100' AND "B"."NUM">'00100'))

  

array size를 올리자 logical read 5494 에서 1003 으로 변경되었다. 5배 이상 일량(logical reads )이 줄어들었다. 하지만 아직도 원래의 블록 수인 504 보다배정도 많다. 

 

set arraysize 1000 --array size 1000으로 변경

 

select /*+ leading(a b) */ a.num

  from test1 a,

       test1 b

  where a.num = b.num

    and a.num > '00100'

    and substr(b.num_txt,  -5) > '00100';

 

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

| Id  | Operation          | Name  | Starts | A-Rows |   A-Time   | Buffers |

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

|   0 | SELECT STATEMENT   |       |      1 |  49900 |00:00:00.34 |    1058 |

|*  1 |  HASH JOIN         |       |      1 |  49900 |00:00:00.34 |    1058 |

|*  2 |   TABLE ACCESS FULL| TEST1 |      1 |  49900 |00:00:00.06 |     504 |

|*  3 |   TABLE ACCESS FULL| TEST1 |      1 |  49900 |00:00:00.09 |     554 |

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

 

Predicate Information (identified by operation id):

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

 

   1 - access("A"."NUM"="B"."NUM")

   2 - filter("A"."NUM">'00100')

   3 - filter((SUBSTR("B"."NUM_TXT",(-5))>'00100' AND "B"."NUM">'00100'))

 

array size1000으로 올리자 logical read 1003 에서 554로 변경되었다. 이 정도면 원래의 블럭수인 504와 비슷하다. 554와 504의 차이는 50 블럭이므로 fetch를 50번 했다는 것을 알 수 있다.

 

해결방법
테스트의 결과는 fetch
가 발생할 때마다 직전 블럭을 읽어야 함을 알 수 있다. 따라서 array size를 적절히 늘리면 fetch 회수가 줄어들므로 이전 블럭을 읽는 횟수도 같이 줄어든다. 이에 따라 성능도 향상된다. 하지만 array size를 늘려도 선행테이블은 logical read의 변화가 없다. 왜냐하면 선행테이블은 fetch에 영향을 끼치지 못하며, 후행 테이블이 scan 되어 조인에 성공될 때만 데이터가 client로 전송(fetch) 되기 때문이다.

조인이 없을 때도 비효율은 발생한다
이런 현상은 full table scan과 해시조인의 조합에서만 발생하는 것은 아니다. 조인 없이 from 절에 테이블이 하나뿐일 때도 동일하게 발생한다. 아래의 SQL이 전형적인 예제이다.

 

array  size 10일 때       

 

select num

  from test1;

 

Trace Version   : Oracle Database 11g Enterprise Edition Release 11.2.0.1.0 - Production

Environment     : Array Size = 10

                  Long  Size = 80

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

 

Call     Count CPU Time Elapsed Time       Disk      Query    Current       Rows

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

Parse        1    0.000        0.000          0          0          0          0

Execute      1    0.000        0.000          0          0          0          0

Fetch     5001    0.328        0.219          0       5504          0      50000

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

Total     5003    0.328        0.219          0       5504          0      50000

 

Misses in library cache during parse: 0

Optimizer goal: ALL_ROWS

Parsing user: SYS (ID=0)

 

Rows     Row Source Operation

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

      0  STATEMENT

  50000   TABLE ACCESS FULL TEST1 (cr=5504 pr=0 pw=0 time=67049 us cost=143 size=300000 card=50000)

 

fetch를 5001 번 했기 때문에 원래의 블럭수( 504 )에 비해 logical read량도 약 5000 블럭이 늘었다. 
 


array
 size
100일 때

 

Trace Version   : Oracle Database 11g Enterprise Edition Release 11.2.0.1.0 - Production

Environment     : Array Size = 100

                  Long  Size = 80

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

 

Call     Count CPU Time Elapsed Time       Disk      Query    Current       Rows

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

Parse        1    0.000        0.000          0          0          0          0

Execute      1    0.000        0.000          0          0          0          0

Fetch      501    0.063        0.041          0       1004          0      50000

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

Total      503    0.063        0.041          0       1004          0      50000

 

Misses in library cache during parse: 1

Optimizer goal: ALL_ROWS

Parsing user: SYS (ID=0)

 

Rows     Row Source Operation

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

      0  STATEMENT

  50000   TABLE ACCESS FULL TEST1 (cr=1004 pr=0 pw=0 time=75254 us cost=143 size=300000 card=50000)

 

Array size 10인 경우(5504)에 비해 일량이 약 5배 정도 감소했다. 그 이유는 fetch 회수가 10배로 줄어들었기 때문이다.

 


array  size
1000 일 때

 

Trace Version   : Oracle Database 11g Enterprise Edition Release 11.2.0.1.0 - Production

Environment     : Array Size = 1000

                  Long  Size = 80

 

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

 

Call     Count CPU Time Elapsed Time       Disk      Query    Current       Rows

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

Parse        1    0.000        0.000          0          0          0          0

Execute      1    0.000        0.000          0          0          0          0

Fetch       51    0.031        0.016          0        554          0      50000

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

Total       53    0.031        0.017          0        554          0      50000

 

Misses in library cache during parse: 1

Optimizer goal: ALL_ROWS

Parsing user: SYS (ID=0)

 

Rows     Row Source Operation

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

      0  STATEMENT

  50000   TABLE ACCESS FULL TEST1 (cr=554 pr=0 pw=0 time=50383 us cost=143 size=300000 card=50000)        

 

무작정 크게 한다고 좋아지지 않는다

array size 1000으로 변경하니 array size가 10인 경우(5504 블럭)에 비해 일량이 약 10배 정도 감소했다. 하지만 array size 100 인 경우와 비교해 보면 일량이 고작 2배 정도만 줄어들었다. 다시 말해 여기서 array size를 더 크게 하더라도 얻는 이익은 별로 없다는 것이다. 따라서 무작정 array size를 늘려서는 안 된다. 메모리에 부하를 줄 뿐만 아니라 한번에 많은 데이터가 client로 전송되므로 네트웍 I/O가 과도 하게 늘어날 수 있다. 따라서 clientfetch 할 건수가 많고, 네트웍 망의 성능이 좋다면 1000~ 2000 정도를 유지하는 것이 적당하다. 물론 조회 프로그램에서는 페이징 처리를 하는 것이 가장 좋지만, 업무적으로 전체 건을 볼 수 밖에 없는 경우는 array size를 적절히 조절하는 것이 대안이 될 수 있다.


성능문제의 발생조건 
fetch의 비효율은 select문에서만 발생한다. 즉 insert–select CTAS(create table as select) 그리고 merge 문 등에서는 이런 종류의 성능저하가 발생하지 않는다. 왜냐하면 DML문은 select문과 달리 조회(데이터를 clientfetch) 할 필요가 없고, commit이 되면 바로 종료되기 때문이다.

모든 규칙에 예외는 있다

full table scan + sort merge join 의 조합에서는 fetch의 비효율이 발생하지 않는다. 왜냐하면 full table scan + sort merge join 조합은 hash join의 조합과 달라서 모든 데이터를 sort 해야하기 때문이다. 모든 데이터를 sort하려면 어차피 모든 블럭을 scan해야 하므로 fetch를 여러번 해야만 하는 array size를 사용할 필요가 없는 것이다.  그리고 fetch를 여러번 하지 않기 때문에 항상 일량이 일정하다.

또 다른 예외의 경우는
 1 블럭에 1 row만 저장되는 경우이다. 이런 경우는 블럭을 한번만 엑세스 해도 그 블럭의 모든 데이터를 한번에 fetch 할 수 있으므로, 같은 블록을 반복해서 읽을 필요가 없다. 따라서 array size를 변경해도 일량이 달라지지 않는다.

 

호기심이 있는 독자는 아래의 테이블을 만들고 위의 테스트를 똑같이 진행 해보기 바란다. 위의 test 결과와는 다를 것이다.

 

drop table test1 ;

 

create table test1 as

select lpad(level, 5, '0') as num,

       lpad(level, 7000, '0') as num_txt

  from dual

connect by level <= 50000 ;

 
array size 항상 나쁜가?
우리는 array size가 있음으로 해서 부분범위처리를 할 수있다. full table scan을 동반하는 해시조인의 경우에도 중간에 효율적으로 멈출 수 있다. 예를 들어 결과건수가 1억건이며, 만건을 먼저 조회한 후에 다음 만건을 보고 싶다고 할때, 운반단위(array size)가 1000 이라면 10번 fetch 하면 멈출 수 있다. 반면에 array size가 없다면 중간에 멈출 수 없으므로 1억건을 모두 fetch 한후에나 결과를 화면에서 볼 수 있다.

결론

같은 테이블을 두 번 full table scan 하고, 그 둘을 해시조인하면 대부분의 경우 후행 테이블의 I/O량이 더 많다. 그래서 후행테이블을 scan 할 때가 더 느리다. 왜냐하면 직전 fetch 때에 이미 읽었던 block의 데이터가 모두 fetch 되지 않을 수 있으므로 그 블럭을 한번 더 읽어보아야 확인 할 수 있기 때문이다. 이런 비효율이 많이 발생하는 경우는 array size가 작기 때문이다. 따라서 적절한 array size로 늘려주면 성능문제를 해결 할 수 있다. 

fetch의 비효율은 full table scan이나 full table scan + hash join 조합을 사용할 때만 발생하는 것은 아니다. index scan을 할때도 똑같이 비효율이 발생한다.(주1)  즉 fetch의 비효율 문제는 인덱스를 사용할때나 테이블을 scan할때를 가리지 않고 모두 발생한다. 이런 사실들로 미루어 볼때, 위에서 언급한 몇가지의 예외를 제외한다면, 우리는 다음과 같은 결말을 낼 수 있다.

"select문의 결과건수가 많음에도 불구하고, 페이징 처리가 되지 않고, array size가 작은 조회용 프로그램이라면 fetch의 비효율은 존재한다."



주1 : 인덱스 사용시 fetch의 비효율 문제는 이미 책으로 정리가 되어 있으므로 필자가 언급하지 않는다. 이 문제에  관심이 있는 사람은 조동욱 님의 책 Optimizing Oracle Optimizer를 참조하기 바란다.

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

댓글을 달아 주세요

  1. feelie 2011.01.14 12:45 신고  댓글주소  수정/삭제  댓글쓰기

    좋은 내용 감사합니다.

    늦었지만 새해 복많이 받으시고, 올해 목표하시는 일 다 이루시길 바랍니다.

  2. Ejql 2011.01.17 15:58 신고  댓글주소  수정/삭제  댓글쓰기

    이런 문의가 종종있었나 보네요? 확실히 알고 갑니다. 감사합니다. 추가 원인이 그 이유였군요.

  3. 오라클완전초보 2011.01.18 17:18 신고  댓글주소  수정/삭제  댓글쓰기

    매일 매일 SQL 을 보면서 사는데
    왜 저는 이런걸 발견하지 못할까요.. 아무생각없이 튜닝을 해서 그런가 봅니다.
    반성하게 되네요

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

      너무 걱정하지 마시기 바랍니다.
      튜닝에 집중하다 보면 다른것은 보이지 않기 때문입니다.
      하나의 방법은 Q&A에서 답변을 자주하면 실력이 늡니다.
      답변을 하기위해 공부를 많이 해야하고, 원인을 찾아야 하고.... 하다보면 한단계 업그레드 되어있는 자신을 발견하실 것입니다.
      감사합니다.

  4. sid 2011.01.18 21:00 신고  댓글주소  수정/삭제  댓글쓰기

    “왜 fetch 할 때마다 한 블록을 더 읽어야 하는가?”
    이건 어디서 판단해야 하나요?
    이 부분이 잘 이해가 안가서 계속 보고 있는데, 어디서 블럭을 또 본다는 걸 파악해야 하는지 잘 이해가 안가서요.
    전체적으로 워낙에 잘 풀어쓰셔서 술술 이해가 되는데 그 부분만 막혀버려서, 답답해서 이렇게 질문 드립니다.

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

      안녕하세요.
      select num from test1; 부분의 10046 trace 를 보시면 됩니다. 여기를 보시면 패치회수만큼 블럭을 더 읽는다는 것을 알 수 있습니다. 즉 array size가 10일때 5만건(결과건수)을 패치하려면 5천번을 실행해야 합니다. 이 정보가 10046 trace의 fetch에 나타납니다. 그리고 current에 블럭 i/o량이 나타납니다. trace 상의 굵은 글씨를 중점적으로 보시면 됩니다.

      즉 원래의 블럭량인 504와 패치횟수 5000을 더하면 logical read 량인 5504 가 나옵니다. 이해가 되셨나요?

  5. sid 2011.01.19 10:47 신고  댓글주소  수정/삭제  댓글쓰기

    화면상으론 확인할 수 없나 보군요 ㅎ
    네, 알겠습니다. 지금은 권한상 무리니까 집에가서 한번 테스트 해봐야겠네요.
    좋은 글 감사합니다 ^^

  6. salvationism 2011.01.23 20:41 신고  댓글주소  수정/삭제  댓글쓰기

    "select문의 결과건수가 많음에도 불구하고, 페이징 처리가 되지 않고, array size가 작은 조회용 프로그램이라면 fetch의 비효율은 존재한다."
    자연스럽게 고개를 끄덕이게 되는 단순 명료한 정의 같습니다. 좋은 글 감사합니다. ^^

  7. 나그네 2011.01.24 15:35 신고  댓글주소  수정/삭제  댓글쓰기

    궁금한 점이 있습니다. 어레이 사이즈로 인해 후행 테이블의 로지컬 리드가 높아졌다면 왜 선행 테이블의 로지컬 리드는 안 늘어 나는 건가요? 선행 테이블도 어레이 사이즈에 맞춰서 읽지 않는지요. 이 부분이 궁금합니다.

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

      반갑습니다.
      선행집합만 화면에 뿌리는 것은 의미가 없습니다. 다시말해,select 결과를 화면에 fetch 하려면 조인에 성공한 건만 해야 합니다. 어차피 후행집합이 조인에 성공한 후에 fetch가 시작되므로 성행집합에 성능이 저하되는 array size를 사용할 필요가 없는것 입니다. 이해가 되셨나요?

  8. 나그네 2011.02.16 20:23 신고  댓글주소  수정/삭제  댓글쓰기

    일량이 틀려요 => 일량이 달라요가 맞습니다.

    다르다 = different, 틀리다 = wrong

    우리나라 사람이 가장 잘못 사용하는 단어 중 하나라고 생각합니다.

    요즘 얼마 전 출간된 AWR 관련 서적을 읽고 있는데, 이 책의 저자는 '다르다'는 표현을 전부 '틀리다'로 써 놓으셨더군요.

    일상 대화 중에서야 그러려니 하겠지만, 전문서적에서 전부 잘못 써 놓으니 책 읽기가 싫어질 정도였습니다.

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

      좋은 의견입니다. 제목이 틀렸군요. 개발자에게 들은 것을 그대로 사용하면 안되겠네요.
      한글의 사용이 잘못되어 지적을 하는것은 중요합니다.

      마찬가지로 DB 실력향상도 중요합니다. 국어와 한글을 사랑하시어 댓글을 남기신 만큼,
      이번주에 올라간 분석함수 문제에도 어문규정 만큼 관심을 가져 주시고, 문제를 푸셔서 댓글로 남겨주세요.

      감사합니다.

  9. rose 2011.11.29 18:04 신고  댓글주소  수정/삭제  댓글쓰기

    이런 이유가 있었네요~ 재밌게 잘 읽었습니다 ^^

글의 순서

1.쉽게 이해되는 글을 작성하는 방법

2.은유는 본능이다

3.은유의 사용규칙
4.은유를 사용해야 하는 과학적 이유

5.은유는 당신의 뇌를 속인다

 

이공계 출신은 특히 은유를 많이 사용해야 한다. 그렇지 않으면 이해와 소통이 부족할 것이다.” 나는 이렇게 주장한다. 뭔 헛소린가 하고 생각할 것이다. 왜냐하면 감성을 자제하고, 이성을 강조하는 과학적인 글에서는 은유가 적합하지 않다고 생각하기 때문이다. 반면에 은유는 감성적인 것이므로 문학적인 글에서 사용해야 한다고 생각한다. 이런 생각이 누구의 주장으로부터 나온 것인지 모르겠지만, 명백한 오류이다. 왜냐하면 이공계의 말과 글에서 은유를 통제한 결과는 처참하기 때문이다. 즉 쉽게 이해할 수 없는, 어려운 말과 글을 양산하고 말았다. 서로 이해 할 수 없으면 소통을 할 수 없는 것은 당연한 것이다. 은유는 가장 강력한 이해의 도구인데 안타깝게도 언제부턴가 이공계에서 사용하지 않고 있다.

 

흔히 글쓰기 책에서 글을 이해하기 쉽게 써라라고 언급된다. 하지만 어떤 글이 이해하기 쉬운 글인지는 언급하지 않는다. 또한 회의를 할 때 쉬운 말로 설명하라고한다. 도대체 어떠한 말과 글을 사용해야 쉽게 이해가 되는 것일까? 대답은 의외로 간단하다. 은유를 사용하면 어렵고 추상적인 개념을 쉽게 이해할 수 있다. 왜냐하면 은유는 어려운 개념을 쉬운 개념으로 대체(mapping)하기 때문이다. Post에서는 이해하기 힘든 말과 글에서 은유의 사용법을 제시한다. 또한 이해하기 힘든 글에서 은유를 써야 하는 과학적인 근거도 제시한다. 하지만 과학적인 근거보다 더 중요한 것은 은유는 인간의 본능이라는 점이다. 즉 은유를 사용하지 않는 것은 인간의 본성을 외면하는 것이다.

 

정의만으로는 상태나 특징을 설명할 수 없다

많은 이들이 개념을 이해시키기 위해 은유는 사용하지 않고, ‘사전적 정의(definition)’를 사용한다. 하지만 어떤 개념의 상태나 개념의 특징을 설명하고자 할 때는 정의는 사용할 수 없다. 왜냐하면 정의라는 것은 너무 사전적이라 일반화 시킬 수 밖에 없기 때문이다. 예컨대 뇌 과학에서 이야기 하는 사랑의 특징에 대해 논의 해보자. ‘뇌 과학에 따르면 사랑이라는 감정은 2 ~ 3년 간만 지속된다.’ 라는 사실을 설명할 때 사랑에 관한 사전적 정의를 사용할 수 없다. 오히려 사랑은 한 순간의 불꽃이다혹은 콩깍지가 끼는 기간은 짧다처럼 은유로 말하는 것이 더 적합하다.

 

은유와 구체적인 예제를 추가하라

개념을 정의하는 문장을 더 보자. Trickle Down이란 넘쳐흐르는 물이 바닥을 적신다라는 뜻이다. 이렇게 정의를 해도 Trickle Down이 구체적으로 무엇인지 쉽게 와 닿지 않는다. 이때 가장 좋은 것이 은유와 구체적인 예제로 정의부분을 감싸는 것이다. 아래의 예제는 은유 à 정의 à예제 순서로 구성된다.

 

Trickle Down Effect대기업과 중소기업의 Win Win 전략이다(은유)

Trickle Down이란 넘쳐흐르는 물이 바닥을 적신다라는 의미이다.(정의) 대기업이 탄생하면 여러 종류의 중소규모의 하청업체들도 생겨나는데, 대기업이 흑자가 나서 생산량을 늘리면 중소기업의 생산량도 늘어나므로 바닥경제도 살아나게 된다. 이것이 Trickle Down의 대표적인 예이다. 반대로 대기업의 생산량이 줄어들면 중소기업도 치명타를 입는다.(예제) ….. 이후 생략

 

이해하기 쉬운 글의 구조: 은유로 시작하고 예제로 끝낸다

위의 글처럼 나는 종종 은유를 제목/소제목으로 자주 사용한다. 왜냐하면 제목/소제목은 명확하고, 간결해야 하며, 많은 의미를 함축해야 하는데, 이때 가장적절 한 것이 은유이다. 시간이 얼마나 중요한지 설명을 하려면 A4지 한 장으로 설명을 하여도 부족할 것이다. 하지만 은유는 시간은 금이다라는 단 한 줄로 많은 의미를 함축한다. 예를 들면 시간을 낭비하지 마라, 인생은 짧다, 소중한 것부터 시간을 투자하라, 등등 셀 수 없이 많은 의미가 함축되어있다. 따라서 제목/소제목에 가장 적합한 것이 은유이다. 은유-->정의-->예제의 3단계를 거치면 아무리 어려운 개념이라 하더라도 독자를 이해시킬 수 있다. 은유는 꼭 제목/소제목으로 써야 하는 것은 아니며, 단락의 첫 문장으로 사용해도 무방하다.

 

은유는 본능이다

우리는 매일 은유를 사용한다. 하지만 은유를 사용한다고 느끼지 못한다. 예를 들어보자.

 

l  여기에 많은 시간을 투자할 수 없다.  

l  내게 시간을 빌려주실 수 있습니까?  

l  너는 나의 시간을 낭비하고 있다.     

l  시간을 절약해라.

 

위의 모든 예제는 시간은 돈은유를 간접적으로 표현한 것이다. 우리는 위의 표현들을 자주 사용하지만, 그것을 은유라고 생각하지는 않는다. 왜냐하면 무의식적으로 튀어 나오는 말이며, 우리가 의도적으로 은유를 쓰지 않았기 때문이다. 하지만 당신이 위의 말을 사용했다면 시간은 돈이라는 은유를 본능적으로 사용한 것이다. 이런 류의 말과 글은 많이 나타난다. ‘토론은 전쟁이라는 은유를 간접적으로 사용하는 예를 보자.

 

l  어제 회의실에서 그가 내 의견을 공격했다.

l  너의 논증은 허술해서 반대 세력에 쉽게 무너질 것이다.

l  그의 이론을 무너뜨리기 쉽지 않다.

l  너의 논증을 강화해야 반론에 견딜 수 있다.

l  그녀의 의견은 견고해서 내가 이길 수 없다.

 

만약 당신이 위의 예제들이 이상하게 느껴지지 않는다면, 당신이 은유를 자주 사용했거나, 상대방이 은유를 자주 사용했기 때문이다. 이런 식의 간접은유는 우리가 본능적으로 매일 사용하고 있다.

 

은유는 모호한 개념을 명확히 만든다

우리가 시간’(time) 이라는 개념은 명확히 그리고 직관적으로 알 수 있다. 그런데 좀더 추상적인 개념인 시간의 가치’(시간이 얼마나 중요한지)는 훨씬 어렵게 느껴진다. ‘시간의 가치를 말로 설명할 때 가장 좋은 것은 은유이다. 예를 들어보자. ‘시간은 돈이다’, ‘시간은 금이다라고 표현한다면 시간의 가치는 직관적으로 이해할 수 있다. 추상적이고 복잡한 대상을 명확하고 구체적인 대상으로 변경시키는 것이 은유이다. 즉 은유가 손에 잡히지 않는 어려운 개념을 이해하기 쉽게 만들어 준다는 이야기이다. 은유를 사용하지 않고 시간의 가치를 설명해 보라. 아마 쉽지 않을 것이다. 또한 은유를 사용한 경우보다 이해하기 힘들 것이다.

 

은유를 사용할 때의 규칙: 어려운 A는 쉬운 B이다

은유는 기본적으로 어려운 개념을 쉬운 것으로 대체시키는 것이다. 따라서 ‘A B이다처럼 사용해야 한다. 더 중요한 것은 어려운 개념은 A에 위치시키고 쉬운 개념을 B에 위치시켜야 한다. 은유를 사용하면서 가장 많이 저지르는 오류가 A B의 위치가 뒤바뀌거나, A B에 모두 어려운 개념을 사용하거나, A B에 모두 쉬운 개념을 위치시키는 경우이다. 다시 말해 A에는 추상적이거나, 모호하거나, 명확하지 않거나, 어려운 개념을 나타내야 한다. 이와는 반대로 B에는 구체적이거나, 직관적이거나, 명확하거나, 쉬운 개념을 나타내야 한다. 이를 어기는 사례를 자주 볼 수 있다.

 

은유를 사용해야 하는 과학적인 이유

인간이 개념을 이해하려고 할 때 어떤 과정을 거치는지 인지과학에서 사용하는 인지모형으로 설명해보자. 인지모형을 이해하면 이공계의 언어에 왜 은유를 써야만 하는지 알 수 있게 된다. 여기서는 글을 읽고 개념을 이해하는 과정을 다룬다. 하지만 글뿐만이 아니라 말도 동일한 과정을 거친다. 말과 글은 감각기관(시각과 청각)만 다를 뿐 개념이해의 원리는 같다.

 

인지모형: 집중 à 눈으로 읽기( 문장입력 ) à 멘탈모델작성 à 이해

글을 읽고 개념을 이해를 하려면 먼저 주의를 집중해야 한다. 그리고 문장을 눈으로 보아야 한다. 이를 문장입력이라고 한다. 눈으로 입력된 문장은 뇌로 이동된다. 뇌 속으로 이동된 정보를 토대로 멘탈모델(mental model)을 작성하게 된다. 멘탈모델은 이 글은 이러 저러한 개념이다라는 의미를 이미지(심상)로 만드는 것이다. 즉 글 전체의 의미를 머릿속에서 예측하는 작업이 멘탈모델을 작성하는 단계이다. 이 단계에서는 뇌에 저장되어 있는 기억과 경험들을 이용하여 글의 의미를 예측한다. 이때 글의 내용이 이미 알고 있는 개념이라면 멘탈모델이 쉽게 작성된다. 멘탈모델이 작성 완료되면 연이어 다음문장을 읽고 동일한 단계(멘탈모델 작성)를 거친다. 모든 글을 다 읽을 때까지 위의 과정이 반복된다.

 

이해되지 않으면 문장을 다시 읽어야 한다

하지만 모르는 개념이 있거나, 명확하지 않고, 복잡한 개념이 있는 경우는 우리의 기억 속에서 찾을 수 없으므로 멘탈모델을 작성하기 힘들다. 우리가 글을 이해하기 힘들 때 이미 읽었던 문장을 반복적으로 읽는 이유는 멘탈모델을 작성하기 어렵기 때문이다. 글을 다시 읽고 이해가 될 때까지 멘탈모델작성을 반복적으로 시도한다이와 반대로 문장을 한번만 읽고 멘탈모델이 성공적으로 작성된 경우는 반복 없이 다음문장으로 건너뛰기 때문에 고속으로 처리된다. 이 모든 것을 그림으로 정리한 것이 아래의 인지모형이다.

 


 

멘탈모델작성은 가장 무거운 작업이다

멘탈모델은 구성통합모형이라고도 부른다. 멘탈모델을 작성하려면 다시 네 가지 세부적인 작업(문장 parsing, 미시명제 표상형성, 거시명제 이해, 상황모형 생성)이 실행 되어야 하기 때문에 힘든 작업이다. 그래서 이 작업을 최소화 시켜야 한다. 쉬운 개념을 이용하여 한번에 이해될 수 있도록 글을 써야 하는 이유도 멘탈모델을 작성할 때의 부하 때문이다. 멘탈모델의 네 가지 세부적인 작업을 구체적으로 알고 싶은 사람은 관련서적인 이해:인지 패러다임(Walter Kintsch ) 을 참조하기 바란다.

 

은유는 뇌를 속이는 것

은유를 사용한다는 것은 어려운 개념을 쉬운 개념으로 대체하여 이해하는 것이다. 따라서 어려운 개념을 설명하더라도 은유를 사용하면 우리의 기억과 경험을 이용하여 멘탈모델을 빠르게 작성할 수 있다. 즉 고속처리가 가능하다. 정확히 말하면 은유는 뇌를 속이는 것이다. 실제로는 어려운 개념이지만 속임수(은유)를 사용하여 쉬운 개념으로 속인다. 이렇게 하면 우리의 기억에서 관련 개념을 쉽게 참조할 수 있으므로 멘탈모델을 한 번 만 작성하고 다음문장을 읽을 수 있다.

 

결론

말과 글을 상대방이 이해하기 쉽게 만들려면 자신만의 은유를 개발하라. 당신이 만든 은유는 말과 글에 독창성을 부여할 것이다. 당신만의 새로운 은유를 만드는 것은 규칙(어려운 A 쉬운 B)만 지킨다면 전혀 어렵지 않다. 나는 이 글을 쓰기 위해 Trickle Down(A) win win 전략(B)을 은유로 사용했다. 이렇게 사용하는 것은 창조적인 기술을 요구하지 않는다. 왜냐하면 은유는 우리에게 잠재되어 있는 본능이기 때문이다.

 

은유-->정의-->예제의 구조는 어려운 개념을 이해시키는데 적합하다. 어차피 언어(말과 글)란 소통을 위해서 존재하는 것이다. 소통이 되려면 내 생각을 상대방에게 이해시켜야 한다. 서로의 생각을 모른다면 소통할 수 없다. 즉 이해가 소통의 필수 조건이며, 이해가 힘들 때 가장 좋은 처방이 은유이다. 은유는 어렵고 복잡한 개념을 우리가 이미 알고 있는 개념에 빗대어 쉬운 것으로 변경시켜 준다. 따라서 인문계열이든 이공계열이든 은유를 자제할 하등의 이유가 없다. 오히려 복잡한 개념을 가진 과학적인 말과 글에서 상대방을 이해를 시키는 것이 목적이라면 은유를 사용하라. 그대가 시인이 아니라고 할지라도.

 

 

 

PS : 참조서적

은유에 대해 더 깊은 공부를 원하는 사람은 아래의 책을 참조하라.

1.과학의 언어(Carol Reeves ) : 과학적인 글에서 은유가 어떻게 사용되는지 나타낸다.

2.삶으로서의 은유(George Lakoff) : 일상에서 은유가 어떻게 사용되는지 분석한다. 

 

인지과학에 관심이 있는 사람은 아래의 책을 참조하라.

3.인지과학 - 과거 현재 미래(이정모 저)

 

인간이 말과 글을 어떻게 이해하는지에 관심이 있으면 아래의 책을 참조하라. 책이 두 권으로 되어있다.

4.이해:인지 패러다임(Walter Kintsch )

 

위에서 설명한 인지모형과정이라는 것도 물리적으로는 신경세포(뉴런)의 상호작용이다. 뉴런은 뇌의 네트워크 망인(시냅스)를 이용한다. 뇌 과학의 기본을 알려면 아래의 책을 참조하기 바란다. 아주 얇은 그림책이다.

5. 구조(뉴턴코리아)

 

참고사항

은유에 대하여 한 권의 책을 꼽으라면 단연 삶으로서의 은유이다
2,3,4
번 책은 모두 인지과학에 관련된 책이다. 위의 책 중에 이해: 인지 패러다임이라는 책은 전문가도 어려워하므로 무작정 사지 말고 서점에서 난이도를 확인하고 구입하기 바란다. 필자의 경우 두 번 읽고 겨우 이해 하였다이 책을 정복하려면 인지과학과 인지심리학, 그리고 뇌과학 입문서를 탐독하고 보는 것이 바람직하다.


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

댓글을 달아 주세요

  1. Favicon of http://fstory97.blog.me BlogIcon 숲속얘기 2011.01.04 10:24 신고  댓글주소  수정/삭제  댓글쓰기

    좋은글 감사합니다. 도움이 많이 될것 같습니다.
    오픈캐스트에 실을게요.
    http://opencast.naver.com/casthome/list.nhn?castId=FS565&volumeSeq=49

  2. Favicon of http://xenerdo.com BlogIcon 제너시스템즈 2011.01.04 11:17 신고  댓글주소  수정/삭제  댓글쓰기

    쉬운 글쓰기에 은유는 필수였군요. 늘 글을 쓰면서도 어떻게하면 좀 더 쉽게 글을 쓸 수 있을지 잘 생각하지 못했는데 앞으로는 은유를 적극적으로 사용해야겠습니다.^^

  3. 임백두 2011.01.04 11:52 신고  댓글주소  수정/삭제  댓글쓰기

    와~~ 정말 좋은 글이네요

    비쩍 마른 내 지성에 촉촉한 가습기를 틀어놓은 듯한.....ㅋㅋ

  4. feelie 2011.01.06 12:50 신고  댓글주소  수정/삭제  댓글쓰기

    오늘 당장 '삶으로서의 은유’ 책을 구입하여 읽어봐야겠네요...

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

      제 주위에 읽기가 힘들어서 포기하는 분도 계십니다.(두분 중 한분이 포기)
      4번 책보다는 쉽지만, 난이도 가 어느정도 있으므로 교보문고에 가셔서 한번 보고 구입하세요.

  5. 2011.01.10 11:13  댓글주소  수정/삭제  댓글쓰기

    비밀댓글입니다

  6. 지나가다 2013.01.25 10:17 신고  댓글주소  수정/삭제  댓글쓰기

    좋은 내용 감사드립니다.