본문으로 건너뛰기

Subquery

1. 서브쿼리 소개

  • 조인을 통해 흩어진 테이블을 연결할 수 있습니다.
  • 하지만 데이터에 질문을 던지다 보면, JOIN만으로는 한 번에 답하기 어려운, 여러 단계의 사고를 거쳐야 하는 문제들을 만나게 됩니다.

1.1 문제 상황

"우리 쇼핑몰에서 판매하는 상품들의 평균 가격보다 비싼 상품은 무엇이 있을까?"

  • 이 질문에 답하기 위해 대부분의 사람들은 자연스럽게 두 단계로 나누어 생각할 것입니다.

1단계: 전체 상품의 평균 가격을 구한다

SELECT AVG(price) FROM products;

실행 결과

AVG(price)
167166.6667
  • 평균 가격이 약 167,167.67원이라는 것을 알 수 있습니다.

2단계: 그 평균 가격보다 비싼 상품을 찾는다

SELECT name, price
FROM products
WHERE price > 167166.67;

실행 결과

nameprice
4K UHD 모니터350000
스마트 워치280000

1.2 두 번의 쿼리 실행의 문제점

번거롭다

  • 매번 첫 번째 쿼리의 결과를 복사해서 두 번째 쿼리에 붙여 넣어야 합니다.

오류에 취약하다

  • 만약 상품 데이터가 실시간으로 추가되거나 가격이 변경된다면 어떨까요?
  • 1단계 쿼리를 실행하고 2단계 쿼리를 실행하는 그 짧은 순간에도 평균 가격은 변할 수 있습니다.
  • 이렇게 되면 잘못된 기준으로 데이터를 조회하게 될 수도 있습니다.

해결책

  • 이 두 단계를 논리적으로 완벽한 하나의 작업 단위로 묶고 싶습니다.
  • 이럴 때 사용하는 기술이 바로 **서브쿼리(Subquery)**입니다.

1.3 서브쿼리의 개념

  • 서브쿼리는 말 그대로 하나의 SQL 쿼리 문 안에 포함된 또 다른 SELECT 쿼리를 의미합니다.
  • 바깥쪽의 메인쿼리가 실행되기 전에, 괄호 () 안에 있는 서브쿼리가 먼저 실행됩니다.
  • 데이터베이스는 서브쿼리의 실행 결과를 바깥쪽 메인쿼리에게 전달하여, 메인쿼리가 그 결과를 사용해서 최종 작업을 수행하게 됩니다.
SELECT name, price
FROM products
WHERE price > (SELECT AVG(price) FROM products);

쿼리 실행 순서

  1. 데이터베이스는 괄호 안의 서브쿼리 SELECT AVG(price) FROM products가장 먼저 실행합니다.
  2. 서브쿼리가 실행된 결과인 167166.67이라는 단일 값을 얻습니다.
  3. 이제 원래의 쿼리는 내부적으로 다음과 같이 변합니다: SELECT name, price FROM products WHERE price > 167166.67;
  4. 최종적으로 이 변환된 메인쿼리가 실행되어 결과를 보여줍니다.

1.4 서브쿼리 종류와 특징

  • 서브쿼리는 반환하는 행과 컬럼의 수에 따라 종류가 나뉩니다.
  • 사용되는 위치와 연산자에 따라 그 역할이 결정됩니다.
구분반환 형태주요 사용 위치연산자 / 구문명칭핵심 용도
단일 컬럼단일 행SELECT, WHERE, HAVING=, >, <스칼라 서브쿼리단일 값이 필요한 모든 곳
단일 컬럼다중 행WHERE, HAVINGIN, ANY, ALL다중 행 서브쿼리값 목록과 비교
다중 컬럼단일 행WHERE, HAVING(c1, c2) = ...다중 컬럼 서브쿼리여러 컬럼 값을 1:1로 비교
다중 컬럼다중 행WHERE, HAVING(c1, c2) IN ...다중 컬럼 서브쿼리여러 컬럼 조합 비교
다중 컬럼다중 행FROMFROM (...) AS alias테이블 서브쿼리 (인라인 뷰)가상의 테이블을 생성하여 재가공

참고사항

  • 테이블 서브쿼리는 컬럼, 행 수가 단일이어도 쓸 수 있습니다. 실무에선 통상 다중 컬럼, 다중 행에 자주 사용합니다.
  • 각 종류별로 다양한 명칭이 사용됩니다. 외우지 말고 예제를 진행하면서 자연스럽게 이해하세요.
  • 서브쿼리는 JOIN과 함께 복잡한 데이터를 분석하는 양대 산맥과도 같은 기술입니다.
  • JOIN이 테이블을 수평으로 넓혀나가는 기술이라면, 서브쿼리는 쿼리 내부에 논리적인 단계를 만들어 안으로 깊게 파고드는 기술이라고 할 수 있습니다.

2. 스칼라 서브쿼리

  • 단일 컬럼, 단일 행 서브쿼리입니다.
  • 서브쿼리를 실행했을 때 그 결과가 오직 하나의 행, 하나의 컬럼으로 나오는 경우를 말합니다.

2.1 스칼라의 의미

용어 - 스칼라

  • **스칼라(Scalar)**는 원래 수학과 물리학에서 온 단어로, **'단 하나의 값'**을 의미합니다.
  • 결과가 '하나의 값'으로 정해지기 때문에, 이 값을 익숙한 단일 행 비교 연산자들(=, >, <, >=, <=, <>)과 함께 사용할 수 있습니다.
  • "A는 B보다 크다"처럼, 비교 대상 B가 하나의 명확한 값이어야 말이 되는 것과 같은 이치입니다.

2.2 문제 상황

"특정 주문(order_id = 1)을 한 고객과 같은 도시에 사는 모든 고객을 찾고 싶다."

1단계: order_id가 1인 고객의 도시 알아내기

SELECT u.address
FROM users u
JOIN orders o ON u.user_id = o.user_id
WHERE o.order_id = 1;

실행 결과

address
서울시 강남구
  • 주문자 '션'이 '서울시 강남구'에 산다는 것을 알아냈습니다.

2단계: 해당 도시에 사는 모든 고객 찾기

SELECT name, address
FROM users
WHERE address = '서울시 강남구';

실행 결과

nameaddress
서울시 강남구
마리 퀴리서울시 강남구

2.3 단일 행 서브쿼리로 해결하기

  • 이제 이 두 단계를 하나의 쿼리로 합쳐봅시다.
  • 1단계 쿼리가 "비교할 기준값(도시 이름)을 찾아내는" 역할을 하므로, 이 쿼리를 통째로 서브쿼리로 만들어 WHERE 절의 비교 대상 위치에 넣어주면 됩니다.
SELECT name, address
FROM users
WHERE address = (SELECT u.address
FROM users u
INNER JOIN orders o ON u.user_id = o.user_id
WHERE o.order_id = 1);

실행 결과

nameaddress
서울시 강남구
마리 퀴리서울시 강남구

