SQL을 처음 배울 때 가장 먼저 접하는 문법 중 하나가 바로 서브쿼리(Subquery)다.
조건절 안에 쿼리를 넣어 복잡한 조건을 해결하거나 중간 데이터를 만들어내는 데 매우 직관적이라서 많은 개발자가 자주 사용한다.
하지만 실무에서 무심코 사용한 서브쿼리가 MySQL 성능 병목의 원인이 되는 경우가 매우 많다.

🧠 서브쿼리란?
서브쿼리는 다른 쿼리의 안쪽에 포함되는 SELECT 문을 말한다.
보통 다음과 같이 사용된다.
- WHERE절에서 조건으로 사용되는 서브쿼리
- FROM절에서 임시 테이블처럼 사용하는 서브쿼리
- SELECT절에서 계산식처럼 쓰이는 서브쿼리
예시:
SELECT name FROM users
WHERE id = (SELECT user_id FROM orders WHERE order_id = 100);
또는
SELECT name FROM (
SELECT * FROM users WHERE level = 'admin'
) AS admins;
구문 자체는 간단하지만 내부 동작은 그렇지 않다.
⚙️ 서브쿼리의 성능 특징
✅ 1. 서브쿼리는 쿼리 최적화가 제한적이다.
MySQL 옵티마이저는 서브쿼리를 독립된 쿼리 블록으로 취급하기 때문에 JOIN처럼 자유롭게 재정렬하거나 병합하지 못한다.
특히 상관 서브쿼리(correlated subquery)의 경우 외부 쿼리의 각 row마다 서브쿼리가 반복 실행되며 성능이 급격히 떨어질 수 있다.
예시:
SELECT name FROM users u
WHERE EXISTS (
SELECT 1 FROM orders o
WHERE o.user_id = u.id AND o.status = 'pending'
);
위 쿼리는 users의 row 수만큼 orders 테이블을 반복 조회하게 된다.
이럴 경우는 JOIN으로 변경하는 것이 훨씬 효율적이다.
✅ 2. FROM절 서브쿼리는 임시 테이블을 생성한다.
SELECT COUNT(*) FROM (
SELECT * FROM users WHERE is_deleted = 0
) AS active_users;
이런 서브쿼리는 내부적으로 임시 테이블을 메모리에 생성하고 나서 바깥 쿼리가 그걸 참조하는 방식으로 작동한다.
따라서 인덱스가 무시되고, LIMIT, ORDER BY 등이 제대로 최적화되지 않는 상황도 생긴다.
특히 정렬, 그룹핑이 있는 경우 Using temporary; Using filesort 같은 Extra 정보가 EXPLAIN에서 함께 나타나면 성능 경고 신호다.
✅ 3. SELECT절 안의 서브쿼리는 N+1 쿼리 패턴을 만든다.
SELECT name,
(SELECT COUNT(*) FROM orders WHERE user_id = users.id) AS order_count
FROM users;
위 쿼리는 users row 수만큼 orders 테이블을 반복 검색한다.
데이터가 많을 경우 심각한 병목을 유발할 수 있다.
🧪 실무에서 서브쿼리를 JOIN으로 리팩토링한 사례
서브쿼리 사용:
SELECT name FROM users
WHERE id IN (
SELECT user_id FROM orders WHERE amount > 100
);
→ 실행 시간: 2.5초
JOIN으로 변경:
SELECT DISTINCT u.name
FROM users u
JOIN orders o ON u.id = o.user_id
WHERE o.amount > 100;
→ 실행 시간: 0.12초
단순히 JOIN으로 바꿔준 것만으로도 20배 이상 성능 향상.
서브쿼리는 가독성은 높을 수 있어도 성능 면에서는 JOIN보다 불리한 경우가 많다.
🚨 서브쿼리 사용 시 주의사항 요약
- 서브쿼리가 반복 실행되는 구조라면 JOIN으로 재작성 시도
- FROM절 서브쿼리는 임시 테이블 생성이 필요하므로 인덱스를 사용하지 못할 수 있음
- SELECT절 안의 서브쿼리는 row 수만큼 반복 실행되므로 주의
- 복잡한 조건 조합은 CTE로 분리하거나 뷰(View)로 관리하는 것도 고려
- 항상 EXPLAIN으로 실행계획 확인 필수
✅ 정리
- 서브쿼리는 간결하고 직관적이지만 성능 측면에서는 항상 최선의 선택은 아니다.
- 상관 서브쿼리는 성능을 심각하게 떨어뜨릴 수 있으며, JOIN으로 변환 가능한 경우 반드시 변경을 고려해야 한다.
- FROM절 서브쿼리는 임시 테이블로 인해 인덱스가 무시되는 문제가 있으므로 되도록 지양한다.
- 실무에서는 서브쿼리보다 JOIN이나 CTE가 더 효율적일 수 있으며, 항상 EXPLAIN 분석을 통해 실제 실행 계획을 확인해야 한다.
🔗 공식 문서 참고
MySQL 8.0 Reference Manual - Subqueries
'DB' 카테고리의 다른 글
| [MySQL] CTE vs 서브쿼리 성능 비교 및 튜닝 포인트 🧠 (0) | 2025.07.08 |
|---|---|
| [MySQL] 서브쿼리 vs JOIN 실전 성능 비교 예제 (0) | 2025.07.08 |
| [MySQL] CTE(Common Table Expression) 개념과 성능 특성 (0) | 2025.07.08 |
| [MySQL] JOIN의 성능 원리와 최적화 전략 (0) | 2025.07.08 |
| [MySQL] 서브쿼리 vs JOIN vs CTE 기본 개념 비교 총정리 (2) | 2025.07.08 |
| [MySQL] 슬로우 쿼리 실전 튜닝 사례로 배우는 병목 원인 분석 (1) | 2025.07.07 |
| [MySQL] mysqldumpslow, pt-query-digest로 슬로우 쿼리 분석하기 🔧 (4) | 2025.07.04 |
| [MySQL] 슬로우 쿼리 로그 파일 위치와 확인 방법 🗂️ (0) | 2025.07.04 |