먼저 필자와 페이스 북으로 친구가 될 사람은 http://www.facebook.com/donggyu.o로 와서 친구로 추가해 주기 바란다. 물론 먼저 가입을 해야 친구추가가 가능하다.

 

약 한 달간 페이스북을 사용해 보니 트위터와는 다르게 페이스북은 모두를 1촌으로 취급하였다. 따라서 친구를 추가한다는 것은 트위터의 Follower(추종자)개념과는 전혀 다른 것이다. 트위터, 싸이월드 등의 다른 SNS와 비교해 본 결과 아래의 세 가지 결론을 낼 수 있었다.

 

첫 번째, 페이스북은 Total Communication Package이다

Total Package라는 용어를 쓴 이유는 여러 가지 기능을 뭉쳐놓았기 때문이다. 예컨대 메신저(MSN, 네이트온) + 아이러브 스쿨(동창회) + 싸이월드(1촌관리, 도토리, 앨범관리) + 트위터(댓글놀이) + 인터넷 카페(동호회) + 블로그 라고 할 수 있다. 이 여섯 가지 기능을 사용하기 쉽게 만들어 놓은 것이다. 간단히 기능을 살펴보자.

 

사용자 삽입 이미지

뉴스 피드: 나와 친구들이 남긴 각종 소식과 댓글을 볼 수 있다. 트위터와 비슷하다
.

쪽지 : 네이버의 쪽지기능에 해당한다.

친구 : 친구요청, 친구수락, 친구 찾기에 사용한다. 친구관리는 싸이월드의 1촌관리와 비슷하고 친구 찾기는 아이러브스쿨과 비슷하다.

사진: 앨범을 관리한다. 싸이월드의 앨범과 비슷하다.

그룹: 동호회를 만들 수 있다. 인터넷 카페와 비슷하다.


노트: 블로그와 비슷하다. 그림을 올릴수는 있지만 아직까지 첨부파일을 올릴 수가없다.

링크: 맘에 드는 페이지를 연결시킨다.

선물: 싸이월드의 도토리에 해당한다.

온라인 친구: 메신저를 이용해 실시간 채팅을 할 수 있는 사람들을 보여준다.

채팅 : 화면 오른쪽 하단의 채팅박스를 클릭하면 대화를 할 수 있는 대상자 List가 보인다. 여기서 아무나 선택하면 채팅이 가능하다.

 










두 번째, 양방향 대화가 가능하다

페이스북의 경쟁자라고 할 수 있는 트위터와 기능을 비교해보자. 트위터의 중심적 기능은 댓글놀이 이다. 예를 들면 이외수 작가가 글을 트위터에 올리면 여러 사람이 공감할 수 있고 작가의 글에 댓글을 쓸 수 있다. 하지만 트위터의 소통은 단 방향이라는 것이 치명적인 단점이다. 즉 내가 글을 트위터에 올린다고 해서 이외수 씨가 내글에 댓글을 쓸리가 없다는 것이다. 하지만 페이스북은 이와 반대로 양방향 대화가 특징이다.

 

사용자 삽입 이미지

위의 글을 본다면 필자가 쓴 글에 친구가 댓글을 달았다. 하지만 아래의 글은 반대이다.


사용자 삽입 이미지

친구가 쓴 글이 뉴스피드에 바로 나타난다. 댓글을 달지 않으면 못살게(?) 만들어 놓았다.

 

세 번째, Door Complex를 완화시킨다

많은 수의 트위터 사용자들은 내가 댓글을 남긴 만큼 상대방도 내 글에 댓 글을 달았으면 좋겠다고 생각할 것이다. 물론 대화하는 양쪽 모두가 Follow 한다면 가능한 일이겠지만 이런 일은 아주 친한 사이인 경우만 가능하다. 친하지 않다면 상대방에게 Follow 해달라고 부탁 혹은 애걸(?)을 해야 한다. 하지만 많은 사람들이 Door Complex 때문에 그렇게 하지 못하고 망설인다. Door Complex란 주로 영업을 업으로 하는 사람들에게 해당되는 것이다. 예를 들어, 신문사 영업사원이 신문구독요청을 하려고 대문 앞에서 초인종을 누를 때 집주인이 싫어요라고 하면 어떡하나?’ 하는 막연한 두려움이 Door Complex이다. 이런 류의 두려움으로 고통 받는 사람들은 심지어 타인에게 많은 돈을 주고 일을 대신시키기도 한다. 이러한 Complex는 누구에게나 있으며 트위터의 Follow요청에도 해당된다.

 

페이스북은 Door Complex를 참신하고 교묘한 방법으로 제거한다. 예컨대, 내가 A에게 친구요청을 하면 페이스북이 ~님이 회원님과 Facebook 친구가 되고 싶어 합니다.라고 전달해준다. 즉 내가 초인종을 누르는 것이 아니라 우체부(페이스북)가 편지를 배달해줌으로써 면상에서 친구요청을 거부당할 수 있는 두려움을 완화시켜준다. 아무 기능도 아닌 것처럼 보이지만 본인이 이메일로 직접 글을 써서 중매쟁이 없이 친구요청을 하는 경우와 비교해본다면 차이가 크다는 것을 실감할 것이다. 친구요청을 하면 상대방은 아래와 같은 메시지를 이메일 이나 페이스북에서 보게 된다.

 

사용자 삽입 이미지

친구를 요청하는 사람은 뒤에 숨어있고 어떠한 말도 하지 않는다. 마치 페이스북이 맞선을 보이는 중매쟁이 역할을 하는 듯 하다. 페이스북이 5억 명의 이용자를 거느린 이유도 Door 콤플렉스를 없애거나 완화시켜주었기 때문이다. 현재시점으로 트위터 이용자는 2억 명이다. 만약 트위터가 양방향 소통을 가능하게 설계했다면 이용자는 4억 명 + 알파가 되었을 것이다. 숫자가 단순히 두 배가 아닌 이유는 친구의 Network(친구의 친구)까지 지인으로 등록할 수 있기 때문이다.

 

페이스북의 사상을 알려면 링크라는 책을 읽어보기 바란다. 네트워킹을 하나의 학문으로 끌어올린 책이다. 이 책의 결론은 허브를 거치면 1~2 단계 만에 세상의 모든 사람과 소통이 가능하다는 것이다. 허브는 엄청나게 많은 수의 노드들과 링크를 가짐으로써 시스템 내의 노드 간의 경로를 짧게 만드는 기능을 한다. 바로 페이스북이 허브인 것이다.

 

결론

페이스북의 다양한 기능으로. 메신저, 싸이월드, 트위터, 아이러브스쿨, 인터넷 카페 등을 따로 방문할 필요가 없게 되었다. 더 좋은 것은 지인을 모두 1촌으로 관리해 주기 때문에 양방향 댓글놀이(파도타기)가 가능하다는 점이다. 무엇보다 페이스북이 이용자수 5억 명이라는 맹위를 떨치게 된 이유는 Door Complex 없이 두 사람을 친구로 만들어 주기 때문이다. 또한 경이로운 이용자수는 페이스북이 허브 역할을 충실히 한다는 근거가 되기도 한다. 아직 한국에서는 시작단계이기 때문에 트위터 사용자보다 이용인구가 작지만 멀지 않은 미래에 역전이 예상된다.

 

