서브쿼리 2

37584 단어 sqlsql

📌 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 테이블을 이용한다.

이때 리뷰가 있는 상품들의 가격 중에서 최대 가격 / 리뷰의 평균 별점 / 리뷰를 남긴 고유한 회원 이메일의 수를 구하려고 한다.

  1. 이 세 테이블을 모두 INNER JOIN하고, 거기서 price, star, email 컬럼만 조회하라.
  2. SELECT 문을 derived table로 활용하라.
  3. 그리고 derived table에는 copang_report(코팡 보고서)라는 alias를 붙여라.
  4. 조회하는 각 컬럼에 아래와 같은 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문으로 인해 해석하기 어렵다.

서브쿼리를 중첩하지 않고 더 간편하게 조회하는 방법을 알아보자.


좋은 웹페이지 즐겨찾기