개요 : MySQL SEMIJOIN 힌트 사용법
세미조인 최적화 4가지
FirstMatch
- Main Query의 Filtering 효율이 좋은 경우 사용되는 케이스 - SubQuery에 대해서 1건의 데이터가 일치하는 경우 바로 SubQuery 실행을 중지하고 결과를 반환 (1건을 검색하고 멈추는 단축 실행 경로를 사용) - IN-to-EXISTS 보다 FirstMatch가 좋은점 : 동등 조건 전파가 SubQuery 뿐만 아니라 Main Query에서도 가능 * in-to-exists : in query를 exists 쿼리로 변경해서 질의 * 동등 조건 전파 : where col1 < col2 and col2 = '20240101' ---> where col1 < '20240101' 치환
FirstMatch 특징 - Main Query를 조회 후 SubQuery를 실행 - SubQuery에 Group by(전체 검색하여 연산) 연산 있을 때 FirstMatch 사용 불가 - Main Query에 인덱스가 존재하여야 함 - 조건이 적절하지 않아서 인덱스를 사용할 수 없는 경우 FULLSCAN이 발생 -> 이러한 경우 Materialization 방식 고려 |
Materialization
- Materialization 라는 임시 테이블을 생성해서 SubQuery 결과를 저장하고 Inner Join을 사용해서 결과를 도출하는 방식
Materialization 특징 - SubQuery는 상관서브쿼리가 아니어야 한다(비상관 서브쿼리) - SubQuery에 Group by 사용 가능 - 임시 테이블 사용하므로 메모리에 부하가 생길 수 도 있음 |
LooseScan
- 세미조인에서 모든 record를 읽지 않고 조건에 사용된 Index를 유니크 한 값만 읽어오는 방식 - SubQuery쪽 Index에서 Unique 한 값을 추출 후 Main Query Table을 조인해서 처리하는 방식 |
DUPSWEEDOUT
- SEMIJOIN힌트의 경우 이름을 지정하지 않은 전략은 가능한 경우 optimizer_switch 시스템 변수에 따라 활성화된 전략을 기반으로 SEMIJOIN이 사용, 전략의 이름이 지정되었지만 명령문에 적용할 수 없는 경우 DUPSWEEDOUT을 사용 - NO_SEMIJOIN 힌트의 경우 이름이 지정된 전략이 없으면 SEMIJOIN이 사용 안됨, 명령문에 적용 가능한 모든 전략을 배제하는 전략의 이름이 지정된 경우 DUPSWEEDOUT이 사용됨 - DUPSWEEDOUT을 사용하지 않으면 경우에 따라 옵티마이저가 최적인 아닌 실행계획을 생성, 이는 그리디(탐욕) 검색중 휴리스틱 정리로 인해 발생하며, optimizer_prune_level=0을 설정하여 피할 수 있음
|
*MySQL 5.5 이하 SubQuery 최적화 문제로 INNER JOIN 사용, 5.6이상 SEMIJOIN 사용 가능
[튜닝 예제]
사용 힌트 권장(둘 중에 사용) :
/*+ SEMIJOIN(@subq1 FIRSTMATCH, DUPWEEDOUT) */
/*+ SEMIJOIN(@subq1 MATERIALIZATION, DUPWEEDOUT) */
[튜닝 전 쿼리 & 실행계획] 3.679 (s)
A 테이블 PK : PKCOL, A_IX02 : YMD, DV_CD, DV_CD2, CD
X 테이블 PK : PKCOL1, PKNUM2, X_IX03 : TEL_DV_CD
EXPLAIN ANALYZE
SELECT A.YMD, COUNT(*) CNT
FROM A
WHERE A.CD NOT IN ('22')
AND A.DV_CD NOT IN ('01','02')
AND A.YMD BETWEEN DATE_FORMAT(ADDDATE('20250101',-5),'%Y%m%d')
AND DATE_FORMAT(ADDDATE('20250101',5),'%Y%m%d')
AND EXISTS(SELECT 1
FROM X
WHERE X.TEL_DV_CD = '00'
AND A.PKCOL = X.PKCOL1)
GROUP BY A.XXX_YMD
-> Table scan on <temporary> (actual time=0.002..0.007 rows=17 loops=1)
-> Aggregate using temporary table (actual time=3679.117..3679.123 rows=17 loops=1)
-> Nested loop inner join (cost=530627.28 rows=271591) (actual time=2213.917..3523.712 rows=216879 loops=1)
-> Remove duplicates from input sorted on X_IX03 (cost=106973.58 rows=1039670) (actual time=0.050..670.777 rows=850280 loops=1)
-> Covering index lookup on X on X_IX03 (TEL_DV_CD='00') (cost=106973.58 rows=1039670) (actual time=0.48..476.261 rows=862612 loops=1)
-> Filter: (A.CD <> '22') and (A.DV_CD not in ('01','02')) and (A.YMD between<cache>(date_format(('20250101' + interval -(5) day),'%Y%m%d')) and <cache>(date_format(('20250101' + interval 5 day),'%Y%m%d')))) (cost=319686.73 rows=0) (actual time=0.003..0.003 rows=0 loops=850280)
-> Single-row index lookup on A using PRIMARY (PKCOL=X.PKCOL1) (cost=319686.73 rows=1) (actual time=0.003..0.003 rows=1 loops=850280)
[튜닝 후 쿼리 & 실행계획1] 1.448(s)
EXPLAIN ANALYZE
SELECT /*+ SEMIJOIN(@subq1 MATERIALIZATION, DUPSWEEDOUT) */ A.YMD, COUNT(*) CNT
FROM A
WHERE A.CD NOT IN ('22')
AND A.DV_CD NOT IN ('01','02')
AND A.YMD BETWEEN DATE_FORMAT(ADDDATE('20250101',-5),'%Y%m%d')
AND DATE_FORMAT(ADDDATE('20250101',5),'%Y%m%d')
AND EXISTS(SELECT 1
FROM X
WHERE X.TEL_DV_CD = '00'
AND A.PKCOL = X.PKCOL1)
GROUP BY A.XXX_YMD
-> Group aggregate: count(*) (cost=70011670865.49 rows=350057705667) (actual time=946.385..1467.407 rows=17 loops=1)
-> Nested loop inner join (cost=35005900298.81 rows=350057705667) (actual time=939.509..1448.989 rows=216870 loops=1)
-> Filter: ((A.CD <> '22') and (A.DV_CD not in ('01','02')) and (A.YMD between <cache>(date_format(('20250101' + interval -(5) day),'%Y%m%d')) and <cache>(date_format(('20250101' + interval 5 day),'%Y%m%d')))) (cost=96062.06 rows=336701) (actual time=0.026..232.122 rows=223093 loops=1)
-> Covering index range scan on A using A_IX02 over ('20241227' <= YMD <= '20250106' AND NULL < DV_CD) (cost=96062.96 rows=467640) (actual time=0.021..159.775 rows=235732 loops=1)
-> Single-row index lookup on <subquery2> using <auto_distinct_key) (PKCOL1=A.PKCOL) (actual time=0.001..0.001 rows=1 loops-223093)
-> Materialize with deduplication (cost=210871.52..210871.52 rows=1039670) (actual time=1155.121..1177.010 rows=850280 loops=1)
-> Covering index loopup on X using X_IX03 (TEL_DV_CD='00') (cost=106904.52 rows=1039670) (actual time=0.036..327.789 rows=862612 loops=1)
[튜닝 후 쿼리 & 실행계획2] 0.786 (s)
EXPLAIN ANALYZE
SELECT /*+ SEMIJOIN(@subq1 FIRSTMATCH, DUPSWEEDOUT) */ A.YMD, COUNT(*) CNT
FROM A
WHERE A.CD NOT IN ('22')
AND A.DV_CD NOT IN ('01','02')
AND A.YMD BETWEEN DATE_FORMAT(ADDDATE('20250101',-5),'%Y%m%d')
AND DATE_FORMAT(ADDDATE('20250101',5),'%Y%m%d')
AND EXISTS(SELECT /*+ QB_NAME(subq1) */1
FROM X
WHERE X.TEL_DV_CD = '00'
AND A.PKCOL = X.PKCOL1)
GROUP BY A.XXX_YMD
-> Group aggregate: count(*) (cost=313607.94 rows=401948) (actual time=10.184..785.654 rows=17 loops=1)
-> Nested loop semijoin (cost=273413.12 rows=401948) (actual time=0.038..763.252 rows=216870 loops=1)
-> Filter: (A.CD <> '22') and (A.DV_CD not in ('01','02')) and (A.YMD between <cache>(date_format(('20250101' + interval -(5) day),'%Y%m%d')) and <cache>(date_format(('20250101' + interval 5 day),'%Y%m%d')))) (cost=96062.06 rows=336701) (actual time=0.024..240.037 rows=223093 loops=1)
-> Covering index range scan on A using IX02 over ('20241227' <= YMD <= '20250106' AND NULL < DV_CD) (cost=96062.06 rows=467640) (actual time=0.020..161.669 rows=235732 loops=1)
-> Covering index lookup on X using X_IX03 (TEL_DV_CD='00', PKCOL1=A.PKCOL) (cost=0.49 rows=1) (actual time=0.002..0.002 rows=1 loops=223093)