페이스북이 결국 인터넷 검색기능을 추가한다고 발표했다. 결국 포털 업체들과의 경쟁이 불가피할 것으로 보인다. 경쟁 업체들의 관점에서는 페이스북을 적으로 볼 것이 아니라 오히려 페이스북을 이용하여 어떤 이익을 올릴 것인가에 관심을 가져야 한다. 예를 들어 페이스북의 좋아요 버튼을 활용하여 사용자가 블로그에서 작성한 글을 페이스북에 연동함으로써 더 많은 블로거에게 글을 노출시킬 수 있다. 영업부서라면 제품을 홍보할 때 좋아요 버튼을 이용하면 더 많은 매출을 올릴 수 있을 것이다. 영업사원은 페이스북을 통하여 고객을 관리할 수 있다. SNS는 하루가 멀다 하고 새로운 서비스로 진화하고 있다.
다윈의 이론이 인터넷 세상에도 영향을 끼친 것인가
?

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

댓글을 달아 주세요

  1. 윤상원 2010.08.31 20:59 신고  댓글주소  수정/삭제  댓글쓰기

    오동규님, 블로그를 즐겨 구독하는 독자 입니다. 항상 좋은 정보 올려주셔서 감사합니다~

    이번에 페이스북 글을 보고 페이스북에 가입하고 오동규님에게 친구추가까지 하였네요.

    앞으로 종종 페이스을 통해 인사드리겠습니다. 아는 척이라도 해주세요~

    그럼, 수고하세요~

  2. lynn 2010.08.31 22:22 신고  댓글주소  수정/삭제  댓글쓰기

    페이스북을 시작해보려고 검색을 하다가 들리게 되었습니다
    도움이 많이 되었네요 감사합니다^_^

  3. 정재열 2010.09.01 09:30 신고  댓글주소  수정/삭제  댓글쓰기

    DB관련글만 눈팅하다가 페이스북 관련 포스팅이 업데이트 되어있는 것을 보고 즐겨찾기 오류인가 했습니다.

    햇병아리 경력이라 올려주시는 좋은 정보들의 10%도 캐치하지 못하고 있지만 꾸준히 즐겨보고 있습니다.

    앞으로도 꾸준한 포스팅을 감히 부탁드리며 페이스북 가입하고 저도 친구추가 할테니 받아주세요 :]

    좋은 하루 보내시길!

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

      ^^ 즐겨찾기 오류가 아닙니다.
      블로그내용이 너무 딱딱하다는 의견이 있어 부드러운 것을 넣어보았습니다.
      페이스북으로 블로그에서 못하는 소통을 같이 해보시죠.

  4. 마늘장 2010.09.01 15:54 신고  댓글주소  수정/삭제  댓글쓰기

    IT로 밥벌이를 하면서도 facebook이 뭔지 오늘 처음 알았네요.
    다들 스마트폰 쓴다고 하는데 그것도 좀 써봐야 할거 같구요...
    새로운것들을 접할때마다 사는 재미가 또 생깁니다.

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

      사용해보니 꽤 좋은점을 발견할 수 있었습니다.
      저도 사용한지 한달이라 아직 잘은 모릅니다.
      같이 대화하면서 알아가도록 하시죠.

  5. 롤리팝 2010.09.02 20:41 신고  댓글주소  수정/삭제  댓글쓰기

    와우!!! 정말 좋은 말씀 해 주셨네요
    사실 싸이랑 블로그 등등 다른거 하다가 다 접고
    페북 시작한지 몇개월 안되었거든요
    페북이 진짜 편하더라구요 아직 한국에서는 유저가 많이 없지만 해외에선 난리죠

    최근에는 트위터다 뭐다 난리라서 트위터는 가입해봤는데
    영 재미가 없더라구요
    내가 상대방에 댓글 달면 그게 보이지도 않고
    상대방이 그거 대꾸 해주지도 않고 그쵸????
    근데 페북은 자기 페북에서 모든걸 할 수 있잖아요
    방문자 숫자 신경 안써도 되고
    사진 올려서 tag 하면 tag 한 친구들도 전부 자기 페북에서 다 볼 수 있고
    서로 댓글 다는거 바로바로 보이고
    페북이 진짜 짱인듯

    트위터의 젤 단점이 말씀해주신 그거 맞는것 같아요
    상대방 댓글 못보고
    한방향이라는거
    어쩐지 전 한참을 찾았거든요
    왜 내 댓글이 안보이나
    상대방은 내 글을 확인은 한건가
    왜 나한테는 답변이 안오지? 그런거 ㅋㅋㅋㅋㅋㅋㅋㅋㅋ

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

      롤리팝님 반갑습니다.
      저도 그런 이유로 트위터보다는 페이스북이 더 입맛에 맞더군요. 조만간 한국 sns 1위업체인 싸이월드가 새버젼을 내놓는다고 하니 한판 전쟁이 될것 같습니다.

  6. 롤리팝 2010.09.03 12:17 신고  댓글주소  수정/삭제  댓글쓰기

    싸이가 좀 시들해 졌긴 하죠.
    페이스북은 사실 꾸미지 않아도 되고 돈 들 필요도 없고
    싸이는 무조건 돈을 들여야 하니
    이쁘게 안꾸미면 방문자도 없고 왠지 허접해 보이고
    그걸 노린거겠지만
    아무튼 싸이가 이것저것 따라한다고
    일촌평도 날짜 달아놓고 어쩌고 했는데 되려 더 욕을 먹고 있죠
    과연 어떤걸 내놓으려고 그러는지 ㅎㅎ

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

      싸이는 여성팬들이 많은 모양입니다.
      이쁘게 꾸미는 걸 보면....
      외모는 싸이가 나은데 이번에 내용이 어떻게 바뀔지 궁금하네요.
      싸이가 소스를 공개하여 많은 수의 외부 개발자들을 끌어들인다면 성공할 듯 합니다.

  7. 아하순천 2010.09.14 01:18 신고  댓글주소  수정/삭제  댓글쓰기

    2013순천만국제정원박람회 트위터 팔로우 이벤트 실시!
    여러분 정원박람회 트위터 팔로우하시고 정원박람회 정보도 얻고,
    기념품도 받아가세요^^~
    기간 : 2010.9.10~9.14
    방법 : ECOGEO2013을 팔로우 한후 이벤트내용 아래 “2013순천만국제정원박람회
    대박기원”이라고 쓰고 리트윗 하세요.
    발표 : 9.15(수) 30명 선정 트위터에 발표
    상품 : 2013순천만국제정원박람회 기념품

가능한가?

SQL문은 실행순서가 있다. 일반적으로 Order By가 가장 마지막에 수행된다. 만약 Order By가 없다면 마지막에 실행되는 것은 Group By이다. 데이터를 처리하는 작업은 Order By Group By 작업을 수행하기 이전에 끝난다. 상식적으로 보더라도 데이터를 읽는 작업과 조인작업을 먼저 처리해야 전체 데이터를 Grouping 할 수 있기 때문이다. 그런데 만약 데이터를 처리하는 작업이 Group By 이후에 발생한다면? 이런 일은 논리적으로 발생할 수 없다. 하지만 오늘 한가지 경우를 보여주려 한다.

 

먼저 오라클 SH 스키마의 Sales 테이블에 Local Partition 인덱스를 하나 생성한다.

Sales 테이블은 Time_id로 분기별 Range Partition이 되어있다.

 

CREATE INDEX SALES_TIME_CHANNEL_IX ON SALES

