SUBJECT: 서브쿼리를 사용하여 필터, 값 추출, 임시 테이블 생성하기
References
web
text book
- Learning sql 3rd edition/Alan Beaulieu/9781492057611
- SQL level up/Mic/9788968482519
Subquery란 무엇인가?
쿼리 내부에 포함되는 다른 SQL문이며, 대부분 포함하는 구문보다 먼저 수행된다.
일반 쿼리구문과 동일하게, 아래와 같은 결과를 얻을 수 있다.
- 하나의 행의 데이터를 가지는 단일 Row
- 하나의 행의 데이터를 가지는 다수의 Rows
- 다수의 행의 데이터를 가지는 다수의 Rows
이는 선언스코프에서 임시테이블과 같은 역할을 한다.
즉, subquery를 포함하는 SQL구문 전체가 종료되면 DB서버는 해당 메모리를 해제한다.
서브쿼리는 쿼리를 나눠서 해야할 것을 single transaction에 처리한 다는 점에서 성능상 이점이 있다.
Subqueury Types
크게는 두 가지 종류로 나뉠 수 있다.
noncorrelated subqueries: 독자적으로 실행되는 구문, scalar subquery라고 불려진다. correlated subqueries: 외부 구문과 연관되어 실행되는 구문
Subquery와 자주 사용하는 연산자
여기에 등장하는 연산자는 모두 WHERE과 결합되며,
SUBQUERY를 피연산 대상으로 동작하기에 매우 적합한 목록이다.
IN and NOT IN operator
하나의 값을 대상 집합에 대해서 직접 비교 할 수 없기 때문에,
하나의 값이 대상 집합에 포함되는지를 통한 비교로 IN을 사용한다.
/* 1. ARRAY와 비교되는 IN의 활용 */ SELECT country_id FROM country WHERE country IN ('Canada', Mexico');
하지만,
- 비교할 대상 집합이 갯수가 적거나 (쿼리 구문의 간소화)
- 대상 집합의 값을 지정할 수 있는 경우라면 (다른 테이블과 무관하거나, 알고 있는)
단순히 아래와 같이 WHERE CONDITION-A OR CONDITION-B의 형태로 작성하는 것이 더 합리적이다.
/* 2. 피연산 대상이 갯수가 적은 경우 Reasonable */ SELECT country_id FROM country WHERE country = 'Canada' OR country = 'Mexico';
특정하게 지정할 수 있는 문자열, 날짜, 숫자로 비교하는 경우도 있겠지만,
다른 테이블에서 추출한 값에서 그 대상을 찾는 경우가 조금 더 많을 것이다.
/* 3. 피연산 대상이 많거나 불분명할때 Subquery와 조합 */ SELECT country_id, city FROM city WHERE country_id IN ( SELECT country_id FROM country country NOT IN ('Canada', 'Mexico') );
ALL operator
ALL연산자는 하나의 값과 집합의 모든 값에 대한 비교를 가능하게 한다.
WHERE <VALUE OR COLUMN> ANY <CMP operator> <SUBQUERY>
(=, <>, <, >, etc.)등의 연산자와 함께 조합되어 사용되어야 한다.
/* <> 연산자와 결합, NOT IN과 동일 */
SELECT first_name, last_name FROM customer
WHERE customer_id <> ALL (
SELECT customer_id FROM payment WHERE amount = 0
);
SELECT customer_id, count(*) FROM rental
GROUP BY customer_id
HAVING count(*) > ALL (
SELECT count(*) FROM rental
INNER JOIN customer c
ON r.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 ('United States', 'Mexico', 'Canada')
GROUP BY r.customer_id
);
ANY operator
ALL연산자와 동일하게 WHERE <VALUE OR COLUMN> ANY <CMP operator> <SUBQUERY>의 형태로, 값 집합을 대상으로 특정값을 비교할 때 사용된다.
그러나 차이점이 있다면, 집합 내에 하나의 대상이라도 <CMP operator> 조건에 부합한다면 true로 평가된다는 점이다.
/* 구매를 한 유저의 구매 총합과 유저 */
/* 구매 총합이 3개의 국가별 구매내역 총합 중 최소보다 큰*/
SELECT customer_id, sum(amount) AS paid
FROM payment
GROUP BY customer_id
HAVING paid > 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
);
EXISTS Operator
Correlated subquery를 사용하면서 가장 흔히 사용되는 연산자이다.
서브쿼리의 결과인 temporary 테이블에 row가 있는지 없는지를 검사하는 것으로 WHERE과 결합되어 사용된다.
즉, EXISTS에 사용된 서브쿼리에서 SELECT 구의 COLUMN에 대한 명시는 유효할 수 있는 것 이기만 하면 무엇이건 상관없다.
SELECT a.first_name, a.last_name
FROM actor
WHERE EXISTS (
SELECT 1 FROM film_actor
INNER JOIN film ON film.film_id = film_actor.film_id
WHERE film_actor.actor_id = actor.actor_id
AND film.rating = 'R'
);
film이 'R'등급인 영화에 출연한 actor 정보만 추출할 수 있도록 하는데,
WHERE [NOT] EXISTS SUBQUERY로 SUBQUERY와만 결합되어 사용될 수 있는 연산자이며
결과가 하나라도 있는지의 기준으로 평가된다는 점으로 효율이 좋다.
Common usages of Subqueries
Powerful ways to use Subquries
이제부터 서브쿼리를 사용해서 유용한 SQL구문을 작성해볼 것이다.
다음 세 챕터에 걸쳐서 서브쿼리를 통해,
- 커스텀 테이블 생성
- 조건문 생성
- 결과 테이블에 Column Value 생성
등을 알아볼 것이다.
Subquries as Data sources
SELECT c.first_name, c.last_name,
pymnt.num_rentals, pymnt.tot_payments
FROM customer c
INNER JOIN (
SELECT customer_id,
count(*) num_rentals, sum(amount) tot_payments
FROM payment
GROUP BY customer_id
) pymnt
ON c.customer_id = pymnt.customer_id;
데이터 소스인 FROM 테이블로 사용된 서브쿼리는 반드시 "non correlated" 이어야 한다.
- 여기서 서브쿼리는 먼저 실행되고,
- 해당 쿼리가 종료될 때까지 메모리에서 대기상태를 가진다.
서브쿼리는 엄청난 유연성을 제공하는데,
SQL작성자는 목표로 하는 가상의 그 어떤 view라도 생성할 수 있도록
유효한 테이블을 훨씬 넘어갈 수 있고,
이후 결과를 다른 테이블 혹은 서브쿼리로 연결할 수 있기 때문이다.
서브쿼리의 유연성 덕분에, 과거에는 다수의 쿼리나 절차형 언어를 사용했던 것을 하나의 쿼리로 대체할 수 있다.
Data fabrication
실재하는 데이터를 요약하는 서브쿼리를 사용하는 방법처럼,
서브쿼리를 사용하여 존재하지 않는 형태의 테이블을 생성할 수 있다.
Group name | Lower limit | Upper limit |
---|---|---|
Small | 0 | 74.99 |
Average | 75 | 149.99 |
Heavy | 150 | 999999.99 |
with pymnt_grps AS (
SELECT 'Small' name, 0 low_limit, 74.99 upper_limit
UNION ALL
SELECT 'Average' name, 75 low_limit, 149.99 upper_limit
UNION ALL
SELECT 'Heavy' name, 150 low_limit, 999999.99 upper_limit;
)
SELECT pymnt_grp.name, count(*) num_customers
FROM (
SELECT customer_id, count(*) num_rentals, sum(amount) tot_payments
FROM payment
GROUP BY customer_id
) pymnt
INNER JOIN pymnt_grps
ON pymnt.tot_payments BETWEEN pymnt_grps.low_limit AND pymnt_grps.upper_limit
GROUP BY pymnt_grps.name;
물론 영구적인 혹은 임시 테이블을 생성해서 사용할 수 도 있다.
하지만 그럴 경우, 데이터베이스를 임시적으로 필요한, 혹은 덜 중요한 것들이 흩뿌려진(littered) 모습으로 만들어
이 파편들은 존재 자체도 잊을만한 대상이 될 것이다.
반면, 서브쿼리를 사용하면
"데이터베이스에 추가되는 테이블은 오직 분명한 필요가 존재하는 경우에만 추가되어야 한다."
라는 정책에 달라붙게(adhere) 될 수 있다.
Task-oriented Subqueries
SELECT * FROM <HEART-TABLE> INNER JOIN <ADDITIONAL-TABLES>
TABLE들에 분포된 데이터가 Normalized 되어 있어서,
특정 정보에 부가적인 정보를 더해 Rich하게 데이터를 구성해야하는 경우,
heart of the query(Minimum data For purpose only and no duplicates)를 Subquery에 작성하여 분리하고,
나머지 heart of query를 기반으로 확장될 수 있는 부가적인 정보는 이어지는 JOIN 등으로 두는 방식으로 구성하는 것이
저자의 입장에서는 SQL Statement구성이 Clear하다고 판단된다 [idiom01].
[idiom01] | beauty is in the eye of the beholder
미에대한 기준은 사람의 눈에 달려 있다.
|
예를 들어 도시정보가 포함된 유저의 정보를 가져온다고 했을때,
CITY-ADDRESS-USER 순서로 INNER JOIN을 할 수 도 있지만,
USER-ADDRESS-CITY 순서로 바꿔주는 것이 해당 SQL의 목적을 조금 더 분명히 드러낼 수 있다는 장점이 있다.
/* Not clear for describing the purpose of query */
SELECT p.customer_id, a.address_id, ct.city_id, SUM(p.amount) paid, count(*) cnt
FROM payemnt 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
GROUP BY c.first_name, c.last_name, ct.city;
/* Better clear for understanding the heart of SQL */
with main_tbl as (
SELECT p.customer_id, SUM(p.amount) paid, count(*) cnt
FROM payemnt p
GROUP BY customer_id
)
SELECT main_tbl.*, ct.city_id, a.address_id
FROM main_tbl
INNER JOIN customer c
ON main_tbl.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;
SELECT main_tbl.*, ct.city_id, a.address_id만으로도,
원하는 정보는 대부분 main_tbl에 존재하고 이후 연결되는 부분은 부수적인 정보임을 추측하기 용이하다.
또한 id값만 포함하는 것으로, 최소한의 식별정보만 유지하도록 하는 부분도 주목할만 하다.
Common table expressions(with statements)
Common table expression은 쿼리의 상단부에, WITH구와 함께 사용되는 이름이 있는 서브쿼리를 말한다.
WITH로 선언되는 테이블은 뒤에 있는 것이 앞에 있는 CTE의 이름을 그대로 사용할 수 있다.
Subqueries as Expression Generators
With single-column, single row scarlar subqueries.
단일 값의 결과를 반환하는 scarlar 서브쿼리는 아래와 같다.
- filter condition, WHERE v = (Subquery)
- SELECT (Subquery)
- ORDER BY (Subquery)
- INSERT INTO (A, B, C) VALUES ((Subquery-A), (Subquery-B), (Subquery-C))
Better to use subquery
결합과 관련된 쿼리를 수행할때, 성능측면에서 서브쿼리를 사용하는 것이 도움이 된다.
결합할때 중요한 것은 최대한 결합 대상 레코드 수를 줄이는 것이 중요하다.
결합을 통해 얻고자 하는 데이터가 어떤 결과인지를 최대한 염두하여,
대표적인 예로 집약 처리가 포함된다면, 아래와 같은 두 가지 방법이 존재한다.
- 결합을 먼저 수행
SELECT a.first_name, a.last_name, count(*) AS films
FROM actor AS a
INNER JOIN film_actor AS fa
ON a.actor_id = fa.actor_id
GROUP BY a.actor_id;
- 집약을 먼저 수행
WITH fa AS (
SELECT actor_id, count(*) AS films
FROM film_actor
GROUP BY actor_id
)
SELECT a.first_name, a.last_name, fa.films
FROM actor AS a
INNER JOIN fa
ON a.actor_id = fa.actor_id;
- actor table rows : 200
- film_actor table rows : 5462
결합을 먼저하는 경우: | JOIN 결합의 결과인 중간 테이블에서(5462개의 rows)
group by를 실행해서 200개로 줄어드는 결과 테이블을 생성한다.
group by 비용을 줄일 수 있는 장점
|
---|---|
집약을 먼저하는 경우: | GROUP BY 집약의 결과인 중간 테이블에서(200개의 rows)
이를 join하여 결과 테이블을 생성한다.
I/O 비용을 줄일 수 있는 장점
|
Hint
- 옵티마이저가 잘 판별하지 못할때에는 사람이 직접 연산순서를 명시해주면 성능적으로 좋은 결과를 얻을 수 있다.
- I/O는 SQL 성능의 가장 큰 평가 요인
- 서브쿼리와 결합을 윈도우 함수로 대체하면 성능을 개선할 가능성이 있음.