TECH/sql

[SQL 성능 최적화] 서브쿼리 vs 조인: 성능 차이와 상황별 선택 가이드 (EXISTS 활용법 포함)

런코리치 2026. 2. 26. 15:00

[SQL] 서브쿼리 vs 조인: 성능 차이의 진실과 상황별 선택 가이드

개발을 하다 보면 같은 데이터를 추출하기 위해 서브쿼리를 쓸지, 조인을 쓸지 고민에 빠지곤 합니다. 과거에는 "서브쿼리는 느리니 무조건 조인으로 바꿔라"라는 말이 정설처럼 여겨졌지만, 최신 DBMS의 성능 향상으로 이제는 상황에 따른 전략적 선택이 중요해졌습니다.

오늘은 서브쿼리와 조인의 내부 동작 원리를 비교하고, 실무에서 성능을 극대화할 수 있는 선택 기준을 상세히 알아보겠습니다.


1. 서브쿼리와 조인의 기본 개념

① 조인 (Join)

두 개 이상의 테이블을 연결하여 하나의 결과 집합을 만드는 방식입니다. 카테시안 곱(Cartesian Product)을 먼저 생성한 뒤 조건에 맞는 행을 걸러내는 논리적 구조를 가집니다.

② 서브쿼리 (Subquery)

하나의 쿼리 안에 포함된 또 다른 쿼리를 말합니다. 메인 쿼리에 값을 전달하거나, 특정 조건의 존재 여부를 확인하는 용도로 주로 사용됩니다.


2. 성능 차이는 어디에서 발생하는가?

과거에 서브쿼리가 느렸던 이유는 연산의 반복성 때문입니다.

  • 비상관 서브쿼리: 서브쿼리가 먼저 한 번 실행되고 그 결과를 메인 쿼리가 이용하므로 비교적 효율적입니다.
  • 상관 서브쿼리 (Correlated Subquery): 메인 쿼리의 각 행마다 서브쿼리가 매번 실행됩니다. 만약 메인 쿼리의 행이 100만 개라면 서브쿼리도 100만 번 호출되어 성능이 급격히 저하됩니다.

반면, 조인은 옵티마이저가 인덱스 상황에 따라 Nested Loop, Hash Join, Sort Merge Join 등 최적의 경로를 선택할 수 있어 대량의 데이터를 처리할 때 유리합니다.


3. 실무 예제로 보는 비포 & 애프터

💡 시나리오: 최근 1년 이내에 주문 내역이 있는 고객 목록 조회

[서브쿼리 방식 - IN 활용]

SQL
SELECT customer_id, customer_name
FROM customers
WHERE customer_id IN (
    SELECT customer_id 
    FROM orders 
    WHERE order_date >= NOW() - INTERVAL '1 year'
);
  • 장점: 쿼리가 직관적이며, 고객 정보가 중복 출력되지 않습니다.
  • 단점: 서브쿼리 결과가 너무 크면 IN 절 내의 데이터 처리에 부하가 걸립니다.

[조인 방식]

SQL
SELECT DISTINCT c.customer_id, c.customer_name
FROM customers c
JOIN orders o ON c.customer_id = o.customer_id
WHERE o.order_date >= NOW() - INTERVAL '1 year';
  • 장점: 옵티마이저가 인덱스를 활용해 대량 데이터를 효율적으로 처리합니다.
  • 단점: 고객당 주문이 여러 건일 경우 중복 데이터가 발생하여 DISTINCT를 써야 하므로 추가적인 정렬 비용이 발생할 수 있습니다.

4. 언제 무엇을 써야 할까? (결정적 가이드)

✅ 조인(Join)을 써야 하는 경우

  1. 여러 테이블의 컬럼이 동시에 필요할 때: 서브쿼리는 결과값으로 메인 쿼리의 컬럼만 보낼 수 있지만, 조인은 연결된 모든 테이블의 필드를 SELECT 할 수 있습니다.
  2. 대용량 데이터 집합을 결합할 때: 해시 조인(Hash Join) 등을 활용할 수 있는 조인 방식이 대량 데이터 처리에서 압도적으로 유리합니다.

✅ 서브쿼리(Subquery)를 써야 하는 경우

  1. 복잡한 집계 후 메인 쿼리와 비교할 때: 그룹화(GROUP BY)를 먼저 수행한 결과를 메인 테이블과 비교해야 할 때 유용합니다.
  2. 존재 여부만 확인할 때 (EXISTS): EXISTS는 조건을 만족하는 행을 찾는 순간 탐색을 멈추기 때문에, 단순 존재 유무 확인은 조인보다 빠를 수 있습니다.
  3. 가독성이 중요할 때: 쿼리 구조가 논리적으로 분리되어 있어 코드를 읽기가 더 편한 경우가 많습니다.

5. 최신 DB 트렌드: 서브쿼리 언네스팅(Unnesting)

최근 Oracle이나 MySQL 8.0+, PostgreSQL의 옵티마이저는 사용자가 서브쿼리를 작성하더라도 내부적으로 이를 조인 형태로 변환(Unnesting)하여 최적화합니다. 따라서 무조건적인 변환보다는 실행 계획(Execution Plan)을 직접 확인해보고 결정하는 것이 가장 정확합니다.


6. 성능 최적화 팁

  • EXISTS를 활용하라: IN은 서브쿼리 결과를 메모리에 모두 올리지만, EXISTS는 조건에 맞는 첫 번째 데이터를 찾으면 즉시 반환하므로 성능상 이점이 큽니다.
  • SELECT 절의 서브쿼리(스칼라 서브쿼리)를 주의하라: SELECT 절에 들어가는 서브쿼리는 메인 쿼리의 레코드 수만큼 반복 실행되므로, 데이터가 많을 때는 LEFT JOIN으로 변경하는 것이 훨씬 빠릅니다.

🏁 마무리하며: "결론은 균형이다"

서브쿼리와 조인은 대결 구도가 아닙니다. 결과 집합이 작고 직관적인 코드가 필요할 때는 서브쿼리를, 대량의 데이터를 정교하고 빠르게 처리해야 할 때는 조인을 선택하는 것이 정답입니다.

가장 좋은 방법은 자신의 쿼리 앞에 EXPLAIN을 붙여보는 습관을 갖는 것입니다. 데이터의 양과 인덱스의 유무에 따라 옵티마이저의 선택은 매번 달라지기 때문입니다.