개요 : 테이블의 설계 원칙을 준수하지 않은 몇개 테이블 때문에 두 개의 테이블의 collation이 달라서 인덱스 스캔이 불가능한 문제가 발생한다.

결과 : 해결 방법은 테이블의 collation을 일치 시켜주는 것이고 임시 방편은 조인 조건절에 collation을 일치 시키기 위해서 collate문을 기술해 주는 것이나 collation에 따라 비교 방식이 달라서 똑같은 결과를 보장할 수 는 없다.

 

 

[튜닝 전]

  • Table Collation이 utf8mb4_0900_bin과 utf8mb4_0900_ai_ci로 서로 달라서 인덱스를 타지 못하는 성능 문제가 발생함 
  • 비교 방식이 달라서 결과가 달라질 수 있으므로 테이블의 collation을 바꾸는 것이 기본 원칙임
  • utf8mb4_0900_bin : 정확한 문자 비교 
  • utf8mb4_0900_ai_ci : 대소문자 구별 안함, '가' 와 'ㄱㅏ'를 같은 문자로 인식하나 '각', 'ㄱㅏㄱ', '가ㄱ' 은 다른 문자로 인식 ...
SELECT COUNT(*)
  FROM MT_STR_MRG_M MRG
  LEFT JOIN MT_STR_MRG_REQ REQ
    ON MRG.STR_CD  = REQ.STR_CD
   AND MRG.MD_CD   = REQ.MD_CD
   AND MRG.CPCO_CD = REQ.CPCO_CD
   ...
 WHERE 1=1
   AND MRG.MD_CD. = '002907'
 ;
 
 -> Aggregate: count(0) (actual time=6917228.154..1917228.154 rows=1 loops=1)
  -> Nested loop left join (actual time=224.185..6917160.201 rows=31291 loops=1)
   -> Index lookup on MRG using IX_MT_STR_MRG_M_02 (MD_CD='002927') (actual time=0.594..206.021 rows=31921 loops=1)
   -> Filter: ((REQ.STR_CD=MRG.STR_CD) and (REQ.MD_CD=MRG.MD_CD) and (REQ.CPCO_CD=MRG.CPCO_CD) and ..) (actual time=184.314..216.687 rows=0 loops=31921)
    -> Index range scan on REQ(re-planned for each iteration (actual time=0.076..181.705 rows=237637 loops=31921)

 

  • 실행 계획에서 REG 테이블에서 인덱스를 탈 수 없어서 조인 조건이 필터 조건으로 사용되고 있고 각 조인시도마다 다시 실행계획을 계획하는 비효율이 발생함-> INDEX SCAN or TABLE FULL SCAN을 선택 못하는 문제가 발생하여 115분 소요, REG에 NO_INDEX 힌트 사용시 0.4초 걸림 

 

[튜닝 후]

  • 테이블 설계 원칙에 따라 두 테이블의 collation을 "utf8mb4_0900_bin" 으로 설정해야 함 
  • 임시 방편으로 collate 구문으로 collation을 변경함, 여기서 collate는 OUTER, INNER테이블 어디에 써도 상관없으므로 테이블 설계 원칙에 맞게 INNER 테이블 조인 조건에 collate구문을 기술함  
SELECT COUNT(*)
  FROM MT_STR_MRG_M MRG
  LEFT JOIN MT_STR_MRG_REQ REQ
    ON MRG.STR_CD  = REQ.STR_CD COLLATE utf8mb4_0900_bin
   AND MRG.MD_CD   = REQ.MD_CD  COLLATE utf8mb4_0900_bin
   AND MRG.CPCO_CD = REQ.CPCO_CD COLLATE utf8mb4_0900_bin
   ...
 WHERE 1=1
   AND MRG.MD_CD. = '002907'
 ;
 
 -> Aggregate: count(0) (actual time=275.567..275.567 rows=1 loops=1)
  -> Nested loop left join (actual time=0.366..273.214 rows=31291 loops=1)
   -> Index lookup MRG using IX_MT_STR_MRG_M_02 (MD_CD='002927') (actual time=0.346..85.274 rows=31921 loops=1)
   -> Filter: ((MRG.STR_CD = REQ.STR_CD COLLATE utf8mb4_0900_bin) and (MRG.MD_CD = REQ.MD_CD COLLATE utf8mb4_0900_bin) and (MRG.CPCO_CD = REQ.CPCO_CD COLLATE utf8mb4_0900_bin) and ..) (actual time=0.006..0.006 rows=0 loops=31921)
    -> Single-row covering index lookup on REQ using PRIMARY(STQ=MRG.STR_CD, MD_CD='002907', CPCO_CD=MRG.CPCO_CD, ...) (actual time=0.005..0.005 rows=0 loops=31921)

 

 

[re-planned for each iteration] 

  • 실행계획을 계획하지 못하는 re-planned for each iteration 문제는 NO_INDEX(REQ) 힌트를 주어 TABLE SCAN을 하게해서 해결할 수도 있음
  • 실행계획을 제대로 설정하지 못하는 근본 원인은 두 테이블간의 collation이 다름에 있지만 Table scan으로 선택하지 못하는데서 옵티마이저의 한계가 있음을 알 수 있음
EXPLAIN ANALYZE
SELECT /*+NO_INDEX(REQ) */COUNT(*)
  FROM MT_STR_MRG_M MRG
  LEFT JOIN MT_STR_MRG_REQ REQ
    ON MRG.STR_CD  = REQ.STR_CD 
   AND MRG.MD_CD   = REQ.MD_CD  
   AND MRG.CPCO_CD = REQ.CPCO_CD
   ...
 WHERE 1=1
   AND MRG.MD_CD = '002907'
 ;
 
 -> Aggregate: count(0) (actual time=401.100..401.100 rows=1 loops=1)
  -> left hash join (MRG.STR_CD = REQ.STR_CD), (MRG.MD_CD = REQ.MD_CD), (MRG.CPCO_CD = REQ.CPCO_CD) ... (actual time=197.258..399.114 rows=31291 loops=1)
   -> Index lookup on MRG using IX_MT_STR_MRG_M_02 (MD_CD='002927') (actual time=0.867..176.482 rows=31921 loops=1)
   -> Hash
     -> Filter: ('002907' = REQ.MD_CD) (actual time=2.404..186.146 rows=9732 loops=1)
       -> Table scan on REQ (actual time=0.077..162.028 rows=237637 loops=1)

 

 

 

[collation 비교 방식] 

  • utf8mb4_bin : 문자열의 코드 포인트 값을 바탕으로 비교, 후행 공백 인식 안함 (PAD SPACE)
  • utf8mb4_0900_bin : 문자열의 utf8mb4 인코딩 값을 바탕으로 비교, utf8mb4_bin과 정렬 순서는 동일하지만, 처리 속도가 좀 더 빠름 
  • utf8mb4_0900_as_cs : DUCET(Default Unicode Collation Element Table) 가중치 데이터를 바탕으로 비교 

 

[collation 요약]

  • utf8mb4_0900_ai_ci : 대소문자 구별 안함, '가' vs 'ㄱㅏ' -> 같은 값으로 인식, '각' vs '가ㄱ' vs 'ㄱㅏㄱ' 은 다른 문자로 인식 이유는 데이터 비교 기준이 다름에 있음
  • utf8mb4_bin : 대소문자 구별함, 후행 공백 인식을 안함, PAD SPACE 방식으로 공백을 붙여서 비교하여 'a' vs 'a '를 같은 문자로 인식
  • utf8mb4_0900_as_cs : 대소문자 구별함, NO PAD, 단순히 대소문자가 잘 구분되는 정도
  • utf8mb4_0900_bin : 대소문자 구별함, NO PAD, 모든 문자를 명확하게 구분하고자 할 때 (정확한 문자 비교)
-- NO PAD 방식, 결과 : 0 ('a' vs 'a ' -> 다름) 
select 'a ' collate 'utf8mb4_0900_ai_ci' = 'a' collate 'utf8mb4_0900_ai_ci'; -- 0
select 'a ' collate 'utf8mb4_0900_as_cs' = 'a' collate 'utf8mb4_0900_as_cs'; -- 0
select 'a ' collate 'utf8mb4_0900_bin' = 'a' collate 'utf8mb4_0900_bin'; -- 0

-- PAD SPACE 방식, 결과 : 1 ('a' vs 'a ' -> 같음)
select 'a ' collate 'utf8mb4_bin' = 'a' collate 'utf8mb4_bin'; -- 1


-- utf8mb4_0900_ai_ci
-- '가' vs 'ㄱㅏ' -> 같음
select '가' collate 'utf8mb4_0900_ai_ci' = 'ㄱㅏ' collate 'utf8mb4_0900_ai_ci'; -- 1 
-- '각' vs 'ㄱㅏㄱ' vs '가ㄱ' -> 다름
select '각' collate 'utf8mb4_0900_ai_ci' = 'ㄱㅏㄱ' collate 'utf8mb4_0900_ai_ci'; -- 0
select '각' collate 'utf8mb4_0900_ai_ci' = '가ㄱ' collate 'utf8mb4_0900_ai_ci'; -- 0
-- 'abc' vs 'ABC' -> 같음  -> 대소문자 구분 안함 
select 'abc' collate 'utf8mb4_0900_ai_ci' = 'ABC' collate 'utf8mb4_0900_ai_ci'; -- 1 



-- utf8mb4_0900_bin
-- 'abc' vs 'ABC' -> 다름 -> 대소문자 구분 함   
select 'abc' collate 'utf8mb4_0900_bin' = 'ABC' collate 'utf8mb4_0900_bin'; -- 0 
-- '가' vs 'ㄱㅏ' -> 다름 -> 정확한 문자 비교
select '가' collate 'utf8mb4_0900_bin' = 'ㄱㅏ' collate 'utf8mb4_0900_bin'; -- 0

 

 

* collation 참조 사이트 : yozm.wishket.com/magazine/detail/2746/

[튜닝 전 ]

  • ORD_YMD 범위가 넓을수록 늦어지는 쿼리
  • 한달치 데이터를 읽으면서 셀프 not exists문을 처리하여 조인시도횟수 421799번이 발생하여 더 늦어지는 SQL 문
-- PK(Cardinality) : ORD_NO(13,596,635), ORD_SNO(17,309,028)
-- IX_ON_ORD_P_10 : ORD_YMD, STR_D, TRAN_DCMP_PRCS_STUS_CD

SELECT SUM(..)
      , ...
  FROM ON_ORD_P A FORCE INDEX(IX_ON_ORD_P_10) 
 INNER JOIN MT_OPER_ORG_SHOP_MD_MPNG OSMM
    ON OSMM.STR_CD = A.STR_CD
   ...
 WHERE 1=1
   AND NOT EXISTS(SELECT 'X'
                    FROM ON_ORD_P X
                   WHERE X.CHNL_ORD_NO = A.OTRS_CHNL_ORD_NO
                     AND X.CHNL_ORD_SNO = A.OTRS_CHNL_ORD_SNO
                     AND X.SALS_CHNL_DVS_CD = A.SALS_CHNL_DVS_CD
                     AND X.TRAN_DCMP_PRCS_STUS_CD = '1'
                     AND A.CHNL_ORD_PDCT_DVS_CD = '021'
                     AND A.SALS_CHNL_DVS_CD = '110'
                     AND A.TRAN_DCMP_PRCS_STUS_CD IN ('0','8')
                  )
   AND A.ORD_YMD BETWEEN '20250301' AND '20250331'
   AND A.STR_CD IN ('14')
   ...
  
  -> Aggregate : ... (actual time=7140.011..7140.012 rows=1 loops=1)
    -> Nested loop antijoin (actual time=402.939..6818.300 rows=421799 loops=1)
      -> Nested loop inner join (actual time=402.915..4446.856 rows=421799 loops=1)
        -> Nested loop inner join (actual time=398.333..413.352 rows=13619 loops=1)
  ...
        -> Filter:((A.SALS_CHNL_DVS_CD='110') AND ...)) (actual time=0.140..0.295 rows=31 loops=13619)
         -> Index lookup on A using IX_ON_ORD_P_15(STR_CD='14', MD_CD=OSMM.MD_CD,SHOP_CD=OSMM.SHOP_CD), with index condition:(A.ORD_YMD between '20250301' and '20250331') (actual time=0.140..0.279 rows=31 loops=13619)
      -> Filter:((A.CHNL_ORD_PDCT_DVS_CD='021') and (A.TRAN_DCMP_PRCD_STUS_CD in('0','8'))) (actual time=0.005..0.005 rows=0 loops=421799)
       -> Covering index lookup on X using IX_ON_ORD_P_06 (CHNL_ORD_NO=A.OTRS_CHNL_ORD_NO, CHNL_ORD_SNO=A.OTRS_CHNL_ORD_SNO, SALS_CHNL_DVS_CD='110', TRANS_DCMP_PRCS_STUS_CD='1') (actual time=0.004..0.005 rows=1 loops=421799)

 

 

