PostgreSQL 교재 · 8편 / 24편

GROUP BY 와 집계 — COUNT·SUM·AVG·HAVING

"그룹별 통계" 의 정석과 HAVING 의 정확한 자리.

SQL 기초읽는 시간 7분2026-05-17
GROUP BY 가 데이터를 그룹별로 묶고 집계 함수가 합치는 도식

"카테고리별 상품 수" "월별 매출" "사용자별 최근 주문 횟수" — 거의 모든 대시보드 쿼리가 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가지와 카티시안 곱의 함정.

📚 PostgreSQL 배우기 교재
이전: 7편 ORDER BY · 현재: 8편 (SQL 기초) · 다음 → 9편 JOIN · 진행: 8/24

© 2026 주나이테크(주) @JUNAITECH