(TIME_ID, CHANNEL_ID) LOCAL;

                                 

먼저 정상적으로 처리되는 경우를 보자. IN 조건을 Pair로 여러개 주어본다. 

 

SELECT /*+ gather_plan_statistics */ TIME_ID, COUNT(TIME_ID)

  FROM SALES

 WHERE (TIME_ID, CHANNEL_ID) IN (  (TO_DATE('19980214'), 3),

                                   (TO_DATE('19980214'), 2),

                                   (TO_DATE('19980214'), 4) )

 GROUP BY TIME_ID;

  

위의 SQL에서 주의해야 될 것은 IN 조건이 TIME_ID CHANNEL_ID 로 동시에 공급된다는 것이다.

 

TIME_ID  COUNT(TIME_ID)

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

98/02/14            391

1 row selected.

 

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

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

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

|   0 | SELECT STATEMENT        |                       |      1 |      1 |       3 |

|   1 |  SORT GROUP BY NOSORT   |                       |      1 |      1 |       3 |

|   2 |   PARTITION RANGE SINGLE|                       |      1 |    391 |       3 |

|*  3 |    INDEX RANGE SCAN     | SALES_TIME_CHANNEL_IX |      1 |    391 |       3 |

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

 

Predicate Information (identified by operation id):

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

   3 - access("TIME_ID"=TO_DATE('19980214'))

       filter(("CHANNEL_ID"=2 OR "CHANNEL_ID"=3 OR "CHANNEL_ID"=4))


정상적으로 Grouping 되어 1건이 나왔고 Plan상에 Group By도 마지막에 수행되었다.

 
INLIST ITERATOR ! 니가 나를 배신하다니...

이제 1998 2 12일의 데이터를 추가로 공급해보자.

 

SELECT /*+ gather_plan_statistics */

       TIME_ID, COUNT(TIME_ID)

  FROM SALES

 WHERE (TIME_ID, CHANNEL_ID) IN (  (TO_DATE('19980212'), 4),

                                   (TO_DATE('19980214'), 2),

                                   (TO_DATE('19980214'), 3),

                                   (TO_DATE('19980214'), 4) )

 GROUP BY TIME_ID;

 

TIME_ID  COUNT(TIME_ID)

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

98/02/12             50

98/02/14             54

98/02/14            287

98/02/14             50

 

4 rows selected.

 

답이 틀리다(Wrong Result Bug)

뭔가 이상하다. TIME_ID Grouping 하였으므로 결과건수는 1998 2 12일과 1998 2 14일로 두건만 나와야 한다. 그런데 2 14일 데이터가 Grouping 되지 않고 3건이 나와버렸다. 답이 틀리므로 이것은 버그이다.

 

왜 이런 일이 발생할까?

비밀은 실행계획에 있다.


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

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

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

|   0 | SELECT STATEMENT          |                       |      1 |      4 |       7 |

|   1 |  INLIST ITERATOR          |                       |      1 |      4 |       7 |

|   2 |   PARTITION RANGE ITERATOR|                       |      4 |      4 |       7 |

|   3 |    SORT GROUP BY NOSORT   |                       |      4 |      4 |       7 |

|*  4 |     INDEX RANGE SCAN      | SALES_TIME_CHANNEL_IX |      4 |    441 |       7 |

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

  

상식적으로는 Grouping을 가장 마지막에 한번만 해야 한다. 하지만 실행계획을 보면 그렇지 못하고 INLIST ITERATOR 작업 이전에 Grouping을 해버린다. ID 기준으로 3번이 그것인데 INLIST의 개수만큼 SORT GROUP BY가 반복된다. Starts 항목이 그것을 증명한다. 개념적으로 말하면 각각의 WHERE 조건마다 Group By를 수행하고 그 결과들을 Union 한 것이다. 

 

항상 버그가 발생하는 것은 아니다

파티션과 IN 조건이 만나야 버그가 발생한다. 또한 WHERE 조건이 변경되지 않고 INLIST ITERATOR가 발생해야 버그가 발생한다. 이 글에서 최초로 실행 시킨 SQL WHERE 조건은 TIME_ID = TO_DATE('19980214') AND CHANNEL_ID IN (2,3,4) 로 바뀌어 INLIST ITERATOR로 처리되지 않고 INDEX RANGE SCAND으로 처리되었으므로 버그가 없다.

 

해결책은 처리순서를 변경하는 것

이런 경우의 해결책은 아주 간단하다. 우리의 상식대로 하면 된다. 즉 데이터를 모두 처리하고 Grouping을 가장 마지막에 실행하는 것이다. 아래의 SQL이 그것이다.

 

SELECT TIME_ID, COUNT(*)

  FROM ( SELECT /*+ NO_MERGE */  TIME_ID

           FROM SALES

           WHERE (TIME_ID, CHANNEL_ID) IN (  (TO_DATE('19980212'), 4),

                                             (TO_DATE('19980214'), 2),

                                             (TO_DATE('19980214'), 3),

                                             (TO_DATE('19980214'), 4) )

       )

GROUP BY TIME_ID; 

 

TIME_ID  COUNT(TIME_ID)

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

98/02/12             50

98/02/14            391

 

2 rows selected.

 

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

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

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

|   0 | SELECT STATEMENT           |                       |      1 |      2 |       9 |

|   1 |  SORT GROUP BY NOSORT      |                       |      1 |      2 |       9 |

|   2 |   INLIST ITERATOR          |                       |      1 |    441 |       9 |

|   3 |    PARTITION RANGE ITERATOR|                       |      4 |    441 |       9 |

|   4 |     VIEW                   |                       |      4 |    441 |       9 |

|*  5 |      INDEX RANGE SCAN      | SALES_TIME_CHANNEL_IX |      4 |    441 |       9 |

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


 

예상대로 Group By는 가장 마지막에 처리되었으므로 결과도 정상적으로 두건이 출력되었다. SQL의 실행순서만 알고 있으면 이 정도의 버그는 패치가 없어도 해결이 가능하다. NO_MERGE 힌트를 사용한 이유는 View Merge(뷰 해체)를 방지하기 위함이다. Simple View 이므로 힌트를 빼면 무조건 인라인뷰가 해체되어 버그가 재생된다.

 

또 다른 해결책을 보자.

 


SELECT /*+ INDEX(SALES SALES_TIME_CHANNEL_IX) */

       TIME_ID, COUNT(TIME_ID)

  FROM SALES

 WHERE    (TIME_ID = TO_DATE('19980214') AND CHANNEL_ID BETWEEN 2 AND 4 )

       OR (TIME_ID = TO_DATE('19980212') AND CHANNEL_ID =4 )         

 GROUP BY TIME_ID; 

 

 

TIME_ID  COUNT(TIME_ID)

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

98/02/12             50

98/02/14            391

 

2 rows selected.

 


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

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

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

|   0 | SELECT STATEMENT         |                       |      1 |      2 |       5 |

|   1 |  HASH GROUP BY           |                       |      1 |      2 |       5 |

|   2 |   CONCATENATION          |                       |      1 |    441 |       5 |

|   3 |    PARTITION RANGE SINGLE|                       |      1 |    391 |       3 |

|*  4 |     INDEX RANGE SCAN     | SALES_TIME_CHANNEL_IX |      1 |    391 |       3 |

|   5 |    PARTITION RANGE SINGLE|                       |      1 |     50 |       2 |

