MySQL과 MariaDB의 SQL 튜닝을 보다 간편하게 진행할 수 있는, SUT(StartUP Tuning)를 소개합니다.

 

SUT는 실제 MySQL 튜닝을 전문적으로 수행하기 위해 직접 만들 툴입니다. 또한 MySQL,MariaDB의 성능 개선 교육을 진행하면서 좀 더 편하게 교육 과정을 진행하기 위해 만든 툴이기도 합니다.(교육 관련 정보는 페이지 아래에 있습니다.)

튜닝에 중점을 두고 만든 툴입니다. 그러므로 SQL 작성과 개발에는 적절하지 않습니다.

SQL 개발은 기존의 다른 툴들의 사용을 권장합니다.

  • 개발자: SweetBoss & GPT4
  • 개발언어: Python
  • 지원 O/S: Windows 10이상
  • 확인된 지원 DBMS
    • MySQL 8
    • MariaDB 10.0.5

 

버젼별 다운로드 경로(다운로드받아 압축을 푸신후 SUT.exe를 실행해주세요.)

 

무료이니, 마음편하게 사용하시기 바랍니다.^^

 

 

 

MySQL을 활용한 SQL 튜닝 입문 교육에 관심있는 분들은 아래 페이지를 참고해주세요.

 

StartUPTuning - MySQL을 활용한 SQL 튜닝 입문

「SQL BOOSTER」, 「평생 필요한 데이터 분석」 저자이신 유일환 수석(스윗보스)께서 또 사고를 쳤습니다. 바쁜 프로젝트 일정에도 불구하고 특유의 부지런함으로 새로운 교...

cafe.naver.com

 

 

 

MySQL은 8 버젼부터 해시 조인을 지원하고 있습니다.

해시 조인을 유도할 수 있는 옵티마이져 힌트는 아래의 MySQL 레퍼런스를 통해 확인할 수 있습니다.

 

https://dev.mysql.com/doc/refman/8.0/en/optimizer-hints.html

 

MySQL :: MySQL 8.0 Reference Manual :: 8.9.3 Optimizer Hints

One means of control over optimizer strategies is to set the optimizer_switch system variable (see Section 8.9.2, “Switchable Optimizations”). Changes to this variable affect execution of all subsequent queries; to affect one query differently from an

dev.mysql.com

 

현재(2023년 1월) 기준으로 위의 레퍼런스를 살펴보면, 해시 조인과 관련된 힌트로, HASH_JOIN과 NO_HASH_JOIN이 있는 것을 알 수 있습니다. 하지만, Description을 보면 해당 힌트는 MySQL 8.0.18 에서만 사용할 수 있는 힌트입니다.

 

저는 현재 8.0.22 를 사용하고 있습니다. 이처럼 8.0.18 이후 버젼에서는 해시 조인을 컨트를하려면 BNL이나 NO_BNL이란 힌트를 사용해야 합니다.

위 레퍼런스에는, BNL 힌트의 설명으로 아래와 같이 정리되어 있습니다.

  - Prior to MySQL 8.0.20: affects Block Nested-Loop join processing;

  - MySQL 8.0.18 and later: also affects hash join optimization;

  - MySQL 8.0.20 and later: affects hash join optimization only

 

BNL은 원래 Block Nested-Loop 를 뜻합니다. NL(Nested-Loops) 조인을 처리할 때, 조인 버퍼를 사용해 NL 조인 성능을 향상시키는 기능입니다. MySQL 8 이전 버젼은 해시 조인을 지원하지 않았기 때문에, BNL 기능으로 해시 조인을 어느 정도 대체했던거 같습니다.

 

어찌되었든, 8.0.2 부터는 BNL 힌트가 해시 조인 최적화에 영향을 준다니 혼란스럽습니다. 아마도 8.0.2부터는 본연의 BNL(Block Nested-Loops) 기능 자체는 없어진거 같습니다. 그냥 BNL 힌트를 제거하고 HASH_JOIN 힌트를 살려줬으면 혼란이 없지 않았을까 생각이 듭니다만, 제가 모르는 다양한 이유가 있겠죠.

 

그러면, BNL 힌트를 사용해 해시 조인을 한번 테스트해볼까 합니다. 생각보다? 아니, 생각만큼 힌트가 잘 안먹습니다.

 

먼저, 저희가 테스트할 테이블인 M_CUS와 T_ORD_BIG에는 어떤 인덱스가 있는지 먼저 살펴봅니다.

인덱스는 조인의 내부적인 처리 방법(NL, HASH, Sort Merge)에 영향을 끼치는 중요 요소입니다.

MySQL에서 인덱스는 'show index from 테이블명' 명령어를 통해 쉽게 확인할 수 있습니다.

현재는 아래와 같이 인덱스가 구성되어 있습니다.

  - M_CUS: PRIMARY(CUS_ID)

  - T_ORD_BIG: PRIMARY(ORD_SEQ), T_ORD_BIG_X1(ORD_DT)

  - 테스트 데이터는 다음 글에서 구성할 수 있습니다.: https://sweetquant.tistory.com/346

 

위와 같이 인덱스가 구성된 상태에서 아래와 같이 SQL을 실행해 봅니다. 실행 계획을 보면, M_CUS와 T_ORD_BIG을 NL 방식으로 조인하고 있습니다. 조인 조건으로 사용된 CUS_ID 컬럼의 인덱스는 M_CUS에만 있으므로, M_CUS를 후행 집합으로 NL 처리하고 있습니다.

EXPLAIN ANALYZE
SELECT  T1.CUS_ID
        ,MAX(T1.CUS_NM) CUS_NM
        ,SUM(T2.ORD_AMT) ORD_AMT
        ,COUNT(*) ORD_CNT
FROM    MYTUNDB.M_CUS T1
        INNER JOIN MYTUNDB.T_ORD_BIG T2
          ON (T1.CUS_ID =  T2.CUS_ID)
WHERE   T2.ORD_DT >= STR_TO_DATE('20170201','%Y%m%d')
AND     T2.ORD_DT < STR_TO_DATE('20170301','%Y%m%d')
GROUP BY T1.CUS_ID

-> Table scan on <temporary>  (actual time=0.003..0.015 rows=72 loops=1)
     -> Aggregate using temporary table  (actual time=187.347..187.376 rows=72 loops=1)
         -> Nested loop inner join  (cost=31569.86 rows=39462) (actual time=1.043..126.440 rows=19800 loops=1)
             -> Index range scan on T2 using T_ORD_BIG_X1, with index condition: ((t2.ORD_DT >= <cache>(str_to_date('20170201','%Y%m%d'))) and (t2.ORD_DT < <cache>(str_to_date('20170301','%Y%m%d'))))  (cost=17758.16 rows=39462) (actual time=0.973..91.609 rows=19800 loops=1)
             -> Single-row index lookup on T1 using PRIMARY (CUS_ID=t2.CUS_ID)  (cost=0.25 rows=1) (actual time=0.001..0.001 rows=1 loops=19800)

 

이제, NL이 아닌, 해시 조인으로 위 SQL이 처리되도록 하기 위해 아래와 같이 BNL 힌트를 적용합니다.

실행계획을 확인해보면, 힌트가 무시되고 NL 조인이 여전히 사용중인 것을 알 수 있습니다.

힌트를 잘 못 준건 아닐까란 생각에, /*+ BNL(T1) */ 이나, /*+ BNL(T1, T2) */, /*+ BNL(T2, T1) */ 등 다양하게 시도해봤으나 해시 조인이 발생하지는 않습니다.

아마도 MySQL의 옵티마이져가 생각했을때, 현재 SQL과 테이블, 인덱스 구성에서는 해시 조인이 비효율적이라고 판단한 것이겠죠.

EXPLAIN ANALYZE
SELECT  /*+ BNL(T2) */
        T1.CUS_ID
        ,MAX(T1.CUS_NM) CUS_NM
        ,SUM(T2.ORD_AMT) ORD_AMT
        ,COUNT(*) ORD_CNT
FROM    MYTUNDB.M_CUS T1
        INNER JOIN MYTUNDB.T_ORD_BIG T2
          ON (T1.CUS_ID =  T2.CUS_ID)
WHERE   T2.ORD_DT >= STR_TO_DATE('20170201','%Y%m%d')
AND     T2.ORD_DT < STR_TO_DATE('20170301','%Y%m%d')
GROUP BY T1.CUS_ID;

-> Table scan on <temporary>  (actual time=0.002..0.014 rows=72 loops=1)
     -> Aggregate using temporary table  (actual time=120.311..120.340 rows=72 loops=1)
         -> Nested loop inner join  (cost=31569.86 rows=39462) (actual time=0.240..80.108 rows=19800 loops=1)
             -> Index range scan on T2 using T_ORD_BIG_X1, with index condition: ((t2.ORD_DT >= <cache>(str_to_date('20170201','%Y%m%d'))) and (t2.ORD_DT < <cache>(str_to_date('20170301','%Y%m%d'))))  (cost=17758.16 rows=39462) (actual time=0.223..58.354 rows=19800 loops=1)
             -> Single-row index lookup on T1 using PRIMARY (CUS_ID=t2.CUS_ID)  (cost=0.25 rows=1) (actual time=0.000..0.001 rows=1 loops=19800)

 

자, 그러면, 해시 조인을 발생시키기 위햇 JOIN_ORDER 힌트를 적용해보면 어떨까요?

T1를 먼저 접근하도록 JOIN_ORDER(T1, T2)를 적용해봅입니다.

이 경우, T2(T_ORD_BIG)쪽에는 CUS_ID에 대한 인덱스가 없으므로 해시 조인이 발생할 가능성이 높습니다. (FULL SCAN 방식으로 NL을 처리할 수는 없으니까요.)

BNL 힌트 없이, JOIN_ORDER 만으로 조인 순서를 변경하자 해시 조인이 사용된 것을 알 수 있습니다. 

EXPLAIN ANALYZE
SELECT  /*+ JOIN_ORDER(T1, T2) */
        T1.CUS_ID
        ,MAX(T1.CUS_NM) CUS_NM
        ,SUM(T2.ORD_AMT) ORD_AMT
        ,COUNT(*) ORD_CNT
FROM    MYTUNDB.M_CUS T1
        INNER JOIN MYTUNDB.T_ORD_BIG T2
          ON (T1.CUS_ID =  T2.CUS_ID)
WHERE   T2.ORD_DT >= STR_TO_DATE('20170201','%Y%m%d')
AND     T2.ORD_DT < STR_TO_DATE('20170301','%Y%m%d')
GROUP BY T1.CUS_ID;

-> Table scan on <temporary>  (actual time=0.002..0.014 rows=72 loops=1)
     -> Aggregate using temporary table  (actual time=106.843..106.871 rows=72 loops=1)
         -> Inner hash join (t2.CUS_ID = t1.CUS_ID)  (cost=1598243.65 rows=355158) (actual time=0.386..67.943 rows=19800 loops=1)
             -> Index range scan on T2 using T_ORD_BIG_X1, with index condition: ((t2.ORD_DT >= <cache>(str_to_date('20170201','%Y%m%d'))) and (t2.ORD_DT < <cache>(str_to_date('20170301','%Y%m%d'))))  (cost=13816.34 rows=39462) (actual time=0.250..55.218 rows=19800 loops=1)
             -> Hash
                 -> Table scan on T1  (cost=9.25 rows=90) (actual time=0.058..0.081 rows=90 loops=1)

 

이번에는 조인 순서를 T2->T1으로 변경하고 BNL 힌트까지 추가해봅니다. 혹시나 JOIN_ORDER와 BNL을 같이 써야만 해시 조인이 사용되는지 의심되서 입니다.

하지만, 다시 NL 조인이 사용되는 것을 볼 수 있습니다. 아무래도 M_CUS의 PRIMARY 인덱스 사용의 우선 순위가 높은거 아닌가 싶습니다.

EXPLAIN ANALYZE
SELECT  /*+ JOIN_ORDER(T2, T1) BNL(T2, T1) */
        T1.CUS_ID
        ,MAX(T1.CUS_NM) CUS_NM
        ,SUM(T2.ORD_AMT) ORD_AMT
        ,COUNT(*) ORD_CNT
FROM    MYTUNDB.M_CUS T1
        INNER JOIN MYTUNDB.T_ORD_BIG T2
          ON (T1.CUS_ID =  T2.CUS_ID)
WHERE   T2.ORD_DT >= STR_TO_DATE('20170201','%Y%m%d')
AND     T2.ORD_DT < STR_TO_DATE('20170301','%Y%m%d')
GROUP BY T1.CUS_ID;


 -> Table scan on <temporary>  (actual time=0.002..0.011 rows=72 loops=1)
     -> Aggregate using temporary table  (actual time=93.163..93.184 rows=72 loops=1)
         -> Nested loop inner join  (cost=31569.86 rows=39462) (actual time=0.236..62.395 rows=19800 loops=1)
             -> Index range scan on T2 using T_ORD_BIG_X1, with index condition: ((t2.ORD_DT >= <cache>(str_to_date('20170201','%Y%m%d'))) and (t2.ORD_DT < <cache>(str_to_date('20170301','%Y%m%d'))))  (cost=17758.16 rows=39462) (actual time=0.219..44.639 rows=19800 loops=1)
             -> Single-row index lookup on T1 using PRIMARY (CUS_ID=t2.CUS_ID)  (cost=0.25 rows=1) (actual time=0.000..0.000 rows=1 loops=19800)

 