[튜닝 후]

  • ON_ORD_P 테이블의 셀프 NOT EXISTS 구문을 CASE WHEN 구문을 사용하여 1번만 읽고 처리하도록 변경하여 셀프 조인시 발생하는 421799번의 조인 시도로 인한 부하를 줄임
  • ON_ORD_P 테이블의 한달치 데이터를 조회시 NOT EXISTS 셀프 조인으로 인한 부하로 소요 시간이 7초 가량 걸리던 쿼리가 3초로 줄음
SELECT SUM(...)
      , ...
  FROM MT_OPER_ORG_SHOP_MD_MPNG OSMM
 INNER JOIN LATERAL(SELECT A.*
                          ,CASE WHEN A.TRAN_DCMP_PRCS_STUS_CD='1'
                                THEN CASE WHEN A.CHNL_ORD_PDCT_DVS_CD = '021'
                                               AND A.SALS_CHNL_DVS_CD = '110'
                                               AND A.TRAN_DCMP_PRCS_STUS_CD IN ('0','8')
                                          THEN 'NO'
                                          ELSE 'YES' END
                                ELSE 'YES'
                                END CHECK_YN 
                      FROM ON_ORD_P A FORCE INDEX (IX_ON_ORD_P_10)
                     WHERE OSMM.STR_CD = A.STR_CD
                       AND OSMM.SHOP_CD = A.SHOP_CD
                       AND OSMM.MD_CD = A.MD_CD
                       AND A.ORD_YMD BETWEEN '20250301' AND '20250331'
                       AND A.TRAN_DCMP_PRCS_STUS_CD IN ('1','0','8')
                       AND A.ORD_STUS_DVS_CD != '02'
                       AND A.CHNL_ORD_NO NOT LIKE 'RE%'
                       AND A.SALS_CHNL_DVS_CD = '110'
                       AND A.STR_CD IN ('14')
                       ...
                      ) A ON TRUE
 WHERE A.CHECK_YN = 'YES'
 ....
 
 -> Aggregate: count(0), sum(...), ..(actual time=2979.308..2979.309 rows=1 loops=1)
   -> Inner hash join (A.MD_CD=OSMM.MD_CD), (A.SHOP_CD=OSMM.SHOP_CD) (actual time=435.955..2667.838 rows=421799 loops=1)
    -> Filter :((A.SALS_CHNL_DVS_CD='110') and((case when (A.TRAN_DCMP_PRCS_STUS_CD='1') then (case when ((A.CHNL_ORD_PDCT_DVS_CD ='021') and (A.SALS_CHNL_DVS_CD='110') and (A.TRAN_DCMP_PRCS_STUS_CD in ('0','8'))) then 'NO' else 'YES' end) else 'YES' end) = 'YES') and (A.ORD_STUS_DVS_CD <> '02') and (not((A.CHNL_ORD_NO like 'RE%'))) and (A.ORD_EXPS_DVS_CD not in ('03','04'))) (actual time=0.062..2069.512 rows=421799 loops=1)
     -> Index range scan on A using IX_ON_ORD_P_10 over ('20250301' <= ORD_YMD <= '20250331' AND STR_CD ='14' AND '0' <= TRAN_DCMP_PRCS_STUS_CD <='8') (actual time=0.053..1814.637 rows=421799 loops=1)
      -> Hash
       -> Filter :((OSMM.STR_CD = '14') and ...) (actual time=399.767..429.903 rows=13619 loops=1)
        -> Covering index rang scan on OSMM using IX_MT_OPER_ORG_SHOP_MD_MPNG_02 over (STR_CD='15' AND MD_CD < '000000') OR (STR_CD = '14' AND '000000' < MD_CD) (actual time=0.039..8.518 rows=13660 loops=1)
         -> Select #3(subqury in condition; run only once)
          -> Filter: ... (actual time=0.040..0.040 rows=1 loops=10405)
           -> Limit: 1 row(s) (actual time=0.039..0.039 rows=1 loops=10405)
           ...
             -> Nested loop left join (actual time=0.414..396.747 rows=1371 loops=1)
              -> Nested loop left join (actual time=0.399..395.307 rows=1371 loops=1)
               -> Nested loop inner join (actual time=0.386..389.540 rows=1371 loops=1)
                -> Nested loop inner join (actual time=0.073..17.870 rows=1371 loops=1)
              ...

 

