관리 메뉴

한다 공부

[MySQL] SQL 문제풀이 (GROUP BY) 본문

CS/SQL

[MySQL] SQL 문제풀이 (GROUP BY)

사과당근 2023. 10. 27. 00:10

SQL 고득점 Kit

고득점 키트에서 풀지 않은 그룹바이 문제 11개를 풀고자 합니다 ^_^

 

오잉 그런데 순서대로 풀고 있었는데,, 며칠 뒤 그룹바이 문제목록을 보니까 순서가 바뀌어있네요 ?! 

 뒤죽박죽으로 11문제를 풀어보았습니다..,,


1) 조건에 맞는 사용자와 총 거래금액 조회하기

그룹바이로 분류된 문제지만 조인과 연관도 있는 문제였다

우선 두 테이블을 조인한다. 그리고 where문을 통해 개별 행마다 done인 것을 추출해낼 것이다

그리고 유저 아이디로 그룹화한 다음, 유저가 판 물건 금액의 합이 700000이상인 것을 고른다.

(여담: 속성은 주로 복붙하는데.. nickname이 문제 설명에 nickanme으로 표기되어있어서 복붙 실수를 했다 ㅋㅋㅋ) 

SELECT U.USER_ID AS USER_ID, U.NICKNAME AS NICKNAME, SUM(B.PRICE) AS TOTAL_SALES
FROM USED_GOODS_BOARD AS B LEFT JOIN USED_GOODS_USER AS U
ON U.USER_ID= B.WRITER_ID
WHERE B.STATUS='DONE'
GROUP BY U.USER_ID
HAVING TOTAL_SALES >= 700000
ORDER BY TOTAL_SALES;

2) 즐겨찾기가 가장 많은 식당 정보 출력하기

서브쿼리를 사용해야하는 문제이다.

정답 코드는 아래와 같다. (사실... 정답입니다! 는 뜨지만 정답이 아니다...ㅋㅋㅋ 아래 내용 참고 ㅠ)

찬찬히 들여다 보면.. 

SELECT FOOD_TYPE, REST_ID, REST_NAME, FAVORITES
FROM REST_INFO
WHERE FAVORITES IN (
    SELECT MAX(FAVORITES)
    FROM REST_INFO
    GROUP BY FOOD_TYPE)
GROUP BY FOOD_TYPE
ORDER BY FOOD_TYPE DESC;

...

질문) 아래 코드 처럼 MAX (FAVORITES)를 가져오면 안되남

SELECT FOOD_TYPE, REST_ID, REST_NAME, FAVORITES, MAX(FAVORITES)
FROM REST_INFO
GROUP BY FOOD_TYPE
ORDER BY FOOD_TYPE DESC;

 

답변) GROUP BY를 쓰면 MAX 함수를 썼을 때,

MAX(그룹바이했을때 썼던 속성) 값은 잘 불러 오는데, 그 외의 속성을 select 하면 엉뚱한 값을 불러온다.

그래서 그룹바이를 한번한 뒤, 그룹마다의 MAX값이 뭔지 꺼내오고, 그 값이랑 같은 값을 가진 row를 꺼내온 것이다. 이를 구현하기 위해 서브쿼리를 쓴 것이다.

 