쿼리 실행 흐름

  1. 괄호 안의 서브쿼리가 먼저 실행되어 단일 값 '서울시 강남구'를 반환합니다.
  2. 메인쿼리는 WHERE address = '서울시 강남구'와 동일한 형태로 바뀐다.
  3. 최종적으로 메인쿼리가 실행되어 원하는 결과를 보여줍니다.

2.4 스칼라 서브쿼리의 치명적 오류

  • 단일 행을 반환하는 스칼라 서브쿼리를 사용할 때 가장 주의해야 할 점은, 서브쿼리의 결과가 반드시, 무슨 일이 있어도, 단 하나의 행만 반환해야 한다는 것입니다.
  • 만약 서브쿼리가 두 개 이상의 행을 반환하면 어떻게 될까?

오류 예시

-- 이 쿼리는 의도적으로 오류를 발생시킨다.
SELECT name, address
FROM users
WHERE address = (SELECT address FROM users WHERE name IN ('션', '네이트'));
  • 괄호 안의 서브쿼리는 '서울시 강남구'와 '경기도 성남시'라는 두 개의 행을 반환합니다.
  • 메인쿼리의 WHERE address = ... 비교문은 이 두 개의 값을 어떻게 처리해야 할지 몰라 혼란에 빠집니다.
Error Code: 1242. Subquery returns more than 1 row

해결 방법

  • 단일 행 비교 연산자(=, >, < 등)를 사용할 때는, 서브쿼리의 결과가 반드시 단일 행일 때만 사용해야 합니다.
  • 예를 들어 order_iduser_id처럼 PKUNIQUE 제약 조건이 걸린 컬럼을 조건으로 조회하는 경우가 대표적입니다.
  • 서브쿼리가 여러 개의 행을 반환하는 것이 당연한 상황에서는 = 같은 단일 행 연산자가 아닌, 여러 개의 값을 다룰 수 있는 IN과 같은 다중 행 연산자를 사용해야 합니다.

3. 다중 행 서브쿼리

  • 서브쿼리의 결과가 여러 행을 반환하는 경우, = 같은 단일 행 비교 연산자를 사용하면 에러가 발생합니다.
  • 현실의 문제는 종종 여러 개의 결과를 반환하는 서브쿼리를 필요로 합니다.

3.1 문제 상황

"'전자기기' 카테고리에 속한 모든 상품들을 주문한 주문 내역을 전부 보고 싶다."

생각의 흐름

  1. 먼저, '전자기기' 카테고리에 속한 상품들의 product_id를 모두 찾습니다.
  2. 그다음, orders 테이블에서 product_id가 우리가 찾아낸 product_id 목록 안에 포함된 주문들을 모두 찾습니다.
  • 1단계의 결과는 당연히 여러 개일 것입니다.
  • 우리 쇼핑몰에는 여러 종류의 '전자기기' 상품이 있기 때문입니다.
  • 이처럼 서브쿼리의 결과가 여러 행으로 반환되는 것이 당연할 때 사용하는 것이 바로 다중 행 서브쿼리입니다.

3.2 IN 연산자: 목록에 포함된 값과 일치하는지 확인

  • IN 연산자는 다중 행 서브쿼리와 함께 가장 흔하게 사용되는, 가장 직관적인 연산자입니다.
  • WHERE 컬럼명 IN (값1, 값2, ...)처럼, 특정 컬럼의 값이 괄호 안의 목록 중 하나라도 일치하면 참(true)을 반환합니다.

1단계: '전자기기' 상품의 ID 목록 조회

SELECT product_id FROM products WHERE category = '전자기기'
ORDER BY product_id;

실행 결과

product_id
1
2
3
6
  • 예상대로 '전자기기' 카테고리 상품들의 id 목록 (1, 2, 3, 6)이 반환되었습니다.

2단계: IN을 이용해 최종 결과 조회

SELECT * FROM orders
WHERE product_id IN (SELECT product_id
FROM products
WHERE category = '전자기기')
ORDER BY order_id;

쿼리 실행 흐름

  1. 괄호 안의 서브쿼리가 먼저 실행되어 (1, 2, 3, 6)이라는 id 목록을 반환합니다.
  2. 메인쿼리는 WHERE product_id IN (1, 2, 3, 6)과 동일한 형태로 바뀝니다.
  3. 최종적으로 메인쿼리가 실행됩니다.

실행 결과

order_iduser_idproduct_idorder_datequantitystatus
1112025-06-10 10:00:001COMPLETED
3222025-06-11 14:20:001SHIPPED
5432025-06-15 11:30:001PENDING
6512025-06-16 18:00:001COMPLETED
7212025-06-17 12:00:002SHIPPED
  • IN 연산자를 활용하여 여러 결과를 반환하는 서브쿼리를 깔끔하게 처리했습니다.
  • 참고로 목록에 없는 것을 찾고 싶을 때는 NOT IN을 사용하면 됩니다.

3.3 ANY, ALL 연산자: 목록의 모든/일부 값과 비교

  • ANYALL은 주로 >, <와 같은 비교 연산자와 함께 사용되어, 서브쿼리가 반환한 여러 값들과 비교하는 역할을 합니다.

ANY 연산자

  • > ANY (서브쿼리): 서브쿼리가 반환한 여러 결과값 중 어느 하나보다만 크면 참입니다. 즉, 최소값보다 크면 참이 됩니다.
  • < ANY (서브쿼리): 최대값보다 작으면 참입니다.
  • = ANY (서브쿼리): IN과 완전히 동일한 의미입니다. 목록 중 어느 하나와 같으면 참입니다.

ALL 연산자

  • > ALL (서브쿼리): 서브쿼리가 반환한 여러 결과값 모두보다 커야만 참입니다. 즉, 최대값보다 커야 참이 됩니다.
  • < ALL (서브쿼리): 최소값보다 작으면 참입니다.

예시: 서브쿼리가 (100, 200, 300)을 반환했다고 가정

  • WHERE price > ANY (100, 200, 300): 이 조건은 price > 100과 같습니다.
  • WHERE price > ALL (100, 200, 300): 이 조건은 price > 300과 같습니다.

3.4 '전자기기' 카테고리의 어떤 상품보다도 비싼 상품 찾기

'전자기기' 카테고리의 상품 가격 조회

SELECT price FROM products WHERE category = '전자기기';

실행 결과

price
75000
120000
350000
280000

ANY를 사용한 쿼리

SELECT name, price
FROM products
WHERE price > ANY (SELECT price FROM products WHERE category = '전자기기');
  • 이 쿼리는 price > 75000 OR price > 120000 OR price > 350000 OR price > 280000와 같이 동작합니다.
  • 결국 price가 목록의 최소값인 75000보다 크기만 하면 이 조건이 참이 되므로, price > 75000과 동일한 결과를 냅니다.

실행 결과

nameprice
기계식 키보드120000
4K UHD 모니터350000
고급 가죽 지갑150000
스마트 워치280000

3.5 '전자기기' 카테고리의 모든 상품보다 비싼 상품 찾기

ALL을 사용한 쿼리