[튜닝 전]

  • SA_SALS_CAD_P$ 테이블에 PK가 존재하나 변별력이 없어서 4백만건 가량을 읽고 필터한 후에 462건을 읽어옴.
  • 필터 된 후의 결과값이 462건으로 줄어드는 것으로 보아서 필터 조건 중에 PK 인덱스 컬럼에 존재하지 않는 CLM_DCNT_DVS_CD의 1번째 distinct값과 2번째 컬럼 분포도를 살펴봐야 함.
  • CLM_DCNT_DVS_CD 컬럼의 distinct값은 16으로 변별력이 매우 낮으나 컬럼 분포도를 보면 NULL값이 99.9%로 NULL이외의 값에 대한 변별력이 매우 높으므로 해당 컬럼을 인덱스 구성 컬럼에 포함하여 인덱스를 생성해야 함.
  • 튜닝 전 소요시간 : 3.732 (sec)
PRIMARY KEY : SALS_YMD, STR_CD, POS_NO, DLNG_NO, SALS_DCMP_WRK_SNO
SELECT COUNT(DISTINCT STR_CD) FROM SA_SALS_CAD_P$; --41
SELECT COUNT(DISTINCT CLM_DCNT_DVS_CD) FROM SA_SALS_CAD_P$; --16


SELECT ...
  FROM SA_SALS_CAD_P$ SCAP
 ...
 WHERE SCAP.SALS_YMD BETWEEN '20250401' AND '20250430' -- :fromdate and :todate
   AND SCAP.STR_CD = '14'
   AND SPP.CPCO_CD = '96354'
   AND SCAP.CLM_DCNT_DVS_CD = '6' 
   
 -> Sort: SCAP.SALS_YMD, SCAP.POST_NO, SCAP.DLNG_NO, MD.MD_CD (actual time=3732.265..3732.364 rows=597 loops=1)
   -> Stream results ..
    -> Nested loop inner join ..
     -> Nested loop inner join ..
      -> Nested loop inner join ..
       -> Nested loop inner join (cost=397633.04 rows=2554) (actual time 7.244..3724.087 rows=597 loops=1)
        -> Filter: ((SCAP.CLM_DCNT_DVS_CD = '6') and (SCAP.STR_CD = '14') and (SCAP.SALS_YMD between '20250401' and '20250430')) (actual time=7.201..3715.534 rows=462 loops=1)
         -> Index range scan on SCAP using PRIMARY over ('20250401' <= SALS_YMD <= '20250430' AND STR_CD = '14') (actual time=0.019..3353.431 rows=3937233 loops=1)
        -> Filter: ((SPP.CPCO_CD = '96354') and (SPP.MD_CD is not null)) (cost=0.25 rows=0) (actual time=0.016..0.018 rows=1 loops=462)
         -> Index lookup on SPP using PRIMARY (SALS_YMD=SCAP.SALS_YMD, STR_CD='14', POS_NO=SCAP.POS_NO, DLNG_NO=SCAP.DLNG_NO) (cost=0.25 rows=1) (actual time=0.015..0.017 rows=1 loops=462)
         ...
         ...

 

  • CLM_DCNT_DVS_CD 컬럼의 분포도 
    : NULL값의 데이터의 99% 이상을 차지하고 있으므로 해당 컬럼을 인덱스 구성 컬럼으로 선택함
