프로그래머스에서 문제를 풀다가
GROUP BY 와 관련해 정리하고 넘어가면 좋을 부분을 발견해서 공유하려고 한다.
해당 문제를 통해서
다음의 궁금증을 해결할 수 있었다.
💡
GROUP BY에 여러 개의 컬럼을 지정할 수 있을까?
가능하다면 어떻게 동작하게 될까?💡
GROUP BY를 왜 집계 함수와 함께 사용하는 것일까?💡
GROUP BY를 사용할 때SELECT절에 아무 컬럼이나 지정할 수 없는 이유는 무엇일까?
자세한 조건과 데이터 테이블은 링크를 참고하기 바란다 :)
[프로그래머스] 대여 횟수가 많은 자동차들의 월별 대여 횟수 구하기
최종 쿼리는 글 하단에 작성해두었다.
[ 대여 횟수가 많은 자동차들의 월별 대여 횟수 구하기 ]
CAR_RENTAL_COMPANY_RENTAL_HISTORY 테이블에서
대여 시작일을 기준으로 2022년 8월부터 2022년 10월까지 총 대여 횟수가 5회 이상인 자동차들에 대해서
해당 기간 동안의 월별 자동차 ID별 총 대여 횟수(컬럼명: RECORDS) 리스트를 출력하는 SQL문을 작성해주세요.
결과는 월을 기준으로 오름차순 정렬하고, 월이 같다면 자동차 ID를 기준으로 내림차순 정렬해주세요.
특정 월의 총 대여 횟수가 0인 경우에는 결과에서 제외해주세요.
일단
대여 시작일을 기준으로 2022년 8월부터 2022년 10월까지 총 대여 횟수가 5회 이상인 자동차들을 추출한 후,
해당 자동차에 대해서 메인 쿼리가 동작해야 하기 때문에
서브쿼리로 해당하는 자동차들을 먼저 추출해 보겠다.
SELECT CAR_ID
FROM CAR_RENTAL_COMPANY_RENTAL_HISTORY
WHERE START_DATE BETWEEN '2022-08-01' AND '2022-10-31'
GROUP BY CAR_ID
HAVING COUNT(*) >= 5
작성한 서브쿼리의 동작을 생각해 보자.
각 자동차를 식별할 수 있는 컬럼은 CAR_ID 이다.
CAR_RENTAL_COMPANY_RENTAL_HISTORY 테이블에서
대여 시작일(START_DATE)이 2022년 8월부터 2022년 10월 사이에 해당하는(BETWEEN ~ AND ~)
자동차(CAR_ID)를 그룹화(GROUP BY)하였다.
그리고 그룹화한 각각의 자동차(CAR_ID) 중에서
총 대여 횟수(COUNT(*))가 5회 이상인
CAR_ID 를 조회(SELECT)하게 된다.
작성한 서브쿼리를 실행하면
CAR_ID 컬럼에서 대여 시작일을 기준으로 2022년 8월부터 2022년 10월까지 총 대여 횟수가 5회 이상인 CAR_ID 목록을 확인할 수 있다.
이제 메인쿼리를 작성해 보자.
SELECT MONTH(START_DATE) AS MONTH,
CAR_ID,
COUNT(*) AS RECORDS
FROM CAR_RENTAL_COMPANY_RENTAL_HISTORY
WHERE CAR_ID IN (
... 작성한 서브 쿼리 ...
)
AND START_DATE BETWEEN '2022-08-01' AND '2022-10-31'
GROUP BY MONTH(START_DATE),
CAR_ID
ORDER BY MONTH(START_DATE),
CAR_ID DESC
마찬가지로 CAR_RENTAL_COMPANY_RENTAL_HISTORY 테이블에서
WHERE 절에 작성해야 할 메인쿼리의 조건은 아래와 같다.
2-1. 서브쿼리에서 추출한 자동차 목록에 해당해야 하고
-> WHERE CAR_ID IN 서브쿼리
AND
2-2. 대여 시작일이 해당 기간 동안(BETWEEN ~ AND ~)에 속해야 한다.
🧐 해당 조건은 서브쿼리에도 등장했는데 왜 두 번이나 사용하는 걸까?
두 조건의 역할이 다르기 때문이다.
✓ 서브쿼리 -> 5회 이상 대여한 CAR_ID 를 선별하기 위해
✓ 메인쿼리 -> 선별된 차들의 8~10월 대여 기록을 카운팅하기 위해
3. 월별(GROUP BY), 자동차 ID별(GROUP BY)로 그룹화하여
4. MONTH(START_DATE), CAR_ID, 총 대여 횟수(COUNT(*))를 조회한다.
5. 결과는 월을 기준으로 오름차순 정렬하고,
월이 같다면 자동차 ID를 기준으로 내림차순 정렬(ORDER BY)
메인쿼리의 흐름은 이렇다.
여기서 궁금증이 생겼다.
문제를 해결하기 위해 월별, 자동차 ID별로 그룹화를 해야 한다는 건 알겠는데
그룹화의 결과가 어떤 흐름으로 출력되는 거지?
즉, GROUP BY 에 여러 개의 컬럼을 지정하면 어떻게 동작하는 것인지 이해가 잘 되지 않아 예제를 통해 비교하면서 정리해 보았다.
일단, GROUP BY 에 한 개의 컬럼을 지정하는 경우부터 살펴보자.
SELECT CAR_ID,
COUNT(*)
FROM 테이블
GROUP BY CAR_ID
"자동차 ID별" 로 그룹화하기 때문에, 각 자동차별로 총 몇 번 대여되었는지의 횟수를 카운트하게 된다.
이제 문제에서 작성한
GROUP BY 에 두 개의 컬럼을 지정하는 경우를 살펴보자.
SELECT MONTH(START_DATE) AS MONTH,
CAR_ID,
COUNT(*)
FROM 테이블
GROUP BY MONTH(START_DATE),
CAR_ID
해당 쿼리의의 경우에는
“월별 + 자동차 ID별” 로 그룹화를 하는데, 각 컬럼의 조합이 같은 행끼리 묶인다.
즉, MONTH + CAR_ID 가 일치하는 레코드끼리 그룹으로 묶이게 된다.
예를 들어 생각해 보면
MONTH 가 8이고, CARD_ID 가 1인 행끼리는 하나의 그룹으로 묶이게 되는 것이다.
따라서 이 그룹에서 COUNT(*) 를 하게 되면
8월에 1번 자동차가 총 몇 번 대여되었는지의 횟수를 카운트하게 된다.
이렇게 MONTH 와 CAR_ID 를 그룹화함으로써
문제에서 요구하는 월별, 자동차별 총 대여 횟수를 구할 수 있다.
컬럼이 2개 이상 오는 경우에도 마찬가지다.
GROUP BY 컬럼1, 컬럼2, 컬럼3, ...컬럼1 + 컬럼2 + 컬럼3 + ... 의 조합이 완전히 같은 행끼리 그룹으로 묶인다.
💡 즉,GROUP BY에 여러 개의 컬럼 지정하면각 컬럼의 조합이 같은 행끼리 그룹으로 묶이게 되고
결과값은GROUP BY절에 기술된 컬럼 항목들의 행의 개수에 의해 결정된다.
일단 짚고 넘어가야 하는 부분은
집계 함수 없이 GROUP BY 를 사용해도 에러가 발생하는 것은 아니라는 것이다.
그럼에도 GROUP BY 를 집계 함수와 함께 사용하는 이유를 살펴보자.
GROUP BY의 핵심은 "동일한 값들끼리 그룹으로 묶는 것" 이다.
그리고 그 그룹들에 대해 요약(집계)된 정보를 알고 싶을 때 집계 함수가 등장한다.
집계 함수 없이 사용하는 예제 함께 살펴보면
GROUP BY 와 집계 함수를 함께 사용해야 하는 이유가 와닿을 것이다.
-- 집계 함수 없이 GROUP BY 만 쓰는 경우
SELECT MONTH(START_DATE) AS MONTH,
CAR_ID
FROM 테이블
GROUP BY MONTH(START_DATE),
CAR_ID
이렇게 사용하면
각 (MONTH, CAR_ID) 조합에 대해 임의의 한 행의 값이 반환될 수 있지만
이는 DBMS마다 동작이 다르고 어떤 행의 값이 선택될지 예측할 수 없다.
만약 그룹화를 했는데 집계하지 않은 컬럼을 SELECT 절에 그냥 지정하면
SQL에서는 "이 그룹 안에서 어느 값을 가져올지 모르겠는데?" 라며
어떤 값을 보여줘야 할지 판단할 수 없기 때문에 문제가 발생하게 된다.
즉, 예측이 불가능하다는 것이다.
그룹화는 되지만, 각 그룹에서 어떤 행을 보여줄지 알 수 없어 비결정적이게 된다.
그래서 실제 값으로 의미 있는 요약을 하려면 반드시 집계 함수를 써야 한다.
또, 이렇게까지 생각하지 않아도
만약 MONTH가 8 이고, CAR_ID 가 1인 행들을 그룹화하고 나서
집계 함수를 사용하지 않는다면
에러가 발생하지는 않지만 기껏 그룹화를 한 의미가 없다는 것도 생각해볼 수 있다 😊
다시 위로 올라가서
GROUP BY 와 집계 함수를 함께 사용한 경우를 살펴보면
"차량별로 월별 대여 횟수"를 정확히 집계하는
각 (MONTH, CAR_ID) 조합별로 정확한 요약값이 나온다는 것을 확인할 수 있다.
💡
GROUP BY는집계 함수 없이도 사용할 수는 있지만,
집계 함수와 함께 사용할 때 의미가 크다는 것을 기억하면 된다 :)
[SQL] 데이터 그룹화하기: GROUP BY 해당 글에서
GROUP BY 를 사용할 때 꼭 알아야 할 규칙을 정리해 뒀으니 읽어보는 것도 좋겠다 :)
해당 주제는 앞에서 다룬 내용과 겹치는 부분이 있지만
SQL의 논리적 실행 순서와 관련된 중요한 개념이기 때문에 정리해 보았다.
GROUP BY 는 행들을 그룹으로 묶기 위한 문법이다.
그리고 SELECT 절에서는 "그룹마다 하나씩의" 결과를 보여주어야 한다.
따라서 SELECT 절에서는 집계 함수 또는 그룹 기준 컬럼만 사용 가능하다.
SELECT 절에
집계함수를 쓰는 건 OK
→ 그룹을 요약하는 것이니까!
그룹 기준이 아닌 컬럼,
즉 GROUP BY 에 지정되지 않은 컬럼을 쓰는 건 X
→ 그 컬럼의 값을 대표 하나만 보여줄 수 없기 때문이다.
SELECT MONTH(START_DATE) AS MONTH,
CAR_ID
COUNT(*)
FROM 테이블
GROUP BY MONTH(START_DATE)
MONTH 로 그룹화를 했는데
CAR_ID 은 집계함수가 아니고, GROUP BY 에도 없다.
이때 CAR_ID 도 함께 조회(SELECT)하려고 하면 SQL은 이렇게 혼란스러워한다:
“ MONTH 에는 각각의 월 데이터가 있는데,
이 각각의 월에 해당하는 CAR_ID 는 어떤 자동차의 것을 보여줘야 하지? ”
그래서 SQL은 CAR_ID 에 대해 정의할 수 없음으로 판단하고 오류를 발생시킨다.
💡
GROUP BY를 사용하면
SELECT절에서 자유롭게 아무 컬럼이나 가져올 수 없다는 것을 명심하면 된다:)
지금까지
GROUP BY 를 사용할 때 지켜야 할 사항을 알아보았고,
이에 유의하며 쿼리를 작성하여 문제에서 요구한 조건을 모두 만족시키는 결과를 출력할 수 있었다.
아래는 작성한 최종 쿼리이다.
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 BETWEEN '2022-08-01' AND '2022-10-31'
GROUP BY CAR_ID
HAVING COUNT(*) >= 5
)
AND START_DATE BETWEEN '2022-08-01' AND '2022-10-31'
GROUP BY MONTH(START_DATE),
CAR_ID
ORDER BY MONTH(START_DATE), CAR_ID DESC
단일 테이블에서 데이터를 추출하기 때문에 크게 복잡한 문제는 아니었지만,
SQL에서
GROUP BY를 사용할 때의 주의사항을 통해
GROUP BY의 역할과 존재 이유를 다시 한 번 짚고 넘어가면서데이터로 의미 있는 결과를 뽑아내기 위한 인사이트를 얻을 수 있었다 :)
해당 문제를 이렇게 해결했지만
혹시 더 효율적으로 풀 수 있는 방법이 있다면 언제든 편하게 알려주길 바란다 😊