|*  6 |     INDEX RANGE SCAN     | SALES_TIME_CHANNEL_IX |      1 |     50 |       2 |

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


 

SQL WHERE절을 수정하여 INLIST ITERATOR가 아닌 CONCATENATION으로 유도 하였다. 위의 실행계획 또한 Group By가 가장 마지막이 실행되므로 버그가 발생되지 않는다. SQL을 수정하기 싫고 힌트만으로 해결하려면 아래의 SQL을 보라. 

 


SELECT /*+ USE_CONCAT(1) */

       TIME_ID, COUNT(TIME_ID)

  FROM SALES

 WHERE (TIME_ID, CHANNEL_ID) IN (  (TO_DATE('19980212'), 4),

                                   (TO_DATE('19980214'), 2),

                                   (TO_DATE('19980214'), 3),

                                   (TO_DATE('19980214'), 4) )

 GROUP BY TIME_ID;

 

TIME_ID  COUNT(TIME_ID)

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

98/02/12             50

98/02/14            391

 

2 rows selected.

 

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

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

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

|   0 | SELECT STATEMENT         |                       |      1 |      2 |       9 |

|   1 |  HASH GROUP BY           |                       |      1 |      2 |       9 |

|   2 |   CONCATENATION          |                       |      1 |    441 |       9 |

|   3 |    PARTITION RANGE SINGLE|                       |      1 |     50 |       2 |

|*  4 |     INDEX RANGE SCAN     | SALES_TIME_CHANNEL_IX |      1 |     50 |       2 |

|   5 |    PARTITION RANGE SINGLE|                       |      1 |    287 |       3 |

|*  6 |     INDEX RANGE SCAN     | SALES_TIME_CHANNEL_IX |      1 |    287 |       3 |

|   7 |    PARTITION RANGE SINGLE|                       |      1 |     54 |       2 |

|*  8 |     INDEX RANGE SCAN     | SALES_TIME_CHANNEL_IX |      1 |     54 |       2 |

|   9 |    PARTITION RANGE SINGLE|                       |      1 |     50 |       2 |

|* 10 |     INDEX RANGE SCAN     | SALES_TIME_CHANNEL_IX |      1 |     50 |       2 |

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


위의 SQL 또한 정상적으로 2건이 출력된다.

버그를 유발하는 원인을 제거하라
마지막으로 볼 해결책은 파티션과 INLIST ITERATOR의 불편한 만남(?)을 제거하는 것이다.


SELECT /*+ INDEX_COMBINE(a) */

       TIME_ID, COUNT(TIME_ID)

  FROM SALES a

 WHERE (TIME_ID, CHANNEL_ID) IN (  (TO_DATE('19980212'), 4),

                                   (TO_DATE('19980214'), 2),

                                   (TO_DATE('19980214'), 3),

                                   (TO_DATE('19980214'), 4) )

GROUP BY TIME_ID;

 

TIME_ID  COUNT(TIME_ID)

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

98/02/12             50

98/02/14            391

 

2 rows selected.

 

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

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

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

|   0 | SELECT STATEMENT                    |                   |      1 |     2 |      61 |

|   1 |  PARTITION RANGE INLIST             |                   |      1 |     2 |      61 |

|   2 |   HASH GROUP BY                     |                   |      1 |     2 |      61 |

|   3 |    TABLE ACCESS BY LOCAL INDEX ROWID| SALES             |      1 |   441 |      61 |

|   4 |     BITMAP CONVERSION TO ROWIDS     |                   |      1 |   441 |      17 |

|   5 |      BITMAP OR                      |                   |      1 |     1 |      17 |

|   6 |       BITMAP AND                    |                   |      1 |     1 |       4 |

|*  7 |        BITMAP INDEX SINGLE VALUE    | SALES_TIME_BIX    |      1 |     1 |       2 |

|*  8 |        BITMAP INDEX SINGLE VALUE    | SALES_CHANNEL_BIX |      1 |     1 |       2 |

|   9 |       BITMAP AND                    |                   |      1 |     1 |       4 |

|* 10 |        BITMAP INDEX SINGLE VALUE    | SALES_TIME_BIX    |      1 |     1 |       2 |

|* 11 |        BITMAP INDEX SINGLE VALUE    | SALES_CHANNEL_BIX |      1 |     1 |       2 |

|  12 |       BITMAP AND                    |                   |      1 |     1 |       5 |

|* 13 |        BITMAP INDEX SINGLE VALUE    | SALES_TIME_BIX    |      1 |     1 |       2 |

|* 14 |        BITMAP INDEX SINGLE VALUE    | SALES_CHANNEL_BIX |      1 |     2 |       3 |

|  15 |       BITMAP AND                    |                   |      1 |     1 |       4 |

|* 16 |        BITMAP INDEX SINGLE VALUE    | SALES_TIME_BIX    |      1 |     1 |       2 |

|* 17 |        BITMAP INDEX SINGLE VALUE    | SALES_CHANNEL_BIX |      1 |     1 |       2 |

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

 

Bitmap Operation으로 인하여 INLIST ITERATOR가 사라졌으므로 버그는 보이지 않는다.

 

결론

이 버그는 Oracle 10g 에서 발견되었지만 11gR2 버전까지 해결되지 않고 있다. 파티션과 Local Index를 사용하고 Pair IN 조건이 INLIST ITERATOR로 풀리면 발생된다. 해결방법은 SQL을 수정하여 Group By를 마지막에 수행하거나 INLIST ITERATOR를 제거하면 된다. 버그패치가 없다고 해서 잘못된 결과를 보여줄 수는 없다. 패치가 나올 때 까지는 다른 해결책을 강구해야 한다. 잘 생각해보면 위의 경우처럼 원리만 안다면 상식 선에서 해결할 수 있는 문제가 매우 많다.

 

버그, 오라클에 알려야 해
대부분의 경우 버그는 사용자가 오라클에 보고해야 패치를 만들게 된다. 그러므로 버그가 발견되면 반드시 SR을 진행하여 오라클에 알려야 한다. 버그를 발견한 사람은 SQL을 수정하여 버그를 피할 수 있지만 동료나 다른 사이트에 있는 모든 사람들은 해결책을 모를 수 있고 심지어 버그가 있는지 조차 모를 수 있다. 하지만 많은 수의 사람들은 버그를 피해가는 방법만 발견된다면 내 문제는 해결 되었으므로 오라클에 버그를 통보하지 않고 조용히 넘어갈 것이다. 혹시 위의 버그가 몇년간 남아 있는 이유도 조용한(?) 사람들 때문이 아닐까?

한가지 방법은 오라클사에서 버그리포팅을 하는 사람에게 작은 선물을 주는 것이다. 제품의 품질도 높이고 고객의 참여를 유도하는 Win Win 전략이 될 수 있다.


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

댓글을 달아 주세요

(The Logical Optimizer) 내용중 Part 2 부분의 PPT 파일이 완성되어 올립니다.
Tstory
10MB보다 큰 파일은 올릴 수 없게 되어있군요. 파일의 사이즈가 커서 분할 압축하여 올립니다
.
압축을 푸시면 아래그림처럼 3개의 파일이 됩니다. 각각 10MB 정도 되는군요.


사용자 삽입 이미지


