아래의 [SQL-1]을 위한 인덱스 전략은 정말 다양하다. 다양한 방법 중에, 세 가지 가능성을 열어주는 인덱스 전략에 대해 살펴보려고 한다.
[SQL-1]
SELECT *
FROM startdbpg.ms_mbr_big t1
INNER JOIN startdbpg.tr_ord_big t2
ON (t1.mbr_id = t2.mbr_id)
WHERE t1.nick_nm LIKE 'Air%'
AND t2.ord_ymd = '20241221';
SQL
복사
위 SQL은 두 개의 테이블이 조인되고 있다. 두 테이블을 조인하는 방법은, Nest Loop, Merge 또는 Hash 셋 중에 하나다. 여기서는, ms_mbr_big을 드라이빙 또는 빌드 집합으로만 처리한다. 그러므로 tr_ord_big은 드리븐 또는 프로브 집합이 되며, 그에 맞게 인덱스를 만들려고 한다.
tr_ord_big에는 다음의 두 가지 인덱스 전략을 고려해볼 수 있다.
•
tx01: 조인 조건을 처리하는 mbr_id 컬럼을 인덱스 앞쪽에 위치
•
tx02: 조인 조건을 처리하는 mbr_id 컬럼을 인덱스 뒤쪽에 위치
tx01은 조인 조건을 처리하는 mbr_id 컬럼이 앞쪽에, tx02는 해당 컬럼이 뒤쪽에 위치해 있다. (이 외에도 단독 컬럼 인덱스도 고려할 수 있다. 여기서는 단독 컬럼 인덱스는 제외한다.) 아래 스크립트로 인덱스를 생성한다.
CREATE INDEX tr_ord_big_tx01 ON startdbpg.tr_ord_big(mbr_id, ord_ymd);
CREATE INDEX tr_ord_big_tx02 ON startdbpg.tr_ord_big(ord_ymd, mbr_id);
SQL
복사
위의 인덱스 중에 tx01을 사용하면, 해당 인덱스는 [SQL-1]에서 Nest Loop 조인에만 적합하다. 다른 조인에는 적절하지 않은 인덱스가 된다. 힌트를 변경해가면서 [SQL-1]을 실행해보면 아래와 같은 실행계획을 얻을 수 있다.
EXPLAIN (ANALYZE,COSTS OFF)
/*+ IndexScan(t2 tr_ord_big_tx01) NestLoop(t1 t2) */
SELECT *
FROM startdbpg.ms_mbr_big t1
INNER JOIN startdbpg.tr_ord_big t2
ON (t1.mbr_id = t2.mbr_id)
WHERE t1.nick_nm LIKE 'Air%'
AND t2.ord_ymd = '20241221';
/*+ IndexScan(t2 tr_ord_big_tx01) NestLoop(t1 t2) */
Nested Loop (actual time=0.083..12.321 rows=455 loops=1)
-> Seq Scan on ms_mbr_big t1 (actual time=0.015..5.020 rows=2000 loops=1)
Filter: ((nick_nm)::text ~~ 'Air%'::text)
Rows Removed by Filter: 97990
-> Index Scan using tr_ord_big_tx01 on tr_ord_big t2 (actual time=0.003..0.003 rows=0 loops=2000)
Index Cond: (((mbr_id)::text = (t1.mbr_id)::text) AND ((ord_ymd)::text = '20241221'::text))
Planning Time: 0.231 ms
Execution Time: 12.363 ms
/*+ IndexScan(t2 tr_ord_big_tx01) HashJoin(t1 t2) */
Hash Join (actual time=163.716..798.932 rows=455 loops=1)
Hash Cond: ((t2.mbr_id)::text = (t1.mbr_id)::text)
-> Index Scan using tr_ord_big_tx01 on tr_ord_big t2 (actual time=0.094..628.891 rows=25560 loops=1)
Index Cond: ((ord_ymd)::text = '20241221'::text)
-> Hash (actual time=163.587..163.589 rows=2000 loops=1)
Buckets: 2048 (originally 1024) Batches: 1 (originally 1) Memory Usage: 217kB
-> Seq Scan on ms_mbr_big t1 (actual time=108.786..163.255 rows=2000 loops=1)
Filter: ((nick_nm)::text ~~ 'Air%'::text)
Rows Removed by Filter: 97990
Planning Time: 0.148 ms
Execution Time: 799.576 ms
/*+ IndexScan(t2 tr_ord_big_tx01) MergeJoin(t1 t2) */
Merge Join (actual time=92.605..683.203 rows=455 loops=1)
Merge Cond: ((t1.mbr_id)::text = (t2.mbr_id)::text)
-> Index Scan using ms_mbr_big_pk on ms_mbr_big t1 (actual time=92.530..102.095 rows=2000 loops=1)
Filter: ((nick_nm)::text ~~ 'Air%'::text)
Rows Removed by Filter: 97990
-> Index Scan using tr_ord_big_tx01 on tr_ord_big t2 (actual time=0.059..576.619 rows=25474 loops=1)
Index Cond: ((ord_ymd)::text = '20241221'::text)
Planning Time: 0.283 ms
Execution Time: 683.857 ms
SQL
복사
이번에는, tx02 인덱스를 사용하도록 힌트를 적용해보자. Nest Loop 뿐만 아니라, Merge, Hash 조인에 모두 효율적으로 동작한다.
/*+ IndexScan(t2 tr_ord_big_tx02) NestLoop(t1 t2) */
Nested Loop (actual time=0.077..12.779 rows=455 loops=1)
-> Seq Scan on ms_mbr_big t1 (actual time=0.015..6.112 rows=2000 loops=1)
Filter: ((nick_nm)::text ~~ 'Air%'::text)
Rows Removed by Filter: 97990
-> Index Scan using tr_ord_big_tx02 on tr_ord_big t2 (actual time=0.003..0.003 rows=0 loops=2000)
Index Cond: (((ord_ymd)::text = '20241221'::text) AND ((mbr_id)::text = (t1.mbr_id)::text))
Planning Time: 0.253 ms
Execution Time: 12.929 ms
/*+ IndexScan(t2 tr_ord_big_tx02) HashJoin(t1 t2) */
Hash Join (actual time=6.907..15.992 rows=455 loops=1)
Hash Cond: ((t2.mbr_id)::text = (t1.mbr_id)::text)
-> Index Scan using tr_ord_big_tx02 on tr_ord_big t2 (actual time=0.030..6.667 rows=25560 loops=1)
Index Cond: ((ord_ymd)::text = '20241221'::text)
-> Hash (actual time=6.865..6.867 rows=2000 loops=1)
Buckets: 2048 (originally 1024) Batches: 1 (originally 1) Memory Usage: 217kB
-> Seq Scan on ms_mbr_big t1 (actual time=0.008..6.662 rows=2000 loops=1)
Filter: ((nick_nm)::text ~~ 'Air%'::text)
Rows Removed by Filter: 97990
Planning Time: 0.258 ms
Execution Time: 16.040 ms
/*+ IndexScan(t2 tr_ord_big_tx02) MergeJoin(t1 t2) */
Merge Join (actual time=5.271..18.340 rows=455 loops=1)
Merge Cond: ((t1.mbr_id)::text = (t2.mbr_id)::text)
-> Sort (actual time=5.244..5.385 rows=2000 loops=1)
Sort Key: t1.mbr_id
Sort Method: quicksort Memory: 236kB
-> Seq Scan on ms_mbr_big t1 (actual time=0.005..4.996 rows=2000 loops=1)
Filter: ((nick_nm)::text ~~ 'Air%'::text)
Rows Removed by Filter: 97990
-> Index Scan using tr_ord_big_tx02 on tr_ord_big t2 (actual time=0.020..8.853 rows=25474 loops=1)
Index Cond: ((ord_ymd)::text = '20241221'::text)
Planning Time: 0.188 ms
Execution Time: 18.388 ms
SQL
복사
인덱스 별로, 조인 방식에 따른 실행시간을 정리하면 아래와 같다. tx01을 사용하면 Hash와 Merge 조인에서는 성능상으로 이득이 없음을 알 수 있다.
tx01 | tx02 | |
NestLoop | 12.363 ms | 12.929 ms |
Hash Join | 799.576 ms | 16.040.ms |
Merge Join | 683.857 ms | 18.388 ms |
인덱스를 생성할 때는, WHERE 절의 조건을 걸러내는 것만이 전부가 아니다. 조인 방식도 고려해야 한다. tx02와 같은 유연한(?) 방식으로 인덱스를 설계하면, 조인 방식의 선택은 옵티마이져에 맡길 수 있다. 물론, tx01과 같이 인덱스를 구성해 무조건 Nest Loop로 풀리게 해야 할 때도 있다.
왜 tx02는 세 가지 전략이 모두 가능한지는, 인덱스의 원리와 조인 방식의 원리를 추가로 공부해야 한다. 여기서 간단히 설명할만한 부분은 아니다. 중요한 것은 절대로, ‘조인 조건 컬럼을 인덱스 뒤쪽에 두면 된다.’라고 외우지 않는 것이다. [SQL-1]은 이에 효과가 있도록 필자가 SQL을 구성한 것이다. 실무에서 만나는 SQL은 더욱 복잡하고 다양하다. 결론만 외워서 실무에서 써먹을 수 있는 것은 별로 없다. 더 많은 공부를 통해 원리를 이해하기 바란다.
끝으로, 힌트없이 [SQL-1]을 실행하면, Nest Loop 조인으로 tx02 인덱스를 사용한다.
준비한 내용은 여기까지입니다.
데이터 활용이 필요한 모든 분을 위한 SQL 실무서를 소개합니다.
•
SQL 입문자를 위한 실습환경 구성과 SELECT 기초부터
•
SQL 중급자를 위한 GROUP BY와 JOIN, 그리고 분석함수까지!
•
그림과 친절한 설명, 손끝으로 익히는 실습 SQL과 실무형 퀴즈를 더한 올인원 SQL 실무서
책 정보 링크>>>
SQL STARTER For MySQLSQL STARTER For MySQL
