요약
- 의존적 서브쿼리는 외부 쿼리의 각 행마다 반복 실행되어 성능이 나쁨
- 서브쿼리를 파생 테이블로 만들어 JOIN하면 한 번만 실행되도록 최적화 가능
본문
의존적 서브쿼리란?
서브쿼리가 외부 쿼리의 컬럼을 참조하는 구조. 외부 쿼리의 각 행마다 서브쿼리가 반복 실행된다.
SELECT *
FROM orders o
WHERE o.amount > (
SELECT AVG(amount)
FROM orders
WHERE customer_id = o.customer_id -- 외부 테이블 참조
);
식별 방법
EXPLAIN 결과의 select_type 컬럼에서 DEPENDENT SUBQUERY 표시 확인
JOIN으로 최적화
서브쿼리를 파생 테이블(Derived Table)로 만들어 한 번만 실행되게 변경:
SELECT o1.*
FROM orders o1
JOIN (
SELECT customer_id, AVG(amount) as avg_amount
FROM orders
GROUP BY customer_id -- 핵심: GROUP BY로 집계를 미리 수행
) ovg ON o1.customer_id = ovg.customer_id
WHERE o1.amount > ovg.avg_amount;
주의사항
- 결과가 달라질 수 있음: 1:N 관계에서 JOIN은 행이 증가할 수 있어
DISTINCT필요할 수 있음 - NULL 처리 차이:
NOT IN은 NULL이 있으면 전체 결과가 비어버림 - 항상 빠른 건 아님: MySQL 8.0+ 옵티마이저가 자동으로 세미조인 최적화를 적용하기도 함
LIMIT이 있으면 서브쿼리가 더 빠를 수 있다
LIMIT이 있는 쿼리에서는 서브쿼리가 JOIN보다 빠를 수 있다.
| 항목 | 서브쿼리 | JOIN |
|---|---|---|
| LIMIT 적용 시점 | 조건 만족 즉시 종료 | JOIN 완료 후 적용 |
| 처리 행 수 | 필요한 만큼만 | 전체 JOIN 결과 |
서브쿼리 - Limit push-down으로 early termination 가능:
-> Filter: ... and ((select #3) is not null) (rows=200 loops=1)
-> Select #3 (subquery; dependent)
-> Limit: 1 row(s) -- EXISTS 체크 후 바로 탈출
- 200건 찾는 즉시 종료
- 서브쿼리도
Limit: 1로 첫 매치만 찾으면 됨
JOIN - 전체 JOIN 완료 후 LIMIT 적용:
-> Nested loop inner join (rows=200 loops=1)
-> Index lookup on ss ... (loops=3738) -- 3,738건 전부 처리
- JOIN 결과가 나와야 필터링 가능
- 불필요한 행까지 모두 처리
실제 성능 비교 (동일 쿼리):
- 서브쿼리: 47.7ms (5,003건 스캔, 서브쿼리 1,576번)
- JOIN: 307ms (29,717건 스캔, JOIN 3,738번)
결론: LIMIT이 있고 선택도가 높은 조건이라면 서브쿼리가 유리할 수 있다. 반드시 EXPLAIN ANALYZE로 비교할 것.
참고
- Index Intersection은 여러 단일 인덱스 결과를 조합한다
- OR 연산은 인덱스의 연속 스캔을 방해해 비효율을 유발한다.
복합 인덱스 설계 원칙 - 동등 조건 컬럼은 왼쪽에, 범위(BETWEEN, <, > 등) 조건 컬럼은 오른쪽에- pt-query-digest로 슬로우 쿼리에 대한 정보를 자세히 볼 수 있다.
- 선택도와 카디날리티 차이
- B+Tree는 디스크 접근을 최소화하기 위해 설계된 균형 트리 자료구조다