SELECT name, price
FROM products
WHERE price > ALL (SELECT price FROM products WHERE category = '전자기기');
  • 서브쿼리는 (75000, 120000, 350000, 280000)를 반환합니다.
  • > ALL 조건은 이 모든 값보다 커야 하므로, 목록의 최대값인 350000보다 커야 참이 됩니다.
  • 즉, price > 350000과 동일하게 동작합니다.

실행 결과

(결과 없음)

  • 현재 products 테이블에는 350,000원보다 비싼 상품이 없으므로 아무런 결과도 반환되지 않습니다.

3.6 실무 팁: MIN, MAX 집계 함수와의 비교

  • ANYALL은 특정 조건을 해결하는 데 유용하지만, 사실 실무에서는 IN 연산자나 MIN(), MAX() 같은 집계 함수를 이용한 서브쿼리로 대체할 수 있는 경우가 많습니다.
  • 오히려 집계 함수를 쓰는 것이 코드가 더 명확하고 직관적일 때가 많습니다.

> ANY 쿼리 대체 (MIN 사용)

SELECT name, price
FROM products
WHERE price > (SELECT MIN(price) FROM products WHERE category = '전자기기');

> ALL 쿼리 대체 (MAX 사용)

SELECT name, price
FROM products
WHERE price > (SELECT MAX(price) FROM products WHERE category = '전자기기');
  • 두 쿼리 모두 ANY, ALL을 사용했을 때와 완전히 동일한 결과를 반환합니다.
  • 많은 개발자들이 ANY, ALL보다는 MIN, MAX를 사용한 코드를 더 이해하기 쉽다고 생각합니다.
  • 그렇기 때문에 ANY, ALL의 사용 빈도는 IN이나 집계 함수에 비해 낮은 편입니다.

4. 다중 컬럼 서브쿼리

  • 지금까지 우리는 서브쿼리가 하나의 컬럼만을 반환하는 경우만 다루었습니다.
  • 하지만 서브쿼리는 SELECT 문이므로, 당연히 여러 개의 컬럼을 반환할 수도 있습니다.

4.1 다중 컬럼 서브쿼리란?

  • **다중 컬럼 서브쿼리(Multi-Column Subquery)**는 서브쿼리의 SELECT 절에 두 개 이상의 컬럼이 포함되는 경우를 말합니다.
  • 이 기법은 메인쿼리의 WHERE 절에서 여러 컬럼을 동시에 비교해야 할 때 매우 유용합니다.

4.2 문제 상황

"고객 '네이트'(user_id=2)가 한 주문(order_id=3)이 있습니다. 이 주문과 동일한 고객이면서 주문 처리 상태(status)도 같은 모든 주문을 찾아보자."

  • 이 문제의 핵심은 user_idstatus라는 두 개의 조건을 동시에 만족하는 주문을 찾는 것입니다.

1단계: 비교 기준이 될 고객 ID와 주문 상태 조회

SELECT user_id, status FROM orders WHERE order_id = 3;
  • order_idPK이므로 이 쿼리는 반드시 하나의 행만 반환합니다.

실행 결과

user_idstatus
2SHIPPED
  • 서브쿼리는 (2, 'SHIPPED')라는 단일 행, 다중 컬럼 결과를 반환합니다.

2단계: 다중 컬럼 비교

SELECT order_id, user_id, status, order_date
FROM orders
WHERE (user_id, status) = (SELECT user_id, status
FROM orders
WHERE order_id = 3);
  • 메인쿼리의 WHERE 절에도 비교할 컬럼들을 괄호로 묶어 (user_id, status)와 같은 형태로 작성해주어야 합니다.

쿼리 실행 흐름

  1. 괄호 안의 서브쿼리가 먼저 실행되어 (2, 'SHIPPED')라는 한 쌍의 값을 반환합니다.
  2. 메인쿼리는 WHERE (user_id, status) = (2, 'SHIPPED')와 동일한 형태로 바뀝니다.
  3. 이것은 WHERE user_id = 2 AND status = 'SHIPPED'와 논리적으로 같습니다.
  4. 최종적으로 orders 테이블에서 user_id가 2이고 status가 'SHIPPED'인 모든 주문을 찾아 반환합니다.

실행 결과

order_iduser_idstatusorder_date
32SHIPPED2025-06-11 14:20:00
72SHIPPED2025-06-17 12:00:00

기준이 된 주문 자신을 제외하기

SELECT order_id, user_id, status, order_date
FROM orders
WHERE (user_id, status) = (SELECT user_id, status
FROM orders
WHERE order_id = 3)
AND order_id != 3; -- 자기 자신은 제외

실행 결과

order_iduser_idstatusorder_date
72SHIPPED2025-06-17 12:00:00

4.3 주의할 점

  • 다중 컬럼 서브쿼리를 = 연산자와 함께 사용할 때는, 단일 행 서브쿼리와 마찬가지로 서브쿼리의 결과가 반드시 하나의 행이어야 합니다.
  • 만약 서브쿼리가 두 개 이상의 행을 반환하면 데이터베이스는 어떤 행과 비교해야 할지 알 수 없으므로 오류를 발생시킵니다.

4.4 다중 컬럼 서브쿼리와 IN 연산자

  • 다중 컬럼 서브쿼리 역시 서브쿼리의 결과가 여러 행일 수 있습니다.
  • 이때는 = 대신 IN 연산자를 사용해야 합니다.

문제: "각 고객별로 가장 먼저 한 주문의 정보를 조회해라"

각 고객별로 가장 빠른 주문 날짜 구하기

SELECT user_id, MIN(order_date)
FROM orders
GROUP BY user_id;

실행 결과

user_idMIN(order_date)
12025-06-10 10:00:00
22025-06-11 14:20:00
32025-06-12 09:00:00
42025-06-15 11:30:00
52025-06-16 18:00:00

다중 컬럼 IN을 사용한 최종 쿼리

SELECT
o.order_id,
o.user_id,
u.name,
p.name AS product_name,
o.order_date
FROM orders o
JOIN users u ON o.user_id = u.user_id
JOIN products p ON o.product_id = p.product_id
WHERE (o.user_id, o.order_date) IN (
SELECT user_id, MIN(order_date)
FROM orders
GROUP BY user_id
);

실행 결과

order_iduser_idnameproduct_nameorder_date
11프리미엄 게이밍 마우스2025-06-10 10:00:00
32네이트기계식 키보드2025-06-11 14:20:00
43세종대왕관계형 데이터베이스 입문2025-06-12 09:00:00
54이순신4K UHD 모니터2025-06-15 11:30:00
65마리 퀴리프리미엄 게이밍 마우스2025-06-16 18:00:00
  • 메인쿼리의 각 행에 대해 (user_id, order_date) 쌍이 서브쿼리가 반환한 여러 쌍 중 하나와 일치하는 경우에만 최종 결과에 포함됩니다.
  • 이처럼 다중 컬럼 서브쿼리는 두 개 이상의 속성이 조합되어 특정 의미를 가질 때, 그 조합 자체를 조건으로 사용하여 데이터를 조회하는 간결하고 강력한 방법을 제공합니다.