첫 번째 파일(The Logical Optimizer_Part II_1) Basic 부분(2.A ~2.16)까지 입니다.
두 번째 파일(The Logical Optimizer_Part II_2) Subquery부분(2.17~2.29)까지 입니다.
세 번째 파일(The Logical Optimizer_Part II_2) Data Warehouse부분(2.30~Part2 마무리)까지 입니다.

PPT
파일로 다시 한번 정리하시기 바랍니다.
압축  프로그램 7zip
감사합니다.

사용자 삽입 이미지
사용자 삽입 이미지
사용자 삽입 이미지
신고
Posted by extremedb

댓글을 달아 주세요

  1. 썸바디 2010.08.13 09:41 신고  댓글주소  수정/삭제  댓글쓰기

    늘 좋은 정보 감사합니다~~
    근데 다운받은 파일 압축이 잘 안풀리네요 ㅡㅡ

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

      분할 압축이므로 모두 다운받은 후에 푸셔야 합니다.
      7zip 프로그램을 다운받으시거나 알집으로 압축을 푸시면 됩니다. 7zip 프로그램을 다운받을 수 있게 글을 수정하였습니다. 해결 되셨나요?

  2. 썸바디 2010.08.13 10:21 신고  댓글주소  수정/삭제  댓글쓰기

    7zip 으로 하니 압축 잘 풀리네요~ 감사합니다~^^

  3. 써니 2010.08.16 23:44 신고  댓글주소  수정/삭제  댓글쓰기

    먼저, 좋은 정보 감사드립니다.

    제가 최근 DBUA를 이용한 9i --> 10gR2(10.2.0.4), 11gR1(11.1.0.7) 로 Upgrade를 한 이후에 기존 SQL Plan에 비해
    현저하게 안좋은 Plan을 보이고 있어, 여기 저기 Web Site를 찾다가 우연히 이 Site를 알게 되었습니다.

    올려 주신 정보이외에도 최근 이곳에서 많은 도움을 받고 있습니다.
    이렇게 글을 올리게된 이유는 다름이 아니오라 한가지 궁금한 점이 있어서 입니다.

    Upgrade 한 이후에 업무 특성상 주요 Table들에 대해서, 매일 Analyze를 하고 있습니다.
    그런데, 9i에서 보여 주었던 SQL Plan에 비해 안좋은 결과를 보이고 있어서 원인 분석 중
    Upgrade된 DB에서 해당 Table에 대한 통계정보를 삭제 후, 다시 Plan을 보니 9i와 같은 Plan을 보여주고 있습니다.

    마치, 10gR2 와 11gR1의 Optimizer가 멍청해진것 같은 현상입니다.
    이걸 어찌 받아 들여야 할까요?
    (예로, 심지어는 Index도 안타고 Table Full Scan 하고 있습니다...
    Table에 대한 통계정보를 삭제 후엔 Index Scan 합니다.)

    지금은 SQL문 곳곳에 Hint문을 사용하여 해결하고 있으나, 본질적인 해결책이 아닌 듯 하여
    답답한 마음에 글 올립니다.
    /*+OPT_PARAM('_OPTIMIZER_PUSH_PRED_COST_BASED', 'FALSE') */
    /*+ opt_param('_optimizer_cost_based_transformation', 'off') */
    와 같은 Hints를 사용하고 있습니다.

    한 말씀 남겨주시면 감사하겠습니다.

    감사합니다.
    (딱히, 질문을 올릴만한 곳이 없어 이곳에 올립니다.)

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

      써니님 안녕하세요.
      답변이 늦어 죄송합니다.
      말씀하신 옵티마이져의 문제는 예전부터 많이 있었습니다.
      old 버젼에서 new 버젼으로 upgrade 함에도 불구하고 악성 Plan으로 되는 경우가 있습니다.
      하지만 그것은 SQL의 5% 내외일 것입니다. 다시말하면 성능이 좋아진 것이 많은 부분을 차지하고 있지만 그것은 눈에 띄질 않습니다. 예를들어 0.2초 걸리던 것이 0.1초걸린다면 이런것은 문제가 되지 않지요. 하지만 약 100개중의 5개의 경우는 악성 plan을 만드는 경우가 많습니다.
      이런 경우는 어쩔 수 없습니다. 사람이 개입하여 올바른 길을 알려주는 수 밖에요.

      참고로 위에서 이야기한 5% 라는것은 정확한것이 아닙니다. 어림짐작으로 이야기한것이고 실제로는 시스템과 버젼에 따라 약간은 달라질 수 있습니다.

      먼저 두가지를 점검해 보시기바랍니다.
      1.통계정보를 충실히 수집했는지?
      예륻들어
      건수가 아주 많은 테이블은 0.01%
      건수가 조금 많은 테이블은 0.1%
      건수가 보통인 테이블은 5%
      건수가 적은 테이블은 10%
      건수가 아주 적은 테이블은 100%
      건수에 상관없이 기초성 테이블(고객, 상품, 부서, 직원, 계좌, 공통코드)등은 100%

      이렇게 하시면 됩니다. 이것은 예시 이므로 실제하실때는 구체적으로 하셔야 겠죠. 제가 수행한 사이트에는 통계정보를 수집할때 Oracle10g R2의 경우 AUTO 옵션을 쓰지 않습니다.

      local 파티션통계는 수집하지 않는것이 좋습니다. 즉 Global 통계만 관리하시면 됩니다. 단 전제조건이 있습니다. 각 파티션마다 실행계획이 달라져야 하는 경우는 local 파티션 통계를 수집하시는 것이 옳습니다. 반대로 모든 파티션의 실행계획을 고정시키고자 할때는 global 파티션의 통계정보만 관리해도 충분합니다.

      2.적절한 인덱스가 존재하는지?
      이것 또한 어려운 문제입니다.
      어려움을 토로하시는 걸로 봐서 Query Transformation 문제 같습니다. 각각의 SQL과 PLAN을 보고 적절한 인덱스가 있는지 판단 하셔야 합니다.
      예를 들어 인라인뷰가 있고 그 내부의 where절에 상수조건이 있다고 할때 거기에 JPPD가 발생했다고 치면 조인조건이 인라인뷰 안으로 파고 듭니다. 그런데 상수조건으로만 인덱스를 만들어주면 JPPD의 효과는 줄어들겁니다. 인덱스가 상수조건 + 조인조건으로 결합인덱스를 만들어주어야 JPPD의 효과가 최적으로 나타납니다. 아래의 SQL을 보세요.

      SELECT d.department_id, d.department_name, e.employee_id, e.job_id, e.email_phone_num
      FROM department d,
      (SELECT employee_id, department_id, job_id, phone_number AS email_phone_num
      FROM employee
      WHERE job_id = :v_job2 )e
      WHERE d.department_id = e.department_id(+)
      AND d.location_id = 1700;

      위의 SQL에서 EMPL0YEE 테이블에 존재해야 할 최적의 인덱스는 JOB_ID 가 아니라 JOB_ID + department_id 인덱스 입니다. 변경되지 않은 SQL만 보았을 때는 JOB_ID 인덱스만 있으면 될것 같지만 변경된 SQL을 보면 결합인덱스가 왜 필요한지 아실겁니다. 아래의 변경된 SQL을 보시죠.

      SELECT d.department_id, d.department_name, e.employee_id, e.job_id, e.email_phone_num
      FROM department d,
      LATERAL (SELECT employee_id, department_id, job_id, phone_number AS email_phone_num
      FROM employee e2
      WHERE e2.job_id = :v_job2
      AND e2.department_id = d.department_id ) e
      WHERE d.location_id = 1700 ;

      위의 변경된 SQL을 보신다면 결합인덱스가 최적임을 아실것 입니다. 물론 결합인덱스의 효율이 더 좋은경우를 이야기 하는 겁니다. 쿼리변환의 문제는 통계정보의 적절성과 인덱스의 최적화 문제가 거의 대부분 입니다.

      하지만 이 두가지가 완벽히 되어 있다고 할지라도 옵티마이져가 완벽하지 않으므로 5% 미만의 경우는 악성 PLAN을 생성하기 때문에 사람이 힌트나 쿼리튜닝을 통하여 손을 봐주어야 합니다. 옵티마이져가 아무리 업그레이드 되어도 사람의 손길이 필요하다는 것입니다. 아마도 앞으로 20년간은 그럴것 같습니다.
      감사합니다.

  4. 써니 2010.08.18 13:28 신고  댓글주소  수정/삭제  댓글쓰기

    좋은 말씀 진심으로 감사드립니다.
    앞으로도 많은 공부가 필요할 듯 합니다.

    다시 한 번 감사의 말씀드립니다.

  5. 써니 2010.08.20 00:41 신고  댓글주소  수정/삭제  댓글쓰기

    브라이언 홍님 관심주셔서 고맙습니다.

    그리고 extremedb님 오늘도 좋은 말씀 감사드립니다. ^^

  6. 써니 2010.08.20 11:29 신고  댓글주소  수정/삭제  댓글쓰기

    여기저기 문서를 찾아보니,
    Analyze 와 dbms_stats Procedure의 차이점이 심할 수도 있겠습니다.

    위에서 언급한 Index를 사용하지 못않는 Table을 대상으로 Test한 결과
    Analyze 와 비교해서 dbms_stats Procedure를 이용해서 통계를 구한 결과가
    제가 원하는 Plan을 보여주고 있습니다.

    관련 자료를 참고로 올리고 싶은데.. 올릴 수 있는 방법이 없네요..^^
    혹 다른 분들을 위해서 다음 정보를 남김니다.

    What’s Up With dbms_stats?
    by Terry Sutton
    Database Specialists, Inc.

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

      헉! analyze로 실행하고 계셨나요?
      말씀하신대로 그차이는 엄청 큽니다. 앞으로도 차이가 더 벌어질 것입니다.
      11g에서 dbms_stats는 정확성과 성능면에서 또 한번 진화되었습니다. 아래의 글을 참고하세요.
      http://scidb.tistory.com/entry/11g-DBMSSTATS-개선사항

  7. 브라이언홍 2010.08.23 09:22 신고  댓글주소  수정/삭제  댓글쓰기

    저도 현재 이런 경우를 많이 접하고 있습니다.
    써니님꼐서 사용하시는 힌트는 저의 경우 Long Parse일 경우에 사용합니다.
    "왜 Long Parse가 발생하느냐?"가 관건일 것 같습니다.

    문득 어제 밤에 이런 생각을 해 보았습니다.
    제 친구 중 하나는 물건을 구입할 때 딱 한가지 기준이 있답니다. 그래서 쇼핑할때 시간이 많이 걸리지 않는다고 하더군요.
    그런데 저는 이것저것 비교하기를 좋아합니다. 심지어 이마트에서 본 물건이 롯데마트에서 더 좋은 디자인과 더 좋은 가격 더 좋은 품질을 있었는지 기억을 더듬습니다. 참~~~ 쇼핑하기 힘들지요.. ㅡ,ㅡ; 신중하다라고 말하기엔 너무 오타쿠 같아서ㅋㅋ

    옵티마이저가 비용기반으로 작동하기에 너무 많은 것을 고민하고 있는것은 아닐까요?
    그래서 과감히 그 기능을 꺼버리면 파싱하는 시간이 줄어드는게 당연하겠지요~~ 그러나 실행계획이 최적이 안되면 또 낭패입니다.

    제가 예전에 이런 문제로 엑셈에 올린글이 있어 공유해봅니다. 혹시 보셨는지 모르겠지만 ..
    http://121.254.172.39:8080/pls/apex/f?p=101:11:0::::P11_QUESTION_ID:2470200346608331

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

      하드파싱시간은 아래의 두가지를 합친것 입니다.
      Logical Optimizing + Pysical Optimizing
      그래서 위의 관련 파라미터를 꺼 놓으면 시간이 줄어들 수는 있으나 또다른 문제가 발생합니다. 실행계획이 악성이 될 수 있습니다. 즉 파라미터를 끄는 방식으로는 두마리 토끼를 다 잡기가 어렵다는 것입니다.

      두마리 토끼를 다 잡는 방법이 있습니다. 하지만 이방법은 100개중에 문제가 되는 SQL에만(5% 미만) 적용하시는것이 좋을것 입니다.

      1.Hard Parsing시간을 고려하지않고 최적의 실행계획을 찾는다.
      2.최적의 실행계획을 유도하는 오라클 내부힌트(Internal Hint)를 찾는다. DBMS_XPLAN.DISPLAY_CURSOR 의 Outline Data를 참조하시면 됩니다.
      3. 그 힌트들을 해당 SQL에 적용한다.

      모든 힌트를 적용할 필요는 없습니다. 두가지 카테고리의 힌트만 적용하시면 됩니다.
      1.LOGICAL 힌트 (unnest, merge, push_pred, USE_CONCT...)
      2.PHYSICAL 힌트 ( 조인순서 (leading), 조인방법(use_nl/hash/merge), 엑세스방법(index, full) )

      환경적 힌트, 예컨데 OPTIMIZER_FEATURES_ENABLE이나 DB_VERSION ,all_ROWS 등의 힌트는 빼셔도 됩니다. 환경적 힌트 또한 Logical 과 Physical Optimization을 결정하기 위한것 입니다. 그러한 것들을 미리 결정해 놓았으므로 환경적 힌트는 필요가 없습니다.

      이렇게 한다면 Hard Parsing시간이 최소화 되면서도 최적의 실행계획을 유지할 수 있습니다. 왜냐하면 옵티마이져가 고민하여 결정해야할 것을 고민할 필요없이 만들어버렸기 때문입니다. 즉 여러마트들을 돌아다니면서 시간을 죽이며 어렵게 쇼핑할 필요가 없습니다. 또한 개발자가 힌트를 적용하지 않는다고 하여도 오라클이 그러한 힌트를 내부적으로 적용할 것입니다.

      제가 집필한 책(The Logical Optimizer)에도 239 페이지에 이부분을 언급하였습니다.
      감사합니다.

      주의사항은 이렇게 적용한 SQL은 별도의 목록을 만들어 관리하는 것이 좋습니다. SQL이 변경될때 다시 1~3번을 적용해야 되기 때문입니다.