끝으로, 실제 성능과 상관없이, 죽었다 깨어나도 T2->T1 순서로 조인 순서를 유지하면서, 해시 조인을 만들어 보고 싶다면, 조인 조건의 강제 변화를 고려할 수 있습니다.

아래와 같이 T1.CUS_ID를 TRIM 처리해서 인덱스를 사용못하도록 합니다. 그리고 JOIN_ORDER 힌트만 사용해서 T2 먼저 접근하도록 합니다. BNL 힌트를 주지 않았지만, 해시 조인이 발생한 것을 알 수 있습니다.

(아래와 같이 조인 조건절을 일부러 변형해 해시 조인을 유도하는 것은 절대 좋은 방법이 아니죠.)

EXPLAIN ANALYZE
SELECT  /*+ JOIN_ORDER(T2, T1) */
        T1.CUS_ID
        ,MAX(T1.CUS_NM) CUS_NM
        ,SUM(T2.ORD_AMT) ORD_AMT
        ,COUNT(*) ORD_CNT
FROM    MYTUNDB.M_CUS T1
        INNER JOIN MYTUNDB.T_ORD_BIG T2
          ON (TRIM(T1.CUS_ID) =  T2.CUS_ID) -- > T1.CUS_ID를 일부러 TRIM 처리
WHERE   T2.ORD_DT >= STR_TO_DATE('20170201','%Y%m%d')
AND     T2.ORD_DT < STR_TO_DATE('20170301','%Y%m%d')
GROUP BY T1.CUS_ID;

-> Table scan on <temporary>  (actual time=0.002..0.024 rows=72 loops=1)
     -> Aggregate using temporary table  (actual time=280.777..280.815 rows=72 loops=1)
         -> Inner hash join (trim(t1.CUS_ID) = t2.CUS_ID)  (cost=372923.03 rows=3551580) (actual time=141.649..239.391 rows=19800 loops=1)
             -> Table scan on T1  (cost=0.00 rows=90) (actual time=0.078..0.119 rows=90 loops=1)
             -> Hash
                 -> Index range scan on T2 using T_ORD_BIG_X1, with index condition: ((t2.ORD_DT >= <cache>(str_to_date('20170201','%Y%m%d'))) and (t2.ORD_DT < <cache>(str_to_date('20170301','%Y%m%d'))))  (cost=17758.16 rows=39462) (actual time=0.243..48.248 rows=19800 loops=1)

 

지금까지 살펴본 바로는, 해시 조인을 사용하는 힌트인 BNL이 그렇게 잘 작동하는거 같지는 않습니다. 조인을 처리할 수 있는 막강한 PRIMARY 인덱스가 있다면, 아마도 해시 조인보다 NL 조인을 선호하는 것은 아닌가 생각이 듭니다.

BNL 힌트는 잘 작동하지 않지만, NO_BNL 힌트는 또 잘 먹는거 같드라고요.

아래와 같이 해시 조인이 자동으로 작동했던, JOIN_ORDER(T1, T2)를 적용한 후에 NO_BNL까지 힌트를 걸어주면 NL 방식으로 조인이 처리되어 매우 느린 것을 알 수 있습니다.

EXPLAIN ANALYZE
SELECT  /*+ JOIN_ORDER(T1, T2) NO_BNL(T2) */
        T1.CUS_ID
        ,MAX(T1.CUS_NM) CUS_NM
        ,SUM(T2.ORD_AMT) ORD_AMT
        ,COUNT(*) ORD_CNT
FROM    MYTUNDB.M_CUS T1
        INNER JOIN MYTUNDB.T_ORD_BIG T2
          ON (T1.CUS_ID =  T2.CUS_ID)
WHERE   T2.ORD_DT >= STR_TO_DATE('20170201','%Y%m%d')
AND     T2.ORD_DT < STR_TO_DATE('20170301','%Y%m%d')
GROUP BY T1.CUS_ID;

-> Group aggregate: max(t1.CUS_NM), sum(t2.ORD_AMT), count(0)  (actual time=187.159..6557.740 rows=72 loops=1)
     -> Nested loop inner join  (cost=1598243.65 rows=355158) (actual time=16.554..6530.982 rows=19800 loops=1)
         -> Index scan on T1 using PRIMARY  (cost=9.25 rows=90) (actual time=0.060..0.328 rows=90 loops=1)
         -> Filter: (t2.CUS_ID = t1.CUS_ID)  (cost=13816.34 rows=3946) (actual time=24.775..72.503 rows=220 loops=90)
             -> Index range scan on T2 using T_ORD_BIG_X1, with index condition: ((t2.ORD_DT >= <cache>(str_to_date('20170201','%Y%m%d'))) and (t2.ORD_DT < <cache>(str_to_date('20170301','%Y%m%d'))))  (cost=13816.34 rows=39462) (actual time=0.290..61.939 rows=19800 loops=90)

 

 

본 글에서는 BNL을 이용해 해시 조인이 발생한 케이스는 나오지 않았으나, 인덱스의 구성, 쿼리 조건의 변경에 따라 BNL 힌트가 작동이 될 수도 있다고 생각합니다. 어쨋든, 8.0.2 부터는 BNL 힌트를 사용해 해시 조인을 유도할 수 있다는 점과, 힌트를 준다고 해도, 무조건 해시 조인이 발생하지 않는다는 점을 알고 있으면 될거 같습니다.

 

오늘은 여기까지입니다. 감사합니다.

 

 

MySQL의 옵티마이져 힌트를 사용해 조인 순서를 조정하는 법을 살펴보겠습니다.

 

MySQL에 어떤 옵티마이져 힌트가 있는지는 아래 레퍼런스를 참고하면 알 수 있습니다.

* MySQL의 경우 버젼에 따라 작동하지 않는 힌트가 있으므로 주의가 필요합니다.

https://dev.mysql.com/doc/refman/8.0/en/optimizer-hints.html

 

MySQL :: MySQL 8.0 Reference Manual :: 8.9.3 Optimizer Hints

One means of control over optimizer strategies is to set the optimizer_switch system variable (see Section 8.9.2, “Switchable Optimizations”). Changes to this variable affect execution of all subsequent queries; to affect one query differently from an

dev.mysql.com

 

아래와 같은 SQL을 실행해봅니다.