5. 상관 서브쿼리

  • 이전까지 우리는 WHERE 절에 서브쿼리를 사용하여, 메인쿼리와는 독립적으로 실행된 결과를 필터링 조건으로 사용하는 법을 알아봤습니다.
  • 즉, 서브쿼리가 먼저 한 번 실행되어 값을 만들어내면, 메인쿼리가 그 값을 이어받아 사용하는 방식이었습니다.

5.1 문제 상황

"각 상품별로, 자신이 속한 카테고리의 평균 가격 이상의 상품들을 찾아라."

  • 이 문제의 핵심은 '전체 평균 가격'이 아닌, **'자신이 속한 바로 그 카테고리의 평균 가격'**과 비교해야 한다는 점입니다.
  • '프리미엄 게이밍 마우스'는 '전자기기' 카테고리에 속합니다. '전자기기' 카테고리의 평균 가격과 비교해야 합니다.
  • '관계형 데이터베이스 입문' 책은 '도서' 카테고리에 속합니다. '도서' 카테고리의 평균 가격과 비교해야 합니다.

5.2 상관 서브쿼리의 개념

  • 서브쿼리가 메인쿼리에서 현재 처리 중인 행의 특정 값(예: 카테고리명)을 알아야만 계산을 수행할 수 있을 때, 바로 **상관 서브쿼리(Correlated Subquery)**를 사용해야 합니다.
  • 여기서 상관(Correlated)의 의미는 메인쿼리와 서브쿼리가 서로 영향을 준다는 뜻입니다.
  • 서브쿼리가 독립적으로 실행될 수 없고, 메인쿼리의 값을 참조하여 실행되는 것이 특징입니다.

비상관 서브쿼리 (Non-correlated)

  • 서브쿼리가 단 한 번 실행된 후, 그 결과를 메인쿼리가 사용합니다.

상관 서브쿼리 (Correlated)

상관 서브쿼리의 동작 방식은 기존 서브쿼리와 완전히 다릅니다.

  1. 메인쿼리가 먼저 한 행을 읽습니다.
  2. 읽혀진 행의 값을 서브쿼리에 전달하여, 서브쿼리가 실행됩니다.
  3. 서브쿼리 결과를 이용해 메인쿼리의 WHERE 조건을 판단합니다.
  4. 메인쿼리의 다음 행을 읽고, 2-3번 과정을 반복합니다.
  • 즉, 서브쿼리가 메인쿼리의 행 수만큼 반복 실행될 수 있습니다.

5.3 상관 서브쿼리로 문제 해결하기

각 카테고리별 평균 가격 계산

  • 전자기기: (75000 + 120000 + 350000 + 280000) / 4 = 206,250원
  • 도서: 28000원 (상품이 1개)
  • 패션: 150000원 (상품이 1개)
SELECT
product_id,
name,
category,
price
FROM
products p1
WHERE
price >= (
SELECT
AVG(price)
FROM
products p2
WHERE
p2.category = p1.category
);
  • 가장 중요한 부분은 서브쿼리 안의 WHERE p2.category = p1.category입니다.
  • p1은 메인쿼리의 products 테이블을 가리키는 별칭입니다.
  • p2는 서브쿼리의 products 테이블을 가리키는 별칭입니다.
  • 이 조건문은 "서브쿼리에서 평균을 계산할 때, 메인쿼리가 현재 보고 있는 상품(p1)과 동일한 카테고리를 가진 상품들(p2)만을 대상으로 하라"는 의미입니다.

실행 결과

product_idnamecategoryprice
34K UHD 모니터전자기기350000
4관계형 데이터베이스 입문도서28000
5고급 가죽 지갑패션150000
6스마트 워치전자기기280000
  • 각자 자신이 속한 카테고리의 평균 가격(전자기기: 206,250원, 도서: 28,000원, 패션: 150,000원) 이상인 상품들만 정확하게 조회된 것을 확인할 수 있습니다.

5.4 EXISTS: 존재 여부 확인

  • EXISTS 연산자는 상관 서브쿼리의 대표적인 활용 사례다.
  • EXISTS는 서브쿼리가 반환하는 결과값 자체에는 관심이 없고, 오직 서브쿼리의 결과로 행이 하나라도 존재하는지 여부만 체크합니다.

EXISTS 연산

  • 서브쿼리 결과 행이 1개 이상이면 TRUE
  • 서브쿼리 결과 행이 0개이면 FALSE

5.5 한 번이라도 주문된 상품 조회하기

문제 상황

  • products 테이블에는 있지만, orders 테이블에는 한 번도 등장하지 않은 상품, 즉 '재고'로만 남아있는 상품을 제외하고, 실제 주문이 발생한 상품의 이름과 가격을 조회하고 싶다.

IN을 사용한 해결 방법

SELECT
product_id,
name,
price
FROM
products
WHERE
product_id IN (SELECT DISTINCT product_id FROM orders);

EXISTS를 사용한 더 효율적인 방법

SELECT
product_id,
name,
price
FROM
products p
WHERE EXISTS (
SELECT 1
FROM orders o
WHERE o.product_id = p.product_id
);
  • po는 각각 productsorders 테이블의 별칭입니다.
  • WHERE o.product_id = p.product_id에서 상관 서브쿼리를 사용했습니다.
  • 서브쿼리가 독립적으로 실행되지 않고 메인쿼리의 p 테이블 값에 의존하여 실행된다는 의미입니다.
  • 서브쿼리가 SELECT 1을 사용하는 것을 볼 수 있습니다. EXISTS는 결과 데이터가 무엇인지는 전혀 신경 쓰지 않고, 행이 존재하는지 여부만 보기 때문에 관례적으로 SELECT 1과 같이 상수를 사용해 불필요한 데이터 조회를 피합니다.

쿼리 실행 흐름

EXISTS의 실행 흐름은 IN과 완전히 다릅니다.

  1. 메인쿼리가 products(p) 테이블의 첫 번째 행인 '프리미엄 게이밍 마우스'(p.product_id = 1)를 읽습니다.
  2. p.product_id 값을 가지고 서브쿼리가 실행됩니다: SELECT 1 FROM orders o WHERE o.product_id = 1
  3. orders 테이블에는 product_id가 1인 주문이 3개 존재합니다. 데이터베이스는 조건을 만족하는 첫 번째 행을 찾자마자 더 이상 테이블을 탐색하지 않고, 서브쿼리가 결과를 반환할 수 있다고 판단합니다. (예를 들어 product_id가 1인 주문이 10,000개 존재하더라도 EXISTS는 첫 번째 행만 찾으면 바로 TRUE를 반환합니다. 따라서 나머지 9,999개를 찾지 않아도 됩니다.)
  4. EXISTSTRUE가 됩니다.
  5. WHERE TRUE 조건이 충족되었으므로, '프리미엄 게이밍 마우스'는 최종 결과에 포함됩니다.
  6. p.product_id = 2, 3, 4도 같은 내용이 반복되며, 이들은 최종 결과에 포함됩니다.
  7. 메인쿼리가 products 테이블의 다음 행인 '고급 가죽 지갑'(p.product_id = 5)을 읽습니다.
  8. 다시 서브쿼리가 실행됩니다: SELECT 1 FROM orders o WHERE o.product_id = 5
  9. orders 테이블을 탐색했지만 product_id가 5인 주문은 존재하지 않습니다. 서브쿼리는 아무런 행도 반환하지 못합니다.
  10. EXISTSFALSE가 됩니다.
  11. WHERE FALSE 조건이므로, '고급 가죽 지갑'은 최종 결과에서 제외됩니다.
  12. p.product_id = 6도 마찬가지로 WHERE FALSE 조건이 되면서 최종 결과에서 제외됩니다.

