서브쿼리 2
📌 FROM절 서브쿼리
- 하나의 값을 리턴
- 하나의 column인 여러 개의 row를 리턴 (column 형태)
- 여러 개의 column에 여러 개의 row를 리턴 (테이블 형태)
SELECT
SUBSTRING(address, 1, 2) AS region,
COUNT(*) AS review_count
FROM review AS r LEFT OUTER JOIN member AS m
ON r.mem_id = m.id
GROUP BY SUBSTRING(address, 1, 2);
region 컬럼에 존재하는 NULL
과 안드
를 제거한다.
SELECT
SUBSTRING(address, 1, 2) AS region,
COUNT(*) AS review_count
FROM review AS r LEFT OUTER JOIN member AS m
ON r.mem_id = m.id
GROUP BY SUBSTRING(address, 1, 2)
HAVING region IS NOT NULL AND region != '안드';
위의 SQL문을 하나의 서브쿼리
로 만들어 보자.
서브쿼리로 탄생한 테이블을 derived table
이라고 한다.
✅ 위 테이블에서 rivew의 평균 개수
SELECT AVG(review_count)
FROM
(SELECT
SUBSTRING(address, 1, 2) AS region,
COUNT(*) AS review_count
FROM review AS r LEFT OUTER JOIN member AS m
ON r.mem_id = m.id
GROUP BY SUBSTRING(address, 1, 2)
HAVING region IS NOT NULL AND region != '안드') AS review_count_summary;
주의 ❗
FROM
뒤에서 서브쿼리로derived table
을 생성할 때는 반드시alias
를 붙여줘야 한다.
AS review_count_summary를 작성하지 않고 실행하면 오류가 발생한다.
✅ 평균 리뷰 개수와 최대 리뷰 개수와 최소 리뷰 개수
SELECT
AVG(review_count),
MAX(review_count),
MIN(review_count)
FROM
(SELECT
SUBSTRING(address, 1, 2) AS region,
COUNT(*) AS review_count
FROM review AS r LEFT OUTER JOIN member AS m
ON r.mem_id = m.id
GROUP BY SUBSTRING(address, 1, 2)
HAVING region IS NOT NULL AND region != '안드') AS review_count_summary;
📝 정리
FROM
절 뒤에도서브쿼리
를 사용할 수 있다.- 그 때
서브쿼리
가 만들게 되는테이블
을derived table
이라고 한다. derived table
은 해당 SQL문 안에서만 유효한 하나의 테이블이다.derived table
은 반드시alias
를 붙여야 오류가 발생하지 않는다.
📝 서브쿼리 종류 총 정리
서브쿼리는 리턴하는 결과의 형태에 따라 여러 종류로 나눌 수 있다.
다양한 종류가 있지만 실무적으로 유용한 3가지만 정리해보자.
📌 단일값을 리턴하는 서브쿼리
SELECT MAX(age) FROM member;
스칼라 서브쿼리
라고도 한다.- 스칼라 서브쿼리는
SELECT
절에서 하나의 컬럼처럼 사용 가능하다. - 스칼라 서브쿼리는
WHERE
절에서 조건 표현식과 비교하는 값으로 사용 가능하다.
📌 하나의 column에 여러 row들이 있는 형태의 결과를 리턴하는 서브쿼리
SELECT SUBSTRING(address, 1, 2) FROM member;
IN
,ANY(=SOME)
,ALL
등의 키워드와 함께 사용 가능하다.
📌 하나의 테이블 형태의 결과를 리턴하는 서브쿼리
SELECT * FROM member;
derived table
이라고 한다.derived table
에는 반드시alias
를 붙여줘야 한다.
📝 EXISTS, NOT EXISTS와 상관 서브쿼리
이전에는 서브쿼리를 어떠한 형식을 리턴하는지에 따라 종류를 나누었다.
그러나 서브쿼리는 리턴 결과가 아닌 다른 측면에서도 분류할 수 있다.
- 비상관 서브쿼리
- 상관 서브쿼리
📌 비상관 서브쿼리
SELECT * FROM item
WHERE id IN
(SELECT item_id FROM review GROUP BY item_id HAVING COUNT(*) >= 3);
서브쿼리 부분만 별도로 실행해보자.
SELECT item_id FROM review GROUP BY item_id HAVING COUNT(*) >= 3;
- 그 자체만으로도 실행이 가능한 서브쿼리이다.
- 서브쿼리가 둘러싼 outer query와 별개로, 독립적으로 실행되기 때문에 가능하다.
- outer query와 상관관계가 없는 서브쿼리를
비상관 서브쿼리
라고 한다.
📌 상관 서브쿼리
SELECT * FROM item
WHERE EXISTS (SELECT * FROM review WHERE review.item_id = item.id)
주목할 점은 item 테이블의 이름이 서브쿼리의 FROM 절에 있는 게 아니라 outer query
에 있다는 점이다.
서브쿼리가 필요로 하는 item 테이블이 outer query
에 적혀있기 때문에 이 서브쿼리는 단독으로 실행되지 못한다.
👉 코드 해석
- 일단 item 테이블의 첫 번째 row를 생각해보자.
- 그 row의 id(item.id) 값과 같은 값을 item_id(review.item_id) 컬럼에 가진 review 테이블의 row(가/들이) 있는지 조회한다.
- 만약에 존재하면(EXISTS) WHERE 절은 True가 되고, item 테이블의 row는 최종 조회 결과에 담긴다.
- 이 과정을 item 테이블의 두 번째, 세 번째, ... n번째 row에 대해 반복한다.
결국 item 테이블 중에서 그 id 컬럼 값이 review 테이블의 item_id 컬럼에 존재하는 row들만 추려지게 된다. 즉, 상품들 중에서 리뷰가 달린 상품들만 조회한 것이다.
반대로 리뷰가 달리지 않은 상품들만 조회하는 방법도 있다.
NOT EXISTS
를 사용한다.
SELECT * FROM item
WHERE NOT EXISTS (SELECT * FROM review WHERE review.item_id = item.id)
서브쿼리가 outer query에 적힌 테이블 이름 등과 상관 관계를 갖고 있어서 그 단독으로는 실행되지 못하는 서브쿼리를 상관 서브쿼리
라고 한다.
✅ 과제 : 서브쿼리 종합 과제
member 테이블, item 테이블, reivew 테이블을 이용한다.
이때 리뷰가 있는 상품들의 가격 중에서 최대 가격 / 리뷰의 평균 별점 / 리뷰를 남긴 고유한 회원 이메일의 수를 구하려고 한다.
- 이 세 테이블을 모두
INNER JOIN
하고, 거기서 price, star, email 컬럼만 조회하라. - 그
SELECT
문을derived table
로 활용하라. - 그리고 derived table에는 copang_report(코팡 보고서)라는
alias
를 붙여라. - 조회하는 각 컬럼에 아래와 같은
alias
를 붙여주세요.
- MAX(copang_report.price) → max_price
- AVG(copang_report.star) → avg_star
- COUNT(DISTINCT(copang_report.email)) → distinct_email_count
💻 풀이
SELECT
MAX(price) AS max_price,
AVG(star) AS avg_star,
COUNT(DISTINCT(email)) AS distinct_email_count
FROM
(SELECT
i.price,
r.star,
m.email
FROM member AS m INNER JOIN review AS r ON m.id = r.mem_id
INNER JOIN item AS i ON i.id = r.item_id) AS copang_report;
👉 결과
📌 서브쿼리 vs 조인
SELECT
id,
name,
(SELECT inventory_count FROM stock WHERE stock.item_id = item.id)
FROM item;
- item 테이블의 id 컬럼
- item 테이블의 name 컬럼
- stock 테이블의 inventory_count 컬럼
세 번째 컬럼은 서브쿼리
로 표현되었고, 상관 서브쿼리
이다.
세 번째 서브쿼리를 해석해보자.
- item 테이블의 첫 번째 row를 생각한다.
- 그 row의 id 컬럼의 값과 같은 값을 item_id 컬럼에 가진 stock 테이블의 row를 찾는다.
- 찾은 stock 테이블 row의 inventory_count 컬럼의 값을 리턴한다.
- 이 과정을 반복한다.
사실 위의 결과는 JOIN
만으로도 충분히 해결 가능하다.
SELECT i.id, i.name, s.inventory_count
FROM item AS i LEFT OUTER JOIN stock AS s
ON s.item_id = i.id;
서브쿼리와 조인이 같은 결과를 나타낸다.
상관 서브쿼리
와 조인
중에 무엇을 써야 할까 ❓
정답은 없다. 데이터를 분석할 때 더 익숙하고 직관적으로 이해할 수 있는 것을 선택한다. 하지만 만약 테이블에 아주 많은 수의 row들이 있을 때는 두 가지 방법 간에 속도 차이가 날 수도 있다.
📌 서브쿼리로 더 간결해진 CASE 함수 내부
SELECT
email,
CONCAT(height, 'cm', ', ', weight, 'kg') AS '키와 몸무게',
weight / ((height/100) * (height/100)) AS BIM,
(CASE
WHEN weight IS NULL OR height IS NULL THEN '비만 여부 알 수 없음'
WHEN weight / ((height/100) * (height/100)) >= 25 THEN '과제충 또는 비만'
WHEN weight / ((height/100) * (height/100)) >= 18.5
AND weight / ((height/100) * (height/100)) < 25 THEN '정상'
ELSE '저체중'
END) AS obesity_check
FROM copang_main.member
ORDER BY obesity_check ASC;
SELECT절에서 alias
로 붙인 BMI를 CASE 함수 내에서 사용하지 못했었다.
하지만 이제 서브쿼리
를 이용해 위처럼 번거로운 문제를 해결할 수 있다.
SELECT
email,
CONCAT(height, 'cm', ', ', weight, 'kg') AS '키와 몸무게',
weight / ((height/100) * (height/100)) AS BIM,
(CASE
WHEN weight IS NULL OR height IS NULL THEN '비만 여부 알 수 없음'
WHEN BMI >= 25 THEN '과제충 또는 비만'
WHEN BMI >= 18.5
AND BMI < 25 THEN '정상'
ELSE '저체중'
END) AS obesity_check
FROM
(SELECT *, weight / ((height/100) * (height/100)) AS BMI FROM copang_main.member) AS subquery_for_BMI
ORDER BY obesity_check ASC;
BMI라는 alias
를 붙인 SELECT
문을 서브쿼리
로 만들었다.
이 서브쿼리는 FROm
뒤에 있기 때문에 derived table
로 인식된다.
그 derived table에 alias
를 붙인 상태이다.
이로써 subqyery_for_BMI
는 마치 원래 존재하던 테이블인 것처럼 자유롭게 사용할 수 있다.
📌 서브쿼리 중첩과 문제점
서브쿼리
를 사용하면 하나의 SQL문만으로도 자유롭게 원하는 데이터를 얻을 수 있다. 또한 서브쿼리 안에 서브쿼를 또 사용이 가능하다. 이를 서브쿼리를 중첩한다고 한다.
SELECT
i.id,
i.name,
AVG(star) AS avg_star,
COUNT(*) AS count_star
FROM item AS i LEFT OUTER JOIN review AS r ON r.item_id = i.id
LEFT OUTER JOIN member AS m ON r.mem_id = m.id
WHERE m.gender = 'f'
GROUP BY i.id, i.name
HAVING COUNT(*) >= 2
ORDER BY AVG(star) DESC, COUNT(*) DESC;
테이블을 세 개 JOIN
하던 SQL문이다.
✅ 별점의 평균 값이 가장 큰 상품의 정보만 조회
SELECT i.id, i.name, AVG(star) AS avg_star, COUNT(*) AS count_star
FROM item AS i LEFT OUTER JOIN review AS r ON r.item_id = i.id
LEFT OUTER JOIN member AS m ON r.mem_id = m.id
WHERE m.gender = 'f'
GROUP BY i.id, i.name
HAVING COUNT(*) >= 2 AND avg_star =
(
SELECT MAX(avg_star) FROM(
SELECT i.id, i.name, AVG(star) AS avg_star, COUNT(*) AS count_star
FROM item AS i LEFT OUTER JOIN review AS r ON r.item_id = i.id
LEFT OUTER JOIN member AS m ON r.mem_id = m.id
WHERE m.gender = 'f'
GROUP BY i.id, i.name
HAVING COUNT(*) >= 2
ORDER BY AVG(star) DESC, COUNT(*) DESC
) AS final
)
ORDER BY AVG(star) DESC, COUNT(*) DESC;
👉 1번
위에서 출력했던 SQL문을 그대로 옮겨 적은 것이다.
FROM
뒤에서 서브쿼리
로 사용되고 final이라는 alias
를 붙였다.
즉, derived table
이다.
👉 2번
위의 derived table
에서 별점 평균 값의 최댓값을 구한다.
서브쿼리(2번) 안에 또 서브쿼리(1번)가 있는 형태이다.
즉, 서브쿼리를 중첩
하여 사용했다.
👉 4번
1번에서 사용한 원래의 SQL문과 같은 내용을 전체의 SQL문(4번)에도 작성되었다.
👉 3번
HAVING 조건의 COUNT(*) >= 2
그리고 avg_star가 별점의 최댓값인 것만 조회한 후 정렬 조건대로 정렬한다.
📝 단점
- SQL문이 너무 길어져서 가독성이 떨어진다.
- 중복되는 SQL문으로 인해 해석하기 어렵다.
서브쿼리를 중첩하지 않고 더 간편하게 조회하는 방법을 알아보자.
Author And Source
이 문제에 관하여(서브쿼리 2), 우리는 이곳에서 더 많은 자료를 발견하고 링크를 클릭하여 보았다 https://velog.io/@revudn46/서브쿼리와-뷰-2저자 귀속: 원작자 정보가 원작자 URL에 포함되어 있으며 저작권은 원작자 소유입니다.
우수한 개발자 콘텐츠 발견에 전념 (Collection and Share based on the CC Protocol.)