이번에 소개할 내용은 오라클에서 제공하는 operation 중에 하나인 ‘INDEX FULL SCAN (MIN/MAX)’에 대해 살펴보고자 합니다.
[ 출처 – 오라클클럽 위키]
위의 그림은 인덱스 B-Tree를 그림으로 표현한 내용인데, INDEX FULL SCAN은 LEAF 블록을 처음부터 끝까지 모두 읽는 ‘수평적 탐색’을 의미합니다. 이 중 ’INDEX FULL SCAN (MIN/MAX)’ operation은 MIN() 또는 MAX() 함수로 데이터 추출 시 INDEX LEAF BLOCK의 양 끝(MIN/MAX)의 데이터를 읽어 빠르게 데이터를 추출하는 방법입니다. 이렇게 데이터를 추출할 경우 전체 데이터를 모두 읽지 않고 MIN/MAX 데이터를 빠르게 가져올 수 있어 성능을 극대화 시킬 수 있습니다.
‘INDEX FULL SCAN (MIN/MAX)’ operation이 어떻게 데이터를 액세스하는지 예제를 통해 살펴보도록 하겠습니다.
1. 테스트 데이터 생성
(MIN/MAX) operation을 살펴보기 위해 테스트 데이터를 만들도록 하겠습니다. 테스트 데이터는 ‘PARENT_T : CHILD_T = 1 : M’ 관계의 테이블을 만들고 CHILD_T 테이블에는 약 100만건의 데이터를 만들도록 하겠습니다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
-- 0. 오라클 버전 확인 SELECT * FROM V$VERSION ; BANNER -------------------------------------------------------------------------------- Oracle Database 11g Enterprise Edition Release 11.2.0.3.0 - 64bit Production PL/SQL Release 11.2.0.3.0 - Production CORE 11.2.0.3.0 Production ; -- 1. 테이블 생성 DROP TABLE PARENT_T; CREATE TABLE PARENT_T AS SELECT LEVEL CNT, CHR(65 + LEVEL - 1) GUBN FROM DUAL CONNECT BY LEVEL <= 7 ; DROP TABLE CHILD_T; CREATE TABLE CHILD_T AS SELECT LENGTH(LEVEL) CNT, TO_DATE('0001-01-01', 'YYYY-MM-DD') + LEVEL - 1 CHILD_DATE FROM DUAL CONNECT BY LEVEL <= 1000000 ; -- 2. 인덱스 생성 CREATE INDEX PARENT_T_N01 ON PARENT_T (CNT); CREATE INDEX CHILD_T_N01 ON CHILD_T (CHILD_DATE) PARALLEL 8; ALTER INDEX CHILD_T_N01 NOPARALLEL; CREATE INDEX CHILD_T_N02 ON CHILD_T (CNT, CHILD_DATE) PARALLEL 8; ALTER INDEX CHILD_T_N02 NOPARALLEL; |
테스트 환경은 오라클 11g이며, PARENT_T 테이블에는 7건의 코드성 데이터를, CHILD_T 테이블에는 숫자 자리수를 하나의 그룹으로 하고 여기에 날짜값이 순차적으로 증가하도록 CHILD_DATE 값을 넣었습니다. 또한, PARENT_T, CHILD_T 테이블에 각각 인덱스를 생성하였으며, CHILD_T의 경우 ‘CHILD_DATE’, ‘CNT + CHILD_DATE’ 2개 인덱스를 생성하였습니다. 아래는 각각의 테이블 데이터 출력 결과입니다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 |
SELECT * FROM PARENT_T ; CNT GUBN ---------- ---- 1 A 2 B 3 C 4 D 5 E 6 F 7 G 7 rows selected ; SELECT * FROM CHILD_T WHERE ROWNUM <= 20 ; CNT CHILD_DATE ---------- ----------- 1 0001-01-01 1 0001-01-02 1 0001-01-03 1 0001-01-04 1 0001-01-05 1 0001-01-06 1 0001-01-07 1 0001-01-08 1 0001-01-09 2 0001-01-10 2 0001-01-11 2 0001-01-12 2 0001-01-13 2 0001-01-14 2 0001-01-15 2 0001-01-16 2 0001-01-17 2 0001-01-18 2 0001-01-19 2 0001-01-20 20 rows selected ; |
2. (MIN/MAX) operation
위에서 만든 샘플 데이터를 통해 (MIN/MAX) operation을 살펴보도록 하겠습니다. 우선 일반 그룹함수인 SUM() 함수를 이용하여 MIN/MAX() 함수와 어떤 차이가 있는지 보겠습니다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 |
-- 1. SUM() 함수 SELECT SUM(CNT) SUM_CHILD_DATE FROM CHILD_T ; SUM_CHILD_DATE -------------- 5888896 ; Call Count CPU Time Elapsed Time Disk Query Current Rows ------- ------ -------- ------------ ---------- ---------- ---------- ---------- Parse 1 0.010 0.016 0 102 2 0 Execute 1 0.000 0.000 0 0 0 0 Fetch 2 0.280 2.147 2131 2230 0 1 ------- ------ -------- ------------ ---------- ---------- ---------- ---------- Total 4 0.290 2.163 2131 2332 2 1 Misses in library cache during parse: 1 Optimizer goal: ALL_ROWS Parsing user: APPS (ID=44) Rows Row Source Operation ------- --------------------------------------------------- 0 STATEMENT 1 SORT AGGREGATE (cr=2230 pr=2131 pw=0 time=2147299 us) 1000000 TABLE ACCESS FULL CHILD_T (cr=2230 pr=2131 pw=0 time=9845638 us cost=633 size=17254172 card=1327244) ; -- 2. MAX() 함수 SELECT MAX(CHILD_DATE) MAX_CHILD_DATE FROM CHILD_T ; MAX_CHILD_DATE -------------- 2738-11-26 ; 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 2 0.000 0.000 0 3 0 1 ------- ------ -------- ------------ ---------- ---------- ---------- ---------- Total 4 0.000 0.000 0 3 0 1 Misses in library cache during parse: 0 Optimizer goal: ALL_ROWS Parsing user: APPS (ID=44) Rows Row Source Operation ------- --------------------------------------------------- 0 STATEMENT 1 SORT AGGREGATE (cr=3 pr=0 pw=0 time=50 us) 1 INDEX FULL SCAN (MIN/MAX) CHILD_T_N01 (cr=3 pr=0 pw=0 time=37 us cost=3 size=9 card=1)(Object ID 176297043) ; |
보시는 것처럼 100만건이 존재하는 CHILD_T 테이블에 SUM() 함수를 사용할 경우 당연히 100만건을 모두 액세스하고 SUM을 구해야 합니다. 하지만 MAX() 함수의 경우 인덱스에서 딱 1건만 액세스하고 바로 데이터를 가져오고 있으며, 수행시간 및 I/O도 약 1,000배 감소된 걸 볼 수 있습니다.
하지만 (MIN/MAX) operation은 MIN, MAX 값을 동시에 가져올 수는 없습니다. 만약 MIN, MAX 함수를 동시에 사용할 경우 어떻게 되는지 살펴보겠습니다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
SELECT MIN(CHILD_DATE) MIN_CHILD_DATE, MAX(CHILD_DATE) MAX_CHILD_DATE FROM CHILD_T ; MIN_CHILD_DATE MAX_CHILD_DATE -------------- -------------- 0001-01-01 2738-11-26 ; Call Count CPU Time Elapsed Time Disk Query Current Rows ------- ------ -------- ------------ ---------- ---------- ---------- ---------- Parse 1 0.010 0.013 0 102 0 0 Execute 1 0.000 0.000 0 0 0 0 Fetch 2 0.340 0.666 2015 2230 0 1 ------- ------ -------- ------------ ---------- ---------- ---------- ---------- Total 4 0.350 0.679 2015 2332 0 1 Misses in library cache during parse: 1 Optimizer goal: ALL_ROWS Parsing user: APPS (ID=44) Rows Row Source Operation ------- --------------------------------------------------- 0 STATEMENT 1 SORT AGGREGATE (cr=2230 pr=2015 pw=0 time=665887 us) 1000000 TABLE ACCESS FULL CHILD_T (cr=2230 pr=2015 pw=0 time=1135074 us cost=636 size=11945196 card=1327244) ; |
위의 결과처럼 MIN, MAX 함수를 동시에 사용할 경우 (MIN/MAX) operation은 실행계획에 나타나지 않으며, TABLE FULL SCAN으로 전체 데이터를 액세스하여 가져오는 것을 볼 수 있습니다. 그럼 MIN, MAX 값을 동시에 가져오면서 (MIN/MAX) operation을 사용하려면 어떻게 해야 할까요?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
SELECT MIN(MIN_CHILD_DATE) MIN_CHILD_DATE, MAX(MAX_CHILD_DATE) MAX_CHILD_DATE FROM (SELECT MIN(CHILD_DATE) MIN_CHILD_DATE, NULL MAX_CHILD_DATE FROM CHILD_T UNION ALL SELECT NULL MIN_CHILD_DATE, MAX(CHILD_DATE) MAX_CHILD_DATE FROM CHILD_T ) ; MIN_CHILD_DATE MAX_CHILD_DATE -------------- -------------- 0001-01-01 2738-11-26 ; Call Count CPU Time Elapsed Time Disk Query Current Rows ------- ------ -------- ------------ ---------- ---------- ---------- ---------- Parse 1 0.020 0.037 0 204 0 0 Execute 1 0.000 0.000 0 0 0 0 Fetch 2 0.000 0.008 5 6 0 1 ------- ------ -------- ------------ ---------- ---------- ---------- ---------- Total 4 0.020 0.045 5 210 0 1 Misses in library cache during parse: 1 Optimizer goal: ALL_ROWS Parsing user: APPS (ID=44) Rows Row Source Operation ------- --------------------------------------------------- 0 STATEMENT 1 SORT AGGREGATE (cr=6 pr=5 pw=0 time=7838 us) 2 VIEW (cr=6 pr=5 pw=0 time=7822 us cost=1271 size=36 card=2) 2 UNION-ALL (cr=6 pr=5 pw=0 time=7817 us) 1 SORT AGGREGATE (cr=3 pr=3 pw=0 time=4661 us) 1 INDEX FULL SCAN (MIN/MAX) CHILD_T_N01 (cr=3 pr=3 pw=0 time=4646 us)(Object ID 176297043) 1 SORT AGGREGATE (cr=3 pr=2 pw=0 time=3139 us) 1 INDEX FULL SCAN (MIN/MAX) CHILD_T_N01 (cr=3 pr=2 pw=0 time=3121 us)(Object ID 176297043) ; |
방법은 바로 MIN과 MAX를 가져오는 SQL을 각각 구현한 후 이를 UNION ALL로 묶어 데이터를 추출할 수 있습니다. 즉, MIN/MAX는 동시에 추출할 수 없다는 것을 이 테스트를 통해 확인할 수 있습니다.
(MIN/MAX) operation의 장점은 테이블에 조건이 없어도 INDEX FULL SCAN을 통해 데이터를 빠르게 가져온다는 장점이 있는데, 만약 조건이 들어갈 경우 operation에 어떤 변화가 있는지 살펴보겠습니다. 아래 예제는 CHILD_T 테이블에 CNT 조건을 추가하여 MAX(CHILD_DATE) 값을 가져오는 SQL입니다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
SELECT MAX(CHILD_DATE) MAX_CHILD_DATE FROM CHILD_T WHERE CNT = 5 ; MAX_CHILD_DATE -------------- 0274-10-13 ; Call Count CPU Time Elapsed Time Disk Query Current Rows ------- ------ -------- ------------ ---------- ---------- ---------- ---------- Parse 1 0.020 0.020 0 113 0 0 Execute 1 0.000 0.000 0 0 0 0 Fetch 2 0.000 0.000 0 3 0 1 ------- ------ -------- ------------ ---------- ---------- ---------- ---------- Total 4 0.020 0.020 0 116 0 1 Misses in library cache during parse: 1 Optimizer goal: ALL_ROWS Parsing user: APPS (ID=44) Rows Row Source Operation ------- --------------------------------------------------- 0 STATEMENT 1 SORT AGGREGATE (cr=3 pr=0 pw=0 time=35 us) 1 FIRST ROW (cr=3 pr=0 pw=0 time=28 us cost=3 size=22 card=1) 1 INDEX RANGE SCAN (MIN/MAX) CHILD_T_N02 (cr=3 pr=0 pw=0 time=27 us cost=3 size=22 card=1)(Object ID 176297044) ; |
CHILD_T 테이블에 CNT=5 조건을 추가하여 MAX(CHILD_DATE) 값을 구할 경우에도 마찬가지로 빠르게 한건을 가져오고 있는데, 조건이 안 들어간 경우와 비교하여 실행계획에 변화가 생겼습니다. 변화가 생긴 operation은 바로 ‘INDEX FULL SCAN (MIN/MAX)’에서 ‘INDEX RANGE SCAN (MIN/MAX)’으로 바뀐 점입니다. 즉, MIN/MAX operation의 경우 조건이 없을 경우 INDEX FULL SCAN을 통해 양 끝의 데이터를 가져올 수 있으며, 조건이 있을 경우 해당 조건을 INDEX RANGE SCAN으로 액세스 한 뒤 해당 인덱스의 양 끝 데이터를 또한 MIN/MAX로 가져올 수 있다는 것을 의미합니다.
3. (MIN/MAX) operation 튜닝
그럼 CNT 값을 조건으로 넣기 않고 CNT 값에 따른 MAX_CHILD_DATE 값을 가져오려고 하면 어떻게 될까요? 아래의 예제에서 살펴보겠습니다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
SELECT CNT, MAX(CHILD_DATE) FROM CHILD_T GROUP BY CNT ORDER BY CNT ; CNT MAX(CHILD_DATE) ---------- --------------- 1 0001-01-09 2 0001-04-09 3 0003-09-26 4 0028-05-17 5 0274-10-13 6 2738-11-25 7 2738-11-26 7 rows selected ; Call Count CPU Time Elapsed Time Disk Query Current Rows ------- ------ -------- ------------ ---------- ---------- ---------- ---------- Parse 1 0.000 0.014 0 102 0 0 Execute 1 0.000 0.000 0 0 0 0 Fetch 2 0.560 1.365 2131 2230 0 7 ------- ------ -------- ------------ ---------- ---------- ---------- ---------- Total 4 0.560 1.380 2131 2332 0 7 Misses in library cache during parse: 1 Optimizer goal: ALL_ROWS Parsing user: APPS (ID=44) Rows Row Source Operation ------- --------------------------------------------------- 0 STATEMENT 7 HASH GROUP BY (cr=2230 pr=2131 pw=0 time=1364850 us cost=756 size=29199368 card=1327244) 1000000 TABLE ACCESS FULL CHILD_T (cr=2230 pr=2131 pw=0 time=3307440 us cost=636 size=29199368 card=1327244) ; |
보시는 것처럼, TABLE FULL SCAN을 통해 100만건을 모두 액세스한 후 데이터를 추출하는 것을 볼 수 있습니다. 즉 CHILD_T_N02(CNT, CHILD_DATE) 인덱스를 활용하여 MIN/MAX operation을 사용하지 못하는 것을 확인할 수 있습니다.
그럼 CNT 값에 값을 직접 상수로 입력할 경우 MIN/MAX operation을 사용할 수 있는지 살펴보겠습니다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
SELECT CNT, MAX(CHILD_DATE) FROM CHILD_T WHERE CNT IN (1, 2, 3, 4, 5, 6, 7) GROUP BY CNT ORDER BY CNT ; Call Count CPU Time Elapsed Time Disk Query Current Rows ------- ------ -------- ------------ ---------- ---------- ---------- ---------- Parse 1 0.020 0.023 0 102 2 0 Execute 1 0.000 0.000 0 0 0 0 Fetch 2 1.010 12.247 3073 3087 0 7 ------- ------ -------- ------------ ---------- ---------- ---------- ---------- Total 4 1.030 12.269 3073 3189 2 7 Misses in library cache during parse: 1 Optimizer goal: ALL_ROWS Parsing user: APPS (ID=44) Rows Row Source Operation ------- --------------------------------------------------- 0 STATEMENT 7 SORT GROUP BY NOSORT (cr=3087 pr=3073 pw=0 time=29819 us cost=16 size=29199368 card=1327244) 1000000 INLIST ITERATOR (cr=3087 pr=3073 pw=0 time=13385581 us) 1000000 INDEX RANGE SCAN CHILD_T_N02 (cr=3087 pr=3073 pw=0 time=13015854 us cost=16 size=29199368 card=1327244)(Object ID 176297044) ; |
보시는 것처럼 CNT 값을 상수로 넣을 경우 TABLE FULL SCAN이 아닌 INDEX RANGE SCAN으로 operation이 변경되었지만 여전히 MIN/MAX operation으로 데이터를 가져오지 않기 때문에 100만건을 액세스하는 것은 변함이 없습니다. 여기서 두 번째 특징을 찾을 수 있는데 MIN/MAX operation은 반드시 특정값에 속한 경우에만 MIN/MAX 값을 가져올 수 있다는 것입니다. CHILD_T 테이블에서 조건없이MAX(CHILD_DATE) 값을 추출할 경우 ‘CHILD_T_N01(CHILD_DATE)’ 인덱스를 통해 ‘INDEX FULL SCAN (MIN/MAX)’ operation을 사용할 수 있으며, CNT 값이 들어갈 경우 ‘CHILD_T_N02(CNT, CHILD_DATE)’ 인덱스를 통해 특정 선두값(CNT)에 대해서만 ‘INDEX RANGE SCAN (MIN/MAX)’을 사용할 수 있습니다. 하지만 선두값이 여러개일 경우 MIN/MAX operation을 사용할 수 없으며 전체 데이터를 모두 읽어야 하는 한계가 있습니다.
이와 같이 선두 컬럼을 여러개 추출해야 할 경우 MIN/MAX operation을 이용하여 빠르게 데이터를 추출할 수 있는 방법은 없는것일까요? 방법이 있습니다. 그것은 바로 위에서 나열한 특정을 활용하는 것인데, ‘INDEX RANGE SCAN (MIN/MAX)’을 사용하려면 특정 선두값을 반드시 한건만 입력해야 한다는 것을 알았으므로 이 특징을 활용하도록 SQL을 변경해주면 됩니다. 아래의 예제에서 살펴보겠습니다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
SELECT P.CNT, (SELECT MAX(C.CHILD_DATE) FROM CHILD_T C WHERE C.CNT = P.CNT) MAX_CHILD_DATE FROM PARENT_T P ORDER BY CNT ; CNT MAX_CHILD_DATE ---------- -------------- 1 0001-01-09 2 0001-04-09 3 0003-09-26 4 0028-05-17 5 0274-10-13 6 2738-11-25 7 2738-11-26 7 rows selected ; Call Count CPU Time Elapsed Time Disk Query Current Rows ------- ------ -------- ------------ ---------- ---------- ---------- ---------- Parse 1 0.020 0.025 0 107 0 0 Execute 1 0.000 0.000 0 0 0 0 Fetch 2 0.000 0.001 2 20 0 7 ------- ------ -------- ------------ ---------- ---------- ---------- ---------- Total 4 0.020 0.026 2 127 0 7 Misses in library cache during parse: 1 Optimizer goal: ALL_ROWS Parsing user: APPS (ID=44) Rows Row Source Operation ------- --------------------------------------------------- 0 STATEMENT 7 SORT AGGREGATE (cr=16 pr=2 pw=0 time=1194 us) 7 FIRST ROW (cr=16 pr=2 pw=0 time=1143 us cost=3 size=22 card=1) 7 INDEX RANGE SCAN (MIN/MAX) CHILD_T_N02 (cr=16 pr=2 pw=0 time=1129 us cost=3 size=22 card=1)(Object ID 176297044) 7 TABLE ACCESS FULL PARENT_T (cr=4 pr=0 pw=0 time=28 us cost=3 size=91 card=7) ; |
CHILD_T 테이블에 선두컬럼을 넣으려면, ’PARENT_T : CHILD_T = 1 : M’ 관계를 활용하면 되며 이를 1:1 관계로 만들 경우, 즉 CHILD_T 테이블을 스칼라 서브쿼리로 변경할 경우 성능 선두컬럼(CNT)을 항상 받을 수 있기 때문에 MIN/MAX operation을 이용할 수 있으며, 이와 같이 극대화된 성능 개선이 가능합니다.
4. MIN/MAX operation 정리
이처럼 MIN/MAX operation 특징을 정확히 이해하고 활용할 경우 성능개선이 가능하며 이 기능을 정리한 내용은 아래와 같습니다.
1 2 3 4 5 6 7 8 9 10 11 |
1) 인덱스 LEAF 블록의 양 끝을 읽어 가져오기 때문에 데이터를 빠르게 가져올 수 있다. 2) MIN(), MAX() 함수를 동시에 사용할 경우 나타나지 않으며, 동시에 값을 가져와야 할 경우 이를 분리해서 가져와야 한다. 3) INDEX FULL SCAN 뿐만이 아니라 INDEX RANGE SCAN을 통해서도 구현이 가능하다. 4) INDEX RANGE SCAN (MIN/MAX) operation을 사용하기 위해서는 반드시 선두컬럼의 값이 한건만 액세스되도록 해야 한다. 이를 위해 CHILD 테이블에서 선두컬럼을 얻을 수 없을 경우 PARENT 테이블에서 데이터를 추출 후 CHILD 테이블을 스칼라 서브쿼리로 활용하여 INDEX RANGE SCAN (MIN/MAX) operation을 사용할 수 있다. |
지금까지 MIN/MAX operation에 대해 살펴보았습니다
내용을 보시고 궁금하신 점이 있으시면 리플 남겨주시기 바랍니다.
감사합니다.
About the Author
● 경력 10년(현 LG전자 GERP DBA/Tuning 업무)
● 세미나 : 오라클 SQL 튜닝 방법의 이해, DBMS_XPLAN과 10046 Trace를 이용한 SQL문 튜닝, 개발자를 위한 어플리케이션튜닝 세미나
Related Posts
- [DB 칼럼] SQL_PROFILE 사용 방법 14-10-31 일반태그: Technical Architecture. DB태그: Oracle.
팀장님 좋은 정보 감사합니다^^
좋은정보 감사드립니다..