CLM_DCNT_DVS_CD 건수 분포도
NULL 54660191 99.88
6 26324 0.05
0 36377 0.07
7 42 0.00
... ... 0.00

 

[튜닝 후]

  • 인덱스 생성 컬럼 : STR_CD, CLM_DCNT_DVS_CD
  • 인덱스 생성 후 소요 시간 : 0.0179 (sec)
 -> Sort: SCAP.SALS_YMD, SCAP.POST_NO, SCAP.DLNG_NO, MD.MD_CD (actual time=17.804..17.905 rows=597 loops=1)
   -> Stream results ..
    -> Nested loop inner join ..
     -> Nested loop inner join ..
      -> Nested loop inner join ..
       -> Nested loop inner join (cost=200.07 rows=36) (actual time 0.103..12.664 rows=597 loops=1)
        -> Filter: ((SCAP.CLM_DCNT_DVS_CD = '6') and (SCAP.STR_CD = '14') and (SCAP.SALS_YMD between '20250401' and '20250430')) (actual time=0.076..4.342 rows=462 loops=1)
         -> Intersect rows sorted by row ID (cost=105.90 rows=231) (actual time=0.073..4.103 rows=462 loops=1)
          -> Index range scan on SCAP using IX_SA_SALS_CAD_P$_03 over (STR_CD = '14' AND CLM_DCNT_DVS_CD = '6' AND '20250401' <= SALS_YMD <= '20250430') (actual time=0.056..2.430 rows=462 loops=1)
        -> Filter: ((SPP.CPCO_CD = '96354') and (SPP.MD_CD is not null)) (cost=0.25 rows=0) (actual time=0.015..0.018 rows=1 loops=462)
         -> Index lookup on SPP using PRIMARY (SALS_YMD=SCAP.SALS_YMD, STR_CD='14', POS_NO=SCAP.POS_NO, DLNG_NO=SCAP.DLNG_NO) (cost=0.25 rows=1) (actual time=0.015..0.017 rows=1 loops=462)
         ...
         ...

 

  • 튜닝 이전에 4백만건(3937233) 가량을 읽고 필터 된 후에 462건으로 줄었으나 인덱스 생성 후에 인덱스를 읽고 462건을 읽어와서 필터 된 후에도 462건으로 해당 인덱스를 읽고 처리하는데 0.002초 소요함(이전에는 3.353초 소요함). 
  • 전체 수행 시간은 3.732에서 0.0179로 줄어듦 
  • CLM_DCNT_DVS_CD 컬럼은 코드 컬럼으로 distinct가 16건으로 매우 낮은 건수 임에도 불구하고 해당 컬럼의 분포도가 NULL에 99%이상 치중되어 있어서 NULL이외의 값 조회에 상당히 높은 성능을 제공하는 컬럼임

