Search

오픈소스 DBMS의 인라인 뷰 성능 돌파구: LATERAL JOIN

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 실무서