이번에 소개할 내용은 오라클에서 제공하는 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.
 
	
팀장님 좋은 정보 감사합니다^^
좋은정보 감사드립니다..