실행 결과

product_idnameprice
1프리미엄 게이밍 마우스75000
2기계식 키보드120000
34K UHD 모니터350000
4관계형 데이터베이스 입문28000

5.6 IN vs. EXISTS: 실무에서는?

구분INEXISTS
실행 방식서브쿼리를 먼저 실행해 결과 목록을 만든 후, 메인쿼리에서 사용메인쿼리의 각 행에 대해 서브쿼리를 실행하여 조건 확인
특징서브쿼리 결과가 작을 때 직관적이고 빠를 수 있음상관 서브쿼리. 서브쿼리 테이블이 클 때 효율적
최적화orders 테이블 전체를 스캔해야 할 수 있음조건을 만족하는 첫 행만 찾으면 스캔을 멈춤
  • 서브쿼리의 대상이 되는 테이블(orders)이 크다면 EXISTS가 유리합니다.
  • IN은 목록 전체를 비교해야 하지만, EXISTS는 조건을 만족하는 데이터를 '발견'하는 즉시 다음으로 넘어가기 때문에 더 효율적입니다.

5.7 NOT EXISTS: 존재하지 않음을 확인하기

문제 상황: "한 번도 주문된 적이 없는, 이른바 '악성 재고' 상품을 찾아라."

SELECT
product_id,
name,
price,
stock_quantity
FROM
products p
WHERE NOT EXISTS (
SELECT 1
FROM orders o
WHERE o.product_id = p.product_id
);
  • 이 쿼리는 EXISTS 예제와 정반대로 동작하여, 서브쿼리의 결과가 0건일 때 TRUE를 반환합니다.

실행 결과

product_idnamepricestock_quantity
5고급 가죽 지갑15000015
6스마트 워치28000040

5.8 상관 서브쿼리와 성능

  • 상관 서브쿼리는 복잡한 로직을 매우 직관적으로 표현할 수 있게 해주지만, 성능에 주의해야 합니다.
  • 메인쿼리의 행 수만큼 서브쿼리가 반복 실행될 수 있기 때문에, 메인쿼리가 다루는 데이터의 양이 많아지면 쿼리 전체의 성능이 급격히 저하될 수 있습니다.
  • 많은 경우, 상관 서브쿼리는 JOIN(특히 LEFT JOINGROUP BY)으로 동일한 결과를 얻도록 재작성할 수 있으며, 데이터베이스 옵티마이저가 JOIN을 더 효율적으로 처리하는 경우가 많습니다.
  • 하지만 EXISTS는 특정 조건에 맞는 데이터가 있는지 '확인만 하고 넘어가는' 특성 덕분에, IN이나 JOIN보다 훨씬 효율적으로 동작하는 상황도 많습니다.
  • 결론적으로 상관 서브쿼리는 성능 이슈를 인지하고, JOIN으로 표현하기 너무 복잡하거나 EXISTS를 통해 더 효율적인 실행이 가능할 때 적절히 사용하는 것이 중요합니다.

6. SELECT 서브쿼리

  • 지금까지 WHERE 절에서 서브쿼리를 사용하여 필터링할 조건을 동적으로 만들어내는 방법을 알아봤습니다.
  • 서브쿼리가 반환하는 값(들)을 기준으로 메인쿼리가 보여줄 행을 걸러내는 방식이었습니다.
  • 만약 서브쿼리를 SELECT 절 안으로 가져온다면 어떻게 될까요?
  • 서브쿼리는 더 이상 필터가 아닌, 그 자체가 하나의 **'컬럼'**처럼 동작하게 됩니다.

6.1 SELECT 절에서의 스칼라 서브쿼리

  • SELECT 절에서는 단일 값(하나의 행, 하나의 컬럼)을 반환하는 스칼라 서브쿼리를 사용해야 합니다.

6.2 비상관 서브쿼리

문제 상황: "모든 상품 목록을 조회하는데, 각 상품의 가격과 함께 전체 상품의 평균 가격을 모든 행에 함께 표시해서 개별 상품 가격이 평균과 얼마나 차이 나는지 비교해보고 싶다."

  • 이 경우, '전체 상품의 평균 가격'은 어떤 특정 상품 행에 종속되는 값이 아니라, 모든 상품에 대해 동일하게 적용되는 고정된 값입니다.

전체 상품의 평균 가격 구하기

SELECT AVG(price) FROM products;

실행 결과

AVG(price)
167166.6667

SELECT 절에 서브쿼리 사용

SELECT
name,
price,
(SELECT AVG(price) FROM products) AS avg_price
FROM
products;

실행 결과

namepriceavg_price
프리미엄 게이밍 마우스75000167166.6667
기계식 키보드120000167166.6667
4K UHD 모니터350000167166.6667
관계형 데이터베이스 입문28000167166.6667
고급 가죽 지갑150000167166.6667
스마트 워치280000167166.6667

쿼리 실행 흐름

  1. 데이터베이스는 메인쿼리를 실행하기 전에, SELECT 절의 스칼라 서브쿼리를 단 한 번 먼저 실행합니다.
  2. (SELECT AVG(price) FROM products)167166.6667을 반환하고, 데이터베이스는 이 계산된 값을 기억해 두고 사용합니다.
  3. 메인쿼리(SELECT name, price, ... FROM products)가 실행됩니다.
  4. products 테이블의 각 행을 가져올 때마다, avg_price 컬럼에 미리 계산해 둔 값(167166.6667)을 그대로 추가합니다.
  • 이처럼 서브쿼리가 외부 쿼리의 컬럼을 참조하지 않아 독립적으로 실행될 수 있는 경우를 **비상관 서브쿼리(Non-correlated Subquery)**라고 합니다.

6.3 상관 서브쿼리

문제 상황: "전체 상품 목록을 조회하면서, 각 상품별로 총 몇 번의 주문이 있었는지 '총 주문 횟수'를 함께 보여주고 싶다."

  • 이제는 '전체 평균'처럼 고정된 값이 아니라, '프리미엄 게이밍 마우스'의 주문 횟수, '기계식 키보드'의 주문 횟수 등 각 상품 행에 따라 계산 결과가 달라져야 하는 값이 필요합니다.
SELECT
p.product_id,
p.name,
p.price,
(SELECT COUNT(*) FROM orders o WHERE o.product_id = p.product_id) AS order_count
FROM
products p;
  • 가장 중요한 부분은 서브쿼리 안의 WHERE o.product_id = p.product_id 조건입니다.
  • p.product_id는 메인쿼리(FROM products p)가 현재 처리하고 있는 행의 product_id 값을 의미합니다.
  • 서브쿼리가 메인쿼리의 컬럼 값을 참조하는 관계를 가질 때, 이를 **상관 서브쿼리(Correlated Subquery)**라고 부릅니다.

