LATERAL JOIN은 오픈소스 DBMS에서 인라인 뷰로 인한 SQL 성능 문제의 돌파구다.
1. 준비하기
이번 글에서는 동일한 SQL에 대해, 각 DBMS가 어떻게 처리하는지 살펴보고, 오픈소스 DBMS에서 LATERAL 조인을 통해 성능 개선을 하는 과정을 설명한다.
테스트에 사용하는 DBMS별 버전은 다음과 같다.
•
ORACLE: Oracle Database 19c(Windows)
•
PostgreSQL: PostgreSQL 16.4 (Debian 16.4-1.pgdg120+1) on x86_64-pc-linux-gnu(Docker)
•
MySQL: 8.0.35(Windows)
DBMS별로 ms_shop과 tr_ord_big이라는 동일한 구조와 동일한 데이터를 가진 테이블을 생성해 테스트를 진행한다. (ms_shop과 tr_ord는 SQL STARTER에서 사용하는 실습용 테이블이다. tr_ord_big은 tr_ord를 성능 테스트에 맞게 데이터양을 증가시킨 테이블이다.) 테스트를 위해 각 DBMS의 tr_ord_big에는 아래와 같은 tx01 인덱스를 생성한다.
CREATE INDEX startdbora.tr_ord_big_tx01 ON startdbora.tr_ord_big(shop_id, ord_dtm); -- For ORACLE
CREATE INDEX tr_ord_big_tx01 ON startdbpg.tr_ord_big(shop_id, ord_dtm); -- For PostgreSQL
CREATE INDEX tr_ord_big_tx01 ON startdbmy.tr_ord_big(shop_id, ord_dtm); -- For MySQL
SQL
복사
2. Simple View
아래의 [SQL-1]은 ms_shop과 tr_ord_big을 조인하는 SQL이다. tr_ord_big은 인라인 뷰로 처리되어 있다. [SQL-1]은 어느 DBMS든 성능상 이슈가 없다.
[SQL-1] ORACLE
SELECT t1.shop_id ,MAX(t1.shop_nm) shop_nm ,SUM(t2.ord_amt) ord_amt_sum
FROM startdbora.ms_shop t1
INNER JOIN (
SELECT a.shop_id ,a.ord_amt
FROM startdbora.tr_ord_big a
WHERE a.ord_dtm >= TO_DATE('20250101','YYYYMMDD')
AND a.ord_dtm < TO_DATE('20250102','YYYYMMDD')
) t2 ON (t1.shop_id = t2.shop_id)
WHERE t1.shop_nm LIKE 'New York%'
AND t1.shop_size >= 150
GROUP BY t1.shop_id;
--------------------------------------------------------------------------------------------------------------------------------------
| Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers | OMem | 1Mem | Used-Mem |
--------------------------------------------------------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 1 | | 1 |00:00:00.01 | 265 | | | |
| 1 | HASH GROUP BY | | 1 | 1 | 1 |00:00:00.01 | 265 | 812K| 812K| 767K (0)|
| 2 | NESTED LOOPS | | 1 | 16 | 252 |00:00:00.01 | 265 | | | |
| 3 | NESTED LOOPS | | 1 | 41 | 252 |00:00:00.01 | 13 | | | |
|* 4 | TABLE ACCESS FULL | MS_SHOP | 1 | 1 | 2 |00:00:00.01 | 6 | | | |
|* 5 | INDEX RANGE SCAN | TR_ORD_BIG_TX01 | 2 | 41 | 252 |00:00:00.01 | 7 | | | |
| 6 | TABLE ACCESS BY INDEX ROWID| TR_ORD_BIG | 252 | 41 | 252 |00:00:00.01 | 252 | | | |
--------------------------------------------------------------------------------------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
4 - filter(("T1"."SHOP_SIZE">=150 AND "T1"."SHOP_NM" LIKE 'New York%'))
5 - access("T1"."SHOP_ID"="A"."SHOP_ID" AND "A"."ORD_DTM">=TO_DATE(' 2025-01-01 00:00:00', 'syyyy-mm-dd hh24:mi:ss') AND
"A"."ORD_DTM"<TO_DATE(' 2025-01-02 00:00:00', 'syyyy-mm-dd hh24:mi:ss'))
[SQL-1] PostgreSQL
EXPLAIN (ANALYZE,COSTS OFF)
SELECT t1.shop_id ,MAX(t1.shop_nm) shop_nm ,SUM(t2.ord_amt) ord_amt_sum
FROM startdbpg.ms_shop t1
INNER JOIN (
SELECT a.shop_id ,a.ord_amt
FROM startdbpg.tr_ord_big a
WHERE a.ord_dtm >= TO_DATE('20250101','YYYYMMDD')
AND a.ord_dtm < TO_DATE('20250102','YYYYMMDD')
) t2 ON (t1.shop_id = t2.shop_id)
WHERE t1.shop_nm LIKE 'New York%'
AND t1.shop_size >= 150
GROUP BY t1.shop_id;
GroupAggregate (actual time=0.473..0.474 rows=1 loops=1)
Group Key: t1.shop_id
-> Nested Loop (actual time=0.086..0.399 rows=252 loops=1)
-> Index Scan using ms_shop_pk on ms_shop t1 (actual time=0.031..0.108 rows=2 loops=1)
Filter: (((shop_nm)::text ~~ 'New York%'::text) AND (shop_size >= 150))
Rows Removed by Filter: 298
-> Index Scan using tr_ord_big_tx01 on tr_ord_big a (actual time=0.032..0.108 rows=126 loops=2)
Index Cond: (((shop_id)::text = (t1.shop_id)::text) AND (ord_dtm >= to_date('20250101'::text, 'YYYYMMDD'::text)) AND (ord_dtm < to_date('20250102'::text, 'YYYYMMDD'::text)))
Planning Time: 0.440 ms
Execution Time: 0.525 ms
[SQL-1] MySQL
EXPLAIN ANALYZE
SELECT t1.shop_id ,t1.shop_nm ,SUM(t2.ord_amt) ord_amt_sum
FROM startdbmy.ms_shop t1
INNER JOIN (
SELECT a.shop_id ,a.ord_amt
FROM startdbmy.tr_ord_big a
WHERE a.ord_dtm >= STR_TO_DATE('20250101','%Y%m%d')
AND a.ord_dtm < STR_TO_DATE('20250102','%Y%m%d')
) t2 ON (t1.shop_id = t2.shop_id)
WHERE t1.shop_nm LIKE 'New York%'
AND t1.shop_size >= 150
GROUP BY t1.shop_id;
-> Group aggregate: sum(startdbmy.a.ord_amt) (cost=18681 rows=300) (actual time=148..148 rows=1 loops=1)
-> Nested loop inner join (cost=17732 rows=9495) (actual time=98.1..148 rows=252 loops=1)
-> Filter: ((startdbmy.t1.shop_nm like 'New York%') and (startdbmy.t1.shop_size >= 150)) (cost=32 rows=11.1) (actual time=18.5..18.7 rows=2 loops=1)
-> Index scan on t1 using PRIMARY (cost=32 rows=300) (actual time=18.4..18.7 rows=300 loops=1)
-> Index lookup on a using tr_ord_big_tx01 (shop_id=startdbmy.t1.shop_id), with index condition: ((startdbmy.a.ord_dtm >= <cache>(str_to_date('20250101','%Y%m%d'))) and (startdbmy.a.ord_dtm < <cache>(str_to_date('20250102','%Y%m%d')))) (cost=825 rows=855) (actual time=61.8..64.4 rows=126 loops=2)
SQL
복사
DBMS별 실행시간과 처리 과정을 정리하면 다음과 같다.
[SQL-1] | 실행시간 | 인라인 뷰(t2) 처리 방식 |
ORACLE | 0.01 초 | 인라인 뷰를 내부적으로 제거하고 t1과 조인 처리 |
PostgreSQL | 0.0005 초 | 인라인 뷰를 내부적으로 제거하고 t1과 조인 처리 |
MySQL | 0.14 초 | 인라인 뷰를 내부적으로 제거하고 t1과 조인 처리 |
환경에 따른 성능 차이
위 결과로 PostgreSQL이 가장 좋다고 단정하면 안된다. 세 DBMS는 하나의 노트북에 모두 설치된 상태다. 동일한 스키마와 데이터로 구성되어 있지만, DBMS마다 설치 방식(Docker, 윈도우 네이티브)이 다르며 사용하는 자원도 다르다. 그러므로 1초 미만의 실행 시간 차이는, 환경에 따른 차이로 크게 의미가 없다.
[SQL-1]의 인라인 뷰(t2)는 tr_ord_big을 단순 조회하고 있다. GROUP BY나 분석함수, 집계함수 등이 사용되지 않고 있다. 이처럼 인라인 뷰에서 SQL이 단순 조회인 경우를 Simple View라고 한다. 단순 뷰는 DBMS가 알아서, 인라인 뷰를 제거하고 조인 SQL로 변경해 SQL을 처리한다.
3. Complex View
이번에는 아래의 [SQL-2]를 살펴보자. 인라인 뷰 안에서 GROUP BY를 처리하고, 메인쿼리에서는 GROUP BY 처리를 제거했다. (사실 [SQL-2]는 [SQL-1]과 완전히 같은 SQL이다.)
[SQL-2] ORACLE
SELECT t1.shop_id, t1.shop_nm ,t2.ord_amt_sum
FROM startdbora.ms_shop t1
INNER JOIN (
-- 인라인 뷰에서 GROUP BY 처리
SELECT a.shop_id ,SUM(a.ord_amt) ord_amt_sum
FROM startdbora.tr_ord_big a
WHERE a.ord_dtm >= TO_DATE('20250101','YYYYMMDD')
AND a.ord_dtm < TO_DATE('20250102','YYYYMMDD')
GROUP BY a.shop_id
) t2 ON (t1.shop_id = t2.shop_id)
WHERE t1.shop_nm LIKE 'New York%'
AND t1.shop_size >= 150;
--------------------------------------------------------------------------------------------------------------------------------------
| Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers | OMem | 1Mem | Used-Mem |
--------------------------------------------------------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 1 | | 1 |00:00:00.01 | 265 | | | |
| 1 | HASH GROUP BY | | 1 | 16 | 1 |00:00:00.01 | 265 | 948K| 948K| 509K (0)|
| 2 | NESTED LOOPS | | 1 | 16 | 252 |00:00:00.01 | 265 | | | |
| 3 | NESTED LOOPS | | 1 | 41 | 252 |00:00:00.01 | 13 | | | |
|* 4 | TABLE ACCESS FULL | MS_SHOP | 1 | 1 | 2 |00:00:00.01 | 6 | | | |
|* 5 | INDEX RANGE SCAN | TR_ORD_BIG_TX01 | 2 | 41 | 252 |00:00:00.01 | 7 | | | |
| 6 | TABLE ACCESS BY INDEX ROWID| TR_ORD_BIG | 252 | 41 | 252 |00:00:00.01 | 252 | | | |
--------------------------------------------------------------------------------------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
4 - filter(("T1"."SHOP_SIZE">=150 AND "T1"."SHOP_NM" LIKE 'New York%'))
5 - access("T1"."SHOP_ID"="A"."SHOP_ID" AND "A"."ORD_DTM">=TO_DATE(' 2025-01-01 00:00:00', 'syyyy-mm-dd hh24:mi:ss') AND
"A"."ORD_DTM"<TO_DATE(' 2025-01-02 00:00:00', 'syyyy-mm-dd hh24:mi:ss'))
[SQL-2] PostgreSQL
EXPLAIN ANALYZE
SELECT t1.shop_id ,t1.shop_nm ,t2.ord_amt_sum
FROM startdbpg.ms_shop t1
INNER JOIN (
SELECT a.shop_id ,SUM(a.ord_amt) ord_amt_sum
FROM startdbpg.tr_ord_big a
WHERE a.ord_dtm >= TO_DATE('20250101','YYYYMMDD')
AND a.ord_dtm < TO_DATE('20250102','YYYYMMDD')
GROUP BY a.shop_id
) t2 ON (t1.shop_id = t2.shop_id)
WHERE t1.shop_nm LIKE 'New York%'
AND t1.shop_size >= 150;
Hash Join (cost=730871.22..731031.58 rows=5 width=51) (actual time=1245.993..1247.499 rows=1 loops=1)
Hash Cond: ((a.shop_id)::text = (t1.shop_id)::text)
-> GroupAggregate (cost=730862.65..731019.26 rows=297 width=37) (actual time=906.126..909.029 rows=186 loops=1)
Group Key: a.shop_id
-> Sort (cost=730862.65..730913.62 rows=20386 width=10) (actual time=905.999..906.815 rows=21996 loops=1)
Sort Key: a.shop_id
Sort Method: quicksort Memory: 1628kB
-> Bitmap Heap Scan on tr_ord_big a (cost=663883.38..729403.50 rows=20386 width=10) (actual time=891.909..894.944 rows=21996 loops=1)
Recheck Cond: ((ord_dtm >= to_date('20250101'::text, 'YYYYMMDD'::text)) AND (ord_dtm < to_date('20250102'::text, 'YYYYMMDD'::text)))
Heap Blocks: exact=295
-> Bitmap Index Scan on tr_ord_big_tx01 (cost=0.00..663878.29 rows=20386 width=0) (actual time=891.832..891.833 rows=21996 loops=1)
Index Cond: ((ord_dtm >= to_date('20250101'::text, 'YYYYMMDD'::text)) AND (ord_dtm < to_date('20250102'::text, 'YYYYMMDD'::text)))
-> Hash (cost=8.50..8.50 rows=5 width=19) (actual time=338.374..338.375 rows=2 loops=1)
Buckets: 1024 Batches: 1 Memory Usage: 9kB
-> Seq Scan on ms_shop t1 (cost=0.00..8.50 rows=5 width=19) (actual time=338.284..338.344 rows=2 loops=1)
Filter: (((shop_nm)::text ~~ 'New York%'::text) AND (shop_size >= 150))
Rows Removed by Filter: 298
Planning Time: 1.067 ms
Execution Time: 1250.511 ms|
[SQL-2] MySQL
EXPLAIN ANALYZE
SELECT t1.shop_id ,t1.shop_nm ,t2.ord_amt_sum
FROM startdbmy.ms_shop t1
INNER JOIN (
SELECT /*+ NO_INDEX(a) */ -- 테이블 FULL SCAN으로 처리
-- > 힌트가 없으면 tx01 인덱스를 매우 비효율적으로 사용해, 5분 이상 걸려도 완료되지 못한다.
a.shop_id ,SUM(a.ord_amt) ord_amt_sum
FROM startdbmy.tr_ord_big a
WHERE a.ord_dtm >= STR_TO_DATE('20250101','%Y%m%d')
AND a.ord_dtm < STR_TO_DATE('20250102','%Y%m%d')
GROUP BY a.shop_id
) t2 ON (t1.shop_id = t2.shop_id)
WHERE t1.shop_nm LIKE 'New York%'
AND t1.shop_size >= 150;
-> Nested loop inner join (cost=26927 rows=0) (actual time=14421..14423 rows=1 loops=1)
-> Filter: ((startdbmy.t1.shop_nm like 'New York%') and (startdbmy.t1.shop_size >= 150)) (cost=34 rows=11.1) (actual time=1.46..3.25 rows=2 loops=1)
-> Table scan on t1 (cost=34 rows=300) (actual time=1.42..3.19 rows=300 loops=1)
-> Index lookup on t2 using <auto_key0> (shop_id=startdbmy.t1.shop_id) (cost=0.259..2508 rows=9683) (actual time=7210..7210 rows=0.5 loops=2)
-> Materialize (cost=0..0 rows=0) (actual time=14420..14420 rows=186 loops=1)
-> Table scan on <temporary> (actual time=14420..14420 rows=186 loops=1)
-> Aggregate using temporary table (actual time=14420..14420 rows=186 loops=1)
-> Filter: ((startdbmy.a.ord_dtm >= <cache>(str_to_date('20250101','%Y%m%d'))) and (startdbmy.a.ord_dtm < <cache>(str_to_date('20250102','%Y%m%d')))) (cost=2.75e+6 rows=2.91e+6) (actual time=14124..14408 rows=21996 loops=1)
-> Table scan on a (cost=2.75e+6 rows=26.2e+6) (actual time=0.402..12670 rows=26.3e+6 loops=1)
SQL
복사
DBMS별 실행 시간과 처리 과정을 정리하면 다음과 같다. MySQL은 힌트 없이 실행하면 tx01 인덱스를 FULL SCAN 방식으로 사용하며, 5분 이상 걸려도 응답이 나오지 않아 테이블을 FULL SCAN 처리하도록 힌트를 적용했다는 점을 참고하기 바란다. MySQL이 PostgreSQL 보다 많이 느린 이유는 MySQL이 가지고 있는 클러스터드 구조 때문이다. 물론, 이 구조가 무조건 나쁜 것은 아니다. 성능에 도움이 되는 경우도 있다.
[SQL-2] | 실행시간 | 인라인 뷰(t2) 처리 방식 | 메모 |
ORACLE | 0.01 초 | 인라인 뷰를 내부적으로 제거하고 t1과 조인 처리 | [SQL-1]과 같은 처리 방식 |
PostgreSQL | 1.2 초 | t1의 shop_id 조건 없이, t2만 별도 처리한 후에 t1과 조인 처리 | tx01 인덱스의 선두 컬럼(shop_id)을 사용하지 못하고, tx01 인덱스를 FULL SCAN 처리 |
MySQL | 14.4 초 | t1의 shop_id 조건 없이, t2만 별도 처리한 후에 t1과 조인 처리 | MySQL 역시 PostgreSQL과 동일한 방식으로 처리됨.
하지만, tx01 인덱스 사용시, 5분 이상 걸려도 응답이 나오지 않음.
그러므로 tr_ord_big 테이블을 강제로 FULL SCAN으로 처리 |
ORACLE이 최고인가?
[SQL-2]의 실행 시간 만을 통해, ORACLE이 DBMS 중에 가장 좋다고 단정할 수 없다. 하지만, 해당 SQL 패턴과 관련해서는 ORACLE의 Optimizer가 기본적으로 똑똑하다고 할 수는 있다.
[SQL-2]는 인라인 뷰 안에서 tr_ord_big을 GROUP BY 처리했다. 이처럼 인라인 뷰 안에서 데이터를 집계한 경우를 보통 Complex View라고 한다. 말 그대로 복잡한 뷰는, DBMS가 처리할 때, 별도 처리한 후에 메인 쿼리의 테이블과 조인 처리하는 것을 선호한다. 오픈소스 DBMS는 이로 인해 성능이 저하되는 경우가 많다. [SQL-2]를 실행하면 ORACLE을 제외하고 모두 낙제점이다.
4. LATERAL JOIN
사실, [SQL-2]는 [SQL-1]과 동일한 결과가 출력된다. 그러므로 인라인 뷰를 해체하고 [SQL-1]처럼 SQL을 재작성하면 문제가 해결된다. 하지만, 그렇게 할 수 없는 상황이라면, LATERAL 조인이 돌파구가 될 수 있다. 실제 튜닝을 할때도, 인라인 뷰를 해체할 수 없는 상황이 많다.
아래는 [SQL-2]를 LATERAL 조인으로 처리한 결과다. PostgreSQL과 MySQL만 살펴보자.
[SQL-2] PostgreSQL LATERAL 조인 처리
EXPLAIN ANALYZE
SELECT t1.shop_id ,t1.shop_nm ,t2.ord_amt_sum
FROM startdbpg.ms_shop t1
INNER JOIN LATERAL ( -- > LATERAL 조인으로 변경
SELECT a.shop_id ,SUM(a.ord_amt) ord_amt_sum
FROM startdbpg.tr_ord_big a
WHERE a.ord_dtm >= TO_DATE('20250101','YYYYMMDD')
AND a.ord_dtm < TO_DATE('20250102','YYYYMMDD')
AND a.shop_id = t1.shop_id -- > LATERAL 조인 조건 추가
GROUP BY a.shop_id
) t2 ON (t1.shop_id = t2.shop_id)
WHERE t1.shop_nm LIKE 'New York%'
AND t1.shop_size >= 150;
Nested Loop (cost=0.57..1419.39 rows=1 width=51) (actual time=0.520..0.615 rows=1 loops=1)
-> Seq Scan on ms_shop t1 (cost=0.00..8.50 rows=5 width=19) (actual time=0.035..0.081 rows=2 loops=1)
Filter: (((shop_nm)::text ~~ 'New York%'::text) AND (shop_size >= 150))
Rows Removed by Filter: 298
-> Subquery Scan on t2 (cost=0.57..282.17 rows=1 width=37) (actual time=0.262..0.263 rows=0 loops=2)
Filter: ((t1.shop_id)::text = (t2.shop_id)::text)
-> GroupAggregate (cost=0.57..282.15 rows=1 width=37) (actual time=0.259..0.260 rows=0 loops=2)
-> Index Scan using tr_ord_big_tx01 on tr_ord_big a (cost=0.57..281.97 rows=69 width=10) (actual time=0.045..0.202 rows=126 loops=2)
Index Cond: (((shop_id)::text = (t1.shop_id)::text) AND (ord_dtm >= to_date('20250101'::text, 'YYYYMMDD'::text)) AND (ord_dtm < to_date('20250102'::text, 'YYYYMMDD'::text)))
Planning Time: 0.292 ms
Execution Time: 0.673 ms
[SQL-2] MySQL LATERAL 조인 처리
EXPLAIN ANALYZE
SELECT t1.shop_id ,t1.shop_nm ,t2.ord_amt_sum
FROM startdbmy.ms_shop t1
INNER JOIN LATERAL ( -- > LATERAL 조인 처리
SELECT a.shop_id ,SUM(a.ord_amt) ord_amt_sum
FROM startdbmy.tr_ord_big a
WHERE a.ord_dtm >= STR_TO_DATE('20250101','%Y%m%d')
AND a.ord_dtm < STR_TO_DATE('20250102','%Y%m%d')
AND a.shop_id = t1.shop_id -- > LATERAL 조인 조건 추가
GROUP BY a.shop_id
) t2 ON (t1.shop_id = t2.shop_id)
WHERE t1.shop_nm LIKE 'New York%'
AND t1.shop_size >= 150;
-> Nested loop inner join (cost=1924 rows=9495) (actual time=31..55.4 rows=1 loops=1)
-> Invalidate materialized tables (row from t1) (cost=31 rows=11.1) (actual time=0.123..0.331 rows=2 loops=1)
-> Filter: ((startdbmy.t1.shop_nm like 'New York%') and (startdbmy.t1.shop_size >= 150)) (cost=31 rows=11.1) (actual time=0.123..0.328 rows=2 loops=1)
-> Table scan on t1 (cost=31 rows=300) (actual time=0.0833..0.272 rows=300 loops=1)
-> Index lookup on t2 using <auto_key0> (shop_id=startdbmy.t1.shop_id) (cost=1012..1088 rows=9.4) (actual time=27.6..27.6 rows=0.5 loops=2)
-> Materialize (invalidate on row from t1) (cost=1003..1003 rows=855) (actual time=27.5..27.5 rows=0.5 loops=2)
-> Group aggregate: sum(startdbmy.a.ord_amt) (cost=918 rows=855) (actual time=27.5..27.5 rows=0.5 loops=2)
-> Index lookup on a using tr_ord_big_tx01 (shop_id=startdbmy.t1.shop_id), with index condition: ((startdbmy.a.ord_dtm >= <cache>(str_to_date('20250101','%Y%m%d'))) and (startdbmy.a.ord_dtm < <cache>(str_to_date('20250102','%Y%m%d')))) (cost=832 rows=855) (actual time=26.8..27.5 rows=126 loops=2)
SQL
복사
LATERAL 처리를 고려해, [SQL-2]에 대한 성능 차이를 다시 정리해보면 다음과 같다. PostgreSQL과 MySQL도 정신 차린 것을 알 수 있다.
[SQL-2] | 실행시간 | 인라인 뷰(t2) 처리 방식 | 메모 |
ORACLE | 0.01 초 | 인라인 뷰를 내부적으로 제거하고 t1과 조인 처리 | [SQL-1]과 같은 처리 방식 |
PostgreSQL
LATERAL 처리 | 0.00067 초 | 인라인 뷰 안으로 shop_id 조건이 파고 들어 tx01 인덱스를 사용 | [SQL-1]과 다른 처리 방식이지만,
tx01인덱스를 효율적으로 활용해 성능에 문제가 없다. |
MySQL
LATERAL 처리 | 0.055 초 | 인라인 뷰 안으로 shop_id 조건이 파고 들어 tx01 인덱스를 사용 | [SQL-1]과 다른 처리 방식이지만,
tx01인덱스를 효율적으로 활용해 성능에 문제가 없다. |
5. LATERAL이 답인가?
이 글에서 다룬 예제는 인라인 뷰 내부로 조건이 전달 될 때 성능이 개선되는 전형적인 케이스다. 그러나 현실에서는 정반대로 인라인 뷰를 완전히 독립 실행해야 성능이 나는 쿼리도 많다. 그런 상황에서 LATERAL을 무턱대고 적용하면, 오히려 느려지는 원인이 될 수 있다.
•
LATERAL은 특정 패턴의 성능을 해결할 수 있는 방법 중에 하나로, 언제나 옳은 선택이 아니다.
•
LATERAL의 사용 여부는 SQL문장, 실행계획, 데이터 분포, 인덱스 등을 종합적으로 살펴보고 결정해야 한다.
이 글은 “언제 LATERAL을 써야 하는가”라는 원리까지 설명하지 않는다. 그 주제만으로도 한 챕터가 필요하기 때문이다. 대신 다음과 같은 태도를 권한다.
계속해서 SQL 튜닝을 공부하고 실습해 나가며, LATERAL이 성능의 “돌파구”인지 “걸림돌”인지 스스로 판별할 수 있는 역량을 기르길 바란다.
끝으로, “Oracle에서 잘 돌아가니 PostgreSQL이나 MySQL에서도 문제없다”는 식의 영업 멘트를 그대로 믿지 말기 바란다. DBMS마다 옵티마이저가 다르다. 어떤 쿼리는 Oracle이 더 빠르고, 어떤 쿼리는 PostgreSQL이나 MySQL이 더 빠를 수도 있다. 다른 DBMS로 마이그레이션하면 성능 이슈가 반드시 발생한다는 사실을 염두에 두고, 충분한 테스트와 튜닝을 거쳐야 한다.
준비한 내용은 여기까지입니다.
데이터 활용이 필요한 모든 분을 위한 SQL 실무서를 소개합니다.
•
SQL 입문자를 위한 실습환경 구성과 SELECT 기초부터
•
SQL 중급자를 위한 GROUP BY와 JOIN, 그리고 분석함수까지!
•
그림과 친절한 설명, 손끝으로 익히는 실습 SQL과 실무형 퀴즈를 더한 올인원 SQL 실무서
책 정보 링크>>>
SQL STARTER For MySQLSQL STARTER For MySQL
