이전에 Parallel Query 의 조인시 또다른 튜닝방법(Parallel Join Filter)과 Partition Access Pattern 이라는 글에서 Bloom Filter의 개념을 설명한적 있다. 이전 글들 때문인지 모르겠으나 많은 사람들이 Parallel Query를 사용하거나 Partition을 엑세스 할때 Bloom Filter로 후행 테이블의 건수를 줄여 조인 건수를 최소화하는 것으로만 생각한다. 맞는 말이지만 그것이 전부가 아니다.
그래서 이번에는 Parallel과 Partition에 상관없이 Bloom Filter가 발생하는 경우를 살펴보고자 한다. 이 글을 통하여 풀고자 하는 오해는 Bloom Filter가 Join 최적화를 위한 후행 테이블의 Filter 알고리즘일 뿐만 아니라 Group By를 최적화하는 도구이기도 하다는 것이다.
실행환경: Oracle11gR2, Windows 32bit
Bloom Filter를 사용하지 않는 경우
먼저 Bloom Filter가 발생하지 않게 힌트를 주고 실행한다. 뒤에서 Bloom Filter를 적용한 경우와 성능을 비교하기 위함이다.
SELECT /*+ LEADING(c) NO_MERGE(S) NO_PX_JOIN_FILTER(S) */
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 | A-Rows | A-Time | Buffers | Used-Mem |
------------------------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 23 |00:00:06.58 | 5075 | |
|* 1 | HASH JOIN | | 23 |00:00:06.58 | 5075 | 1194K (0)|
| 2 | TABLE ACCESS BY INDEX ROWID | CUSTOMERS | 151 |00:00:00.01 | 148 | |
| 3 | BITMAP CONVERSION TO ROWIDS| | 151 |00:00:00.01 | 2 | |
|* 4 | BITMAP INDEX SINGLE VALUE | CUSTOMERS_YOB_BIX | 1 |00:00:00.01 | 2 | |
| 5 | VIEW | | 7059 |00:00:06.56 | 4927 | |
| 6 | SORT GROUP BY | | 7059 |00:00:06.54 | 4927 | 9496K (0)|
| 7 | PARTITION RANGE ALL | | 918K|00:00:02.80 | 4927 | |
| 8 | TABLE ACCESS FULL | SALES | 918K|00:00:00.95 | 4927 | |
------------------------------------------------------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
1 - access("S"."CUST_ID"="C"."CUST_ID")
4 - access("C"."CUST_YEAR_OF_BIRTH"=1987)
Id 기준으로 8번에서 Buffers 항목을 보면 전체건(4927 블록)을 Scan 하였다. 그리고 A-Rows 항목을 보면 Sales 테이블에 대해 약 92만건(918K)을 읽었다. 이제 Id 6번을 보자. 전체 건수인 92만건에 대하여 Sort Group By를 적용하는데 부하가 집중되는 것을 알 수 있다. 시간상으로도 Group By를 하는데 3.7초 정도 걸렸으며 PGA를 9496K나 사용하였다. 즉 대부분의 시간을 Sort Group By Operation 에서 소비한 것이다.
이제 위의 SQL에 Bloom Filter를 적용해 보자. Sales 테이블에 파티션이 적용되어 있으나 파티션과 상관없이 Bloom Filter가 적용된다.
SELECT /*+ LEADING(c) NO_MERGE(S) PX_JOIN_FILTER(S) */
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 | A-Rows | A-Time | Buffers | Used-Mem |
-------------------------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 23 |00:00:00.15 | 5075 | |
|* 1 | HASH JOIN | | 23 |00:00:00.15 | 5075 | 1197K (0)|
| 2 | JOIN FILTER CREATE | :BF0000 | 151 |00:00:00.01 | 148 | |
| 3 | TABLE ACCESS BY INDEX ROWID | CUSTOMERS | 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 | | 55 |00:00:00.14 | 4927 | |
| 7 | SORT GROUP BY | | 55 |00:00:00.14 | 4927 |88064 (0)|
| 8 | JOIN FILTER USE | :BF0000 | 7979 |00:00:00.12 | 4927 | |
| 9 | PARTITION RANGE ALL | | 7979 |00:00:00.10 | 4927 | |
|* 10 | TABLE ACCESS FULL | SALES | 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를 사용해보니
위의 실행계획에서 Id 기준으로 8번을 보면 Name 항목에 Bloom Filter가 사용되었다. Bloom Filter의 위력이 얼마나 대단한지 살펴보자. 먼저 Sales 테이블을 Full Table Scan 하였으므로 Buffers는 4927로 Bloom Filter를 사용하지 않는 경우와 똑같다. 하지만 Bloom Filter가 적용되어 92만건이 아닌 7979건(A-Rows 참조)만 살아남았다. 이처럼 Bloom Filter는 Hash Join시 Probe(후행) 집합에서 조인에 참여하는 건수를 줄임으로써 Join 시간을 단축시킨다. Bloom Filter의 효과는 이것이 끝이 아니다. 건수가 줄어듦으로 해서 Sort Group By 작업 또한 92만 건이 아니라 7979건만 하면 된다. Group By에 의한 PGA 사용량을 Bloom Filter가 적용된 실행계획과 비교해보면 100배 이상 차이가 나는 이유도 Bloom Filter의 효과 때문이다.
제약사항
이번에 test한 케이스는 Parallel Query도 아니며 Partition Pruning과도 관련이 없다. 하지만 항상 발생하지는 않는다. 이유는 세 가지 제약사항이 있기 때문이다.
첫 번째, Hash Join을 사용해야 한다. Sort Merge Join이나 Nested Loop Join에서는 발생하지 않는다.
두 번째, Build Input(Driving) 집합에 Filter 조건이 존재해야 한다. 위의 SQL에서는 cust_year_of_birth = 1987가 Filter 조건으로 사용되었다. Filter가 필요한 이유는 선행집합의 Filter조건을 후행집합에서 Bloom Filter로 사용해야 하기 때문이다.
세 번째, Probe(후행) 집합에서 Group By를 사용해야 한다. 위의 SQL에서도 cust_id로 Group By를 하고 있다. 물론 후행집합에 Group By가 적용되려면 뷰나 인라인뷰가 필요하다.
만약 Bloom Filter가 사라져 전체 건이 조인에 참여한다면?
상상하기 싫은 경우지만 Probe(후행) 집합에 Bloom Filter가 사라지는 경우를 살펴보자. 이 경우는 Sales 테이블 전체건수(약 92만건)가 모두 Hash Join에 참여하게 되므로 성능이 저하될 것이다. 아래의 SQL이 그것인데 위의 SQL에서 NO_MERGE(S) 힌트와 PX_JOIN_FILTER(S)만 뺀 것이다.
SELECT /*+ 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 | A-Rows | A-Time | Buffers | Used-Mem |
-------------------------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 23 |00:00:05.39 | 5075 | |
| 1 | SORT GROUP BY | | 23 |00:00:05.39 | 5075 |75776 (0)|
|* 2 | HASH JOIN | | 3230 |00:00:05.37 | 5075 | 1185K (0)|
| 3 | TABLE ACCESS BY INDEX ROWID | CUSTOMERS | 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 | | 918K|00:00:02.70 | 4927 | |
| 7 | TABLE ACCESS FULL | SALES | 918K|00:00:00.94 | 4927 | |
-------------------------------------------------------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
2 - access("S"."CUST_ID"="C"."CUST_ID")
5 - access("C"."CUST_YEAR_OF_BIRTH"=1987)
악성 쿼리변환
힌트를 제거하자 View Merging(뷰 해체)이 발생하여 인라인뷰가 제거되었다. (View Merging이 발생하지 않는 독자는 MERGE(S) 힌트를 추가하기 바란다) 뷰가 없어짐에 따라 후행집합에서 Group By가 없어지고 조인이 끝난 후에 Group By가 발생한다. 후행집합의 Group By가 사라졌으므로 Bloom Filter가 적용되지 않는다. 따라서 Sales 테이블의 전체건 (약 92만건)이 조인에 참여하게 된다. Bloom Filter가 적용된 경우는 단 55건만 조인에 참여하므로 이 차이는 어마 어마한 것이다. 그 결과 전체 수행시간중에서 Hash Join에서만 절반의 시간을 소모하였다. 즉 잘못된 쿼리변환이 발생하여 Bloom Filter를 죽여버린 것이다. View Merging이 발생할 때 Bloom Filter를 적용할 수 없게되어 비효율이 발생되는지 주의깊게 관찰해야 한다.
결론
이번 Test 케이스에서 Bloom Filter의 특징을 두 가지로 압축할 수 있다. Group By 작업량을 최소화 시켜주고 Hash Join 건수를 줄여준다. 이 두 가지 효과가 맞물려 Bloom Filter를 적용한 SQL이 0.15초 만에 끝날 수 있는 것이다. 후행 테이블에서 Bloom Filter로 걸러지는 건수가 많을 때 두 가지 작업(Group By, Hash Join) 모두 최대의 효율을 발휘한다. 바꿔 말하면 Bloom Filter로 제거되는 건수가 미미 하다면 사용해선 안된다.
CVM(Complex View Merging)이 발생하면 여지없이 Bloom Filter가 사라진다. CVM 때문에 성능이 저하된다면 NO_MERGE 힌트를 사용하여 뷰를 유지시켜야 한다. Bloom Filter가 사라지는 경우는 이 경우 뿐만 아니다. 11gR2에서 새로 적용된 Cardinality Feedback 때문에 Bloom Filter가 사라지는 경우가 보고되고 있다. 마지막(세번째) SQL을 최초로 실행시켰을 때와 두번째로 실행시켰을 때 DBMS_XPLAN.DISPLAY_CURSOR의 실행계획이 달라진다면 Cardinality Feedback이 Bloom Filter를 제거시킨것이다. Shared Pool을 Flush하고 두번 연달아 테스트 해보기 바란다. 이런 현상들 때문에 옵티마이져에 새로운 기능이 추가될 때마다 긴장을 늦출 수 없다. 버전이 올라갈수록 튜닝하기가 쉬워지는것인가? 아니면 그 반대인가?
'Oracle > Data Access Pattern' 카테고리의 다른 글
분석함수의 실행계획 - 1부 (12) | 2011.02.07 |
---|---|
Index_desc 힌트와 rownum = 1 조합은 안전한가? (12) | 2010.11.09 |
Inlist Iterator를 사용하지 말아야 할 때 (13) | 2010.06.23 |
오렌지나 TOAD에서 Predicate Information을 참조하는 방법 (7) | 2010.01.12 |
Partition Access Pattern (13) | 2009.12.24 |