GROUP BY 와 집계 — COUNT·SUM·AVG·HAVING
"그룹별 통계" 의 정석과 HAVING 의 정확한 자리.
"카테고리별 상품 수" "월별 매출" "사용자별 최근 주문 횟수" — 거의 모든 대시보드 쿼리가 GROUP BY + 집계 함수입니다. 8편은 5개 집계 함수, 여러 컬럼 그룹화, 그리고 자주 혼동되는 HAVING vs WHERE 의 정확한 자리를 정리합니다.
5가지 집계 함수
| 함수 | 의미 | NULL 처리 |
|---|---|---|
| COUNT(*) | 전체 행 수 | NULL 포함 |
| COUNT(col) | col 이 NULL 아닌 행 수 | NULL 제외 |
| COUNT(DISTINCT col) | 고유값 개수 | NULL 제외 |
| SUM(col) | 합계 | NULL 무시 |
| AVG(col) | 평균 | NULL 무시 (분모에서도 제외) |
| MIN(col) / MAX(col) | 최소·최대 | NULL 무시 |
SELECT COUNT(*) AS total_orders,
COUNT(coupon_code) AS used_coupon,
SUM(total) AS revenue,
AVG(total)::numeric(10,2) AS avg_ticket,
MIN(created_at) AS first_order,
MAX(created_at) AS last_order
FROM orders;
NULL + AVG 의 미묘함. AVG(rating) 은 NULL 행을 분자·분모 모두에서 제외합니다. 만약 "평가 안 한 사람은 0점으로 치고 평균" 을 원한다면 AVG(COALESCE(rating, 0)) 처럼 명시. 분기·분석에서 자주 빠지는 함정.
GROUP BY — 그룹별 집계
-- 카테고리별 상품 수
SELECT category_id, COUNT(*) AS n
FROM products
GROUP BY category_id;
-- 여러 컬럼 (cross-tab 시작)
SELECT region, status, COUNT(*) AS n, SUM(total) AS rev
FROM orders
GROUP BY region, status;
-- 표현식 그룹화
SELECT date_trunc('month', created_at) AS month, COUNT(*) AS n
FROM orders
GROUP BY date_trunc('month', created_at)
ORDER BY month;
SQL 의 황금 규칙. SELECT 절에 등장하는 컬럼은 ① GROUP BY 에 있거나, ② 집계 함수 안에 있거나 둘 중 하나여야 합니다. PostgreSQL 은 이 규칙을 엄격히 지킵니다(MySQL 옛 버전은 느슨해서 헷갈리는 결과가 났음).
-- ❌ 에러 — name 은 GROUP BY 도, 집계 안도 아님
SELECT category_id, name, COUNT(*)
FROM products
GROUP BY category_id;
-- ✅ name 을 GROUP BY 에 추가
SELECT category_id, name, COUNT(*)
FROM products
GROUP BY category_id, name;
HAVING — 그룹 결과에 필터
-- 상품이 10개 이상인 카테고리만
SELECT category_id, COUNT(*) AS n
FROM products
GROUP BY category_id
HAVING COUNT(*) >= 10;
-- 평균 주문 금액이 50K 이상인 지역
SELECT region, AVG(total) AS avg_t
FROM orders
GROUP BY region
HAVING AVG(total) >= 50000;
HAVING vs WHERE — 가장 자주 헷갈리는 한 가지
한 줄 차이.
· WHERE — 그룹화 전에 행에 필터. 집계 함수 사용 불가.
· HAVING — 그룹화 후에 그룹에 필터. 집계 함수 사용 가능.
-- 2026년 주문만 가져와서 (WHERE) → 지역별 합계 (GROUP) → 합계 100만 이상만 (HAVING)
SELECT region, SUM(total) AS rev
FROM orders
WHERE created_at >= '2026-01-01'
GROUP BY region
HAVING SUM(total) >= 1000000
ORDER BY rev DESC;
WHERE 가 먼저 행 수를 줄이는 게 거의 항상 더 빠릅니다. 가능하면 WHERE 로 먼저 좁히고, HAVING 은 그룹 단위 필터에만.
FILTER — 조건부 집계 (PostgreSQL 확장)
-- 한 쿼리로 여러 조건의 카운트
SELECT
COUNT(*) AS total,
COUNT(*) FILTER (WHERE status = 'paid') AS paid,
COUNT(*) FILTER (WHERE status = 'done') AS done,
SUM(total) FILTER (WHERE region = 'KR') AS rev_kr
FROM orders;
표준 SQL 의 FILTER (WHERE ...) 구문. CASE 식 (SUM(CASE WHEN x THEN 1 ELSE 0 END)) 보다 훨씬 가독성 좋고, MySQL 안 됨/Postgres 됨.
GROUPING SETS · ROLLUP · CUBE — 다차원 집계
-- 부분합·총합을 한 쿼리로
SELECT region, category, SUM(total)
FROM orders
GROUP BY ROLLUP (region, category);
-- (region, category) 별 + (region) 별 + 총합 한 번에
OLAP 스타일 쿼리. 대시보드의 합계 행·총합 행을 한 번에 얻을 때 유용. 자주 안 써도, 알아두면 BI 쿼리가 가벼워집니다.
9편 — JOIN 4종 (INNER·LEFT·RIGHT·FULL)
관계형의 진가. 두 테이블을 연결하는 4가지와 카티시안 곱의 함정.
이전: 7편 ORDER BY · 현재: 8편 (SQL 기초) · 다음 → 9편 JOIN · 진행: 8/24