개요 : MYSQL에서 With구문 사용으로 순서조정이 어려울 시에 서브쿼리추가 및 순서 조정  

 

[튜닝 전]  41.562 sec

STRAIGHT_JOIN 힌트를 사용함에도 불구하고 TMP_ORG가 먼저 실행되지 않고 TMP_SALS가 먼저 수행되어서 부하가 발생하는 쿼리

WITH ... AS ()
, TMP_ORG AS ()
, TMP_SALS AS (
  SELECT ..
    FROM PF_ORD_BYDY_PIPD_ACR sals 
   WHERE 1=1
     AND sals.SALS_YMD >= '20241201' AND sals.SALS_YMD <= '20250228'
     ... 
   GROUP BY ...
 )
 , TMP_PIPD AS ( 
 SELECT STRAIGHT_JOIN ...
  FROM TMP_ORG org
 INNER JOIN TMP_SALS sals 
    ON org.STR_CD = sals.STR_CD
   AND org.MD_CD = sals.MD_CD
   AND org.DVS5_CD = sals.SHOP_CD 
 INNER JOIN ...
 ...
 )
 SELECT 
 ...
 FROM TMP_PIPD 
 GROUP BY ...
 ORDER BY ...
 
 -> Sort: TMP_PIPD.MD_CD, TMP_PIPD.MD_NM (actual time=41562.450..41562.450 rows=1 loops=1)
  -> Table scan on <temporary> (actual time=0.004..0.004 rows=1 loops=1)
   -> Nested loop inner join (actual time=37182.179..41144.986 rows=12 loops=1)
    ...
      -> Nested loop inner join ..
       -> Covering index lookup on dtl using IX_CM_CMMN_CD_D_01 (CMMN_CD='SALS_CHNL_DVS_CD') (actual time=0.014..0.025 rows=7 loops=1)
       -> Filter: (sals.SALS_CHNL_DVS_CD=dtl.DTL_CD) (actual time=5311.729..5374.401 rows=50497 loops=7)
    ...
    -> Index lookup on org using <auto_key0> (STR_CD=sals.STR_CD, DVS5_CD=sals.SHOP_CD, MD_CD=sals.MD_CD) (actual time=0.001..0.001 rows=0 loops=353476)
     -> Materialize union CTE TMP_ORG (actual time=298.508..298.510 rows=3 loops=1)
    ...

 

 

[튜닝 후] 4.319 ms 

TMP_SALS 뷰에 조인 조건을 서브쿼리로 추가하고 서브쿼리가 먼저 수행되게 힌트를 사용함 