실행 결과

product_idnamepriceorder_count
1프리미엄 게이밍 마우스750003
2기계식 키보드1200001
34K UHD 모니터3500001
4관계형 데이터베이스 입문280002
5고급 가죽 지갑1500000
6스마트 워치2800000
  • 각 상품별로 정확한 주문 횟수가 새로운 컬럼처럼 조회된 것을 볼 수 있습니다.
  • 한 번도 팔리지 않은 상품의 주문 횟수는 0으로 표시됩니다.

[쿼리 실행 흐름]

  1. 메인쿼리가 products 테이블의 첫 번째 행, '프리미엄 게이밍 마우스'(p.product_id = 1)를 읽습니다.
  2. 이 행의 order_count 값을 계산하기 위해 스칼라 서브쿼리가 실행됩니다. 이때 p.product_id에는 1이 전달됩니다.
    • (SELECT COUNT(*) FROM orders o WHERE o.product_id = 1)
  3. 이 서브쿼리는 orders 테이블에서 product_id가 1인 주문을 세어 3이라는 단일 값을 반환합니다.
  4. 첫 번째 행의 order_count 컬럼에는 3이 기록됩니다.
  5. 메인쿼리가 두 번째 행, '기계식 키보드'(p.product_id = 2)를 읽습니다.
  6. 다시 스칼라 서브쿼리가 실행됩니다. 이번에는 p.product_id2가 전달됩니다.
    • (SELECT COUNT(*) FROM orders o WHERE o.product_id = 2)
  7. 서브쿼리는 1을 반환하고, 두 번째 행의 order_count에는 1이 기록됩니다.
  8. 이 과정이 products 테이블의 모든 행에 대해 반복됩니다.

6.4 실무 팁: 성능에 주의하라

  • 스칼라 서브쿼리는 JOIN으로는 표현하기 복잡한 로직을 직관적으로 표현할 수 있게 해주지만, 강력한 만큼 주의해서 사용해야 합니다.
  • 가장 큰 단점은 성능 저하의 가능성입니다.
  • 특히 상관 서브쿼리는 메인쿼리가 반환하는 행의 수만큼 서브쿼리가 반복 실행되기 때문입니다.
  • 만약 products 테이블에 100만 개의 상품이 있다면, 주문 횟수를 알기 위해 COUNT(*) 쿼리가 100만 번이나 실행되는 셈입니다.

JOIN으로 해결하는 방법

SELECT p.product_id, p.name, p.price, COUNT(o.order_id) AS order_count
FROM products p
LEFT JOIN orders o ON p.product_id = o.product_id
GROUP BY p.product_id, p.name, p.price;
  • 대부분의 경우 데이터베이스 옵티마이저가 JOIN을 더 효율적으로 처리하여 성능이 더 좋습니다.
  • 그럼에도 불구하고 스칼라 서브쿼리는 JOIN이 너무 복잡해지거나, 완전히 다른 테이블에서 간단한 정보 하나만 조회해 올 때 코드를 훨씬 명료하게 만들어주는 장점이 있어 적재적소에 사용하면 매우 유용합니다.

7. 테이블 서브쿼리 (인라인 뷰)

  • 지금까지 서브쿼리가 WHERE 절에서는 '동적 필터'로, SELECT 절에서는 '새로운 컬럼'으로 활약하는 모습을 보았다.
  • 이제 서브쿼리가 자리할 수 있는 마지막 주요 위치, 바로 FROM 절에 대해 알아볼 시간입니다.

7.1 테이블 서브쿼리란?

  • FROM 절에 위치하는 서브쿼리는, 그 실행 결과가 마치 하나의 독립된 가상 테이블처럼 사용되기 때문에 테이블 서브쿼리라 합니다.
  • 쿼리 내에서 '인라인(inline)'으로 즉석에서 정의되는 '뷰(View, 가상 테이블)'와 같다고 해서 **인라인 뷰(Inline View)**라고도 부릅니다.
  • 테이블 서브쿼리의 가장 큰 특징은, 우리가 지금까지 배운 복잡한 SELECT 문(집계, 그룹핑, 조인 등)의 결과를 하나의 명확한 데이터 집합으로 먼저 만들어 놓고, 그 집합을 대상으로 다시 한번 SELECT를 할 수 있게 해줍니다.

7.2 문제 상황

"각 상품 카테고리별로, 가장 비싼 상품의 이름과 가격을 조회하고 싶다."

  • 이 문제를 보면 카테고리별로 묶어야 하니까 GROUP BY를 사용하고, 가장 비싼 상품의 가격은 MAX()를 사용하면 될 것 같다는 생각이 들 것입니다.
-- name이 빠짐
SELECT category, MAX(price)
FROM products
GROUP BY category;

실행 결과

categoryMAX(price)
전자기기350000
도서28000
패션150000
  • 여기에 이름(name)을 추가하면 오류가 발생합니다.
  • products.name 컬럼은 GROUP BY 절에 없기 때문에 사용할 수 없다는 오류입니다.
  • 데이터베이스는 GROUP BY로 묶인 category 별로 MAX(price)는 정확하게 계산할 수 있지만, name은 해당 그룹의 여러 상품명 중에 어떤 것을 보여줄지 선택할 수 없습니다.

7.3 인라인 뷰를 이용한 2단계 접근법

1단계 (인라인 뷰): 카테고리별 최고가격을 미리 구한다

SELECT category, MAX(price) AS max_price
FROM products
GROUP BY category;

실행 결과

categorymax_price
도서28000
전자기기350000
패션150000
  • 이 쿼리를 실행하면 '전자기기', '도서', '패션' 각 카테고리의 최고가격을 담은 3줄짜리 결과가 나옵니다.
  • 이제 이 결과를 category_max_price라는 이름의 가상 테이블이라고 생각합시다.

2단계 (메인쿼리): 원본 테이블과 가상 테이블을 조인한다

  • 이제 원본 products 테이블과, 우리가 방금 만든 가상 테이블(category_max_price)을 조인합니다.

연결 조건

  1. 카테고리 이름이 같아야 합니다. (p.category = cmp.category)
  2. 상품 가격이 그 카테고리의 최고가와 같아야 합니다. (p.price = cmp.max_price)
  • 이 두 조건을 모두 만족하는 상품만이 '카테고리별 최고가 상품'이라는 것을 보장할 수 있습니다.

최종 쿼리 작성

SELECT
p.product_id,
p.name,
p.price
FROM
products p
JOIN
(SELECT
category,
MAX(price) AS max_price
FROM
products
GROUP BY
category) AS cmp
ON
p.category = cmp.category AND p.price = cmp.max_price;
  • FROM 절에 들어가는 서브쿼리에는 반드시 별칭(Alias)을 붙여줘야 한다는 점을 잊지 말아야 합니다.
  • 여기서는 cmp (category max price)라는 별칭을 사용했습니다.