이전 글(NULL AWARE ANTI JOIN SQL을 어떻게 변경시키나?) 에서 NULL AWARE ANTI JOIN 중에서 조인방법이 NESTED LOOPS 조인을 선택한다면 NULL을 체크하는 서브쿼리가 추가된다고 설명하였다. 이번에는 NESTED LOOPS ANTI NULL AWARE가 아닌 HASH JOIN ANTI NULL AWARE에 대하여 알아보자. 들어가기 전에 이번 글을 이해하려면 이전 글의 이해가 필수적이니 먼저 빠르게 읽고 오기 바란다.

 

오해를 하다

(The Logical Optimizer) 158 페이지의 내용에 따르면 WHERE 조건이 추가되면 NULL을 체크하는 Filter가 적용되지 않는다고 하였다. 하지만 이것은 필자의 오해였다. 얼굴이 화끈거리는 오류이다. 아래의 예제를 보자.

 

SELECT /*+ QB_NAME(MAIN) */

       d.department_id, d.department_name, d.location_id

  FROM department d

 WHERE d.department_id NOT IN (SELECT /*+ QB_NAME(SUB) */

                                      e.department_id

                                 FROM employee e

                                WHERE e.job_id = 'PU_CLERK')

   AND d.location_id = 1700;

 

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

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

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

