Zettelkasten

의존적 서브쿼리는 JOIN으로 최적화할 수 있다

·수정 2026.04.23·수정 4

요약

  • 의존적 서브쿼리는 외부 쿼리의 각 행마다 반복 실행되어 성능이 나쁨
  • 서브쿼리를 파생 테이블로 만들어 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로 비교할 것.

참고