WITH ... AS ()
, TMP_ORG AS ()
, TMP_SALS AS (
  SELECT /*+ JOIN_PREFIX(@subq1) */..
    FROM PF_ORD_BYDY_PIPD_ACR sals 
   WHERE 1=1
     AND sals.SALS_YMD >= '20241201' AND sals.SALS_YMD <= '20250228'
     AND (sals.STR_CD, sals.MD_CD, sals.SHOP_CD) IN (SELECT /*+ QB_NAME(subq1) */ STR_CD, MD_CD, DVS5_CD FROM TMP_ORG)
     ... 
   GROUP BY ...
  LIMIT 18446744073709551615
 )
 , TMP_PIPD AS ( 
 SELECT STRAIGHT_JOIN ...
  FROM TMP_ORG org
 INNER JOIN TMP_SALS sals 
    ON org.STR_CD = sals.STR_CD
   AND org.MD_CD = sals.MD_CD
   AND org.DVS5_CD = sals.SHOP_CD 
 INNER JOIN ...
 ...
 )
 SELECT 
 ...
 FROM TMP_PIPD 
 GROUP BY ...
 ORDER BY ...
 
 -> Sort: TMP_PIPD.MD_CD, TMP_PIPD.MD_NM (actual time=4.331..4.331 rows=1 loops=1)
  -> Table scan on <temporary> (actual time=0.004..0.004 rows=1 loops=1)
   -> Nested loop inner join (actual time=3.998..4.157 rows=12 loops=1)
    ...
      -> Nested loop inner join (actual time=1.232..2.764 rows=226 loops=1)
       -> Table scan on <subaquery13> (actual time=0.001..0.001 rows=3 loops=1)
        -> Materialize with deduplication (actual time=0.500..0.501 rows=3 loops=1)
         -> Table scan on TMP_ORG (actual time=0.002..0.002 rows=3 loops=1)
           -> Materialize union CTE TMP_ORG if needed (query plan printed elsewhere) (actual time=0.494..0.495 rows=3 loops=1)
        -> Filter: ((sals.SALS_QTY <>0) ... (actual time=0.243..0.749 rows=75 loops=3)
         -> Index lookup on sals using IX_PF_ORD_BYDY_PIPD_ACR_01 (STR_CD='<subquery13>'.STR_CD, MD_CD='<subquery13>'.MD_CD, SHOP_CD='<subquery13>'.DVS5_CD), with index condition: ((sals.SALS_YMD >= '20241201') and (sals.SALS_YMD <= '20250228')) (actual time=0.240..0.733 rows=93 loops=3)
    ...

 

 

개요 : slow_query에서 7일 단위로 느린 SQL 총수행시간(=실행건수 * 평균소요시간 ) 기준으로 추출하기

 

-- 튜닝 대상 TOP 50 추출

--1. TOP50 저장 테이블 생성

DROP TABLE SLOW_QUERY_TOP50;

CREATE TABLE SLOW_QUERY_TOP50
(
EXTRACT_DATE DATE NOT NULL,
RN DECIMAL NOT NULL,
START_TIME DATETIME NOT NULL,
USER_HOST VARCHAR(100) COLLATE UTF8MB4_0900_BIN NOT NULL,
ROW_SENT DECIMAL NOT NULL,
ROW_EXMAINED DECIMAL NOT NULL,
MIN_QUERY_TIME VARCHAR(10) COLLATE UTF8MB4_0900_BIN NOT NULL,
MAX_QUERY_TIME VARCHAR(10) COLLATE UTF8MB4_0900_BIN NOT NULL,
AVG_QUERY_TIME VARCHAR(10) COLLATE UTF8MB4_0900_BIN NOT NULL,
CNT DECIMAL NOT NULL,
TOTAL_QUERY_TIME VARCHAR(10) COLLATE UTF8MB4_0900_BIN NOT NULL,
SQL_TEXT MEDIUMBLOB NOT NULL,
TUNNING_YN VARCHAR(100),
PRIMARY KEY(EXTRACT_DATE, RN)
)ENGINE=INNODB DEFAULT CHARSET=UTF8MB4 COLLATE=UTF8MB4_0900_BIN;


--2. 추출 대상 넣기
SET @RN = 0;

INSERT SLOW_QUERY_TOP50
SELECT  NOW(), @RN:=@RN+1 AS RN
      , 최근수행시간, 결과건수, 수행중처리행수, 최소수행시간, 최해대수행시간, 평균수행시간, 수행횟수, 총수행시간, 대상쿼리
 FROM ( 
      SELECT  a.start_time AS 최근수행시간, a.user_hot, a.rows_sent 결과건수
            , a.rows_examined 수행중처리행수, x.min_query_time 최소수행시간, x.max_query_time 최대수행시간
            , x.avg_query_time 평균수행시간, x.cnt 수행횟수
            , SET_TO_TIME(TIME_TO_SEC(x.avg_query_time)*cnt) 총수행시간
            , a.sql_text 대상쿼리
        FROM mysql.slow_log a
       INNER JOIN ( 
                  SELECT  SUBSTRING(INSTR(REGEXP_REPLACE(UPPER(sql_text),'[^[:alpha:]]',''),'SELECT')1,200) SQL_TEXT
                        , MAX(start_time) start_time
                        , MIN(TIME_TO_SEC(query_time)) min_query_time_sec
                        , DATE_FORMAT(MIN(query_time),'%T') min_query_time
                        , DATE_FORMAT(MAX(query_time),'%T') max_query_time
                        , DATE_FORMAT(SEC_TO_TIME(avg(TIME_TO_SEC(query_time))), '%T') avg_query_time
                        , count(*) cnt
                    FROM mysql.slow_log a
                   WHERE user_host not like '%DEV%'
                     AND query_time > '00:00:05' -- 튜닝 대상 시간 선정(5초 이상 수행 쿼리 대상)
                     AND a.sql_text not like '%INSERT%'
                     AND a.sql_text not like '%ApplicationName=DBeaver%'
                     AND a.start_time >= date_sub(now(), INTERVAL 7 day) -- 7일전 부터 실행 쿼리 기준
                     AND LENGTH(REGEXP_REPLACE(sql_text,'[^[:alpha:]]','')) > 50 -- 50자 이상 쿼리
                  GROUP BY SUBSTRING(INSTR(REGEXP_REPLACE(UPPER(a.sql_text),'[^[:alpha:]]',''),'SELECT'),1,200)
                  ) x
                  ON SUBSTRING(INSTR(REGEXP_REPLACE(UPPER(a.sql_text),'[^[:alpha:]]',''),'SELECT'),1,200) = x.sql_text
                 AND a.start_time = x.start_time
                 AND x.min_query_time > '00:00:05'
                 AND x.avg_query_time > '00:00:05'
       WHERE user_host not like '%DEV%'
         AND query_time > '00:00:05'
         AND a.sql_text not like '%INSERT%' -- INSERT 대상 제외
         AND a.sql_text not like '%ApplicationName=DBeaver%'
         AND a.start_time >= date_sub(now(), INTERVAL 7 day) -- STR_TO_DATE('2025-02-05 13:00:00','%Y%m%d %H:%i:%S')
       ORDER BY 총수행시간 DESC
       LIMIT 50
     ) a
    ;


-- 결과 추출 
-- 2회차는 NOT EXISTS 추가
SET @RN = 0;

SELECT EXTRACT_DATE||'_'||LPAD(@RN:@RN+1,2,'0') EXTRACT_RN, RN, EXTRACT_DATE, USER_HOST
     , ROW_EXAMINED, ROW_SENT, MIN_QUERY_TIME, AVG_QUERY_TIME, MAX_QUERY_TIME, CNT
     , TOTAL_QUERY_TIME, SQL_TEXT, TUNNING_YN
  FROM SLOW_QUERY_TOP50 A
 WHERE EXTRACT_DATE = '2025-02-05'
   AND NOT EXISTS(SELECT 1 
                    FROM SLOW_QUERY_TOP50 X 
                   WHERE EXTRACT_DATE !='2025-02-05'
                     AND SUBSTRING(INSTR(REGEXP_REPLACE(UPPER(X.SQL_TEXT),'[^[:alpha:]]',''),'SELECT',1,200) = SUBSTRING(INSTR(REGEXP_REPLACE(UPPER(A.SQL_TEXT),'[^[:alpha:]]',''),'SELECT'),1,200))
;


-- 정규식 시간제한으로 에러 시 방안
-- 1. SQL_TEXT 변경 쿼리 : 좀 더 부 정확함 
SUBSTRING(INSTR(REPLACE(UPPER(sql_text),' ',''),'SELECT'),1,200) SQL_TEXT

-- 2. 중간 테이블 추가하여 추출 (추후)

개요 : 병렬 쿼리가 실행되지 않는 문제점 발생하여 원인을 추적하니 Aurora I/O-Optimized DB 클러스터 스토리지 구성이었다.

 

 

MySQL 병렬 쿼리 기능의 제한사항

  • 병렬 쿼리는 Aurora I/O-Optimized DB 클러스터 스토리지 구성에서 지원되지 않는다.
  • db.t2 또는 db.t3 인스턴스 클래스에는 병렬 쿼리는 사용할 수 없다. 병렬 쿼리에 사용하려는 테이블에는 COMPACT 또는 DYNAMIC행 형식을 사용한다.
  • ....
  • AWS > Amazon Aurora 사용 설명서 참고

 

조치 사항 : I/O-Optimized DB 클러스터 구성 -> Standard 전환 요청 

 

https://aws.amazon.com/ko/blogs/korea/new-amazon-aurora-i-o-optimized-cluster-configuration-with-up-to-40-cost-savings-for-i-o-intensive-applications/

 

 

 

 

Standard -> I/O Optimized 전환 : 30일에 한 번

I/O Optimized -> Standard 전환 : 언제든지 

 

Aurora I/O-Optimized는 I/O 비용이 청구되지 않으므로 Standard와 비교하여 컴퓨팅 및 스토리지에 대해 정해진 가격이 청구된다.

I/O-Optimized의 경우 가격 대비 성능이 향상되고 비용을 최대 40% 절감할 수 있다.

 

 

 

위와 같은 I/O 요금 절감으로 인해서 I/O Optimized DB 클러스터 구성으로 되어 있었다. Standard로 변경 요청하고 병렬 쿼리를 실행한다.

 

SET SESSION aurora_parallel_query = ON;
SET SESSION aurora_pq_force = ON;
SET SESSION innodb_parallel_read_threads = 16; -- 4, 8, 16, 32, ....

EXPLAIN ANALYZE
SELECT /*+ PQ(A) */ COUNT(*) FROM IV_TDMR_BYDY_MD_S A;

-> Aggregate: count(int_mux(1)) [Using parallel query(0 group-bys, 1 aggrs)] (cost=79031804.24 rows=341928662) (actual time=151080.384..151080.385 rows=1 loops=1)
  -> Table scan on A [Using parallel query(0 columns)] (cost=44838938.04 rows=341928662) (actual time=163.610..150852.497 rows=330009 loops=1) [parallel query actual (total time= 151011.460, rows returned= 3300009)]

'MySQL > MySQL' 카테고리의 다른 글

MySQL slow_query 느린 쿼리 추출  (0) 2025.04.02
MySQL 공백, 스페이스의 차이  (0) 2025.03.13
NULL 정렬  (0) 2025.01.22
인덱스 엑세스 조건 등치+Between 무시 -> 등치+IN (Subquery) 로 변경  (0) 2025.01.16
Hash Join  (0) 2024.12.26

1. LATERAL + JPPD 로 작성한다. 

  • 조인 시도 횟수가 적어야 한다. 뷰안에 테이블 까지 조인 시도 횟수가 적용 되므로 조인 시도 횟수가 적을 때 효과적이다. 
  • 조인 조건에 따라 MAX, MIN 값 등의 1개의 값을 가져올 때 유용하다. 1건만 가져오게 MAX, MIN 대신에 LIMIT 1 을 적용 할 수도 있다. 1건만 가져오게 처리하여 조인 시도 횟수를 줄일 수도 있다. 

 

2. LEFT(INNER) JOIN (SELECT .... LIMIT 9999999999999)

  • 조인 시도 횟수가 많아야 한다. 조인 시도 횟수가 많을 때 1번과 같이 작성하면 조인 시도 횟수가 뷰안에 모든 테이블에 적용 된다.
    조인 시도 횟수가 한번만 일어나게 뷰안에 테이블의 결과를 먼저 처리해야 한다.
  • 뷰의 건수는 작을수록 효과가 있다. 한 번만 조인 시도 횟수가 일어나도 뷰의 결과 건수가 작을때 더 빨라진다. 설령 뷰의 건수가 많더라도 조인 시도 횟수가 많다면 LIMIT 999999999999(큰 숫자) 로 뷰안의 결과를 먼저 처리해야 한다.

개요 : 튜닝 대상 SQL을 추출하기 위해서 7일간의 수행 횟수 * 평균시간으로 descending하게 정렬하여 50건을 뽑아서 모니터링 대상으로 추출한다. 그 다음주에도 50건을 추출하되 이전에 포함된 목록은 제외한다. 

 

[추출 쿼리]

USE USERS;
DROP TABLE SLOW_QUERY_TOP;
CREATE TABLE SLOW_QUERY_TOP
(
EXTRACT_DATE DATE NOT NULL,
RN DECIMALL NOT NULL,
START_TIME DATETIME NOT NULL,
USER_HOST VARCHAR(100) COLLATE UTF8MB4_0900_BIN NOT NULL,
ROW_SENT DECIMAL NOT NULL,
ROW_EXAMINED DECIMAL NOT NULL,
MIN_QUERY_TIME VARCHAR(10) COLLATE UTF8MB4_0900_BIN NOT NULL,
MAX_QUERY_TIME VARCHAR(10) COLLATE UTF8MB4_0900_BIN NOT NULL,
AVG_QUERY_TIME VARCHAR(10) COLLATE UTF8MB4_0900_BIN NOT NULL,
CNT DECIMAL NOT NULL,
TOTAL_QUERY_TIME VARCHAR(10) COLLATE UTF8MB4_0900 NOT NULL,
SQL_TEXT MEDIUMBLOB NOT NULL,
PRIMARY KEY(EXTRACT_DATE, RN)
) ENGINE=INNODB DEFAULT CHARSET=UTF8MB4 COLLATE=UTF8MB4_0900_BIN;


-- 오늘 추출한 데이터 삭제
DELETE FROM SQLOW_QUERY_TOP WHERE EXTRACT_DATE = DATE_FORMAT(NOW(), '%Y%m%d');

SET @RN = 0; -- 초기화 후 입력

INSERT SLOW_QUERY_TOP
SELECT NOW() AS EXTRACT_DATE, @RN:=@RN+1 AS RN
      ,최근수행시간, A.user_host, 결과건수, 수행중처리행수, 최소수행시간, 평균수행시간, 수행횟수, 총수행시간, 대상쿼리
 FROM (
SELECT A.start_time AS 최근수행시간, A.user_host, A.row_sent 결과건수
      ,A.rows_examined 수행중처리행수, X.min_query_time 최소수행시간 
      ,X.max_query_time 최대수행시간, X.avg_query_time 평균수행시간
      ,X.cnt 수행횟수
      ,SEC_TO_TIME(TIME_TO_SEC(X.avg_query_time)*cnt) 총수행시간
      ,A.sql_text 대상쿼리
 FROM mysql.slow_log A
 INNER JOIN 
      (SELECT SUBSTRING(INSTR(REGEXP_REPLACE(UPPER(sql_text),'[^[:alpah:]]',''),'SELECT'),1,200) SQL_TEXT
            , MAX(start_time) start_time
            , MIN(TIME_TO_SEC(query_time) min_query_time_sec
            , DATE_FORMAT(MIN(query_time),'%T') min_query_time
            , DATE_FORMAT(MAX(query_time),'%T') max_query_time
            , DATE_FORMAT(SEC_TO_TIME(avg(TIME_TO_SEC(query_time))),'%T') avg_query_time
            , count(*) cnt
        FROM mysql.slow_log A
       WHERE user_host not like 'datadog%'
         AND query_time > '00:00:05'
         AND user_host not like 'TUNER%'
         AND user_host not like 'admin%'
         AND user_host not like '%DEV%'
         AND user_host not like '%MDSSEL%'
         AND user_host not like '%ApplicationName=DBeaver%'
         AND A.stgart_time >= date_sub(now(), INTERVAL 7 day)
         AND LENGTH(REGEXP_REPLACE(sql_text,'[^[:alpha:]]','')) > 50  -- 단순 select * from table 방지 
       GROUP BY SUBSTRING(INSTR(REGEXP_REPLACE(UPPER(A.sql_text),'[^[:alpha:]]',''),'SELECT'),1,200)
       ) X
       ON SUBSTRING(INSTR(REGEXP_REPLACE(UPPER(A.sql_text),'[^[:alpha:]]',''),'SELECT'),1,200) = X.SQL_TEXT
      AND A.start_time = X.start_time
      AND X.min_query_time > '00:00:05' -- 5 sec 이상 쿼리만 추출
      AND X.avg_query_time > '00:00:05'
 WHERE A.user_host not like 'datadog%'
   AND A.query_time > '00:00:05'
   AND A.sql_text not like 'ApplicationName=DBeaver%'
   AND A.user_host not like 'TUNER%'
   AND A.user_host not like 'admin%'  
   AND A.user_host not like '%DEV%'
   AND A.user_host not like '%MDSSEL%'
   AND A.start_time >= date_sub(now(), INTERVAL 7 day)
 ORDER BY 총수행시간 desc
 LIMIT 50
 ) A
 ;
 
 
 -- 초기 추출
 SELECT EXTRACT_DATE||'_'||LPAD(RN,2,'0') RN, EXTRACT_DATE, USER_HOST, ROW_EXAMINED, ROW_SENT
      , MIN_QUERY_TIME, AVG_QUERY_TIME, MAX_QUERY_TIME, CNT, TOTAL_QUERY_TIME, SQL_TEXT
  FROM USERS.SLOW_QUERY_TOP A
 WHERE EXTRACT_DATE = DATE_FORMAT(NOW(), '%Y%m%d')
 ORDER BY RN
 LIMIT 50;
 
 
 -- 2회차 이상 추출 
 USE USERS;
 SET @RN = 0;
 SELECT EXTRACT_DATE||'_'||LPAD(@RN:=@RN+1,2,'0') RN, EXTRACT_DATE, USER_HOST, ROW_EXAMINED, ROW_SENT
       ,MIN_QUERY_TIME, AVG_QUERY_TIME, MAX_QUERY_TIME, CNT, TOTAL_QUERY_TIME, SQL_TEXT
  FROM USERS.SLOW_QUERY_TOP A
 WHERE EXTRACT_DATE = '2025-03-11' -- 추출한 날짜
   AND NOT EXISTS (SELECT 1
                     FROM SLOW_QUERY_TOP X
                    WHERE EXTRACT_DATE != '2025-03-11'
                      AND SUBSTRING(INSTR(REGEXP_REPLACE(UPPER(X.sql_text),'[^[:alpha:]]',''),'SELECT'),1,200) = SUBSTRING(INSTR(REGEXP_REPLACE(UPPER(A.sql_text),'[^[:alpha:]]',''),'SELECT',1,200))
 ORDER BY RN
 ;

+ Recent posts