|   0 | SELECT STATEMENT             |                  |    16 |   512 |     5 | 00:00:01 |

|*  1 |  HASH JOIN ANTI NA           |                  |    16 |   512 |     5 | 00:00:01 |

|   2 |   TABLE ACCESS BY INDEX ROWID| DEPARTMENT       |    21 |   420 |     2 | 00:00:01 |

|*  3 |    INDEX RANGE SCAN          | DEPT_LOCATION_IX |    21 |       |     1 | 00:00:01 |

|   4 |   TABLE ACCESS BY INDEX ROWID| EMPLOYEE         |     5 |    60 |     2 | 00:00:01 |

|*  5 |    INDEX RANGE SCAN          | EMP_JOB_IX       |     5 |       |     1 | 00:00:01 |

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

Predicate Information (identified by operation id):

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

   1 - access("D"."DEPARTMENT_ID"="E"."DEPARTMENT_ID")

   3 - access("D"."LOCATION_ID"=1700)

   5 - access("E"."JOB_ID"='PU_CLERK')

 

위의 예제에서 필자는 “서브쿼리의 조건절에 e.JOB_ID = 'PU_CLERK' 조건을 추가하자 IS NULL FILTER가 사라졌다.” 라고 했는데 이 부분이 잘못되었다. WHERE 조건의 추가유무와는 상관없이 조인종류(JOIN METHOD)에 따라서 NULL을 체크하는 FILTER의 유무가 결정된다. 아래의 SQL로써 이 사실을 증명해보자. 아래의 SQL은 조건절을 추가하지 않고도 조인방법만 HASH로 변경하였다. USE_HASH 힌트를 빼면 NESTED LOOPS ANTI SNA 로 풀리고 NULL을 체크하는 서브쿼리가 추가된다. 


SELECT /*+ gather_plan_statistics use_hash(e@sub) */

       d.department_id, d.department_name, location_id

  FROM department d

 WHERE d.department_id NOT IN (SELECT /*+ qb_name(sub) */ 

e.department_id

                                 FROM employee e)

   AND d.location_id = 1700;

 

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

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

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

|   0 | SELECT STATEMENT             |                  |      0 |00:00:00.01 |       9 |

|*  1 |  HASH JOIN ANTI NA           |                  |      0 |00:00:00.01 |       9 |

|   2 |   TABLE ACCESS BY INDEX ROWID| DEPARTMENT       |     21 |00:00:00.01 |       2 |

|*  3 |    INDEX RANGE SCAN          | DEPT_LOCATION_IX |     21 |00:00:00.01 |       1 |

|   4 |   TABLE ACCESS FULL          | EMPLOYEE         |     97 |00:00:00.01 |       7 |

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

 

Predicate Information (identified by operation id):

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

   1 - access("D"."DEPARTMENT_ID"="E"."DEPARTMENT_ID")

   3 - access("D"."LOCATION_ID"=1700)

 

HASH JOIN ANTI NA NULL을 체크하는 NOT EXISTS 서브쿼리를 만들지 않음을 알 수 있다. Predicate Information의 어디에도 NULL을 체크하는 FILTER는 없다. 다시 말하면 HASH JOIN ANTI NA IS NULL Filter 서브쿼리를 만들지 않고 Hash 조인을 할 때 NULL 데이터를 체크하므로 NULL 체크용 서브쿼리가 필요 없는 것이다.  

 

결론

Null을 체크하는 서브쿼리는 NESTED LOOP ANTI NA인 경우만 추가되고 HASH JOIN ANTI NA에서는 생성되지 않는다. 필자는 책을 집필할 자료를 준비할 때 데카르트의 방법을 의도적으로 사용하였지만 이렇게 간단한 원리도 놓치고 말았다. 데카르트의 방법론이 어렵고 특별할 것 같지만 사실은 아주 간단하다. 어떤 것을 연구하거나 진리를 탐구할 때 내가 아는 것이 없다고 가정하는 것이다. 즉 내가 아는 것까지 모른다고 가정하고 모든 것을 검증하라는 것이다. 궁금한 사람은 데카르트의 방법서설을 자세히 읽어보라.

 

몇 년간 데카르트의 방법을 100% 사용하기는 어려웠다. 그 약속을 지킨다는 것은 엄청난 스트레스를 수반한다. 그럼에도 안다고 생각하는 것을 모두 검증하려고 덤볐지만 결국 오류는 막을 수 없었다. 이유는 지식의 저주 때문이다. 어떠한 결과나 현상을 보았을 때 그것의 생김새나 특징이 매우 친숙하다면 내가 알고 있다고 착각 하는 것. 이것은 매우 위험한 일이었다. 이 문제는 필자를 비롯한 모든 과학자 및 연구원들의 고민일 것이다. 이 문제를 해결할 방법은 없는 걸까?


신고
Posted by extremedb

댓글을 달아 주세요

Oracle 10g 까지는 NOT IN 서브쿼리를 사용할 때 NULL을 허용하는 컬럼으로 메인쿼리와 조인하면 Anti Join을 사용할 수 없었고 Filter 서브쿼리로 실행되었기 때문에 성능이 저하되었다. 마찬가지로 메인쿼리쪽의 조인컬럼이 NULL 허용이라도 Filter로 처리된다. 하지만 11g부터는 Anti Join Null Aware를 사용하여 Null인 데이터가 한 건이라도 발견되면 Scan을 중단하므로 성능이 향상된다. (The Logical Optimizer)에서도 이런 사실을 언급하고 있다. 하지만 Anti Join Null Aware로 인해 변환된 SQL의 모습은 책에서 언급되지 않았으므로 이 글을 통하여 알아보자.

 

먼저 가장 기본적인 예제를 실행해보자.

실행환경: Oracle 11.2.0.1

 

--Anti Join Null Aware를 활성화 시킨다. Default True 이므로 실행하지 않아도 됨.

ALTER SESSION SET "_optimizer_null_aware_antijoin" = TRUE;

 

SELECT d.department_id, d.department_name, location_id

  FROM department d

 WHERE d.department_id NOT IN (SELECT e.department_id

                                 FROM employee e)

   AND d.location_id = 1700;

 

NOT IN 서브쿼리는 두 가지 뜻이 있다

위의 SQL을 해석할 때 단순히 location_id = 1700인 부서 중에서 사원이 한 명도 없는 건을 출력한다고 생각하면 한가지를 놓친 것이다. 만약 이런 요건이라면 NOT IN 대신에 NOT EXISTS 서브쿼리를 사용해야 한다. 다시 말해 NOT IN 서브쿼리를 사용하면 employee 테이블의 department_id 값 중에 한 건이라도 Null이 있으면 결과집합이 출력되지 않는다. 실제로도 결과건수가 없다. 이제 위의 SQL에 해당하는 Plan을 보자.

 

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

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

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