그룹바이를 공부하다보면 "GROUP BY는 GROUP BY에 정의한 내용(컬럼 또는 변형된 컬럼)만 SELECT절에 그대로 사용할 수 있다." 라는 말이 있다. 이 말이 바로 그 말이다! (출처: https://wikidocs.net/132723)

 

바로 위의 코드처럼 그룹바이를 할 경우, 그룹바이때 사용한 속성과, 집계함수를 쓴 속성 외의 속성들은 값을 랜덤하게 가져온다.

 

여담으로 아래와 같이 sql_mode를 변경하면 랜덤하게 값을 가져오는게 아니라 error가 발생하게 할 수 있다 (출처: https://school.programmers.co.kr/questions/38703

SET sql_mode='ONLY_FULL_GROUP_BY';

실제로 이 속성을 반영하면 프로그래머스에서 아래와 같은 오류가 뜬다.

해석하면 알 수 없는 값을 select 하고 있어서 오류가 난거다.. 라는 

SQL 실행 중 오류가 발생하였습니다.
Expression #2 of SELECT list is not in GROUP BY clause and contains nonaggregated column 'sql_runner_run.REST_INFO.REST_ID' which is not functionally dependent on columns in GROUP BY clause; this is incompatible with sql_mode=only_full_group_by

 

근데 생각해보니까... 정답입니다! 가 떴던 아까 그 코드.. 뭔가 이상하다!!

아까 그 코드대로 한다면 만약

한식 즐겨찾기 수: 100, 300, 600

일식 즐겨찾기 수: 200, 300 인 경우에

서브쿼리 결과물은 [600, 300] 이 있을 것이다.

그리고 FAVORITES가 [600,300]에 매핑되는 row를 가져와야하는데

최종적으로 원하는 row는 [한식, 600] 이지만... [한식, 300] 값을 불러올 수도 있는거 아녀?

 

그래서~ WHERE 절을 (타입, 즐찾수) IN (SELECT 타입, MAX(즐찾수) FROM 테이블 GROUP BY 타입) 이라고 해야한다. 그래야 타입-즐찾수가 쌍으로 조회되고 올바른 row를 가지고 오게 된다.

 

아래는 진짜 정답 코드이다.

SELECT FOOD_TYPE, REST_ID, REST_NAME, FAVORITES
FROM REST_INFO
WHERE (FOOD_TYPE, FAVORITES) IN (
    SELECT FOOD_TYPE, MAX(FAVORITES)
    FROM REST_INFO
    GROUP BY FOOD_TYPE)
GROUP BY FOOD_TYPE
ORDER BY FOOD_TYPE DESC;

위에 첨부한 위키 문서를 위에서부터 읽어보면 " GROUP BY에 정의하지 않은 컬럼을 SELECT절에서 사용하려면 반드시 집계함수 처리를 해야 한다."라고 한다. 이 말이 곧 그 말이며 ....

 

자 자 .. 다시 생각을 해 봅시다

서브쿼리만 떼고 보면

SELECT FOOD_TYPE, MAX(FAVORITES)
FROM REST_INFO
GROUP BY FOOD_TYPE

위와 같다. 위의 코드는 왜 문제가 안생길까?

SELECT 한 FOOD_TYPE의 경우, 그룹바이에서 사용한 속성이기 때문에 원하는 값을 명확히 셀렉할 수 있다.

그리고 MAX(FAVORITES)의 경우, 집계함수인 MAX를 썼기 때문에 올바른 값을 가져올 수 있다.

그냥 FAVORITES라고 하면 또 문제가 생길 것이다.

여기에 추가로 SELECT에 REST_NAME을 넣는다면 ? 마찬가지로 문제가 생기는 것이다. (랜덤 값 or 그룹화된 row들 중 가장 상단의 row의 REST_NAME이 셀렉한 REST_NAME에 들어가게 됨 -> 우리가 원하지 않는 값이 들어간단 소리!)

 

위에 남긴 프로그래머스의 QnA 링크 타고 들어가면 "GROUP BY 구의 기준이 되는 필드가 아니거나 집계 함수를 통해 어떤 값을 반환해야 할 지 결정해주지 않은 필드를 SELECT 구에 사용할 경우 문법적으로 틀린 SQL 구문입니다." 라는 말이 있는데 이말이 그말이다.

 

헷갈리니까.. 결론만 말하자면 아래 코드가 정답이다!

SELECT FOOD_TYPE, REST_ID, REST_NAME, FAVORITES
FROM REST_INFO
WHERE (FOOD_TYPE, FAVORITES) IN (
    SELECT FOOD_TYPE, MAX(FAVORITES)
    FROM REST_INFO
    GROUP BY FOOD_TYPE)
GROUP BY FOOD_TYPE
ORDER BY FOOD_TYPE DESC;

3) 저자 별 카테고리 별 매출액 집계하기

join을 3번 하면 된다!

판매된 책에 관련된 데이터가 필요하므로 판매량 테이블을 기준으로 left join을 2번 한다.

그리고 2022-01에 판매된 것만 필요하기 때문에, SALES_DATE에 2022-01이 포함된 것을 가져온다.

마지막으로 그룹바이를 통해 아이디와 카테고리를 기준으로 그룹화를 하고, 정렬하면 된다.

SELECT A.AUTHOR_ID, A.AUTHOR_NAME, B.CATEGORY, SUM(S.SALES*B.PRICE) AS TOTAL_SALES
FROM BOOK_SALES AS S LEFT JOIN BOOK AS B
ON S.BOOK_ID = B.BOOK_ID
LEFT JOIN AUTHOR AS A
ON B.AUTHOR_ID = A.AUTHOR_ID
WHERE S.SALES_DATE LIKE '2022-01%'
GROUP BY A.AUTHOR_ID, B.CATEGORY
ORDER BY A.AUTHOR_ID, B.CATEGORY DESC;

4) 자동차 종류 별 특정 옵션이 포함된 자동차 수 구하기

