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;
실행 결과
| name | price |
|---|---|
| 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);
쿼리 실행 순서
- 데이터베이스는 괄호 안의 서브쿼리
SELECT AVG(price) FROM products를 가장 먼저 실행합니다. - 서브쿼리가 실행된 결과인
167166.67이라는 단일 값을 얻습니다. - 이제 원래의 쿼리는 내부적으로 다음과 같이 변합니다:
SELECT name, price FROM products WHERE price > 167166.67; - 최종적으로 이 변환된 메인쿼리가 실행되어 결과를 보여줍니다.
1.4 서브쿼리 종류와 특징
- 서브쿼리는 반환하는 행과 컬럼의 수에 따라 종류가 나뉩니다.
- 사용되는 위치와 연산자에 따라 그 역할이 결정됩니다.
| 구분 | 반환 형태 | 주요 사용 위치 | 연산자 / 구문 | 명칭 | 핵심 용도 |
|---|---|---|---|---|---|
| 단일 컬럼 | 단일 행 | SELECT, WHERE, HAVING | =, >, < 등 | 스칼라 서브쿼리 | 단일 값이 필요한 모든 곳 |
| 단일 컬럼 | 다중 행 | WHERE, HAVING | IN, ANY, ALL | 다중 행 서브쿼리 | 값 목록과 비교 |
| 다중 컬럼 | 단일 행 | WHERE, HAVING | (c1, c2) = ... | 다중 컬럼 서브쿼리 | 여러 컬럼 값을 1:1로 비교 |
| 다중 컬럼 | 다중 행 | WHERE, HAVING | (c1, c2) IN ... | 다중 컬럼 서브쿼리 | 여러 컬럼 조합 비교 |
| 다중 컬럼 | 다중 행 | FROM | FROM (...) 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 = '서울시 강남구';
실행 결과
| name | address |
|---|---|
| 션 | 서울시 강남구 |
| 마리 퀴리 | 서울시 강남구 |
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);
실행 결과
| name | address |
|---|---|
| 션 | 서울시 강남구 |
| 마리 퀴리 | 서울시 강남구 |
쿼리 실행 흐름
- 괄호 안의 서브쿼리가 먼저 실행되어 단일 값
'서울시 강남구'를 반환합니다. - 메인쿼리는
WHERE address = '서울시 강남구'와 동일한 형태로 바뀐다. - 최종적으로 메인쿼리가 실행되어 원하는 결과를 보여줍니다.
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_id나user_id처럼PK나UNIQUE제약 조건이 걸린 컬럼을 조건으로 조회하는 경우가 대표적입니다. - 서브쿼리가 여러 개의 행을 반환하는 것이 당연한 상황에서는
=같은 단일 행 연산자가 아닌, 여러 개의 값을 다룰 수 있는IN과 같은 다중 행 연산자를 사용해야 합니다.
3. 다중 행 서브쿼리
- 서브쿼리의 결과가 여러 행을 반환하는 경우,
=같은 단일 행 비교 연산자를 사용하면 에러가 발생합니다. - 현실의 문제는 종종 여러 개의 결과를 반환하는 서브쿼리를 필요로 합니다.
3.1 문제 상황
"'전자기기' 카테고리에 속한 모든 상품들을 주문한 주문 내역을 전부 보고 싶다."
생각의 흐름
- 먼저, '전자기기' 카테고리에 속한 상품들의
product_id를 모두 찾습니다. - 그다음,
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, 2, 3, 6)이라는 id 목록을 반환합니다. - 메인쿼리는
WHERE product_id IN (1, 2, 3, 6)과 동일한 형태로 바뀝니다. - 최종적으로 메인쿼리가 실행됩니다.
실행 결과
| order_id | user_id | product_id | order_date | quantity | status |
|---|---|---|---|---|---|
| 1 | 1 | 1 | 2025-06-10 10:00:00 | 1 | COMPLETED |
| 3 | 2 | 2 | 2025-06-11 14:20:00 | 1 | SHIPPED |
| 5 | 4 | 3 | 2025-06-15 11:30:00 | 1 | PENDING |
| 6 | 5 | 1 | 2025-06-16 18:00:00 | 1 | COMPLETED |
| 7 | 2 | 1 | 2025-06-17 12:00:00 | 2 | SHIPPED |
IN연산자를 활용하여 여러 결과를 반환하는 서브쿼리를 깔끔하게 처리했습니다.- 참고로 목록에 없는 것을 찾고 싶을 때는
NOT IN을 사용하면 됩니다.
3.3 ANY, ALL 연산자: 목록의 모든/일부 값과 비교
ANY와ALL은 주로>,<와 같은 비교 연산자와 함께 사용되어, 서브쿼리가 반환한 여러 값들과 비교하는 역할을 합니다.
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 > 75000ORprice > 120000ORprice > 350000ORprice > 280000와 같이 동작합니다. - 결국
price가 목록의 최소값인 75000보다 크기만 하면 이 조건이 참이 되므로,price > 75000과 동일한 결과를 냅니다.
실행 결과
| name | price |
|---|---|
| 기계식 키보드 | 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 집계 함수와의 비교
ANY와ALL은 특정 조건을 해결하는 데 유용하지만, 사실 실무에서는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 = '전자기기');