실행 결과

product_idnameprice
34K UHD 모니터350000
4관계형 데이터베이스 입문28000
5고급 가죽 지갑150000

쿼리 실행 흐름

  1. 데이터베이스는 FROM 절의 서브쿼리(인라인 뷰)를 먼저 실행하여, cmp라는 임시 테이블을 메모리에 생성합니다. 이 테이블에는 각 카테고리와 그 카테고리의 최고 가격이 들어있습니다.
  2. 그다음, 메인쿼리가 실행됩니다. products 테이블(별칭 p)과 방금 생성된 임시 테이블 cmpINNER JOIN합니다.
  3. ON 절에 명시된 두 가지 조건(카테고리 일치, 가격 일치)을 모두 만족하는 행만 최종 결과로 선택됩니다.
  • 결과를 보면 GROUP BY의 함정을 피하고, 각 카테고리별로 가장 비싼 상품의 정보를 정확하게 찾아냈습니.
  • 이처럼 인라인 뷰는 복잡한 데이터를 단계적으로 가공해야 할 때, 특히 집계된 결과를 가지고 다시 한번 조인이나 필터링을 수행해야 할 때 매우 유용하다.

7.4 FROM 절의 상관 서브쿼리 - LATERAL

  • FROM 절에서 상관 서브쿼리를 사용하려면 LATERAL이라는 특별한 키워드를 사용해야 합니다.
  • 이 기능은 너무 복잡하고, 성능도 잘 나오지 않는 문제가 있어서 실무에서는 잘 사용하지 않는 편입니다.
  • 따라서 여기서는 따로 설명하지 않겠습니다.
  • FROM 절에 상관 서브쿼리를 꼭 사용해야 하는 특별한 일이 있다면 LATERAL 키워드를 검색해봅시오.

8. 서브쿼리 vs JOIN

  • 지금까지 JOIN과 서브쿼리라는 두 가지 강력한 기술을 배웠습니다.
  • 실제로 많은 문제는 JOIN으로도, 서브쿼리로도 해결할 수 있습니다.
  • 그렇다면 우리는 무엇을 선택해야 할까?
  • 성능과 가독성 측면에서 둘은 어떤 차이가 있을까요?

8.1 문제 상황

"서울에 거주하는 모든 고객들의 주문 목록을 조회해라."

8.2 해결 방법 1: 서브쿼리 사용

  • 서브쿼리를 이용한 접근법은 우리의 사고 흐름과 매우 유사합니다.
  1. 먼저, 서울에 사는 고객들의 user_id 목록을 찾습니다. (users 테이블)
  2. 그다음, 이 user_id 목록에 포함된 order_id를 가진 주문들을 찾습니다. (orders 테이블)
SELECT o.order_id, o.user_id, o.product_id, o.order_date
FROM orders o
WHERE o.user_id IN (SELECT user_id FROM users WHERE address LIKE '서울%');
  • 이 쿼리는 읽기가 매우 쉽다.
  • "users 테이블에서 address가 '서울'인 고객 user_id 목록 안에(IN), user_id가 포함된 orders를 찾아줘" 라고 말하는 것과 같습니다.

실행 결과

order_iduser_idproduct_idorder_date
1112025-06-10 10:00:00
2142025-06-10 10:05:00
4342025-06-12 09:00:00
6512025-06-16 18:00:00

8.3 해결 방법 2: JOIN 사용

  • JOIN을 이용한 접근법은 필요한 테이블들을 일단 모두 연결한 뒤, 원하는 조건을 필터링하는 방식입니다.
SELECT o.order_id, o.user_id, o.product_id, o.order_date
FROM orders o
JOIN users u ON o.user_id = u.user_id
WHERE u.address LIKE '서울%';
  • 이 쿼리는 "주문(orders)과 고객(users) 테이블을 user_id로 연결한 다음, 그중에서 고객 주소가 '서울'인 데이터만 걸러줘" 라고 말하는 것과 같습니다.
  • 결과는 당연히 서브쿼리를 사용했을 때와 동일합니다.

실행 결과

order_iduser_idproduct_idorder_date
1112025-06-10 10:00:00
2142025-06-10 10:05:00
4342025-06-12 09:00:00
6512025-06-16 18:00:00

8.4 성능 vs 가독성: 실무 가이드

성능 (Performance)

  • 일반적으로, 데이터베이스는 JOIN이 서브쿼리보다 성능이 더 좋거나 최소한 동일한 경우가 많습니다.
  • 왜 그럴까요? 그 비밀은 데이터베이스의 '두뇌' 역할을 하는 **쿼리 옵티마이저(Query Optimizer)**에 있습니다.
  • JOIN 구문은 옵티마이저에게 더 많은 정보를 제공합니다. "A와 B 테이블을 특정 조건으로 연결해야 한다"는 전체 그림을 미리 보여주기 때문에, 옵티마이저는 인덱스를 어떻게 활용하고 어떤 테이블을 먼저 읽을지 등 가장 효율적인 실행 계획을 선택할 수 있는 더 넓은 선택지를 갖습니다.
  • 반면, 서브쿼리는 (특히 과거의 데이터베이스에서는) 단계적으로 실행되는 경우가 많았습니다. 서브쿼리를 먼저 실행해서 나온 결과를 메모리에 담아두고, 그 다음 메인쿼리가 그 결과를 참조하는 방식으로 동작하여 비효율을 야기할 수 있었습니다.
  • 하지만! 요즘 데이터베이스의 옵티마이저는 매우 똑똑해져서, 우리가 작성한 예제처럼 간단한 IN 서브쿼리는 내부적으로 최적의 JOIN 구문으로 자동 변환해서 실행하는 경우가 많습니다. 따라서 위 예제의 두 쿼리는 사실상 동일한 성능을 낼 확률이 높습니다.
  • 참고로 이런 최적화는 항상 가능한 것은 아니기 때문에 쿼리 실행 계획 등을 확인하는 것이 좋습니다.

가독성 (Readability)

  • 가독성은 주관적인 영역이지만, 쿼리의 유지보수 측면에서 성능만큼이나 중요합니다.
  • 서브쿼리는 쿼리의 논리적 단계를 명확하게 구분해 주어, 복잡한 로직을 더 이해하기 쉽게 만들어주는 경우가 많습니다.
  • JOIN은 쿼리에 필요한 모든 데이터 소스를 한눈에 보여주고, 여러 테이블의 컬럼을 함께 조회해야 할 때는 구조적으로 더 깔끔합니다.

8.5 최종 결론: 언제 무엇을 써야 할까?

정답은 없습니다. 하지만 실무에서 적용할 수 있는 가이드라인은 다음과 같습니다.

1. JOIN을 우선적으로 고려하세요

  • 일반적인 성능 우위와 범용성을 고려할 때, 문제를 해결할 방법을 JOIN에서 먼저 찾아보는 것이 좋은 출발점입니다.