SELECT CAR_TYPE, COUNT(*) AS CARS
FROM CAR_RENTAL_COMPANY_CAR 
WHERE OPTIONS LIKE '%통풍시트%' OR
    OPTIONS LIKE '%열선시트%' OR
    OPTIONS LIKE '%가죽시트%'
GROUP BY CAR_TYPE
ORDER BY CAR_TYPE;

5) 카테고리 별 도서 판매량 집계하기

두 테이블을 조인해준 다음, 2022-01에 판매된 책들을

카테고리 별로 그룹화 하면 된다.

SELECT B.CATEGORY, SUM(S.SALES) AS TOTAL_SALES
FROM BOOK_SALES AS S LEFT JOIN BOOK AS B
ON S.BOOK_ID = B.BOOK_ID
WHERE S.SALES_DATE LIKE '2022-01%'
GROUP BY CATEGORY
ORDER BY CATEGORY;

6) 자동차 대여 기록에서 대여중/대여 가능 여부 구분하기

우선 자동차 아이디 별로 대여 기록을 그룹화를 한다.

그리고 그룹 내 하나의 기록이라도 대여중으로 처리가 되어있다면,

그 아이디에 해당되는 자동차는 대여중인 것이다.

 

우선 서브쿼리를 이용해 해당 날짜에 대여중인 차량을 모두 추출한다.

그리고 해당 자동차 아이디가, 서브쿼리의 결과물 내에 하나라도 있으면 (=IN) 그 차는 대여중인것이다.

서브쿼리를 이용해서 풀었다.

SELECT CAR_ID, IF(CAR_ID IN (
    SELECT CAR_ID
    FROM CAR_RENTAL_COMPANY_RENTAL_HISTORY
    WHERE START_DATE <= '2022-10-16' AND '2022-10-16' <= END_DATE), '대여중', '대여 가능') AS AVAILABILITY
FROM CAR_RENTAL_COMPANY_RENTAL_HISTORY
GROUP BY CAR_ID
ORDER BY CAR_ID DESC;

7) 식품분류별 가장 비싼 식품의 정보 조회하기

이 문제는 2) 즐겨찾기가 가장 많은 식당 정보 출력하기 문제와 아주 유사하다.

위의 문제를 이해했다면 같은 방식으로 풀 수 있다!

SELECT CATEGORY, PRICE AS MAX_PRICE, PRODUCT_NAME
FROM FOOD_PRODUCT
WHERE (PRICE, CATEGORY) IN
    (SELECT MAX(PRICE), CATEGORY FROM FOOD_PRODUCT GROUP BY CATEGORY)
    AND CATEGORY IN ('과자', '국', '김치', '식용유')
GROUP BY CATEGORY
ORDER BY PRICE DESC;

8) 대여 횟수가 많은 자동차들의 월별 대여 횟수 구하기

우선, 8월과 10월 사이에 5회 이상 대여한 자동차의 리스트를 구하기 위해 서브쿼리를 썼다.

서브쿼리 없이 한번에, 세 달동안 5회 이상인지 합산을 하고, 각각의 달마다 대여 횟수를 계산하려고 하니까 처음엔 문제가 잘 안풀렸다.

 

서브쿼리를 통해, 8-10월 사이에 5회 이상 대여한 자동차의 아이디를 조회한 뒤,

해당 아이디를 가진 자동차의 8-10월 데이터를 가져왔고,

이 데이터를 아이디와 월을 기준으로 그룹화했다.

이때, 특정 월의 대여 기록이 0이라면 제외해야 해서 HAVING에 조건을 추가했다.

SELECT MONTH(START_DATE) AS MONTH, CAR_ID, COUNT(*) AS RECORDS
FROM CAR_RENTAL_COMPANY_RENTAL_HISTORY
WHERE CAR_ID IN (
    SELECT CAR_ID
    FROM CAR_RENTAL_COMPANY_RENTAL_HISTORY
    WHERE START_DATE >= '2022-08-01' AND START_DATE < '2022-11-01'
    GROUP BY CAR_ID
    HAVING COUNT(*) >= 5
) AND START_DATE >= '2022-08-01' AND START_DATE < '2022-11-01'
GROUP BY CAR_ID, MONTH(START_DATE)
HAVING COUNT(*) > 0
ORDER BY MONTH(START_DATE), CAR_ID DESC;

9) 년, 월, 성별 별 상품 구매 회원 수 구하기

이 문제 또한 8번 처럼 여러 속성을 기반으로 그룹화를 하면 되는데, 간과하기 쉬운 부분이 있다.

바로 SELECT 절에 DISTINCT를 사용하는 것이다.