(아래 SQL을 실행하기 위해서는 https://sweetquant.tistory.com/346 글을 참고해 DB를 구성해야 합니다.)

EXPLAIN
SELECT  T1.CUS_ID
        ,MAX(T1.CUS_NM) CUS_NM
        ,SUM(T2.ORD_AMT) ORD_AMT
        ,COUNT(*) ORD_CNT
FROM    MYTUNDB.M_CUS T1
        INNER JOIN MYTUNDB.T_ORD_BIG T2 ON (T1.CUS_ID = T2.CUS_ID)
WHERE   T2.ORD_DT >= STR_TO_DATE('20170201','%Y%m%d')
AND     T2.ORD_DT < STR_TO_DATE('20170301','%Y%m%d')
GROUP BY T1.CUS_ID;

 

결과를 보면 아래와 같습니다. T2(T_ORD_BIG)가 위에 있습니다. MySQL의 경우, 아래와 같은 결과에서는 위쪽에 있는 단계가 먼저 처리됩니다. 다시 말해 T2를 먼저 접근해 T1과 조인이 처리되고 있습니다.

 id   select_type   table   partitions   type     possible_keys   key            key_len   ref                 rows    filtered   Extra                                    
---- ------------- ------- ------------ -------- --------------- -------------- --------- ------------------- ------- ---------- ---------------------------------------- 
1    SIMPLE        T2      None         range    T_ORD_BIG_X1    T_ORD_BIG_X1   6         None                39462   100.0      Using index condition; Using temporary   
1    SIMPLE        T1      None         eq_ref   PRIMARY         PRIMARY        162       mytundb.T2.CUS_ID   1       100.0      None

위 SQL의 실행계획을 EXPLAIN FORMAT = TREE를 사용해 TREE 형태로 살펴보면 아래와 같습니다. T2를 T_ORD_BIG_X1 인덱스로 접근한 후, T1을 NL 조인 방식으로 처리하고 있는 것을 알 수 있습니다.

EXPLAIN FORMAT = TREE
SELECT  T1.CUS_ID
        ,MAX(T1.CUS_NM) CUS_NM
        ,SUM(T2.ORD_AMT) ORD_AMT
        ,COUNT(*) ORD_CNT
FROM    MYTUNDB.M_CUS T1
        INNER JOIN MYTUNDB.T_ORD_BIG T2 ON (T1.CUS_ID = T2.CUS_ID)
WHERE   T2.ORD_DT >= STR_TO_DATE('20170201','%Y%m%d')
AND     T2.ORD_DT < STR_TO_DATE('20170301','%Y%m%d')
GROUP BY T1.CUS_ID;

-> Table scan on <temporary>
     -> Aggregate using temporary table
         -> Nested loop inner join  (cost=31569.86 rows=39462)
             -> Index range scan on T2 using T_ORD_BIG_X1, with index condition: ((t2.ORD_DT >= <cache>(str_to_date('20170201','%Y%m%d'))) and (t2.ORD_DT < <cache>(str_to_date('20170301','%Y%m%d'))))  (cost=17758.16 rows=39462)
             -> Single-row index lookup on T1 using PRIMARY (CUS_ID=t2.CUS_ID)  (cost=0.25 rows=1)

 

 

위 SQL의 조인 순서를 변경해봅니다. T1을 먼저 접근해 T2쪽으로 조인을 하도록 처리해보는 거죠.

아래와 같이 JOIN_ORDER라는 옵티마이져 힌트를 사용할 수 있습니다.

EXPLAIN
SELECT  /*+ JOIN_ORDER(T1, T2) */
        T1.CUS_ID
        ,MAX(T1.CUS_NM) CUS_NM
        ,SUM(T2.ORD_AMT) ORD_AMT
        ,COUNT(*) ORD_CNT
FROM    MYTUNDB.M_CUS T1
        INNER JOIN MYTUNDB.T_ORD_BIG T2 ON (T1.CUS_ID = T2.CUS_ID)
WHERE   T2.ORD_DT >= STR_TO_DATE('20170201','%Y%m%d')
AND     T2.ORD_DT < STR_TO_DATE('20170301','%Y%m%d')
GROUP BY T1.CUS_ID;

아래와 같이 실행계획을 보면, T1부터 접근해서 조인이 처리되는 것을 알 수 있습니다. 힌트가 작동한거죠.

id   select_type   table   partitions   type    possible_keys   key            key_len   ref    rows    filtered   Extra                                                               
---- ------------- ------- ------------ ------- --------------- -------------- --------- ------ ------- ---------- ------------------------------------------------------------------- 
1    SIMPLE        T1      None         ALL     PRIMARY         None           None      None   90      100.0      Using temporary                                                     
1    SIMPLE        T2      None         range   T_ORD_BIG_X1    T_ORD_BIG_X1   6         None   39462   10.0       Using index condition; Using where; Using join buffer (hash join)

TREE 형태로 실행계획을 살펴보면 아래와 같습니다.

실행계획을 잘 살펴보면, T1을 먼저 처리하는 방식으로 조인 순서가 바뀌면서,  Inner hash join이 발생한 것을 알 수 있습니다. 바뀐 조인 순서에 따라 옵티마이져가 HASH 조인으로 변경을 한 것입니다.

EXPLAIN FORMAT = TREE
SELECT  /*+ JOIN_ORDER(T1, T2) */
        T1.CUS_ID
        ,MAX(T1.CUS_NM) CUS_NM
        ,SUM(T2.ORD_AMT) ORD_AMT
        ,COUNT(*) ORD_CNT
FROM    MYTUNDB.M_CUS T1
        INNER JOIN MYTUNDB.T_ORD_BIG T2 ON (T1.CUS_ID = T2.CUS_ID)
WHERE   T2.ORD_DT >= STR_TO_DATE('20170201','%Y%m%d')
AND     T2.ORD_DT < STR_TO_DATE('20170301','%Y%m%d')
GROUP BY T1.CUS_ID;

-> Table scan on <temporary>
     -> Aggregate using temporary table
         -> Inner hash join (t2.CUS_ID = t1.CUS_ID)  (cost=1598243.65 rows=355158)
             -> Index range scan on T2 using T_ORD_BIG_X1, with index condition: ((t2.ORD_DT >= <cache>(str_to_date('20170201','%Y%m%d'))) and (t2.ORD_DT < <cache>(str_to_date('20170301','%Y%m%d'))))  (cost=13816.34 rows=39462)
             -> Hash
                 -> Table scan on T1  (cost=9.25 rows=90)

 

끝으로 주의할 점이 있습니다. 옵티마이져 힌트에서, JOIN_ORDER의 테이블을 정의할 때 반드시 콤마(,)로 구분을 해야 합니다. 아래와 같이 스페이스로만 두 테이블을 분리하면 힌트가 작동하지 않습니다.

EXPLAIN FORMAT = TREE
SELECT  /*+ JOIN_ORDER(T1 T2) */ # 잘못된 힌트, T2과 T2를 콤마(,)로 분리해야함!
        T1.CUS_ID
        ,MAX(T1.CUS_NM) CUS_NM
        ,SUM(T2.ORD_AMT) ORD_AMT
        ,COUNT(*) ORD_CNT
FROM    MYTUNDB.M_CUS T1
        INNER JOIN MYTUNDB.T_ORD_BIG T2 ON (T1.CUS_ID = T2.CUS_ID)
WHERE   T2.ORD_DT >= STR_TO_DATE('20170201','%Y%m%d')
AND     T2.ORD_DT < STR_TO_DATE('20170301','%Y%m%d')
GROUP BY T1.CUS_ID;

-> Table scan on <temporary>
     -> Aggregate using temporary table
         -> Nested loop inner join  (cost=31569.86 rows=39462)
             -> Index range scan on T2 using T_ORD_BIG_X1, with index condition: ((t2.ORD_DT >= <cache>(str_to_date('20170201','%Y%m%d'))) and (t2.ORD_DT < <cache>(str_to_date('20170301','%Y%m%d'))))  (cost=17758.16 rows=39462)
             -> Single-row index lookup on T1 using PRIMARY (CUS_ID=t2.CUS_ID)  (cost=0.25 rows=1)

 

오늘은 여기까지입니다.!

 

 

 

 

이번에 설명할 주제는 '넌클러스터드에서 클러스터드로'란 주제입니다.

MySQL의 인덱스 동작 관련해서 가장 중요한 부분이라 생각됩니다.

 

넌클러스터드 인덱스와 클러스터드 인데스의 차이점을 다시 한 번 짚어보면 아래 그림과 같습니다.

 

넌클러스터드 인덱스와 클러스터드 인덱스 간의 가장 큰 차이점은, 리프 노드의 구성입니다.

  • 넌클러스터드 인덱스: 리프 노드에는 인덱스 컬럼의 값과, 해다 테이블의 클러스터드 인덱스의 컬럼 값이 존재
  • 클러스터드 인덱스: 리프 노드에는 실제 데이터가 존재

이 부분을 정확히 알고 있어야 합니다.

 

자 그러면, 아래 SQL을 살펴봅니다. WHERE 절에서는 ORD_DT에 같다(=) 조건이 사용되었고, SELECT 절에서는 ORD_DT 컬럼만 사용하고 있습니다.

SELECT  T1.ORD_DT
FROM    MYTUNDB.T_ORD_BIG T1
WHERE   T1.ORD_DT = STR_TO_DATE('20170104','%Y%m%d');

EXPLAIN                                                                                                      
------------------------------------------------------------------------------------------------------------ 
-> Index lookup on T1 using T_ORD_BIG_X1 (ORD_DT=str_to_date('20170104','%Y%m%d'))  (cost=92.52 rows=900) (actual time=0.087..0.554 rows=900 loops=1)

 

위 SQL의 실행계획을 보면, T_ORD_BIG_X1 인덱스를 사용해 처리가 된 것을 알 수 있습니다. actual time은 0.087, cost는 92.52라고 측정되어 있습니다. 위 과정을 그림으로 그려보면, 아래와 같습니다. 앞에서도 계속 설명했던 내용입니다.

아래 그림에서 1번 과정은 수직적 탐색, 2번 과정은 수평적 탐색입니다.

1번, 수직적 탐색은 WHERE 절 조건에 해당하는 값이 어디서부터 시작인지, 위치를 찾기 위한 과정입니다.

2번, 수평적 탐색은 WHERE 절 조건을 만족하지 않을때까지 리프 노드를 차례대로 검색하는 과정입니다.

 

이번에는 아래 SQL을 살펴보도록 합니다. 방금 살펴본 위의 SQL과 거의 같습니다. 다른 점은 SELECT 절에 ORD_DT 외에도 ORD_SEQ 컬럼이 있다는 점입니다. cost는 위의 SQL과  같고, actual time이 아주 약간 늘어난 것을 확인 할 수 있지만, 거의 차이가 없다고 보셔도 됩니다.

아래 SQL의 실행 계획 역시 위의 그림과 동일합니다. T_ORD_BIG_X1 인덱스만 사용해 원하는 결과를 처리합니다. 넌클러스터드 인덱스의 리프 노드에는 클러스터드 인덱스의 컬럼인 ORD_SEQ의 값도 존재하므로, ORD_SEQ 값을 가져오기 위한 추가적인 인덱스 접근이나 액션이 필요하지 않습니다.

SELECT  T1.ORD_DT ,T1.ORD_SEQ
FROM    MYTUNDB.T_ORD_BIG T1
WHERE   T1.ORD_DT = STR_TO_DATE('20170104','%Y%m%d');

EXPLAIN                                                                                                      
------------------------------------------------------------------------------------------------------------ 
-> Index lookup on T1 using T_ORD_BIG_X1 (ORD_DT=str_to_date('20170104','%Y%m%d'))  (cost=92.52 rows=900) (actual time=0.133..1.150 rows=900 loops=1)

 

 

이번에는 아래 SQL과 같이 SELECT 절에 T1.*를 사용해 모든 컬럼을 조회하도록 합니다. 실행계획을 살펴보면, 이전 SQL들과 마찬가지로 T_ORD_BIG_X1 인덱스를 사용하고 있습니다. 그런데 뒤쪽의 cost를 보면, 987.60으로 10배 가까이 증가한 것을 알 수 있습니다. actual time 역시 0.475로 증가했습니다.

SELECT  T1.*
FROM    MYTUNDB.T_ORD_BIG T1
WHERE   T1.ORD_DT = STR_TO_DATE('20170104','%Y%m%d');

EXPLAIN                                                                                                      
------------------------------------------------------------------------------------------------------------- 
-> Index lookup on T1 using T_ORD_BIG_X1 (ORD_DT=str_to_date('20170104','%Y%m%d'))  (cost=987.60 rows=900) (actual time=0.475..3.152 rows=900 loops=1)

위 SQL의 경우, 실행계획에는 T_ORD_BIG_X1만 사용한 것으로 표시되어 있습니다. 하지만 실제로는, T_ORD_BIG 테이블의 클러스터드 인덱스도 사용하고 있습니다.

위 SQL의 결과를 만들기 위해서는 ORD_DT가 2017년 1월 4일인 로우들의 모든 값이 필요합니다.

이 값들은 클러스터드 인덱스에 저장되어 있습니다. 그러므로 내부적으로 클러스터드 인덱스를 경유해야만 위 SQL의 결과를 처리할 수 있습니다. 안타깝게도, MySQL의 실행계획에서는 해당 작업을 별도로 표시해 주지는 않습니다.

 

위 SQL을 처리한 과정을 그려보면 아래와 같습니다.

1번, 2번은 넌 클러스터드 인덱스로 WHERE 조건에 해당하는 값의 클러스터드 인덱스의 키 값(ORD_SEQ)을 찾는 과정입니다.

3번은 2번에서 찾은 ORD_SEQ를 사용해, 클러스터드 인덱스를 검색하러 가는 과정입니다.

4번, 5번은 3번의 ORD_SEQ를 사용해 실제 데이터를 찾아오는 과정입니다.

논리적으로 3번, 4번, 5번 작업은 2번에서 찾은 건수만큼 반복 실행됩니다. 내부 매커니즘을 통해 반복 3, 4, 5번의 반복 횟수를 줄일 것이라 예상되지만, 논리적으로는 반복 실행된다고 생각하면 됩니다.

 

 

오늘 살펴볼 내용은 여기까지입니다.

자 그러면, 우리는 SELECT 절에 무턱대고 '*'를 사용해서는 안되겠죠. '*'를 처리하기 위해서는 실제 데이터의  접근이 발생할 수 밖에 없으니까요.

또한 테이블의 접근(클러스터드 인덱스 접근)을 피하기 위해 무턱대고 넌클러스터드 인덱스의 컬럼을 늘릴 수는 없겠죠. 이는 CUD 작업의 성능 저하로 이어지니까요. 그래서 인데스 설계가 어렵습니다.

그런데, MySQL은 좀 더 어렵습니다.  이와 같은 넌클러스터드와 클러스터드 구조를 반드시 머리에 넣고 성능을 고려한 인덱스를 설계를 해야 하니까요.

 

지난 글에 이어서 MySQL의 인덱스 관련 실행계획을 간단하게 살펴보겠습니다.

이번에는 고전적인 방법으로 인덱스 관력 작업이 어떻게 나타나는지 살펴봅니다.

 

2. MySQL의 인덱스 실행 계획: EXPLAIN

아래와 같이 ORD_DT에 범위 조건을 사용해 2017년 1월 4일 하루치의 데이터만 조회하는 SQL을 작성하고 EXPLAIN 처리합니다. 얻은 실행 계획을 보면, type 부분을 보면 range 라고 되어 있는 것을 알 수 있습니다.

type의 range는 범위 조건 처리를 위해 인덱스가 사용되어진 것을 뜻합니다. 어떤 인덱스를 사용했는지는 key 부분에 명시되어 있습니다. 또한 가장 오른쪽 Extra 항목을 보면, Using index라고 추가적으로 명시되어 있습니다. 

EXPLAIN
SELECT  COUNT(*)
FROM    MYTUNDB.T_ORD_BIG T1
WHERE   T1.ORD_DT >= STR_TO_DATE('20170104','%Y%m%d')
AND     T1.ORD_DT < STR_TO_DATE('20170105','%Y%m%d');

id   select_type   table   partitions   type    possible_keys   key            key_len   ref    rows   filtered   Extra                      
---- ------------- ------- ------------ ------- --------------- -------------- --------- ------ ------ ---------- -------------------------- 
1    SIMPLE        T1      None         range   T_ORD_BIG_X1    T_ORD_BIG_X1   6         None   900    100.0      Using where; Using index

 

이번에는 ORD_DT에 같다(=) 조건을 사용해 2017년 1월 4일 하루치 데이터를 조회하도록 합니다. 실행계획을 살펴보면 아래와 같습니다. 이번에는 type이 ref로 표시되어 있습니다. 이처럼 같다(=) 조건에 인덱스가 사용되면 ref라고 표시가 됩니다. 위에서 살펴본 것과 마찬가지로 key에는 어떤 인덱스를 사용했는지가 표시되어 있고, Extra에도 Using index라고 표시 되어 있습니다.

EXPLAIN
SELECT  COUNT(*)
FROM    MYTUNDB.T_ORD_BIG T1
WHERE   T1.ORD_DT = STR_TO_DATE('20170104','%Y%m%d');

id   select_type   table   partitions   type   possible_keys   key            key_len   ref     rows   filtered   Extra         
---- ------------- ------- ------------ ------ --------------- -------------- --------- ------- ------ ---------- ------------- 
1    SIMPLE        T1      None         ref    T_ORD_BIG_X1    T_ORD_BIG_X1   6         const   900    100.0      Using index

 

마지막으로, ORD_DT 컬럼을 변형해 인덱스를 효율적으로 사용할 수 없도록 쿼리를 작성해 실행해봅니다. 실행계획을 확인해 보면, 아래와 같이 type 부분에 index라고 표시되어 있습니다. 이를 보고, '인덱스를 잘 타고 있구나'라고 착각하면 안됩니다. 인덱스를 사용한 것은 맞지만, 효율적으로 사용한 케이스는 아닙니다.

Key에는 어떤 인덱스를 사용했는지, Extra에 using index 라고 표시되어 있지만, 인덱스 리프 노드를 모두 스캔하는 Index full scan을 하고 있는 동작입니다.

EXPLAIN
SELECT  COUNT(*)
FROM    MYTUNDB.T_ORD_BIG T1
WHERE   DATE_FORMAT(T1.ORD_DT,'%Y%m%d') = '20170104'

id   select_type   table   partitions   type    possible_keys   key            key_len   ref    rows     filtered   Extra                      
---- ------------- ------- ------------ ------- --------------- -------------- --------- ------ -------- ---------- -------------------------- 
1    SIMPLE        T1      None         index   None            T_ORD_BIG_X1   6         None   301158   100.0      Using where; Using index

 

고전적인 실행 계획으로 살펴봤을 때, 인덱스를 잘 사용하고 있는지는 type 항목을 보셔야 합니다. type 항목 값에 따라 아래와 같이 정리할 수 있습니다.

  • range: 범위 조건에 대해 인덱스를 사용
  • ref: 같다 조건에 대해 인덱스를 사용
  • index: 인덱스 리프 노드를 모두 스캔한 검색 방법

type 항목을 보고 헷갈리지 않도록 주의를 하시면 될거 같습니다.

 

 

오늘은 여기까지입니다. 감사합니다.

 

저번 글에서는 인덱스 탐색 과정에 대해 간단하게 설명했습니다.

그리고 인덱스 탐색을 어떻게 표현하지는도 살펴봤습니다.

이번에는 실제 MySQL에서 인덱스 관련된 실행 게획을 어떻게 표현하는지 살펴보도록 하겠습니다.

 

 

1. MySQL의 인덱스 실행 계획: EXPLAIN FORMAT = TREE

EXPLAIN FORMAT = TREE로 지정했을 때 인덱스 관련 동작이 어떻게 표현되는지 알아보겠습니다.

이번 글에서는 EXPLAIN FORMAT = TREE 기준을 설명하고, 다음 글에서는 고전적인 실행게획에서 인덱스 관련 동작이 표시되는 방법을 설명할 예정입니다.

 

아래와 같이 SQL을 실행합니다. 아래 SQL은 ORD_DT에는 범위 조건을 사용했습니다.(범위 조건을 사용해 2017년 1월 4일 하루만 조회합니다.)

앞에서 T_ORD_BIG 테이블의 ORD_DT 컬럼에 대해서는 T_ORD_BIG_X1 인덱스를 생성했습니다. 그러므로 아래 SQL은 해당 인덱스를 사용해 처리가 될 확률이 높습니다. 실행계획을 살펴보면, 가장 아래에 'Index range scan'이 표시되어 있습니다. 그리고 'using T_ORD_BIG_X1'이라고 어떤 인덱스를 사용했는지 친절하게 표시되어 있습니다. MySQL이 버젼이 올라갈수록, 실행계획을 해석하기 좋게 보여주고 있습니다.

EXPLAIN FORMAT = TREE
SELECT  COUNT(*)
FROM    MYTUNDB.T_ORD_BIG T1
WHERE   T1.ORD_DT >= STR_TO_DATE('20170104','%Y%m%d')
AND     T1.ORD_DT < STR_TO_DATE('20170105','%Y%m%d');

-> Aggregate: count(0)
     -> Filter: ((mytundb.t1.ORD_DT >= <cache>(str_to_date('20170104','%Y%m%d'))) and (mytundb.t1.ORD_DT < <cache>(str_to_date('20170105','%Y%m%d'))))  (cost=182.54 rows=900)
         -> Index range scan on T1 using T_ORD_BIG_X1  (cost=182.54 rows=900)

 

 

이번에는 ORD_DT에 범위 조건이 아닌, 같다(=) 조건을 사용한 SQL을 실행해봅니다. (위 SQL과 똑같이 2017년 1월 4일 하루만 조회합니다.) 아래와 같슽니다. 실행계획을 살펴보면 'Index range scan'이 사라지고, 'Index lookup'이 표시되었습니다.

EXPLAIN FORMAT = TREE
SELECT  COUNT(*)
FROM    MYTUNDB.T_ORD_BIG T1
WHERE   T1.ORD_DT = STR_TO_DATE('20170104','%Y%m%d');

-> Aggregate: count(0)
     -> Index lookup on T1 using T_ORD_BIG_X1 (ORD_DT=str_to_date('20170104','%Y%m%d'))  (cost=92.53 rows=900)

 

'Index lookup' 역시 'Index range scan' 처럼 수직점 탐색과 수평적 탐색이 발생합니다. 그러므로 결국 실제 작동하는 내용은 Index range scan과 같다고 보시면 됩니다.

MySQL은 인덱스를 사용하는 상황에서, 사용된 조건이 범위(>=, <=, BETWEEN) 조건이면 Index range scan을, 같다(=) 조건이면 Index lookup으로 실행계획을 표시해줍니다.

 

이번에는 아래와 같이 SQL을 실행합니다. WHERE 절에서 ORD_DT 컬럼을 DATE_FORMAT 처리했기 때문에 인덱스를 '효율적'으로 사용 못하는 경우입니다.(인덱스를 사용은 하지만 비효율적인 방법으로 사용하게 됩니다.) 실행계획을 보면 'Index scan'이라고 표시되어 있습니다. 이 부분만 보면, '인덱스를 탔네'라고 생각할 수 있습니다. 하지만, 아래 경우는 인덱스의 리프 노드를 모두 읽어서 조건을 처리한 경우입니다. 

EXPLAIN FORMAT = TREE
SELECT  COUNT(*)
FROM    MYTUNDB.T_ORD_BIG T1
WHERE   DATE_FORMAT(T1.ORD_DT,'%Y%m%d') = '20170104'

-> Aggregate: count(0)
     -> Filter: (date_format(mytundb.t1.ORD_DT,'%Y%m%d') = '20170104')  (cost=31559.80 rows=301158)
         -> Index scan on T1 using T_ORD_BIG_X1  (cost=31559.80 rows=301158)

 

 

방금 살펴본, SQL 세 개를 EXPLAIN ANALYZE를 사용해 각각 실행해봅니다. 아래와 같은 실제 실행 계획을 확인할 수 있습니다.

1. 범위 조건 ANALYZE
 -> Aggregate: count(0)  (actual time=0.462..0.462 rows=1 loops=1)
     -> Filter: ((mytundb.t1.ORD_DT >= <cache>(str_to_date('20170104','%Y%m%d'))) and (mytundb.t1.ORD_DT < <cache>(str_to_date('20170105','%Y%m%d'))))  (cost=180.64 rows=900) (actual time=0.034..0.407 rows=900 loops=1)
         -> Index range scan on T1 using T_ORD_BIG_X1  (cost=180.64 rows=900) (actual time=0.032..0.241 rows=900 loops=1)

2. 같다(=) 조건 ANALYZE 
-> Aggregate: count(0)  (actual time=0.383..0.383 rows=1 loops=1)
     -> Index lookup on T1 using T_ORD_BIG_X1 (ORD_DT=str_to_date('20170104','%Y%m%d'))  (cost=90.63 rows=900) (actual time=0.042..0.316 rows=900 loops=1)
 
3. 컬럼변경(DATE_FORMAT) ANALYZE 
-> Aggregate: count(0)  (actual time=200.378..200.378 rows=1 loops=1)
     -> Filter: (date_format(mytundb.t1.ORD_DT,'%Y%m%d') = '20170104')  (cost=31559.80 rows=301158) (actual time=1.075..200.319 rows=900 loops=1)
         -> Index scan on T1 using T_ORD_BIG_X1  (cost=31559.80 rows=301158) (actual time=0.108..85.289 rows=304700 loops=1)

위 내용을 보면 '1. 범위 조건' 보다 '2. 같다(=) 조건'이 아주 아주 약간 더 빠릅니다.(각 실행계획 가장 위의 actual time) 거의 사람이 차이를 느낄 수 없는 속도일듯합니다. 실제 실행해 보면 1번과 2번 모두 0.000초 정도입니다. 다만, '1. 범위 조건'의 경우는 Filter 라는 단계가 실행계획 중간에 끼어 들어옵니다. '3. 컬럼 변경'의 경우만 0.2초 정도의 시간이 걸립니다. 

 

정리하면 아래와 같습니다.

  • 범위 조건을 인덱스로 처리하면, 실행 계획에 Index range scan 으로 표시된다.
  • 같다 조건을 인덱스로 처리하면, 실행 계획에 Index lookup 으로 표시된다.
  • Index range scan과 Index lookup은 수직적 탐색과 수평적 탐색이 발생하는 같은 작업이라고 볼 수 있습니다.
  • Index scan은 인덱스 리프 노드를 순차적으로 모두 읽는 작업이다.
  • 당연히, Index scan은 Index range scan이나 Index lookup 보다 성능이 좋지 못할 수 있다.
  • Index scan은 오라클의 Index full scan이다.
  • Index scan이 나왔다고 인덱스를 '효율적'으로 사용했다고 착각하지 말자.!

 

 

이정도일거 같습니다. 감사합니다.

 

 

앞에서 클러스터드 인덱스와 넌클러스터드 인덱스의 차이점에 대해서 설명했습니다.
잠시, 클러스터드와 넌클러스터드의 차이는 잠시 접어두고 인덱스를 이용해 데이터 찾는 과정을 살펴보겠습니다.

 

1. 인덱스 탐색

B Tree(B+Tree) 인덱스에서 조건에 맞는 값을 찾기 위해서는 두 단계의 과정으로 인덱스를 탐색합니다.

  • 1 단계: 루트 노드에서 리프 노드로 내려가기(수직적 탐색)
  • 2 단계: 리프 노드를 순차적으로 읽어 가기(수평적 탐색)

위의 과정을 그림으로 살펴보면 아래와 같습니다. 그림의 빨간색 화살표가 1 단계인 수직적 탐색이고, 파란색 화살표가 2 단계인 수평적 탐색입니다.

경우에 따라 2 단계는 생략될 수도 있습니다. 찾고자 하는 값이 한 건이거나, 극히 일부라면 2 단계 과정인 수평적 탐색은 생략될 수도 있습니다. (1 단계인 수직적 탐색으로 찾은 곳에 원하는 데이터가 모두 있다면, 수평적 탐색은 필요가 없는 경우죠.)

 

위 그림을 더 간략하게 그려보면 아래와 같습니다. 인덱스 동작 관련해서 자주 보게될 그림입니다.

 

 

방금 살펴본 설명과 그림이 바로, 우리가 흔이 말하는 '인덱스를 탔네'라고 말하는 것의 동작 방식입니다.
오라클의 실행 계획에서는 이와 같은 동작을 'INDEX RANGE SCAN'이라고 표현합니다.
반면에 MySQL에서는 실행 계획을 보는 방식과 상황에 따라 다양하게 표현이 됩니다. 관련해서는 다음 글에서 설명을 드릴 예정입니다.

 

위와  같은 인덱스 탐색 외에도, 인덱스 리프 노드를 차례대로 모두 읽어야 하는 경우가 있습니다. 해당 경우는 아래 그림과  같이 표시합니다. 오라클에서는 INDEX FULL SCAN이라고 동작하는 경우입니다.

 

 

오늘은 여기까지입니다. 감사합니다.

이번에는 클러스터드 인덱스의 구조를 살펴볼 차례입니다. 넌클러스터드와 클러스터 인덱스의 다음 차이점을 반드시 기억하시고 읽어주시기 바랍니다.

  • 넌클러스터드 인덱스: 리프 노드에 실제 데이터를 찾아 갈 수 있는 '주소 값'이나 '키 값'이 저장된 인덱스
  • 클러스터드 인덱스: 리프 노드에 실제 '데이터'가 저장된 인덱스

 

2. Clustered Index

T_ORD_BIG의 클러스터드 인덱스는 Primary 인덱스입니다.
앞에 글에서도 'SHOW INDEX FROM T_ORD_BIG' 을 실행했을 때, 'Key_name'이 'Primary'로 되어 있는 인덱스가 바로 클러스터드 인덱스라고 설명했습니다.

 

여기서 잠깐, MySQL에서는 인덱스라는 용어를 Key라는 용어와 혼용해서 사용합니다.

그러므로 'SHOW INDEX FROM'을 실행하면 인덱스의 이름 항목이 'Index_name'이 아닌 'Key_name'으로 나옵니다.

 

T_ORD_BIG의 클러스터드(Primary) 인덱스는 PK(Primary Key) 컬럼인 ORD_SEQ로 구성되어 있습니다.

MySQL의 InnoDB 스토리지 엔진에서, 테이블의 PK 컬럼은 무조건 클러스터드 인덱스로 만들어집니다.
다시 말해, '테이블 생성 = 클러스터드 인덱스 생성'이 됩니다. 우리는 테이블을 만들지만, 테이블의 데이터는 클러스터드 인덱스 구조로 관리가 되는겁니다.
아무리, 클러스터드 구조로 테이블을 만들기 싫어도 방법이 없습니다.

클러스터드 인덱스를 피하기 위해 PK를 설정하지 않아도, 내부적으로 6 byte의 Hidden 컬럼을 만들어 PK를 구성한다고 합니다.

(구글링을 해보면, 이와 같은 이야기가 나오는데, MySQL 공식 레퍼런스에서는 이와 같은 부분을 찾을수가 없네요. 혹시 MySQL 레퍼런스에서 보신분 있으면 알려주세요!^^)

 

T_ORD_BIG의 클러스터드 인덱스를 그려보면 아래 그림과 같습니다. 처음에 이야기했듯이, 인덱스의 리프 노드에는 실제 데이터가 저장되어 있는 것을 알 수 있습니다.

 

클러스터드 인덱스의 리프 노드에는 실제 데이터가 저장되어 있으므로, 테이블 별로 하나만 만들 수 있습니다. 넌 클러스터드는 하나의 테이블에 여러개 만들 수 있지만, 클러스터드는 무조건 하나만 만들 수 있으므로 클러스터할 컬럼 선택에 신중한 선택이 필요합니다. 하지만 MySQL은 무조건 PK 컬럼에 클러스터드를 설정하므로 선택의 폭이 제한적입니다.

 

클러스터드 인덱스와 같은 개념으로 오라클에는 IOT가 있습니다. 오라클의 IOT는 선택이라면, MySQL의 클러스터드 인덱스는 강제적입니다.

MS-SQL Server에도 클러스터드 인데스가 있습니다. MS-SQL Server에서 클러스터드 인덱스는 PK 컬럼이 아닌 곳에도 설정할 수 있습니다. 반면에 MySQL에서는 무조건 PK 컬럼에만 클러스터드 인덱스가 설정이 됩니다.

이와 같은 특징을 잘 이해하고 MySQL의 테이블을 설계할 필요가 있습니다.

 

 


넌클러스터드(Non-Clustered) 인덱스와 클러스터드(Clustered) 인덱스에 대해 알아볼 차례입니다. MySQL의 인덱스와 관련된 성능에서 가장 중요한 부분이 아닐까 싶습니다.

앞에서 B Tree 인덱스에 대해 설명드렸는데, B Tree는 인덱스를 만들때 사용하는 기본적인 자료구조입니다.
MySQL에서는(정확히는 MySQL의 InnoDB 스토리지 엔진) B Tree 인덱스를 넌클러스터드와 클러스터드로 세분화해서 사용할 수 있습니다. 넌클러스터드와 클러스터드는 리프 노드의 구성에 따라 구분됩니다. 아래와 같습니다.

  • 넌클러스터드 인덱스: 리프 노드에 실제 데이터를 찾아 갈 수 있는 '주소 값'이나 '키 값'이 저장된 인덱스
  • 클러스터드 인덱스: 리프 노드에 실제 '데이터'가 저장된 인덱스

 

1. Non Clustered Index

우선은 넌클러스터드인덱스를 설명하고, 다음 글에서 클러스터드 인덱스에 대해 살펴볼 예정입니다.
앞의 글에서 T_ORD_BIG의 ORD_DT 컬럼에 대해 인덱스를 생성했습니다. 이 인덱스는 넌클러스터드 인덱스입니다.

 

[MySQL튜닝]인덱스를 만들어 보자

테스트를 위한 DB 구성은 아래 글을 참고해 구성할 수 있습니다. https://sweetquant.tistory.com/m/346 RDBMS의 SQL 성능을 좌우하는 요소 세 가지를 뽑아보자면 아래와 같습니다. SQL문 자체 인덱스(INDEX) 통..

sweetquant.tistory.com

 

아래와 같이 SHOW INDEX FROM 을 사용해 현재 테이블에 생성된 인덱스를 확인할 수 있습니다.

SHOW INDEX FROM MYTUNDB.T_ORD_BIG;

위 SQL을 실행해 결과를 확인해 보면, T_ORD_BIG_X1 인덱스가 넌클러스터드인지, 클러스터드인지 구분되어 있지는 않습니다. 위 SQL 결과 중, Key_name 항목의 결과가 Primary면 클러스터드 인덱스이고 Primary가 아니면 모두 넌 클러스터드 인덱스로 생각하시면 됩니다.

 

MySQL에서는 인덱스를 생성할 때 클러스터드와 넌클러스터드를 지정할 수 없습니다.

테이블을 생성할 때 지정한 Primary Key 외에 추가로 생성하는 인덱스는 모두 넌클러스터드 인덱스 입니다.

T_ORD_BIG의 ORD_DT에 대한 인덱스를 그림으로 표현해보면 아래와 같습니다.

 

위 그림에서, 가장 아래 리프 노드를 보면, 인덱스 컬럼인 ORD_DT외에 ORD_SEQ 정보도 존재합니다.
ORD_SEQ는 T_ORD_BIG의 PK(Primary Key) 컬럼으로서 클러스터드 인덱스로 구성된 컬럼입니다.
이처럼 MySQL의 넌클러스터드 인덱스의 리프 노드에는 해당 인덱스의 컬럼과 함께 PK의 컬럼 값이 존재합니다.
(PK 컬럼 값은 실제 데이터를 찾기 위한 '키 값'으로 사용됩니다.)
반면에 오라클의 넌클러스터드 인덱스의 리프 노드에는 실제 데이터를 바로 찾아 갈 수 있는 주소 값(ROWID)이 저장되어 있습니다.

 

오늘은 여기까지입니다. 감사합니다.

 

 

 

 

MySQL의 인덱스 종류와 B Tree 인덱스에 대해 알아보도록 하겠습니다.

 

1. MySQL의 인덱스 종류

MySQL은 아래와 같은 구조의 인덱스를 제공합니다. 정확히는 MySQL의 스토리지 엔진중 하나인 InnoDB에서 사용할 수 있는 인덱스입니다. MySQL에는 InnoDB 외에도 다양한 스토리지 엔진도 사용할 수 있지만, 대부분 InnoDB를 사용합니다.

  • B Tree 인덱스: 원하는 데이터를 빠르게 검색하기 위한 인덱스
  • R Tree 인덱스(Spatial Index): 위치나 거리등의 검색에 효율적인 인덱스
  • Full text search 인덱스: 문자열 검색에 효율적인 인덱스


RDBMS에서 흔이 말하는 '인덱스' 대부분은 B Tree 구조의 인덱스입니다. (이 글에서도 특별히 언급하지 않으면 B Tree 인덱스를 뜻합니다.) 마찬가지로, 인덱스를 만들때 특별히 옵션을 주지 않으면 B Tree 구조의 인덱스가 만들어집니다.
앞에 글(인덱스 왜 빨라)에서 예를 든 구조(거래처 서류와 서류함) 역시 B Tree 인덱스입니다.

  - https://sweetquant.tistory.com/352

 

[MySQL튜닝]인덱스 왜 빨라?

'인덱스를 만들면 조회가 빨라지더라~'라는 것에 대해서는 이미 앞에서 살짝 테스트를 해봤습니다. 인덱스를 마스터하려면 인덱스의 종류, 구조등 다양한 내용을 공부해야 합니다. 이와 같은 복

sweetquant.tistory.com


실제 RDBMS에서 사용하는 인덱스 구조는 B Tree를 좀 더 발전시킨 B* Tree 또는 B+ Tree 입니다. B Tree, B* Tree, B+ Tree에 대한 차이는 아래 잘 정리된 블로그 글로 대신하도록 하겠습니다. 한 번쯤 읽어보시고 각자 정리해보시기 바랍니다. (사실 아래 글만으로 정리가 안될 수도 있습니다. 대략적인 차이를 이해하는 정도에서 넘어가시는게 좋습니다.)

  - https://wiper2019.tistory.com/301

  - https://ssocoit.tistory.com/217

 

아래의 MySQL 레퍼런스에 의하면, MySQL은 B Tree 구조를 사용한다고 설명되어 있습니다. B+나 B*를 직접적으로 언급하는 부분은 없는거 같습니다. 다만, 많은 책들과 자료에서 MySQL의 인덱스는 B+ Tree라고 설명되어 있습니다.(저 역시 B+ Tree가 맞다고 생각합니다.)

  - https://dev.mysql.com/doc/refman/8.0/en/innodb-physical-structure.html

 

MySQL :: MySQL 8.0 Reference Manual :: 15.6.2.2 The Physical Structure of an InnoDB Index

15.6.2.2 The Physical Structure of an InnoDB Index With the exception of spatial indexes, InnoDB indexes are B-tree data structures. Spatial indexes use R-trees, which are specialized data structures for indexing multi-dimensional data. Index records are

dev.mysql.com

 

2. B Tree 인덱스의 세부 명칭

아래 그림을 통해 B Tree(B+ Tree)를 구성하는 요소별 명칭을 정리해봅니다. 아래 그림은 앞의 글에서 거래처와 서류함을 B Tree 인덱스로 표현한 것으로, '거래처명'으로 B Tree 인덱스를 생성한 경우입니다.

 

 

 

Tree의  가장 위에는 트리 탐색을 시작하는 루트(Root, 뿌리) 노드가 위치합니다. 루트 노드는 하나의 인덱스에 오직 하나만 존재합니다. 루트 아래에는 브런치(Branch, 가지, 또는 미들(Middle)) 노드가 위치합니다. 브런치 노드는 루트에서 가장 아래에 있는 리프(Leaf) 노드를 찾아가는 중간 노드로서, 여러 층이 존재할 수 있습니다.(그림에서는 편의상 한 층만 표현했습니다.) 마지막으로 Tree의 가장 아래에는 리프(Leaf, 잎사귀) 노드가 존재합니다. 리프에는 실제 데이터가 존재하거나 실제 데이터가 위치한 주소 값을 저장합니다. 그리고 리프 노드를 보면 왼쪽부터 알파벳 순서대로 데이터가 입력되어 있으며 근접한 리프 노드끼리는 서로 연결되어 있습니다.

그림을 전체적으로 보면, 나무 뿌리가 가장 위에 있고, 나무 잎사귀가 가장 아래에 있습니다. 나무를 뒤집어서 그린 모양이라고 이해하시면 됩니다.

 

B Tree 인덱스(B+ Tree) 관련해서 중요한 내용을 정리해보면 아래와 같습니다.

  • '루트(뿌리), 브런치(가지, 미들), 리프(잎)' 세 개의 요소로 나누어진다.
  • 리프에는 실제 데이터 또는 실제 데이터가 존재하는 주소 값을 가지고 있다.
  • 리프 노드의 데이터는 정렬되어 있다.
  • 근접한 리프 노드는 서로 연결되어 있다.

 

오늘 살펴볼 내용은 여기까지입니다. B Tree와 같은 자료 구조 이야기가 나와 조금 어려울 수 있지만, 한 번쯤은 깊이 공부해볼 필요가 있는 내용입니다.

감사합니다.

 

 

'인덱스를 만들면 조회가 빨라지더라~'라는 것에 대해서는 이미 앞에서 살짝 테스트를 해봤습니다.
인덱스를 마스터하려면 인덱스의 종류, 구조등 다양한 내용을 공부해야 합니다. 이와 같은 복잡한 내용은 천천히 기회가 될 때 살펴보도록 하고, 우선은 '왜 .. 인덱스를 만들면 빨라지는거야?'에 대해서 개념을 살짝 잡아볼까 합니다.

1. 인덱스 왜 빨라?

회사에 거래처가 아래 그림 좌측과 같이 12개가 있다고 가정해 보겠습니다.
거래처별 거래 서류를 정리하기 위해, 아래 그림 오른쪽과 같은 네 칸으로 구분된 거래처 서류함을 사용하려고 합니다.
'나신입'님께 정리를 부탁했더니, 거래처 서류를 아무렇게나 세 개씩 나누어서 서랍에 넣습니다.
다시 말해, Airbnb 서류가 1번 칸에 있는지, 2번 칸에 있는지 알 수가 없습니다. 필요한 거래처 서류가 어디있는지는 서랍을 열어야만 알수 있는 상황이 된거죠.
필요한 거래처 서류를 운이 좋으면 한 번에 찾을 수 있지만, 운이 나쁘면 서랍 네개를 모두 열어야만 찾을 수 있습니다.


필요한 서류 하나를 찾을 때마다 서랍을 몇 번씩 열고 닫아야 합니다. 매우 비효율적이죠. 거기에 서랍을 열고 닫는 소리도 시끄럽고요.

도저히 참을 수 없어 '나경력'님께 재정리를 부탁해 봅니다.
'나경력'님은 거래처 서류를 모두 꺼내서 알파벳 순서대로 정렬 한 후에, 세 개씩 나누어서 다시 서류함 서랍에 넣습니다. 그리고 서류함 서랍마다 어떤 알파벳으로 시작하는 거래처 서류가 있는지 라벨링합니다.아래 그림과 같이 말이죠


위 그림과 같이 정리한 결과, 필요한 거래처 서류를 찾기 위해서는 서랍을 한 번만 열면 됩니다. Airbnb 거래처 서류는 당연히 1번 서랍에 있고, Tesla 서류는 당연히 4번 서랍에 있다는 것을 누구나 알 수 있습니다.(알파벳 순서만 알고 있다면요)
다시 정리해보면, 거래처 서류를 알파벳 순서로 정렬해 차례대로 서랍에 넣는다. 그리고 서랍에는 라벨링을 해 놓는다.
이것이 바로 인덱스의 원리입니다. "정렬된 구조로 만들어서 필요한 데이터를 빠르게 찾을 수 있게 만든다."
이 정도면, 인덱스 구조가 왜 빠른가에 대한 이해가 되지 않을까 싶습니다.

마지막으로 아래 그림을 살펴볼까요. 정렬된 서류와 서류함을 옆으로 돌리면 삼각형 구조의 모양이 되는 것을 알 수 있습니다. 이처럼 성능 관련해서 인덱스를 설명할 때는 삼각형 도형을 많이 사용한다는 것도 알아놓으시면 좋을거 같습니다.



오늘은 여기까지입니다. 감사합니다.

 

MySQL에서 테이블에 어떤 인덱스가 있고, 인덱스를 생성하거나 제거할 때 어떤 문법으로 처리하는지 살펴보겠습니다.

 

1. 인덱스 확인하기

인덱스는 테이블에 종속적입니다. 아래와 같은 명령어로 해당 테이블에 어떤 인덱스가 있는지 확인할 수 있습니다.

SHOW INDEX FROM MYTUNDB.T_ORD_BIG;

 

2. 인덱스 생성하기

MySQL에서는 인덱스를 생성하는 두 가지 문법이 있습니다. 하나는 CREATE INDEX이고, 또다른 하나는 ALTER TABLE을 통해 테이블에 인덱스를 추가하는 방법입니다. 아래와 같습니다. (아마도 이미 인덱스가 있다면 생성이 안되고 에러가 나겠죠.)

# CREATE INDEX
CREATE INDEX T_ORD_BIG_X1 ON MYTUNDB.T_ORD_BIG(ORD_DT);

# ALTER TABLE로 인덱스 추가
ALTER TABLE MYTUNDB.T_ORD_BIG ADD INDEX T_ORD_BIG_X1(ORD_DT);

 

3. 인덱스 제거하기

인덱스를 제거하는 문법 역시 두 가지입니다. 아래와 같습니다.

# DROP INDEX
DROP INDEX T_ORD_BIG_X1 ON MYTUNDB.T_ORD_BIG;

# ALTER TABLE로 인덱스 제거
ALTER TABLE MYTUNDB.T_ORD_BIG DROP INDEX T_ORD_BIG_X1;

 

 

인덱스 관련된 자세한 문법은 아래 레퍼런스를 참고해주세요.

 

MySQL :: MySQL 8.0 Reference Manual :: 13.1.15 CREATE INDEX Statement

13.1.15 CREATE INDEX Statement CREATE [UNIQUE | FULLTEXT | SPATIAL] INDEX index_name [index_type] ON tbl_name (key_part,...) [index_option] [algorithm_option | lock_option] ... key_part: {col_name [(length)] | (expr)} [ASC | DESC] index_option: { KEY_BLOC

dev.mysql.com

 

읽어주셔서 감사합니다.~!

 

 

 

테스트를 위한 DB 구성은 아래 글을 참고해 구성할 수 있습니다.

 

RDBMS의 SQL 성능을 좌우하는 요소 세 가지를 뽑아보자면 아래와 같습니다.

  • SQL문 자체
  • 인덱스(INDEX)
  • 통계 정보

 

이번 글에서는 세 가지 요소 중에 인덱스에 대해 살짝 맛을 보도록 하겠습니다. 인덱스의 개념에 대해서는 아마도 다음 글에 설명이 될거 같습니다.

 

1. INDEX를 만들어보자.

인덱스는 RDBMS에서 데이터를 빠르게 찾을 수 있도록 해주는 객체입니다. 보통은 테이블별로 인덱스를 여러 개 만들 수 있으며, 데이터를 접근하는 주요 패턴을 분석해 인덱스를 설계합니다.

데이터를 접근하는 주요 패턴을 분석한다는 것을 "아주 간단히" 이야기 하자면, SQL의 WHERE 절이나, 조인절에서 사용된 조건 컬럼을 분석하는 것입니다. ("아주 간단히"를 강조한 이유는, 실전에서 인덱스 설계는 그렇게 간단한 문제가 아니기 때문입니다.)

 

아래와 같은 SQL이 있다고 해보죠.

SELECT  COUNT(*) ORD_CNT
FROM    MYTUNDB.T_ORD_BIG T1
WHERE   T1.ORD_DT >= STR_TO_DATE('20170201','%Y%m%d')
AND     T1.ORD_DT < STR_TO_DATE('20170202','%Y%m%d');

위 SQL을 EXPLAIN ANALYZE를 통해 실제 실행계획을 추출해보면 아래와 같습니다. 아래 실행계획에서 가장 윗 줄에 actual time이 해당 SQL의 총 실행시간입니다. 535.476 ~ 535.478 밀리세컨드(0.53초)가 걸렸습니다.

-> Aggregate: count(0)  (actual time=535.476..535.478 rows=1 loops=1)
     -> Filter: ((mytundb.t1.ORD_DT >= <cache>(str_to_date('20170201','%Y%m%d'))) and (mytundb.t1.ORD_DT < <cache>(str_to_date('20170202','%Y%m%d'))))  (cost=31559.80 rows=33455) (actual time=535.463..535.463 rows=0 loops=1)
         -> Table scan on T1  (cost=31559.80 rows=301158) (actual time=1.767..418.311 rows=304700 loops=1)

 

 

위 SQL에서 WHERE절에 사용된 조건은 T_ORD_BIG의  ORD_DT 컬럼입니다. 그러므로 T_ORD_BIG에 ORD_DT에 인덱스를 만들어 성능 개선을 고려할 수 있습니다. 실제로 인덱스를 만들어보고 성능이 개선되는지 확인해봅니다.

아래 구문을 통해 T_ORD_BIG_X1이라는 인덱스를 생성합니다. T_ORD_BIG_X1은 MYTUNDB.T_ORD_BIG 테이블의 ORD_DT로 구성된 인덱스입니다.

CREATE INDEX T_ORD_BIG_X1 ON MYTUNDB.T_ORD_BIG(ORD_DT);

 

인덱스를 생성했다면, 다시 위의 SELECT SQL을 EXPLAIN ANALYZE로 실행해봅니다. 아래와 같은 실행계획이 나오는 것을 알 수 있습니다. 전체 실행 시간인, 가장 위의 actual time이 0.045 밀리세컨드(0.000045초) 밖에 안되는 것을 알 수 있습니다.

-> Aggregate: count(0)  (actual time=0.045..0.046 rows=1 loops=1)
     -> Filter: ((mytundb.t1.ORD_DT >= <cache>(str_to_date('20170201','%Y%m%d'))) and (mytundb.t1.ORD_DT < <cache>(str_to_date('20170202','%Y%m%d'))))  (cost=1.21 rows=1) (actual time=0.041..0.041 rows=0 loops=1)
         -> Index range scan on T1 using T_ORD_BIG_X1  (cost=1.21 rows=1) (actual time=0.039..0.039 rows=0 loops=1)

 

이처럼, WHERE 절의 조건에 인덱스를 잡는 것만으로, 아래와 같이 성능 개선이 되었습니다.

  • 개선전후 시간 : 0.53초 -> 0.000045초

 

지금 살펴본 내용으로 아래 정도의 개념을 알 수 있습니다. 

  • 인덱스를 만들면 조회 속도가 빨라진다.
  • WHERE 절의 조건 컬럼에 인덱스를 고려한다.

 

하지만, 이 개념과 지식만으로 인덱스를 만들어대기 시작하면 매우 위험해집니다. (시스템이 오히려 문제가 생길 수 있습니다.) 실전에서 튜닝을 하고, 인덱스 설계를 위해서는 더 많은 경험과 공부가 필요합니다.

 

오늘은 여기까지입니다. 감사합니다.

 

 

 

오늘은 MySQL에서 실제 실행계획을 확인하는 법을 살펴보겠습니다. 실제 성능 개선에 있어서, 개선전, 개선후 결과를 측정하기에 매우 유용한 방법이고, 성능 개선에 가장 도움이 되는 실행 계획을 보는 방법이 아닐까 싶습니다.

 

연습과 테스트를 위한 DB 구성이 필요하다면 아래 글을 참고해주세요.

 

1. EXPLAIN ANALYZE

MySQL에서 실제 실행된 SQL의 실행계획을 보기 위해서는 EXPLAIN ANALYZE를 사용합니다. 아래와 같이 EXPLAIN ANALYZE를 추가해서 SQL을 실행하면, SQL이 실제 실행이 되면서, 실제 사용한 실행계획을 확인할 수 있습니다. (이전글에서 살펴본 실행계획은 예상 실행계획입니다.)

EXPLAIN ANALYZE
SELECT  T2.CUS_ID, T2.CUS_NM ,T1.*
FROM    MYTUNDB.T_ORD_BIG T1
		INNER JOIN MYTUNDB.M_CUS T2 ON (T2.CUS_ID = T1.CUS_ID)
WHERE   T1.ORD_SEQ <= 10;

위 SQL을 실행하면, SQL의 결과대신에 아래와 같은 실행계획이 출력됩니다. 실행 계획만 출력되지만 실제 SQL은 실행된 것과 다름없습니다.

-> Nested loop inner join  (cost=14.03 rows=10) (actual time=0.871..0.898 rows=10 loops=1)
     -> Filter: (mytundb.t1.ORD_SEQ <= 10)  (cost=3.03 rows=10) (actual time=0.074..0.089 rows=10 loops=1)
         -> Index range scan on T1 using PRIMARY  (cost=3.03 rows=10) (actual time=0.070..0.080 rows=10 loops=1)
     -> Single-row index lookup on T2 using PRIMARY (CUS_ID=mytundb.t1.CUS_ID)  (cost=1.01 rows=1) (actual time=0.080..0.080 rows=1 loops=10)

아래의 MySQL 블로그를 통해 실제 실행계획에서 얻을 수 있는 정보를 알 수 있습니다.

https://dev.mysql.com/blog-archive/mysql-explain-analyze/

 

위 블로그에서 중요한 부분만 정리하면, 아래의 내용일 거 같습니다.

  • Actual time to get first row (in milliseconds)
  • Actual time to get all rows (in milliseconds)
  • Actual number of rows read
  • Actual number of loops

오늘 살펴볼 내용은 여기까지입니다.

 

추가로, 제가 직접 만든 무료 MySQL 튜닝툴을 소개합니다.~

https://sweetquant.tistory.com/457

 

[StartUP Tuning]MySQL,MariaDB SQL 튜닝툴

MySQL과 MariaDB의 SQL 튜닝을 보다 간편하게 진행할 수 있는, SUT(StartUP Tuning)를 소개합니다. SUT는 실제 MySQL 튜닝을 전문적으로 수행하기 위해 직접 만들 툴입니다. 또한 MySQL,MariaDB의 성능 개선 교육

sweetquant.tistory.com

 

 

 

 

* 이 글에서 사용하는 MySQL의 버젼은 8.0.22 입니다.

 

MySQL을 설치하면 Workbench라는 SQL 툴이 같이 설치됩니다. Workbench에서는 SQL의 실행계획을 그래픽으로 Visual하게 확인할 수도 있습니다.

 

1. Visual하게 실행 계획 보기

쿼리 창에, SQL을 입력한 후에 Workbench의 아래 메뉴를 실행합니다.

  - Workbench 상단 메뉴 중에 Query 선택 > Explain Current Statement 를 선택

  - 또는 Ctlr + Alx +  X 를 바로 누르셔도 됩니다.

SELECT  T2.CUS_ID, T2.CUS_NM ,T1.*
FROM    MYTUNDB.T_ORD_BIG T1
        INNER JOIN MYTUNDB.M_CUS T2 ON (T2.CUS_ID = T1.CUS_ID)
WHERE   T1.ORD_SEQ <= 10;

SQL을 입력한 후에 Explain Current Statement를 선택하면 아래와 같이 Visual하게 실행계획을 확인할 수 있습니다. Tree 형태나 표 형태로 볼때보다 훨씬 더 쿼리가 처리되는 내부적인 흐름을 보기에 유용합니다. 아래의 각 단계에 마우스를 가져다 대면 추가적인 정보도 확인할 수 있습니다. 보여지는 내용을 보면, 'EXPLAIN Format = JSON'의 내용을 요약해서 보여주는거 같습니다.

 

튜닝 관련해서는 오라클 쪽에서 많이 진행되다 보니, 튜너들에게는 위와 같은 형식 보다는 Tree 형태의 실행계획을 더 선호합니다. 어떤 방식으로 보든, 성능 개선이라는 결과를 얻을 수 있다면 각자 편한 형태로 보시는 걸 추천드립니다.

오늘은 여기까지입니다. 감사합니다.

 

 

 

 


SQL 튜닝을 위해서는 실행 계획이라는 것을 볼 줄 알아야 합니다.
실행 계획은 RDBMS의 옵티마이져(쿼리최적화기)가 SQL을 DBMS 내부적으로 어떻게 처리하겠다고 만들어 놓은 작업 계획서입니다.
옵티마이져가 생성한 실행계획에 따라 SQL의 성능은 달라집니다. 옵티마이져는 SQL과 테이블의 통계, 인덱스 등을 참고해 최적의 실행계획을 만드려고 노력합니다.
우선은 MySQL에서 실행계획을 어떻게 볼 수 있는지 정도만 알고 넘어가면 될거 같습니다.

테스트를 위한 DB 구성은 아래 글을 참고해 구성할 수 있습니다.
- https://sweetquant.tistory.com/m/346

1. EXPLAIN

MySQL에서 제공하는 가장 고전적인 방법입니다. EXPLAIN 뒤에 쿼리를 적어 놓고 실행하면 됩니다.

EXPLAIN
SELECT  T2.CUS_ID, T2.CUS_NM ,T1.*
FROM    MYTUNDB.T_ORD_BIG T1
        INNER JOIN MYTUNDB.M_CUS T2 ON (T2.CUS_ID = T1.CUS_ID)
WHERE   T1.ORD_SEQ <= 10;

위와 같이 쿼리를 실행하면 아래와 같이 표형태로 실행계획이 출력됩니다. 오라클과 같은 환경에서 튜닝을 하던 분들께는 매우 생소한 스타일로 실행계획이 표현됩니다. 아래 실행계획을 이해하려면 각각의 항목을 이해해야 하는데, 이에 관련해서는 기회가 된다면 나중에 별도로 다루도록 하겠습니다.

2. EXPLAIN FORMAT = JSON

EXPLAIN 에 JSON 포맷을 지정할 수 있습니다. 아래와 같이 실행하면 JSON 형태의 실행계획이 나오면, EXPLAIN으로만 확인할 수 없는 다양한 정보도 같이 확인이 가능합니다. 다만, 정보가 너무 많고 길어서 비효율적입니다.

EXPLAIN FORMAT = JSON
SELECT  T2.CUS_ID, T2.CUS_NM ,T1.*
FROM    MYTUNDB.T_ORD_BIG T1
		INNER JOIN MYTUNDB.M_CUS T2 ON (T2.CUS_ID = T1.CUS_ID)
WHERE   T1.ORD_SEQ <= 10;

3. EXPLAIN FORMAT = TREE

MySQL 8부터는 Tree 형태의 실행계획을 제공합니다. 오라클을 주로 튜닝하셨던 분들께는 매우 유용한 기술입니다.

EXPLAIN FORMAT = TREE
SELECT  T2.CUS_ID, T2.CUS_NM ,T1.*
FROM    MYTUNDB.T_ORD_BIG T1
		INNER JOIN MYTUNDB.M_CUS T2 ON (T2.CUS_ID = T1.CUS_ID)
WHERE   T1.ORD_SEQ <= 10;

아래와 같은 Tree 형태의 실행계획이 나옵니다.

-> Nested loop inner join  (cost=14.03 rows=10)
     -> Filter: (mytundb.t1.ORD_SEQ <= 10)  (cost=3.03 rows=10)
         -> Index range scan on T1 using PRIMARY  (cost=3.03 rows=10)
     -> Single-row index lookup on T2 using PRIMARY (CUS_ID=mytundb.t1.CUS_ID)  (cost=1.01 rows=1)


Tip.
MySQL Workbench에서 Tree 형태나 Json 형태로 실행계획을 확인해보면, 실행계획의 내용이 모두 출력 안될 수도 있습니다. MySQL Workbench에서 출려 가능한 컬럼의 Bytes를 설정하기 때문입니다. 아래와 같은 과정으로 컬럼(Field)에서 보여줄 수 있는 길이의 제한을 풀어줘야 합니다.
- Workbench 상단 메뉴 중 Edit -> Preferences... 선택 -> 좌측에서 SQL Editior > SQL Execution 선택 >
- Max. Field alue Lengh to Display (in Bytes): 를 9999999 로 설정

MySQL 튜닝에 대해서 적어볼까 합니다.

튜닝을 위해서는 MySQL DBMS 설치와 데이터베이스를 구성해야 합니다.

 

 

1. DBMS 설치

MySQL 튜닝 관련된 글에서는 MySQL 8.0.22 윈도우즈 버젼을 사용합니다. 설치 파일은 아래 경로에서 다운로드 할 수 있습니다.

 

https://dev.mysql.com/downloads/mysql/

 

MySQL :: Download MySQL Community Server

Select Operating System: Select Operating System… Microsoft Windows Ubuntu Linux Debian Linux SUSE Linux Enterprise Server Red Hat Enterprise Linux / Oracle Linux Fedora Linux - Generic Oracle Solaris macOS Source Code Select OS Version: All Windows (x86

dev.mysql.com

현재(22년 10월) 최신 버젼은 8.0.31 이네요. 이 버젼을 다운로드해서 설치해도 상관 없을거 같지만, 버젼을 정확히 맟주고 싶다면, 아래 사이트에서 8.0.22 버젼을 찾아서 받으시면 됩니다.

https://downloads.mysql.com/archives/community/

 

MySQL :: Download MySQL Community Server (Archived Versions)

Please note that these are old versions. New releases will have recent bug fixes and features! To download the latest release of MySQL Community Server, please visit MySQL Downloads. MySQL open source software is provided under the GPL License.

downloads.mysql.com

 

MySQL 8 설치 과정은 대부분 Next 버튼을 누르면 될만큼 간단합니다. 그래도 궁금하다면 아래 글을 참고해주세요.

https://sweetquant.tistory.com/230

 

2.2 MySQL 8 설치하기

여기서는 Windows 기준으로 MySQL 8을 다운로드하고 설치하는 과정을 설명한다. (MySQL은 5.7 버전에서 5.8 버전으로 올라가면서 MySQL 5.8이 아니라 MySQL 8로 부르고 있다.) MySQL 8이 이미 설치되어 있다면,

sweetquant.tistory.com

 

2. 데이터베이스 생성하기

설치한 MySQL에 접속해서 데이터베이스를 생성합니다. 여기서는 아래와 같이 MYTUNDB 라는 이름으로 데이터베이스를 생성합니다.

CREATE DATABASE MYTUNDB;

 

데이터베이스를 생성한 후에는, 테이블 생성과 데이터 입력이 필요합니다. 아래 첨부파일을 다운로드해서 쿼리 툴에서 실행하시면 됩니다.

MySQL튜닝BOOSTER_DB구성.sql
1.04MB

위 파일을 다운로드해서 실행하면 아래와 같은 테이블들이 만들어집니다.

  - M_ITM: 아이템(상품) 마스터

  - M_CUS: 고객

  - T_ORD: 주문

  - T_ORD_DET: 주문 상세

  - T_ORD_BIG: 주문 (튜닝 연습에 적합한 Big 테이블)

 

위 테이블들은 저의 전작인 SQL BOOSTER 에서 사용하는 테이블입니다. 데이터 역시 동일합니다. 다만, SQL BOOSTER는 오라클용이었고, 현재 올린 스크립트는 MySQL용입니다.

 

MySQL 튜닝 연습을 위한 사전 준비는 여기까지입니다.

 

알아놓으면 좋을, MySQL의 프로시저 소스 확인 하는 법입니다.

사용하는 툴에 따라서, 오브젝트 탐색기를 사용하시면 아주 쉽게 확인할 수 있겠지만,

명령어나 INFORMATION_SCHEMA의 오브젝트를 통해서도 확인할 수 있습니다.

1. SHOW 명령어로 확인하기.

  - SHOW 명령어 뒤에 CREATE PROCEDURE라고 명령해주셔야 합니다.

SHOW CREATE PROCEDURE [프로시저명];

2. INFORMATION_SCHEMA의 오브젝트를 조회해 확인하는 방법입니다.

  - 아래와 같이 ROUTINES을 조회하시면 됩니다.

SELECT T1.ROUTINE_DEFINITION
FROM   INFORMATION_SCHEMA.ROUTINES T1
WHERE  T1.ROUTINE_SCHEMA = [스키마명]
AND    T1.ROUTINE_NAME = [프로시저명];

 

이상입니다.

 

추가로, 제가 직접 만든 무료 MySQL 튜닝툴을 소개합니다.~

https://sweetquant.tistory.com/457

 

[StartUP Tuning]MySQL,MariaDB SQL 튜닝툴

MySQL과 MariaDB의 SQL 튜닝을 보다 간편하게 진행할 수 있는, SUT(StartUP Tuning)를 소개합니다. SUT는 실제 MySQL 튜닝을 전문적으로 수행하기 위해 직접 만들 툴입니다. 또한 MySQL,MariaDB의 성능 개선 교육

sweetquant.tistory.com

 

 

Windows용 MySQL 8 설치 과정을 설명합니다.

 

아래 주소에서 Windows 버젼의 MySQL8 Community Server 버젼을 다운 받습니다.

(64bit 윈도우 사용자도 32bit 버젼을 받아서 설치하시면 됩니다.)

해당 버젼에는 MySQL에 접속해서 SQL을 실행할 수 있는 MySQL Workbench도 포함되어 있습니다.

https://dev.mysql.com/downloads/windows/installer/8.0.html

 

 

다음부터는 설치 화면입니다. 대부분 Next나 Execute를 눌러주시면 됩니다.

아래 사항만 주의해 주세요.

- 'Check Requirements' 에서 'Microsoft Visual C++ 2019 redi~'가 필요사항으로 되어 있으면, 정상적으로 설치가 되지 않습니다. 'Check Requirements'에서 'Microsoft Visual C++ 2019 redi~'를 선택한 후에 Execute를 누른후 해당 항목을 설치후 MySQL을 재설치하도록 해주세요.

 

1. Setup Type을 선택합니다.

- Developer Default로 해주세요.(Fig.1)

Fig.1 Setup Type 선택

 

 

2. Check Requirements

- 'Microsoft Visual C++ 2019 Redist..'가 없으면 MySQL을 제대로 설치할 수 없습니다.

- 'Microsoft Visual C++ 2019 Redist..'를 선택하신 후 Execute를 눌러 해당 항목을 다운받아 설치합니다.

- 'Microsoft Visual C++ 2019 Redist..'가 설치 된 후 MySQL 설치를 계속해 나갑니다.

Fig.2 Execute로 Microsoft Visual C++2019 Red...를 설치

 

 

Fig.3 - 위의 두 항목은 설치가 필요 없습니다.

 

 

Fig.4 - 일부 항목이 없어도 괜찮습니다. Yes로 넘어갑니다.

 

 

3. Installation

- Execute로 설치를 진행합니다.

- 모두 설치 된 후 Next버튼으로 넘어갑니다.

  (Connector/Python (3.7) 은 설치 안되어도 상관 없습니다.)

Fig.5 - Execute

 

4. Product Configuration

Fig.6 - Next

 

 

5. High Availabilty

- Next 눌러 주시면 됩니다.

Fig.7 - Next

 

 

6. Type and Networking

- 역시 Next 눌러주시면 됩니다.

- 여기서, Port가 3306으로 되어 있는걸 기억해 주세요.

- MySQL의 기본포트가 3306입니다.

- 필요에 따라 Port를 변경하셔도 됩니다.

Fig.8 - Type and Networking

 

 

7. Authentication Method

- 인증모드를 설정합니다. Use Strong Password가 기본입니다.

- 저 같은 경우 노트북에서만 사용할 것이므로 보다 편한 Use Legacy로 설정했습니다.

Fig.9 Authentication Method

 

8. Accounts and Roles

- Root의 Password을 설정합니다.

- 기억할 수 있는 Password로 설정해 주세요.

- 저는 1qaz2wsx를 사용했습니다.(좋은 방법은 절대 아닙니다.)

- 여기서 일반 사용자를 설정할 수도 있지만 넘어갑니다.

Fig.10 - Accounts and Roles

 

9. Windows Service

- Next를 클릭합니다

Fig.11 - Windows Service

 

10. Apply Configuration

- Execute 눌러주신 후 실해 완료되면, Finish 눌러주시면 됩니다.

Fig.12 - Apply Configuration

 

 

11. Connect To Server

- 계속 Next버튼을 누르시다, 아래 화면에서만 설정했던 Root의 Password를 입력해 Check 를 눌러주시고 Next로 넘어가면 됩니다.

Fig.13 Connect To Server

 

Fig.14 Connect To Server - Check

 

12. Apply Configuration

- Execute 누르신 후 Finish 누르시면 됩니다.

- 나머지 단계들은 모두 Execute나 Next 또 Finish 누르시면 됩니다.

Fig.15 - Apply Configuration

 

 

 

 

 

 

MySQL Workbench CE에서 SQL을 실행하다 보면 아래와 같은 에러가 나오는 경우가 있습니다.

 - Lost connection to MySQL server during query 30.000 sec

 

이는 Workbench에서 오래 걸리는 SQL을 자동으로 끊기 때문입니다.

Workbench의 설정을 변경해야 합니다.

 

Workbench 상단의 Edit 메뉴의 Preferences를 선택합니다. 아래와 같은 화면이 나옵니다.

 

SQL Editor 메뉴를 선택해, DBMS Connection read timeout interval(in seconds) 부분이 30으로 설정되어 있습니다. 이 부분을 999999로 늘려주시면 됩니다~!

 

 

추가로, 제가 직접 만든 무료 MySQL 튜닝툴을 소개합니다.~

https://sweetquant.tistory.com/457

 

[StartUP Tuning]MySQL,MariaDB SQL 튜닝툴

MySQL과 MariaDB의 SQL 튜닝을 보다 간편하게 진행할 수 있는, SUT(StartUP Tuning)를 소개합니다. SUT는 실제 MySQL 튜닝을 전문적으로 수행하기 위해 직접 만들 툴입니다. 또한 MySQL,MariaDB의 성능 개선 교육

sweetquant.tistory.com

 

MySQL을 설치하고 새로운 사용자를 만들어 접속을 하려 할 때 접속이 안될 때가 있습니다. 보통 이런 경우, Windows의 방화벽 설정에 문제가 있을 수 있습니다.

 

ROOT가 아닌 일반 사용자가 접속이 안된다면 아래 과정을 참고해 방화벽 포트 허용 설정을 추가합니다. [그림. 1]과 [그림. 2]를 참고해서 진행해 주세요.

 1. Windows 메뉴를 누른 후 '고급 보안'을 검색해 '고급 보안이 포함된 Windows Defender'를 실행합니다.

 2. 인바운드 규칙을 선택합니다.

 3. 새 규칙을 선택합니다.

 4. 포트를 선택 > 포트에 3306 입력 > 연결 허용 선택 > 규칙 시기, 모두 체크 > 규칙명 임의로 입력

 

[그림. 1]

 

 

 

[그림. 2]

 

이와 같은 설정이 끝난 후에, 만들어진 일반 사용자로 다시 접속해보시기 바랍니다.

 

MySQL에서 실행계획을 확인하기 위해서는 EXPLAIN을 사용합니다.

 

아래와 같이 EXPLAIN을 SELECT SQL 앞에 붙여서 실행합니다. 실행하면, SQL은 실제 실행되지 않고, 예상 실행계획이 출력됩니다.

EXPLAIN
SELECT  T2.CUS_GD
        ,COUNT(*)
FROM    T_ORD_BIG T1
        INNER JOIN M_CUS T2
          ON  (T2.CUS_ID = T1.CUS_ID)
WHERE   T1.ORD_YMD LIKE '201703%'
GROUP BY T2.CUS_GD;

# 결과
id   select_type   table   partitions   type     possible_keys                                             key             key_len   ref                         rows     filtered   Extra                                       
==== ============= ======= ============ ======== ========================================================= =============== ========= =========================== ======== ========== =========================================== 
1    SIMPLE        T1      None         range    X_T_ORD_BIG_1,X_T_ORD_BIG_3,X_T_ORD_BIG_4,X_T_ORD_BIG_5   X_T_ORD_BIG_5   35        None                        360858   100.0      Using where; Using index; Using temporary   
1    SIMPLE        T2      None         eq_ref   PRIMARY                                                   PRIMARY         162       db_mysqlbooster.T1.CUS_ID   1        100.0      None                                        

 

오라클과 같은 Tree 형태가 아니어서 해석이 쉽지 않습니다. 아래와 같은 내용들을 기억하고 실행계획을 해석해야 합니다.

 

* id : 해당 단계의 ID입니다. 값이 작을 수록 먼저 실행된다고 해석할 수 있습니다.

* select_type : 해당 단계의 쿼리 유형을 나타냅니다.
   - SIMPLE :  단순한 SELECT를 나타냅니다. 가장 흔하게 나타는 경우입니다.
   - PRIMARY : UNION이나 서브쿼리 존재시에 가장 바깥쪽 쿼리를 뜻합니다.
   - UNION : UNION이 존재하는 쿼리에서 두 번째 UNION 이후의 쿼리 블록입니다.
   - SUBQUERY : SELECT 절 또는 WHERE 절의ㅣ 서브쿼리입니다.
   - DERVIED : FROM절의 서브쿼리입니다.(인라인-뷰) 

   - DEPENDEN SUBQUERY : SELECT, WHERE 절 서브쿼리가 바깥쪽 SELECT에 컬럼을 사용할 때입니다.
* table : 해당 단계의 관련 테이블입니다.
* type :  해당 단계의 접근 유형입니다.
   - const = PK, UNIQUE KEY로 한 건만 조회 하는 경우입니다. (오라클의 INDEX UNIQUE SCAN)
   - ref = INDEX RANGE SCAN 인데, 같다(=) 조건을 처리하는 경우입니다.
   - range = INDEXRANGE SCAN 인데, 범위 조건입니다.
   - index = INDEX FULL SCAN , 이 부분을 가장 유의해야 합니다. 
   - ALL = TABLE ACCESS FULL(TABLE FULL SCAN)

    * 위에서 type이 index나 ALL이면 성능에 문제가 있을 수 있습니다.
   - eq_req = 조인에서 후행 접근하는 쪽에 나타납니다. INDEX UNIQUE SCAN 정도로 이해하면 됩니다.
* possible_keys : 후보 인덱스 목록입니다.
* key : 실제 사용한 인덱스
* key_len : 인덱스를 몇 바이트 사용했는지 표시합니다. 바이트에 따라, 몇 개 컬럼에 인덱스가 적용되었는지 알 수 있습니다.
* rows : 예측 레코드 건수입니다.

 

추가로, MySQL에서 실행계획을 볼때는 JSON 형태로도 출력이 가능합니다. 아래와 같습니다.

EXPLAIN FORMAT = JSON
SELECT  T2.CUS_GD
        ,COUNT(*)
FROM    T_ORD_BIG T1
        INNER JOIN M_CUS T2
          ON  (T2.CUS_ID = T1.CUS_ID)
WHERE   T1.ORD_YMD LIKE '201703%'
GROUP BY T2.CUS_GD;

{
   "query_block": {
     "select_id": 1,
     "cost_info": {
       "query_cost": "478369.15"
     },
     "grouping_operation": {
       "using_temporary_table": true,
       "using_filesort": false,
       "nested_loop": [
         {
           "table": {
             "table_name": "T1",
             "access_type": "range",
             "possible_keys": [
               "X_T_ORD_BIG_1",
               "X_T_ORD_BIG_3",
               "X_T_ORD_BIG_4",
               "X_T_ORD_BIG_5"
             ],
             "key": "X_T_ORD_BIG_5",
             "used_key_parts": [
               "ORD_YMD"
             ],
             "key_length": "35",
             "rows_examined_per_scan": 360858,
             "rows_produced_per_join": 360858,
             "filtered": "100.00",
             "using_index": true,
             "cost_info": {
               "read_cost": "45339.55",
               "eval_cost": "36085.80",
               "prefix_cost": "81425.35",
               "data_read_per_join": "195M"
             },
             "used_columns": [
               "ORD_SEQ",
               "CUS_ID",
               "ORD_YMD"
             ],
             "attached_condition": "(`db_mysqlbooster`.`t1`.`ORD_YMD` like '201703%')"
           }
         },
         {
           "table": {
             "table_name": "T2",
             "access_type": "eq_ref",
             "possible_keys": [
               "PRIMARY"
             ],
             "key": "PRIMARY",
             "used_key_parts": [
               "CUS_ID"
             ],
             "key_length": "162",
             "ref": [
               "db_mysqlbooster.T1.CUS_ID"
             ],
             "rows_examined_per_scan": 1,
             "rows_produced_per_join": 360858,
             "filtered": "100.00",
             "cost_info": {
               "read_cost": "360858.00",
               "eval_cost": "36085.80",
               "prefix_cost": "478369.15",
               "data_read_per_join": "1G"
             },
             "used_columns": [
               "CUS_ID",
               "CUS_GD"
             ]
           }
         }
       ]
     }
   }
 }

 

아래와 같이 FORMAT을 Tree형태로 출력도 할 수 있습니다. 오라클을 사용하는 사용자라면 훨씬 친숙합니다. (사실 이 방법이 훨씬 실행계획 이해하기에 유리합니다.)

EXPLAIN FORMAT = TREE
SELECT  T2.CUS_GD
        ,COUNT(*)
FROM    T_ORD_BIG T1
        INNER JOIN M_CUS T2
          ON  (T2.CUS_ID = T1.CUS_ID)
WHERE   T1.ORD_YMD LIKE '201703%'
GROUP BY T2.CUS_GD;


-> Table scan on <temporary>
     -> Aggregate using temporary table
         -> Nested loop inner join  (cost=478369.15 rows=360858)
             -> Filter: (t1.ORD_YMD like '201703%')  (cost=81425.35 rows=360858)
                 -> Index range scan on T1 using X_T_ORD_BIG_5  (cost=81425.35 rows=360858)
             -> Single-row index lookup on T2 using PRIMARY (CUS_ID=t1.CUS_ID)  (cost=1.00 rows=1)
 

 

 

이 정도를 알면, 실행계획 해석에 큰 어려움은 없을거 같습니다.

참고로, MySQL 8에서는 EXPLAIN ANALYZE를 통해 트리 형태로 실제 실행된 실행계획도 확인할 수 있습니다.

sweetquant.tistory.com/29

 

MySQL 실제 실행계획

오라클은 SQL 성능에 대해서 다양한 방법으로 측정이 가능합니다. 이로 인해, 튜닝도 비교적 쉽게 할 수 있습니다. 반면에 MySQL은 성능 측정이 쉽지 않습니다. 지금까지 MySQL의 실행계획을 확인하

sweetquant.tistory.com

이상입니다~!

 

오라클은 SQL 성능에 대해서 다양한 방법으로 측정이 가능합니다. 이로 인해, 튜닝도 비교적 쉽게 할 수 있습니다. 반면에 MySQL은 성능 측정이 쉽지 않습니다.

 

지금까지 MySQL의 실행계획을 확인하기 위해서는 EXPLAIN 과 함께 SQL을 실행하면 됩니다. 하지만 EXPLAIN으로 확인할 수 있는 정보는 너무 제한적입니다.

MySQL8 에서는 EXPLAIN에 ANALYZE 옵션을 지정할 수 있습니다. 

dev.mysql.com/doc/refman/8.0/en/explain.html

 

MySQL :: MySQL 8.0 Reference Manual :: 13.8.2 EXPLAIN Statement

13.8.2 EXPLAIN Statement {EXPLAIN | DESCRIBE | DESC} tbl_name [col_name | wild] {EXPLAIN | DESCRIBE | DESC} [explain_type] {explainable_stmt | FOR CONNECTION connection_id} {EXPLAIN | DESCRIBE | DESC} ANALYZE [FORMAT = TREE] select_statement explain_type:

dev.mysql.com

 

제가 가지고 있는 MySQL 테스트 DB에서 기존 방법으로 EXPLAIN 만 사용해서 실행계획을 확인해 보겠습니다.

(테스트 DB는 오라클 기준으로 쓰여진 책 'SQL BOOSTER'의 DB를 MySQL로 컨버젼한 DB입니다.)

 

EXPLAIN
SELECT  COUNT(*)
FROM    T_ORD_BIG T1
        INNER JOIN M_CUS T2
          ON  (T2.CUS_ID = T1.CUS_ID)
WHERE   T1.ORD_YMD LIKE '201703%';

# 결과
id   select_type   table   partitions   type    possible_keys                                             key             key_len   ref                         rows    filtered   Extra                      
==== ============= ======= ============ ======= ========================================================= =============== ========= =========================== ======= ========== ========================== 
1    SIMPLE        T2      None         index   PRIMARY                                                   FK_CUS_1        163       None                        90      100.0      Using index                
1    SIMPLE        T1      None         ref     X_T_ORD_BIG_1,X_T_ORD_BIG_3,X_T_ORD_BIG_4,X_T_ORD_BIG_5   X_T_ORD_BIG_4   162       db_mysqlbooster.T2.CUS_ID   34394   13.33      Using where; Using index   

 

오라클만 사용했던 사람이라면, 이해가 쉽지 않은 구조의 실행계획입니다. 이번에는 EXPLAIN ANALYZE 로 실행계획을 확인해 보겠습니다.

EXPLAIN ANALYZE
SELECT  COUNT(*)
FROM    T_ORD_BIG T1
        INNER JOIN M_CUS T2
          ON  (T2.CUS_ID = T1.CUS_ID)
WHERE   T1.ORD_YMD LIKE '201703%';

# 결과
-> Aggregate: count(0)  (actual time=5384.902..5384.902 rows=1 loops=1)
     -> Nested loop inner join  (cost=444232.85 rows=412658) (actual time=21.379..5358.471 rows=185000 loops=1)
         -> Index scan on T2 using FK_CUS_1  (cost=10.00 rows=90) (actual time=1.522..3.185 rows=90 loops=1)
         -> Filter: (t1.ORD_YMD like '201703%')  (cost=1501.46 rows=4585) (actual time=23.675..59.104 rows=2056 loops=90)
             -> Index lookup on T1 using X_T_ORD_BIG_4 (CUS_ID=t2.CUS_ID)  (cost=1501.46 rows=34394) (actual time=0.437..50.395 rows=33856 loops=90)
 

마치 오라클의 실행계획처럼 자세하고, Tree형태로 실행계획이 나오고 있습니다.! 앞으로는 이 방법을 사용해 MySQL 튜닝을 진행해야 겠네요. 물론 MySQL 버젼에 따라 지원되지 않을 수 있습니다.

+ Recent posts