SQL의 핵심(?)인 서브쿼리에 대해 공부해보자.
부모쿼리 ( 서브쿼리 ) ;
다른 SQL구문(포함구문=부모쿼리,containing statement)에 포함된 쿼리다. 항상 괄호( )
로 묶여있고, 일반적으로는 포함구문보다 먼저 실행된다. 서브쿼리는 아래와 같은 결과셋을 보여줄 수 있다.
서브쿼리는 구문범위가 있는 임시테이블처럼 동작하므로, 포함구문 실행이 완료되면 서브쿼리가 반환한 데이터는 폐기된다. (SQL 구문 실행이 끝나면 서버가 서브쿼리 결과에 할당된 메모리를 해제한다)
여러번 쿼리문을 실행하는 것을 1번의 쿼리만으로 해결해주는 것이 서브쿼리라고 이해했다.
두 종류가 있다.
단독으로 실행될 수 있으며, 포함구문에서 아무것도 참조하지 않는 유형의 서브쿼리다.
a.k.a 스칼라 서브쿼리(scalar subquery)
서브쿼리가 2개 이상의 행을 반환할 경우, 동등조건(<>
, =
, =!
와 같이 1:1연산을 하는 것들)을 사용할 수 없다.
오류가 나는 예시를 보자.
SELECT
city_id
, city
FROM city
WHERE country_id <> (SELECT country_id
FROM country
WHERE country <> 'India');
이 쿼리가 오류가 나는 이유는 당연하다. 서브쿼리에서 인도가 아닌 국가들을 찾았는데, 또 그것이 아닌 국가들을 찾으라고 WHERE
절에 넣었기 때문이다. (country 테이블에서 인도가 아닌 국가들은 한두개가 아니다...)
이런 유형의 서브쿼리와 함께 조건을 작성할 수 있는 4개의 연산자를 알아보자.
in
연산자는 집합 내에서 하나의 값을 검색할 수 있다.
in
연산자를 사용해 서브쿼리를 작성하는 예시를 살펴보자.
SELECT
city_id
, city
FROM city
WHERE country_id IN (SELECT country_id
FROM country
WHERE country IN ('Canada', 'Mexico'));
→ 캐나다 또는 멕시코에 있는 37개의 도시와 도시ID를 보여준다.
not in
연산자는 집합 내에 값이 없는지를 확인할 수 있다.
위의 쿼리에서 in
대신 not in
을 사용해보자.
SELECT
city_id
, city
FROM city
WHERE country_id NOT IN (SELECT country_id
FROM country
WHERE country IN ('Canada', 'Mexico'));
→ 캐나다 또는 멕시코에 없는 모든 도시가 결과값에 나타난다. (563개)
all
연산자는 한 집합의 모든 값과 하나의 값을 비교할 수 있다. 조건을 작성하려면 all
연산자와 함께 비교연산자(=
, <>
, <
, >
등) 중 하나를 사용해야 한다.
SELECT
first_name
, last_name
FROM customer
WHERE customer_id <> ALL (SELECT customer_id
FROM payment
WHERE amount = 0);
→ 무료 영화(payment 테이블에서 지불금액이 0인)를 대여한 적이 없는 모든 고객을 찾을 수 있다.
동일한 결과값을 not in
연산자를 사용한 쿼리로 표현할 수도 있다.
SELECT
first_name
, last_name
FROM customer
WHERE customer_id NOT IN (SELECT customer_id
FROM payment
WHERE amount = 0);
not in
또는all
을 사용할 경우,null
값을 포함하지 않도록 주의하자.
아래와 같은 경우 빈 결과값이 반환된다.SELECT first_name , last_name FROM customer WHERE customer_id NOT IN (122, 452, NULL); ------------- Empty set (0.00 sec)
all
연산자와 마찬가지로 한 집합의 모든 값과 하나의 값을 비교하지만, any
연산자를 사용할 경우에 여러 비교값 중 하나라도 만족하면 true
로 반환된다.
그렇다면,
any
를 사용했으므로 여러 비교값 중 하나라도 만족하면 된다.SELECT
customer_id
, SUM(amount)
FROM payment
GROUP BY customer_id
HAVING SUM(amount) > ANY (SELECT SUM(p.amount)
FROM payment p
INNER JOIN customer c
ON p.customer_id = c.customer_id
INNER JOIN address a
ON c.address_id = a.address_id
INNER JOIN city ct
ON a.city_id = ct.city_id
INNER JOIN country co
ON ct.country_id = co.country_id
WHERE co.country IN ('Bolivia', 'Paraguay', 'Chile')
GROUP BY co.country);
결과값에 그림처럼 country
까지 포함되게 하려면, 포함구문의 SELECT
절에 INNER JOIN
을 통해 테이블들을 조인해줘야한다.
이를 위해서는,
이렇게 2개의 서브쿼리를 써야하는 것을
SELECT
fa.actor_id
, fa.film_id
FROM film_actor fa
WHERE fa.actor_id IN (SELECT actor_id
FROM actor
WHERE last_name = 'Monroe')
AND fa.film_id IN (SELECT film_id
FROM film
WHERE rating = 'PG');
CROSS JOIN
을 이용해 1개의 서브쿼리로 바꿀 수 있다.
SELECT
fa.actor_id
, fa.film_id
FROM film_actor fa
WHERE (a.actor_id, f.film_id) IN (SELECT a.actor_id, f.film_id
FROM actor a
CROSS JOIN film f
WHERE a.last_name = 'Monroe'
AND f.rating = 'PG');
상관 서브쿼리는 비상관 서브쿼리와 달리, 하나 이상의 열을 참조하는 포함구문에 의존적이다. 포함구문을 실행하기 전에 실행되지 않는다. (포함구문을 실행하고 나서, 그 중 어떤 값을 참조하여 서브쿼리가 돌아가는 식)
이를 위해서는,
SELECT
c.first_name
, c.last_name
FROM customer c
WHERE 20 = (SELECT COUNT(*)
FROM rental r
WHERE r.customer_id = c.customer_id); /* 서브쿼리를 상호 연관시킴 */
SELECT
c.first_name
, c.last_name
FROM customer c
WHERE (SELECT SUM(p.amount)
FROM payment p
WHERE p.customer_id = c.customer_id)
BETWEEN 180 AND 240 ;
-------------------------
6 rows in set
first_name|last_name|
----------+---------+
RHONDA |KENNEDY |
CLARA |SHAW |
ELEANOR |HUNT |
MARION |SNYDER |
TOMMY |COLLAZO |
KARL |SEAL |
삽질의 발단 : 고객의 성, 이름과 함께 지불금액 총합도 보고싶었을 뿐이다...
위의 쿼리에서 포함구문의 SELECT
절에 SUM(p.amount)
를 추가하고 payment 테이블과 customer 테이블을 조인했다.
응..? GROUP BY
가 없다구..?
ㅇㅋ 그럼 customer_id를 기준으로 그룹화해주지..! 하고 쿼리 추가 😓
empty set 반환.
아니 뭐가 문제지....?????????????
이렇게도 써보고 저렇게도 써보고, 별의별 해괴망측한 쿼리를 다 실행시켜보다가 발견한 점..!
아 그러면, 조인에 뭔가 문제가 있겠구나 싶어서 서브쿼리를 싹 배제하고 내가 아는 JOIN
과 GROUP BY
, HAVING
만을 이용하여 고객의 성, 이름과 함께 지불금액 총합을 불러와봤다.
SELECT
c.first_name
, c.last_name
, SUM(p.amount)
FROM customer c
INNER JOIN payment p
ON c.customer_id = p.customer_id
GROUP BY c.customer_id
HAVING SUM(p.amount) BETWEEN 180 AND 240 ;
-------------------------
6 rows in set
first_name|last_name|SUM(p.amount)|
----------+---------+-------------+
RHONDA |KENNEDY | 194.61|
CLARA |SHAW | 195.58|
ELEANOR |HUNT | 216.54|
MARION |SNYDER | 194.61|
TOMMY |COLLAZO | 186.62|
KARL |SEAL | 221.55|
잘 ㄴ ㅏ와요..
JOIN
과 함께 쓸 수 없구나.상관 서브쿼리를 쓰거나 OR JOIN
을 쓰거나.
exists
연산자를 사용할 때는select 1
또는select *
로 정의하는게 규칙이다.
SELECT
c.first_name
, c.last_name
FROM customer c
WHERE EXISTS (SELECT 1
FROM rental r
WHERE r.customer_id = c.customer_id
AND date(r.rental_date) < '2005-05-25') ;
----------+-----------+
first_name|last_name |
----------+-----------+
CHARLOTTE |HUNTER |
DELORES |HANSEN |
MINNIE |ROMERO |
CASSANDRA |WALTERS |
ANDREW |PURDY |
MANUEL |MURRELL |
TOMMY |COLLAZO |
NELSON |CHRISTENSON|
----------+-----------+
8 rows in set
반대로 아무 행도 반환하지 않는 서브쿼리를 확인하기 위해서는 not exists
를 사용할 수 있다.
SELECT
a.first_name
, a.last_name
FROM actor a
WHERE NOT EXISTS (SELECT 1
FROM film_actor fa
INNER JOIN film f
ON fa.film_id = f.film_id
WHERE fa.actor_id = a.actor_id
AND f.rating = 'R') ;
----------+---------+
first_name|last_name|
----------+---------+
JANE |JACKMAN |
----------+---------+
서브쿼리는 SELECT
뿐만 아니라 UPDATE
, DELETE
, INSERT
문에서도 많이 사용되고, 상관 서브쿼리는 UPDATE
, DELETE
문에서 자주 쓰인다.
여기서 주의할 점 :
null
값이 존재할 수도 있다. last_update
열을 업데이트하기 전에 null
값이 존재하는지 확인해야 한다. null
값이 존재한다면 열이 null
로 덮어씌워질 것이다.last_update
열의 데이터가 null
로 덮어씌워지지 않도록 보호하는 쿼리를 짜야 한다. UPDATE customer c
SET c.last_update = (SELECT MAX(r.rental_date)
FROM rental r
WHERE r.customer_id = c.customer_id)
WHERE EXISTS (SELECT 1 /* 고객의 대여기록이 1개 이상 있을 때만 */
FROM rental r
WHERE r.customer_id = c.customer_id) ;
→ WHERE
조건문이 true
일 때만 SET
절의 서브쿼리가 실행되므로 last_update
열의 데이터가 null
로 덮어씌워지지 않도록 보호할 수 있다.
MySQL에서는
DELETE
문을 사용할 때 테이블 별칭이 허용되지 않는다.
DELETE FROM customer
WHERE 365 < ALL (SELECT DATEDIFF(NOW(), r.rental_date) days_since_last_rental
FROM rental r
WHERE r.customer_id = customer.customer_id);
서브쿼리의 3 종류를 정리해보자.
SELECT
문 : 스칼라 서브쿼리WHERE
문 : 중첩 서브쿼리 FROM
문 : 인라인 뷰실행순서
FROM
문에서 사용되는 서브쿼리는 비상관 관계여야 한다. (상관 서브쿼리 X)SELECT
c.first_name
, c.last_name
, pay.num_rental
, pay.total_payments
FROM customer c
INNER JOIN (SELECT
customer_id
, COUNT(*) AS num_rental
, SUM(amount) AS total_payments
FROM payment
GROUP BY customer_id
) AS pay
ON c.customer_id = pay.customer_id ;
pay
라는 별칭이 지정되고, customer_id
를 기준으로 customer 테이블과 조인된다.pay
서브쿼리에서 가져온 열과 함께 customer 테이블에서 고객의 성, 이름을 검색한다. 서브쿼리를 사용해 데이터베이스에 존재하지 않는 데이터를 생성할 수도 있다. 이렇게 만들어진 데이터는 마치 테이블처럼 보이지만 쿼리가 실행될 때만 임시적으로 생성되는 뷰view다. 어떤 보고서를 만들때 등에 유용하게 쓰인다.
일단 지불금액별 등급은 아래와 같다. (단위: 달러)
등급(그룹명) | 하한 | 상한 |
---|---|---|
green | 0 | 74.99 |
silver | 75 | 149.99 |
gold | 150 | 9,999,999.99 |
SELECT
pay_grp.name
, COUNT(*) number_customers
FROM (SELECT
customer_id
, COUNT(*) AS num_rentals
, SUM(amount) AS total_payments
FROM payment
GROUP BY customer_id
) AS pay
INNER JOIN (SELECT 'green' name, 0 low_limit, 74.99 high_limit
UNION ALL
SELECT 'silver' name, 75 low_limit, 149.99 high_limit
UNION ALL
SELECT 'gold' name, 150 low_limit, 9999999.99 high_limit
) AS pay_grp
ON pay.total_payments
BETWEEN pay_grp.low_limit AND pay_grp.high_limit
GROUP BY pay_grp.name ;
쿼리문을 해석해보면 아래와 같다.
2개의 서브쿼리는 등급 이름(green, silver, gold)별로 그룹화되어 조인된다.
생각해보면 고객의 이름, 성, 도시는 결과에서만 보여지면 되고, 그룹화에 필요한 대부분의 데이터는 payment테이블에 있음을 알 수 있다. 이런 경우에
customer_id
로만 그룹화가 수행되기 때문에 쿼리가 훨씬 빠르게 실행된다.SELECT
c.first_name
, c.last_name
, ct.city
, pay.total_payments
, pay.total_rentals
FROM (SELECT
customer_id
, SUM(amount) total_payments
, COUNT(*) total_rentals
FROM payment
GROUP BY customer_id
) AS pay
INNER JOIN customer c
ON pay.customer_id = c.customer_id
INNER JOIN address a
ON c.address_id = a.address_id
INNER JOIN city ct
ON a.city_id = ct.city_id ;
파생테이블과 마찬가지로 쿼리문이 끝날 때 까지만 지속되는 일회성 테이블이지만, 쉼표(,
)를 통해 여러 CTE를 만들고 앞에 정의된 CTE를 참조할 수 있다.
사용방법 :
WITH 테이블명 AS (SELECT ...)
WITH actors_s AS
(SELECT
actor_id
, first_name
, last_name
FROM actor
WHERE last_name LIKE 'S%') ,
actors_s_pg AS
(SELECT
s.actor_id
, s.first_name
, s.last_name
, f.film_id
, f.title
FROM actors_s AS s
INNER JOIN film_actor AS fa
ON s.actor_id = fa.actor_id
INNER JOIN film AS f
ON fa.film_id = f.film_id
WHERE f.rating = 'PG'),
actors_s_pg_revenue AS
(SELECT
spg.first_name
, spg.last_name
, p.amount
FROM actors_s_pg AS spg
INNER JOIN inventory AS i
ON i.film_id = spg.film_id
INNER JOIN rental AS r
ON i.inventory_id = r.inventory_id
INNER JOIN payment AS p
ON r.rental_id = p.rental_id)
SELECT
spg_rev.first_name
, spg_rev.last_name
, SUM(spg_rev.amount) total_revenue
FROM actors_s_pg_revenue AS spg_rev
GROUP BY spg_rev.first_name, spg_rev.last_name
ORDER BY 3 DESC ;
스칼라 서브쿼리는 필터조건 뿐 아니라 SELECT
, ORDER BY
, INSERT 문의 values
등 표현식이 있는 모든 곳에서 사용할 수 있다.
JOIN
과 사용하지 말자. IN
, NOT IN
, EXISTS
, NOT EXISTS
와 같은 비교 연산자를 조건에서 사용할 수 있다.
IN
은INNER JOIN
혹은EXISTS
로 사용해도 같은 결과다.
NOT IN
은LEFT JOIN
혹은NOT EXISTS
로 사용해도 같은 결과다.
SELECT
, UPDATE
, DELETE
, INSERT
문에서 쓸 수 있다. INSERT
문의 values
UPDATE
문의 set
SELECT
, FROM
, WHERE
, HAVING
, ORDER BY
절에서 사용할 수 있다.