왜냐하면, 같은 회원이, 같은 년도 같은 월에 다른 상품을 구매할 수 있는데, 이 경우 중복 카운트가 되기 때문이다.

SELECT YEAR(S.SALES_DATE) AS YEAR, MONTH(S.SALES_DATE) AS MONTH, I.GENDER, COUNT(DISTINCT(I.USER_ID)) AS USERS
FROM ONLINE_SALE AS S LEFT JOIN USER_INFO AS I
ON S.USER_ID = I.USER_ID
WHERE I.GENDER IS NOT NULL
GROUP BY YEAR(S.SALES_DATE), MONTH (S.SALES_DATE), I.GENDER
ORDER BY YEAR(S.SALES_DATE), MONTH (S.SALES_DATE), I.GENDER;

10) 입양 시각 구하기(2)

이 문제는 쉬워보였는데 어려웠다.

0 ~ 23 시 까지 각각의 시간대에 입양된 동물 수를 구해야하는데, (없을 경우 0으로 표기)

그냥 그룹바이만 하면 입양된 동물이 있는 시간대만 조회된다. (입양된 동물이 0마리일 경우 해당 시간대 조회 X)

 

그래서 임시로 0 ~ 23 이 포함된 아래와 같은 임시 테이블을 만들어서,

임시 테이블을 기준으로, 기존 테이블을 left join 시켰다.

+------+
| HOUR |
+------+
|   0  |
|   1  |
|   2  |
|   3  |
|  ... |
|  22  |
|  23  |
+------+

 

이 임시 테이블을 만들기 위해 RECURSIVE WITH 을 사용했다. (처음 써봄)

우선 WITH 절은, 서브쿼리 및 임시 테이블 처럼 활용할 수 있는 절이다.

RECURSIVE WITH는 일종의 for문과 같은 효과를 낼 수 있다.

 

두번째 줄에 있는, SELECT 0 AS HOUR 을 통해 초기 값을 설정할 수 있다.

그리고 이후 초기값을 기반으로 반복을 통해 값을 추가할 것이다.

아래 코드는, HOUR의 값을 1씩 증가시키며 HOUR이라는 칼럼에 0 ~ 23을 넣는 코드이다.

처음에 무의식적으로 HOUR < 24 라고 했는데,

이 경우 HOUR가 23일 때도 반복을 돌아서 HOUR + 1 = 24,

즉 0 ~ 24까지 값이 저장된다. 따라서 0 ~ 23이라는 값을 넣으려면 HOUR < 24 라고 해야한다.

 

그리고 이렇게 만들어진 테이블과, 기존의 동물 입양 정보가 적힌 테이블을 join 시켰다.

WITH RECURSIVE TEMP_TABLE AS (
    SELECT 0 AS HOUR
    UNION ALL
    SELECT HOUR+1 FROM TEMP_TABLE
    WHERE HOUR < 23
)

SELECT T.HOUR, COUNT(A.ANIMAL_ID) AS COUNT
FROM TEMP_TABLE AS T LEFT JOIN ANIMAL_OUTS AS A
ON HOUR(A.DATETIME) = T.HOUR
GROUP BY T.HOUR
ORDER BY T.HOUR;

11) 가격대 별 상품 개수 구하기

10000원 단위로 그룹화 해야하기 때문에, /10000을 한 뒤 소숫점 이하를 버리고 * 10000을 했다.

그리고 그 금액을 기준으로 그룹화를 했다.

 

소숫점을 버리려면 FLOOR 함수를 사용하면 된다.

이 함수는 소숫점을 버리고 가장 근접한 정수를 반환한다.

 

추가로, ROUND는 반올림을 하는 자릿수를 정해, 반올림 할 수 있는 함수다.

CEILING은 올림 자릿수를 지정할 수 없으며, 반올림을 해서 정수를 반환하는 함수다.

SELECT FLOOR(PRICE/10000) * 10000 AS PRICE_GROUP, COUNT(*) AS PRODUCTS
FROM PRODUCT
GROUP BY PRICE_GROUP
ORDER BY PRICE_GROUP;

 

GROUP BY 문제풀이 끝!

'CS > SQL' 카테고리의 다른 글

[MySQL] SQL 문제풀이 (String, Date)  (1) 2023.12.23
[MySQL] SQL 문제풀이 (JOIN)  (2) 2023.11.07
[MySQL] SQL 문제풀이 (SELECT)  (0) 2023.10.25
[MySQL] SQL 문제풀이 (6문제)  (0) 2023.09.18
[MySQL] SQL 문법 정리 & 문제풀이 (30문제)  (0) 2023.05.02