서브쿼리(Subquery)는 가장 간단하게 말해 '쿼리 안에 포함된 또 다른 쿼리'를 의미합니다. 서브쿼리를 활용하면 쿼리가 실행되는 런타임 시점에 조건이 결정되는 방식으로 데이터 행을 선택하는 쿼리를 작성할 수 있습니다. 좀 더 공식적으로 정의하자면, 서브쿼리는 다른 SELECT 문의 절(clause) 안에서 사용되는 SELECT 문입니다.
흥미로운 점은 서브쿼리 안에 또 다른 서브쿼리가 포함될 수 있고, 그 안에 다시 서브쿼리가 들어갈 수 있다는 것입니다. 즉, 여러 겹으로 중첩(nesting)이 가능합니다. 또한 서브쿼리는 INSERT, UPDATE, DELETE 문 안에도 중첩하여 사용할 수 있으며, 반드시 괄호( )로 묶어야 한다는 규칙이 있습니다.
서브쿼리의 기본 개념
서브쿼리는 단일 값을 반환한다는 전제 하에 표현식(expression)이 허용되는 거의 모든 위치에서 사용할 수 있습니다. 이는 단일 값을 반환하는 서브쿼리를 FROM 절의 대상 목록에도 나열할 수 있다는 의미이기도 합니다. FROM 절에서 서브쿼리가 사용되면 마치 가상 테이블(virtual table)처럼 취급되는데, 이를 인라인 뷰(Inline View)라고 부릅니다.
서브쿼리는 메인 쿼리의 FROM 절, WHERE 절, HAVING 절 어디에든 배치할 수 있습니다. 용어적으로 보면 서브쿼리를 내부 쿼리(Inner Query) 또는 내부 SELECT(Inner Select)라고 부르고, 서브쿼리를 포함하고 있는 바깥쪽 쿼리는 외부 쿼리(Outer Query), 외부 SELECT(Outer Select) 또는 컨테이너 쿼리(Container Query)라고 지칭합니다.
MySQL의 서브쿼리는 크게 세 가지 일반적인 범주로 나눌 수 있습니다. 아래에서 각각 살펴보겠습니다.
1. 스칼라 서브쿼리(Scalar Subquery)
스칼라 서브쿼리는 단 하나의 값, 즉 한 행 한 열(one row, one column)의 데이터를 반환하는 서브쿼리입니다. 스칼라 서브쿼리는 하나의 단순 피연산자(operand)로 취급되기 때문에, 단일 컬럼이나 리터럴(literal)이 유효하게 사용될 수 있는 거의 모든 곳에서 활용할 수 있습니다.
설명을 위해 'Cars', 'Customers', 'Reservations'라는 세 개의 테이블을 사용하겠습니다. 각 테이블에는 다음과 같은 데이터가 들어 있습니다.
mysql> Select * from Cars; +------+--------------+---------+ | ID | Name | Price | +------+--------------+---------+ | 1 | Nexa | 750000 | | 2 | Maruti Swift | 450000 | | 3 | BMW | 4450000 | | 4 | VOLVO | 2250000 | | 5 | Alto | 250000 | | 6 | Skoda | 1250000 | | 7 | Toyota | 2400000 | | 8 | Ford | 1100000 | +------+--------------+---------+ 8 rows in set (0.02 sec) mysql> Select * from Customers; +-------------+----------+ | Customer_Id | Name | +-------------+----------+ | 1 | Rahul | | 2 | Yashpal | | 3 | Gaurav | | 4 | Virender | +-------------+----------+ 4 rows in set (0.00 sec) mysql> Select * from Reservations; +------+-------------+------------+ | ID | Customer_id | Day | +------+-------------+------------+ | 1 | 1 | 2017-12-30 | | 2 | 2 | 2017-12-28 | | 3 | 2 | 2017-12-29 | | 4 | 1 | 2017-12-25 | | 5 | 3 | 2017-12-26 | +------+-------------+------------+ 5 rows in set (0.00 sec)
앞서 말했듯이 스칼라 서브쿼리는 반드시 단일 값을 반환해야 합니다. 다음 예제가 바로 스칼라 서브쿼리의 대표적인 형태입니다.
mysql> Select Name from Customers WHERE Customer_id = (Select Customer_id FROM Reservations WHERE ID = 5); +--------+ | Name | +--------+ | Gaurav | +--------+ 1 row in set (0.06 sec)
위 쿼리는 Reservations 테이블에서 ID가 5인 예약의 고객 ID를 먼저 찾고, 그 결과값(단일 값)을 이용해 Customers 테이블에서 해당 고객의 이름을 조회합니다.
2. 테이블 서브쿼리(Table Subquery)
테이블 서브쿼리는 하나 이상의 컬럼을 포함한 하나 이상의 행으로 구성된 결과 집합을 반환합니다. 스칼라 서브쿼리가 단일 값만 다룬다면, 테이블 서브쿼리는 여러 행·여러 열의 데이터를 결과로 돌려줄 수 있다는 차이가 있습니다.
다음은 'Cars', 'Customers', 'Reservations' 테이블의 데이터를 활용한 테이블 서브쿼리의 예입니다.
mysql> Select Name from customers where Customer_id IN (SELECT DISTINCT Customer_id from reservations); +---------+ | Name | +---------+ | Rahul | | Yashpal | | Gaurav | +---------+ 3 rows in set (0.05 sec)
이 쿼리는 내부 SELECT 문이 Reservations 테이블에서 예약 이력이 있는 고객 ID 목록(DISTINCT로 중복 제거)을 반환하고, 외부 쿼리는 그 목록에 포함된 고객들의 이름을 IN 연산자로 조회합니다.
3. 상관 서브쿼리(Correlated Subquery)
상관 서브쿼리는 WHERE 절에서 외부 쿼리(바깥쪽 쿼리)의 값을 참조하는 서브쿼리를 말합니다. 이름 그대로 내부 쿼리와 외부 쿼리가 서로 연관(correlated)되어 있으며, 외부 쿼리의 각 행마다 내부 서브쿼리가 반복 실행되는 특징이 있습니다.
다음은 'Cars' 테이블의 데이터를 활용한 상관 서브쿼리의 예입니다.
mysql> Select Name from cars WHERE Price < (SELECT AVG(Price) from Cars); +--------------+ | Name | +--------------+ | Nexa | | Maruti Swift | | Alto | | Skoda | | Ford | +--------------+ 5 rows in set (0.00 sec) mysql> Select Name from cars WHERE Price > (SELECT AVG(Price) from Cars); +--------+ | Name | +--------+ | BMW | | VOLVO | | Toyota | +--------+ 3 rows in set (0.00 sec)
첫 번째 쿼리는 전체 자동차 평균 가격보다 낮은 차량 5대를, 두 번째 쿼리는 평균 가격보다 높은 차량 3대를 조회합니다. 이처럼 상관 서브쿼리는 평균(AVG), 최댓값(MAX), 최솟값(MIN) 같은 집계 함수와 함께 사용하면 매우 강력한 비교 조건을 만들 수 있습니다.
정리
- 스칼라 서브쿼리: 한 행 한 열의 단일 값 반환 — 단일 피연산자처럼 어디든 사용 가능
- 테이블 서브쿼리: 여러 행·여러 열 반환 — IN, FROM 절 등에서 활용
- 상관 서브쿼리: 외부 쿼리의 값을 참조 — 행마다 반복 실행되며 집계 함수와 함께 강력한 조건 구성 가능
세 가지 유형의 특성을 정확히 이해하고 적재적소에 활용하면, 복잡한 조건의 데이터 조회도 깔끔하고 효율적인 SQL로 작성할 수 있습니다.