|   0 | SELECT STATEMENT              |                   |      0 |00:00:00.01 |       7 |

|*  1 |  FILTER                       |                   |      0 |00:00:00.01 |       7 |

|   2 |   NESTED LOOPS ANTI SNA       |                   |      0 |00:00:00.01 |       0 |

|   3 |    TABLE ACCESS BY INDEX ROWID| DEPARTMENT        |      0 |00:00:00.01 |       0 |

|*  4 |     INDEX RANGE SCAN          | DEPT_LOCATION_IX  |      0 |00:00:00.01 |       0 |

|*  5 |    INDEX RANGE SCAN           | EMP_DEPARTMENT_IX |      0 |00:00:00.01 |       0 |

|*  6 |   TABLE ACCESS FULL           | EMPLOYEE          |      1 |00:00:00.01 |       7 |

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

Predicate Information (identified by operation id):

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

   1 - filter( IS NULL)

   4 - access("D"."LOCATION_ID"=1700)

   5 - access("D"."DEPARTMENT_ID"="E"."DEPARTMENT_ID")

   6 - filter("E"."DEPARTMENT_ID" IS NULL)

 

NULL을 발견하면 멈춘다

NESTED LOOPS ANTI NA라는 기능은 Null 데이터를 찾자마자 Scan을 멈추는 것이다. ID 기준으로 6번의 Predicate Information을 보면 NULL인 데이터를 단 한 건(A-Rows 참조)만 찾아내고 Scan을 멈추었다. 이제 NESTED LOOPS ANTI SNA가 어떻게 수행되는지 10053 Trace를 통하여 살펴보자.

 

FPD: Considering simple filter push in query block SEL$526A7031 (#1)

"D"."DEPARTMENT_ID"="E"."DEPARTMENT_ID" AND "D"."LOCATION_ID"=1700 AND  NOT EXISTS (SELECT /*+ QB_NAME ("SUB") */ 0 FROM "EMPLOYEE" "E")

FPD: Considering simple filter push in query block SUB (#2)

"E"."DEPARTMENT_ID" IS NULL

try to generate transitive predicate from check constraints for query block SUB (#2)

finally: "E"."DEPARTMENT_ID" IS NULL

 

FPD(Filter Push Down) 기능으로 인하여 쿼리블럭명이 SUB Not Exists 서브쿼리가 추가 되었고 그 서브쿼리에 DEPARTMENT_ID IS NULL 조건이 추가되었다.

 

SQL 어떻게 바뀌었나?

위의 10053 Trace 결과에 따르면 Logical Optimizer SQL을 아래처럼 바꾼 것이다.

 

SELECT d.department_id, d.department_name, d.location_id

  FROM department d

 WHERE NOT EXISTS (SELECT 0           

                     FROM employee e

                    WHERE e.department_id IS NULL) –-NULL 을 체크하는 서브쿼리

   AND NOT EXISTS (SELECT 0           

                     FROM employee e

                    WHERE e.department_id  = d.department_id)                     

   AND d.location_id = 1700 ;

 

SQL을 보면 NOT IN 서브쿼리가 NOT EXIST 서브쿼리로 바뀌었고 NULL을 체크하는 서브쿼리가 추가되었다. 또한 NULL을 체크하는 서브쿼리의 결과가 한 건이라도 존재하면 SQL은 더 이상 실행되지 않는다는 것을 알 수 있다. NESTED LOOPS ANTI SNA의 비밀이 풀리는 순간이다. ORACLE 9i 10g 에서도 위와 같이 SQL을 작성하면 NESTED LOOPS ANTI SNA의 효과를 볼 수 있다. 하지만 위의 SQL처럼 수동으로 작성하는경우 NULL 한건을 체크 하는데 오래 걸리며 부하가 있다면 이렇게 사용하면 안 된다. 이제 Plan을 보자.

 

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

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

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

|   0 | SELECT STATEMENT              |                   |      0 |00:00:00.01 |       7 |

|*  1 |  FILTER                       |                   |      0 |00:00:00.01 |       7 |

|   2 |   NESTED LOOPS ANTI           |                   |      0 |00:00:00.01 |       0 |

|   3 |    TABLE ACCESS BY INDEX ROWID| DEPARTMENT        |      0 |00:00:00.01 |       0 |

|*  4 |     INDEX RANGE SCAN          | DEPT_LOCATION_IX  |      0 |00:00:00.01 |       0 |

|*  5 |    INDEX RANGE SCAN           | EMP_DEPARTMENT_IX |      0 |00:00:00.01 |       0 |

|*  6 |   TABLE ACCESS FULL           | EMPLOYEE          |      1 |00:00:00.01 |       7 |

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

Predicate Information (identified by operation id):

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

   1 - filter( IS NULL)

   4 - access("D"."LOCATION_ID"=1700)

   5 - access("E"."DEPARTMENT_ID"="D"."DEPARTMENT_ID")

   6 - filter("E"."DEPARTMENT_ID" IS NULL)

 

Operation의 순서에 유의하라

위의 Plan을 과 원본 Plan을 비교해보면 원본이 ANTI SNA라는 것만 제외하면 실행계획과 일량까지 같음을 알 수 있다. 헷갈리지 말아야 할 것은 ID 기준으로 6(NULL 체크 서브쿼리)이 가장 먼저 실행된다는 것이다. 왜냐하면 서브쿼리 내부에 메인쿼리와 조인조건이 없기 때문에 서브쿼리가 먼저 실행될 수 있기 때문이다. 반대로 Filter 서브쿼리내부에 메인쿼리와 조인 조건이 있다면 메인쿼리의 컬럼이 먼저 상수화 되기 때문에 항상 서브쿼리쪽 집합이 후행이 된다. 이런 사실을 모르고 보면 PLAN상으로만 보면 NULL 체크 서브쿼리가 가장 마지막에 실행되는 것으로 착각 할 수 있다.

 

결론

Anti Join Null Aware를 사용하여 Null인 데이터가 한 건이라도 발견되면 Scan을 중단하므로 성능이 향상된다. NULL을 체크하는 Filter 서브쿼리가 추가되기 때문이다. 하지만 그런 서브쿼리가 항상 추가되는 것은 아니다. 추가되는 기준이 따로 있는데 다음 글에서 이 부분을 다루려고 한다.

 

PS

책에 위의 SQL이 빠져있다. SQL PLAN을 출력하여 끼워 넣기 바란다.

신고
Posted by extremedb

댓글을 달아 주세요

  1. 혈기린 2010.08.02 10:19 신고  댓글주소  수정/삭제  댓글쓰기

    좋은 내용감사 드립니다
    보통 흔히 아는 실행계획대로 읽는다면 6 - filter("E"."DEPARTMENT_ID" IS NULL) 이부분이 제일 마지막에 필터로 풀린느데 여기서는 이부분이 젤일 먼저 실행되는군요
    이런건 어떻게 판단하는건가요? 트레이스 내용을 보고 판단하는지요? 아니면 SQL을 보고 판단하는건가요?

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

      안녕하세요. 기린님
      이런 경우는 예외에 속하기 때문에 Plan상의 Operation 부분을 보고 판단할 수 없습니다.
      하지만 SQL을 보면 Uncorreated Subquery(비상관서브쿼리)인지 아닌지 판단할 수 있으므로 어려움은 없을것 입니다..