원칙 5 + 6 — GROUP BY + HAVING
원칙 5 + 6 — GROUP BY + HAVING
묶어서 집계하고, 집계 후 조건을 걸자.
박지수 팀장 : 민서씨, 상품명별로 총 판매금액이 얼마인지 뽑아줄 수 있어요? 그리고 그 중에서 총 판매금액이 500,000원 이상인 것만요.
김민서 사원 : 상품명별로 묶어서... WHERE로는 안 될 것 같고. 각 상품마다 금액을 더하는 건데, SQL에서 그게 되나요?
박지수 팀장 : 돼요. 그게 GROUP BY예요. 그리고 집계한 결과에 조건을 거는 건 HAVING이고요. WHERE가 행을 걸러낸다면, HAVING은 집계 결과를 걸러요.
지금까지는 있는 데이터를 그대로 가져왔습니다. 이번 챕터부터는 데이터를 묶어서 계산합니다. 데이터를 묶는 것은 데이터 분석의 기본이자 비즈니스의 결과를 보는데 가장 기본적인 뷰이기 때문에 여러 뷰를 추출해보시고 익숙해지시길 바랍니다.
이런 결과를 원할 때
상품명별 총 판매금액
| 상품명 | 총판매금액 |
|---|---|
| 노트북 Pro | 3,000,000 |
| 노트북 Air | 2,400,000 |
| 모니터 32인치 | 550,000 |
| 모니터 27인치 | 350,000 |
| ... | ... |
그 중 총판매금액이 500,000원 이상인 것만
| 상품명 | 총판매금액 |
|---|---|
| 노트북 Pro | 3,000,000 |
| 노트북 Air | 2,400,000 |
| 모니터 32인치 | 550,000 |
위는 GROUP BY로, 아래는 HAVING을 더해서 만듭니다.
GROUP BY가 하는 일
GROUP BY는 같은 값을 가진 행들을 하나의 그룹으로 묶습니다. 그리고 그 그룹마다 집계 함수로 계산합니다.
SELECT 묶을열, 집계함수(열)
FROM 테이블이름
GROUP BY 묶을열;
집계 함수는 다섯 가지입니다.
| 함수 | 의미 | 예시 |
|---|---|---|
| COUNT(*) | 행 개수 | 주문 건수 |
| SUM(열) | 합계 | 총 판매금액 |
| AVG(열) | 평균 | 평균 단가 |
| MAX(열) | 최댓값 | 가장 높은 단가 |
| MIN(열) | 최솟값 | 가장 낮은 단가 |
💡 GROUP BY를 쓸 때 핵심 규칙
SELECT에 쓸 수 있는 것은 두 가지뿐입니다.
GROUP BY에 쓴 열- 집계 함수
이 두 가지 외의 열을 SELECT에 쓰면 에러가 납니다.
STEP 1 — 상품명별로 묶어서 세어보자
가장 기본적인 집계입니다. 상품명별로 몇 건씩 팔렸는지 셉니다.
SELECT 상품명, COUNT(*) AS 판매건수
FROM 판매정보
GROUP BY 상품명;
실행 결과
| 상품명 | 판매건수 |
|---|---|
| 노트북 Pro | 2 |
| 무선 키보드 | 2 |
| 마우스 패드 | 2 |
| 모니터 27인치 | 1 |
| USB 허브 | 1 |
| 노트북 Air | 2 |
| 모니터 32인치 | 1 |
| 웹캠 HD | 1 |
12건의 판매 데이터가 상품명별 8행으로 줄었습니다.
STEP 2 — 상품명별 총 판매금액을 구하자
SUM으로 그룹별 합계를 냅니다.
SELECT 상품명, SUM(단가) AS 총판매금액
FROM 판매정보
GROUP BY 상품명;
실행 결과
| 상품명 | 총판매금액 |
|---|---|
| 노트북 Pro | 3,000,000 |
| 무선 키보드 | 90,000 |
| 마우스 패드 | 30,000 |
| 모니터 27인치 | 350,000 |
| USB 허브 | 25,000 |
| 노트북 Air | 2,400,000 |
| 모니터 32인치 | 550,000 |
| 웹캠 HD | 89,000 |
- 노트북 Pro : 1,500,000 × 2건 = 3,000,000
- 마우스 패드 : 15,000 × 2건 = 30,000
- 노트북 Air : 1,200,000 × 2건 = 2,400,000
그룹별로 단가를 더한 결과입니다.
STEP 3 — 집계 함수를 여러 개 써보자
한 번에 여러 집계를 함께 볼 수 있습니다.
SELECT 상품명,
COUNT(*) AS 판매건수,
SUM(단가) AS 총판매금액,
AVG(단가) AS 평균단가,
MAX(단가) AS 최고단가,
MIN(단가) AS 최저단가
FROM 판매정보
GROUP BY 상품명;
실행 결과
| 상품명 | 판매건수 | 총판매금액 | 평균단가 | 최고단가 | 최저단가 |
|---|---|---|---|---|---|
| 노트북 Pro | 2 | 3,000,000 | 1,500,000 | 1,500,000 | 1,500,000 |
| 무선 키보드 | 2 | 90,000 | 45,000 | 45,000 | 45,000 |
| 마우스 패드 | 2 | 30,000 | 15,000 | 15,000 | 15,000 |
| 모니터 27인치 | 1 | 350,000 | 350,000 | 350,000 | 350,000 |
| USB 허브 | 1 | 25,000 | 25,000 | 25,000 | 25,000 |
| 노트북 Air | 2 | 2,400,000 | 1,200,000 | 1,200,000 | 1,200,000 |
| 모니터 32인치 | 1 | 550,000 | 550,000 | 550,000 | 550,000 |
| 웹캠 HD | 1 | 89,000 | 89,000 | 89,000 | 89,000 |
한 쿼리로 상품명별 전체 판매 현황이 나왔습니다.
고객정보 테이블에서도 같은 방식으로 씁니다.
SELECT 등급,
COUNT(*) AS 고객수,
AVG(나이) AS 평균나이
FROM 고객정보
GROUP BY 등급;
실행 결과
| 등급 | 고객수 | 평균나이 |
|---|---|---|
| NULL | 2 | 43 |
| VIP | 3 | 30.5 |
| 일반 | 3 | 31 |
| 프리미엄 | 2 | 29 |
등급별로 고객 수와 평균 나이를 한눈에 볼 수 있습니다. AVG는 NULL 값을 자동으로 제외하고 계산합니다.
STEP 4 — WHERE와 GROUP BY를 함께 쓰자
WHERE는 GROUP BY 이전에 실행됩니다. 먼저 행을 걸러내고, 그 결과를 묶어서 집계합니다.
SELECT 상품명,
COUNT(*) AS 판매건수,
SUM(단가) AS 총판매금액
FROM 판매정보
WHERE 고객ID IN (1, 2, 3)
GROUP BY 상품명;
실행 결과
| 상품명 | 판매건수 | 총판매금액 |
|---|---|---|
| 노트북 Pro | 2 | 3,000,000 |
| 마우스 패드 | 1 | 15,000 |
| 무선 키보드 | 1 | 45,000 |
| 모니터 27인치 | 1 | 350,000 |
전체 12건 중 고객ID가 1, 2, 3인 5건만 먼저 걸러낸 뒤, 그 5건을 상품명별로 묶어 집계했습니다.
💡 WHERE와 GROUP BY의 실행 순서
- FROM — 테이블을 가져온다
- WHERE — 행을 먼저 걸러낸다
- GROUP BY — 걸러진 행을 묶는다
- SELECT — 묶인 결과에서 열을 고른다
WHERE는 집계 전에, 행 단위로 조건을 겁니다. 집계 후에 조건을 걸고 싶다면 HAVING을 씁니다.
STEP 5 — GROUP BY + ORDER BY를 함께 쓰자
집계 결과를 정렬하고 싶을 때는 ORDER BY를 뒤에 붙입니다.
SELECT 상품명,
COUNT(*) AS 판매건수,
SUM(단가) AS 총판매금액
FROM 판매정보
GROUP BY 상품명
ORDER BY 총판매금액 DESC;
실행 결과
| 상품명 | 판매건수 | 총판매금액 |
|---|---|---|
| 노트북 Pro | 2 | 3,000,000 |
| 노트북 Air | 2 | 2,400,000 |
| 모니터 32인치 | 1 | 550,000 |
| 모니터 27인치 | 1 | 350,000 |
| 무선 키보드 | 2 | 90,000 |
| 웹캠 HD | 1 | 89,000 |
| 마우스 패드 | 2 | 30,000 |
| USB 허브 | 1 | 25,000 |
ORDER BY에서 AS로 만든 이름(총판매금액)을 쓸 수 있는 이유는, ORDER BY가 SELECT 이후에 실행되기 때문입니다.
HAVING이 하는 일
GROUP BY로 집계한 결과에 조건을 걸고 싶을 때 HAVING을 씁니다.
SELECT 묶을열, 집계함수(열)
FROM 테이블이름
GROUP BY 묶을열
HAVING 집계조건;
WHERE는 행에 조건을 겁니다. HAVING은 집계 결과에 조건을 겁니다.
STEP 6 — 집계 결과를 걸러내자
총 판매금액이 500,000원 이상인 상품만 보고 싶습니다.
SELECT 상품명,
SUM(단가) AS 총판매금액
FROM 판매정보
GROUP BY 상품명
HAVING SUM(단가) >= 500000;
실행 결과
| 상품명 | 총판매금액 |
|---|---|
| 노트북 Pro | 3,000,000 |
| 노트북 Air | 2,400,000 |
| 모니터 32인치 | 550,000 |
나머지 상품들은 총판매금액이 500,000원 미만이라 조건을 만족하지 않아 제외됐습니다.
💡 HAVING에서 별칭 사용 주의
HAVING은 SELECT보다 먼저 실행됩니다. 그래서 AS로 만든 별칭을 HAVING에서 쓰면 안 됩니다.
-- ❌
SELECT 상품명, SUM(단가) AS 총판매금액
FROM 판매정보
GROUP BY 상품명
HAVING 총판매금액 >= 500000;
-- ✅
SELECT 상품명, SUM(단가) AS 총판매금액
FROM 판매정보
GROUP BY 상품명
HAVING SUM(단가) >= 500000;
STEP 7 — 판매건수로 걸러내자
COUNT 결과에도 HAVING을 쓸 수 있습니다.
SELECT 상품명, COUNT(*) AS 판매건수
FROM 판매정보
GROUP BY 상품명
HAVING COUNT(*) >= 2;
실행 결과
| 상품명 | 판매건수 |
|---|---|
| 노트북 Pro | 2 |
| 무선 키보드 | 2 |
| 마우스 패드 | 2 |
| 노트북 Air | 2 |
1건만 팔린 모니터 27인치, USB 허브, 모니터 32인치, 웹캠 HD가 제외됐습니다.
STEP 8 — 모든 원칙을 합치자
지금까지 배운 원칙을 모두 한 쿼리에 씁니다.
SELECT 상품명,
COUNT(*) AS 판매건수,
SUM(단가) AS 총판매금액
FROM 판매정보
WHERE 고객ID IN (1, 2, 3, 4, 5)
GROUP BY 상품명
HAVING SUM(단가) >= 100000
ORDER BY 총판매금액 DESC
LIMIT 2;
실행 결과
| 상품명 | 판매건수 | 총판매금액 |
|---|---|---|
| 노트북 Pro | 2 | 3,000,000 |
| 노트북 Air | 1 | 1,200,000 |
이 쿼리는 다음 순서로 실행됩니다.
FROM— 판매정보 테이블을 가져온다WHERE— 고객ID가 1~5인 행만 남긴다GROUP BY— 상품명별로 묶는다HAVING— 총판매금액이 100,000원 이상인 그룹만 남긴다SELECT— 상품명, 판매건수, 총판매금액 열을 고른다ORDER BY— 총판매금액 내림차순으로 정렬한다LIMIT— 상위 2개만 가져온다
WHERE vs HAVING — 결정적 차이
둘은 헷갈리기 쉽습니다. 딱 하나만 기억하면 됩니다.
WHERE→ 집계 전, 행에 조건HAVING→ 집계 후, 그룹에 조건
예시로 비교합니다.
-- WHERE: 집계 전에 15,000원짜리를 제외하고 집계
SELECT 상품명, SUM(단가) AS 총판매금액
FROM 판매정보
WHERE 단가 != 15000
GROUP BY 상품명;
실행 결과
| 상품명 | 총판매금액 |
|---|---|
| 노트북 Pro | 3,000,000 |
| 무선 키보드 | 90,000 |
| 모니터 27인치 | 350,000 |
| USB 허브 | 25,000 |
| 노트북 Air | 2,400,000 |
| 모니터 32인치 | 550,000 |
| 웹캠 HD | 89,000 |
마우스 패드(15,000원)가 WHERE로 모두 제외되어 결과에 없습니다.
-- HAVING: 집계 후에 총판매금액이 500,000원 미만인 그룹을 제외
SELECT 상품명, SUM(단가) AS 총판매금액
FROM 판매정보
GROUP BY 상품명
HAVING SUM(단가) >= 500000;
실행 결과
| 상품명 | 총판매금액 |
|---|---|
| 노트북 Pro | 3,000,000 |
| 노트북 Air | 2,400,000 |
| 모니터 32인치 | 550,000 |
모든 상품이 집계에 포함됐지만, 총판매금액이 500,000원 미만인 그룹은 HAVING으로 제외됐습니다.
자주 하는 실수
실수 1 — SELECT에 GROUP BY에 없는 열을 쓴다
-- ❌
SELECT 상품명, 판매일, SUM(단가)
FROM 판매정보
GROUP BY 상품명;
-- ✅
SELECT 상품명, SUM(단가)
FROM 판매정보
GROUP BY 상품명;
판매일을 함께 보고 싶다면 GROUP BY에도 추가해야 합니다.
SELECT 상품명, 판매일, SUM(단가)
FROM 판매정보
GROUP BY 상품명, 판매일;
실수 2 — WHERE에 집계 함수를 쓴다
-- ❌
SELECT 상품명, SUM(단가)
FROM 판매정보
WHERE SUM(단가) >= 500000
GROUP BY 상품명;
-- ✅
SELECT 상품명, SUM(단가)
FROM 판매정보
GROUP BY 상품명
HAVING SUM(단가) >= 500000;
집계 함수는 WHERE에 쓸 수 없습니다. 집계 결과에 조건을 걸 때는 반드시 HAVING을 씁니다.
실수 3 — HAVING에서 별칭을 쓴다
HAVING은 SELECT 이후에 별칭이 생기기 때문에 사용할 수 없습니다.
-- ❌
SELECT 상품명, SUM(단가) AS 총판매금액
FROM 판매정보
GROUP BY 상품명
HAVING 총판매금액 >= 500000;
-- ✅
SELECT 상품명, SUM(단가) AS 총판매금액
FROM 판매정보
GROUP BY 상품명
HAVING SUM(단가) >= 500000;
개념 정리
집계 함수
| 함수 | 의미 | 예시 |
|---|---|---|
| COUNT(*) | 행 개수 | COUNT(*) AS 판매건수 |
| SUM(열) | 합계 | SUM(단가) AS 총판매금액 |
| AVG(열) | 평균 | AVG(나이) AS 평균나이 |
| MAX(열) | 최댓값 | MAX(단가) AS 최고단가 |
| MIN(열) | 최솟값 | MIN(단가) AS 최저단가 |
GROUP BY / HAVING 전체 구조
| 절 | 역할 | 조건 대상 |
|---|---|---|
| WHERE | 집계 전 필터 | 개별 행 |
| GROUP BY | 묶기 | — |
| HAVING | 집계 후 필터 | 집계 결과 |
전체 작성 순서
SELECT 열, 집계함수
FROM 테이블
WHERE 행 조건
GROUP BY 열
HAVING 집계 조건
ORDER BY 열
LIMIT 개수;
연습문제
Q1. ★☆☆
판매정보 테이블에서 상품명별 평균 단가를 상품명, 평균단가으로 조회하세요.
Q2. ★★☆ 고객정보 테이블에서 지역별 고객 수를 구하되, 고객 수가 많은 순서대로 정렬해서 조회하세요.
Q3. ★★★ 판매정보 테이블에서 상품명별 총판매금액을 구하되, 총판매금액이 500,000원 이상인 상품만, 총판매금액 높은 순서로 조회하세요.
정답 및 해설
Q1 정답
SELECT 상품명,
AVG(단가) AS 평균단가
FROM 판매정보
GROUP BY 상품명;
실행 결과
| 상품명 | 평균단가 |
|---|---|
| 노트북 Pro | 1,500,000 |
| 무선 키보드 | 45,000 |
| 마우스 패드 | 15,000 |
| 모니터 27인치 | 350,000 |
| USB 허브 | 25,000 |
| 노트북 Air | 1,200,000 |
| 모니터 32인치 | 550,000 |
| 웹캠 HD | 89,000 |
이 테이블에서는 같은 상품은 같은 단가라 평균이 단가와 같습니다.
실제 데이터에서는 할인 등으로 금액이 다를 수 있어 AVG가 유용합니다.
Q2 정답
SELECT 지역,
COUNT(*) AS 고객수
FROM 고객정보
GROUP BY 지역
ORDER BY 고객수 DESC;
실행 결과
| 지역 | 고객수 |
|---|---|
| 서울 | 4 |
| 부산 | 2 |
| 대구 | 2 |
| NULL | 2 |
GROUP BY로 지역별로 묶고, ORDER BY로 많은 순서로 정렬했습니다.
ORDER BY에서 별칭 고객수를 쓸 수 있는 이유는 ORDER BY가 SELECT 이후에 실행되기 때문입니다.
Q3 정답
SELECT 상품명,
SUM(단가) AS 총판매금액
FROM 판매정보
GROUP BY 상품명
HAVING SUM(단가) >= 500000
ORDER BY 총판매금액 DESC;
실행 결과
| 상품명 | 총판매금액 |
|---|---|
| 노트북 Pro | 3,000,000 |
| 노트북 Air | 2,400,000 |
| 모니터 32인치 | 550,000 |
나머지 상품들은 총판매금액이 500,000원 미만이라 HAVING 조건에서 제외됩니다.
실무 패턴
패턴 1 — 카테고리별 집계 리포트
마케팅 보고서에서 가장 많이 쓰는 패턴입니다.
SELECT 기준열,
COUNT(*) AS 건수,
SUM(금액열) AS 합계,
AVG(금액열) AS 평균
FROM 테이블이름
GROUP BY 기준열
ORDER BY 합계 DESC;
상품명별, 등급별, 지역별 등 어떤 기준이든 GROUP BY에 기준 열만 바꾸면 같은 구조로 씁니다.
패턴 2 — 조건 + 집계 + 필터 세트
SELECT 기준열,
COUNT(*) AS 건수,
SUM(금액열) AS 합계
FROM 테이블이름
WHERE 날짜열 BETWEEN '2024-01-01' AND '2024-01-31'
GROUP BY 기준열
HAVING COUNT(*) >= 2
ORDER BY 합계 DESC
LIMIT 5;
특정 기간의 데이터를 기준별로 집계하고, 일정 건수 이상인 것만 상위 N개로 뽑는 패턴입니다.
마무리
처음에 암호처럼 보였던 코드는 이제 한 줄씩 읽힙니다.
다시 보는 프롤로그 쿼리
SELECT 상품명,
COUNT(*) AS 판매건수,
SUM(단가) AS 총판매금액
FROM 판매정보
WHERE 고객ID IN (1, 2, 3, 4, 5)
GROUP BY 상품명
ORDER BY 총판매금액 DESC;
SELECT상품명, 판매건수, 총판매금액 → 상품명별 판매건수와 총판매금액을FROM판매정보 → 판매정보 테이블에서WHERE고객ID IN (1,2,3,4,5) → 고객ID가 1~5인 것만 걸러낸 뒤GROUP BY상품명 → 상품명별로 묶어서 집계하고ORDER BY총판매금액 DESC → 총판매금액이 높은 순서로 정렬한다
SELECT 상품명,
COUNT(*) AS 판매건수,
SUM(단가) AS 총판매금액
FROM 판매정보
GROUP BY 상품명
HAVING COUNT(*) >= 2
ORDER BY 총판매금액 DESC;
SELECT상품명, 판매건수, 총판매금액 → 상품명별 판매건수와 총판매금액을FROM판매정보 → 판매정보 테이블에서GROUP BY상품명 → 상품명별로 묶어서 집계하고HAVINGCOUNT(*) >= 2 → 2건 이상 팔린 상품만 남긴 뒤ORDER BY총판매금액 DESC → 총판매금액이 높은 순서로 정렬한다
처음에 암호처럼 보였던 코드가 이제 한 줄씩 읽힙니다.
김민서 사원 : 결과가 한눈에 다 보여요. 처음엔 뭔지도 몰랐는데, 이제 회사의 현황을 제가 직접 추출해서 쓸 수 있을 것 같아요.
박지수 팀장 : 그게 이 책의 목표였어요. 이제 마지막 챕터가 남았어요. 이 6가지 원칙과 Claude를 합치면 보고서 수준의 대시보드를 바로 만들 수 있어요.
▶ 에필로그 — Claude로 대시보드 완성하기