2. JOIN으로 표현하기 너무 복잡하거나, 서브쿼리의 가독성이 훨씬 좋다면 서브쿼리를 사용하라

  • 성능이 아주 중요한 쿼리가 아니라면, 동료가 이해하기 쉬운 코드를 작성하는 것이 장기적으로 더 가치 있을 수 있습니다.
  • 특히 인라인 뷰를 사용해야만 깔끔하게 풀리는 문제는 서브쿼리가 정답입니다.

3. EXISTS를 활용하세요

  • IN 서브쿼리의 대안으로, EXISTS라는 서브쿼리 연산자도 있습니다.
  • EXISTS는 서브쿼리의 결과값이 존재하는지 여부만 체크하기 때문에, 특정 상황에서 더 효율적으로 동작하기도 합니다.

4. 성능이 의심될 때는 반드시 측정하세요

  • 가장 중요한 원칙입니다.
  • 추측하지 말고, EXPLAIN과 같은 도구를 사용해 데이터베이스가 어떻게 쿼리를 실행하는지 계획을 분석하고, 실제 실행 시간을 측정하여 더 나은 방법을 선택해야 합니다.

결론

  • JOIN과 서브쿼리는 대립하는 기술이 아니라, 데이터라는 재료를 요리하는 두 가지 필수 도구입니다.
  • 각각의 장단점을 이해하고 상황에 맞게 꺼내 사용할 수 있어야 합니다.

9. 정리

9.1 서브쿼리 종류와 특징

  • 서브쿼리는 반환하는 행과 컬럼의 수에 따라 종류가 나뉘며, 사용되는 위치와 연산자에 따라 그 역할이 결정됩니다.
구분반환 형태주요 사용 위치연산자 / 구문명칭핵심 용도
단일 컬럼단일 행SELECT, WHERE, HAVING=, >, <스칼라 서브쿼리단일 값이 필요한 모든 곳
단일 컬럼다중 행WHERE, HAVINGIN, ANY, ALL다중 행 서브쿼리값 목록과 비교
다중 컬럼단일 행WHERE, HAVING(c1, c2) = ...다중 컬럼 서브쿼리여러 컬럼 값을 1:1로 비교
다중 컬럼다중 행WHERE, HAVING(c1, c2) IN ...다중 컬럼 서브쿼리여러 컬럼 조합 비교
다중 컬럼다중 행FROMFROM (...) AS alias테이블 서브쿼리 (인라인 뷰)가상의 테이블을 생성하여 재가공

9.2 핵심 요약

서브쿼리 소개

  • 서브쿼리는 SQL 쿼리 문 안에 포함된 또 다른 SELECT 쿼리를 의미합니다.
  • 여러 단계로 나누어 처리해야 할 문제를 하나의 쿼리로 해결할 수 있게 돕는다.
  • 서브쿼리가 먼저 실행되고 그 결과가 메인쿼리에서 사용됩니다.
  • 반환하는 행과 열의 수, 사용되는 위치에 따라 종류가 나뉩니다.

스칼라 서브쿼리

  • 단일 행, 단일 열의 값을 반환하는 서브쿼리입니다.
  • 스칼라는 단 하나의 값을 의미합니다.
  • =, >, < 같은 단일 행 비교 연산자와 함께 사용합니다.
  • 서브쿼리의 결과가 반드시 하나의 행만 반환하도록 주의해야 하며, 그렇지 않으면 오류가 발생합니다.

다중 행 서브쿼리

  • 여러 행의 결과를 반환하는 서브쿼리입니다.
  • IN, ANY, ALL 같은 다중 행 연산자와 함께 사용합니다.
  • IN은 목록에 포함된 값과 일치하는지 확인하며 가장 직관적이고 흔하게 사용됩니다.
  • ANYALL은 주로 비교 연산자와 쓰이며 MIN이나 MAX 집계 함수로 대체하는 것이 더 명확할 수 있습니다.

다중 컬럼 서브쿼리

  • 두 개 이상의 컬럼을 반환하는 서브쿼리입니다.
  • WHERE 절에서 여러 컬럼을 동시에 비교해야 할 때 유용합니다.
  • WHERE (컬럼1, 컬럼2) = (서브쿼리) 형태로 사용하며, = 연산자를 쓸 때는 서브쿼리가 단일 행을 반환해야 합니다.
  • 서브쿼리가 여러 행을 반환할 때는 IN 연산자를 사용합니다.

상관 서브쿼리

  • 메인쿼리와 서브쿼리가 서로 연관 관계를 맺고 동작하는 서브쿼리입니다.
  • 메인쿼리의 각 행에 대해 서브쿼리가 반복적으로 실행됩니다.
  • 서브쿼리는 메인쿼리의 컬럼 값을 참조하여 결과를 계산합니다.
  • 예시로 각 상품이 자신이 속한 카테고리의 평균 가격보다 비싼지 확인할 때 사용합니다.
  • EXISTS 연산자는 상관 서브쿼리의 대표적인 활용 사례입니다.
  • EXISTS는 서브쿼리의 결과값이 존재하는지 여부만 확인하며 결과 행이 하나라도 있으면 TRUE를 반환합니다.
  • 서브쿼리 테이블이 매우 클 때 IN보다 효율적인 경우가 많습니다.
  • NOT EXISTS는 서브쿼리 결과가 존재하지 않을 때 TRUE를 반환하여 특정 조건에 해당하지 않는 데이터를 찾을 때 사용합니다.

SELECT 서브쿼리

  • SELECT 절에 위치하는 서브쿼리로 결과가 하나의 컬럼처럼 동작합니다.
  • 반드시 하나의 값만 반환하는 스칼라 서브쿼리여야 합니다.
  • 비상관 서브쿼리는 메인쿼리의 모든 행에 동일한 값을 보여줍니다.
  • 상관 서브쿼리는 메인쿼리의 각 행과 상호작용하며 행마다 다른 계산 결과를 보여줍니다.
  • 상관 서브쿼리는 메인쿼리의 행 수만큼 반복 실행되므로 성능 저하에 주의해야 합니다.

테이블 서브쿼리

  • FROM 절에 위치하는 서브쿼리로 인라인 뷰(Inline View)라고도 부릅니다.
  • 서브쿼리의 실행 결과가 하나의 독립된 가상 테이블처럼 사용됩니다.
  • 집계나 그룹핑된 결과를 다시 한번 조인하거나 필터링해야 할 때 유용합니다.
  • FROM 절의 서브쿼리는 반드시 별칭(Alias)을 가져야 합니다.

서브쿼리 vs JOIN

  • 많은 문제는 서브쿼리와 JOIN 두 가지 방법으로 모두 해결할 수 있습니다.
  • 일반적으로 데이터베이스 옵티마이저는 JOIN을 더 효율적으로 처리하여 성능이 좋은 경우가 많습니다.
  • 하지만 최신 옵티마이저는 간단한 서브쿼리를 JOIN으로 자동 변환하기도 합니다.
  • 가독성 측면에서는 서브쿼리가 논리적 단계를 명확히 보여줘 더 쉬울 때가 있습니다.
  • JOIN을 우선 고려하되 쿼리가 너무 복잡해지면 가독성이 좋은 서브쿼리를 사용하고 성능이 의심될 때는 반드시 측정해야 합니다.