그룹과 집계 (GROUP BY & Aggregate Functions)
집계(Aggregation): 여러 행을 하나의 요약 값으로 줄이는 연산. GROUP BY로 그룹을 나누고 집계 함수로 각 그룹을 요약한다.
왜 쓰는가?
개별 행이 아닌 "부서별 평균 급여", "날짜별 주문 수" 같은 요약 정보가 필요할 때 사용한다. 집계 없이는 애플리케이션에서 모든 행을 가져와 직접 계산해야 하므로 성능·코드 복잡도가 증가한다.
집계 함수
| 함수 | 설명 | NULL 처리 |
|---|---|---|
COUNT(*) |
전체 행 수 | NULL 포함 |
COUNT(컬럼) |
NULL 제외한 행 수 | NULL 제외 |
SUM(컬럼) |
합계 | NULL 무시 |
AVG(컬럼) |
평균 | NULL 무시 |
MAX(컬럼) |
최댓값 | NULL 무시 |
MIN(컬럼) |
최솟값 | NULL 무시 |
SELECT
COUNT(*) AS 전체_회원수,
COUNT(email) AS 이메일_등록수, -- NULL 제외
AVG(age) AS 평균_나이,
MAX(age) AS 최고_나이,
MIN(age) AS 최저_나이
FROM member;
GROUP BY
특정 컬럼 기준으로 행을 묶어 그룹별 집계를 수행한다.
-- 부서별 인원 수, 평균 급여
SELECT
department,
COUNT(*) AS 인원수,
AVG(salary) AS 평균급여
FROM employee
GROUP BY department;
-- 날짜별·상품별 주문 합계 (다중 컬럼 그룹화)
SELECT
order_date,
product_id,
SUM(quantity) AS 총_수량
FROM orders
GROUP BY order_date, product_id;
주의: SELECT 절에 집계 함수가 아닌 컬럼은 반드시 GROUP BY에 포함되어야 한다. 그렇지 않으면 오류(MySQL strict mode) 또는 임의의 값이 반환된다.
HAVING — 그룹 필터링
WHERE은 행을 필터링하고, HAVING은 그룹화 후 집계 결과를 필터링한다.
| 구분 | 실행 시점 | 집계 함수 사용 |
|---|---|---|
WHERE |
GROUP BY 이전 | 불가 |
HAVING |
GROUP BY 이후 | 가능 |
-- 평균 급여가 300만 원 이상인 부서만 조회
SELECT
department,
AVG(salary) AS 평균급여
FROM employee
GROUP BY department
HAVING AVG(salary) >= 3000000;
-- 주문 3건 이상인 회원
SELECT
member_id,
COUNT(*) AS 주문수
FROM orders
GROUP BY member_id
HAVING COUNT(*) >= 3;
실행 순서
WHERE이 먼저 실행되므로 GROUP BY 이전에 행을 줄여 성능 최적화에 활용한다.
-- 2024년 주문만 대상으로 상품별 집계 (WHERE로 먼저 필터)
SELECT
product_id,
SUM(amount) AS 연간매출
FROM orders
WHERE YEAR(order_date) = 2024 -- 먼저 행을 줄임
GROUP BY product_id
HAVING SUM(amount) >= 1000000
ORDER BY 연간매출 DESC;
언제 쓰는지
| 상황 | 집계 함수 | 선택 기준 |
|---|---|---|
| 전체 행 수 | ✅ COUNT(*) | NULL 포함 카운트 |
| 특정 컬럼 카운트 | ✅ COUNT(col) | NULL 제외 카운트 |
| 중복 제거 후 카운트 | ✅ COUNT(DISTINCT col) | 실제 개수 파악 |
| 합계 계산 | ✅ SUM(col) | 매출, 수량 합산 |
| 평균 계산 | ✅ AVG(col) | 평균값, 중간 기준 |
| 최대/최소 | ✅ MAX/MIN(col) | 범위 파악 |
| 부서별/일자별 | ✅ GROUP BY | 그룹 단위 집계 |
| 집계 결과 필터 | ✅ HAVING | 조건 충족 그룹만 |
특징
집계 함수의 특징
1. COUNT(*) vs COUNT(col) 차이
-- employees: id, name, age (100행, age 중 10개가 NULL)
COUNT(*) -- 100 (모든 행)
COUNT(id) -- 100 (id는 NOT NULL)
COUNT(age) -- 90 (NULL 제외)
COUNT(DISTINCT department) -- 10 (중복 제거)
2. NULL 무시 원칙
-- SUM, AVG, MAX, MIN은 NULL을 무시함
SUM([10, NULL, 20]) = 30 (NULL 스킵)
AVG([10, NULL, 20]) = 15 (10+20)/2, NULL 무시
-- COUNT(col)과 AVG는 행 수가 다를 수 있음
5개 행 중 2개 NULL:
COUNT(col) = 3
AVG(col) = (a + b + c) / 3 -- 3으로 나눔, NULL 무시
3. GROUP BY 없는 집계
-- GROUP BY 없으면 전체 테이블이 하나의 그룹
SELECT COUNT(*), AVG(salary) FROM employee;
-- 결과: 1행 (전체 요약)
-- GROUP BY 있으면 그룹별
SELECT department, AVG(salary) FROM employee GROUP BY department;
-- 결과: N행 (부서별)
4. 실행 순서와 성능
FROM → WHERE → GROUP BY → HAVING → SELECT → ORDER BY → LIMIT
중요: WHERE가 먼저 실행되므로 행을 미리 줄이면 GROUP BY 성능 향상
-- 나쁜 예: 전체 데이터를 GROUP BY
SELECT dept, AVG(salary) FROM employee GROUP BY dept;
-- 좋은 예: WHERE로 먼저 필터
SELECT dept, AVG(salary) FROM employee
WHERE salary > 3000000 -- 먼저 행을 줄임
GROUP BY dept;
5. DISTINCT와 집계
-- 중복 제거 후 집계
COUNT(DISTINCT member_id) -- 실제 주문한 고객 수
SUM(DISTINCT amount) -- 중복된 금액은 1번만 합산
AVG(DISTINCT category_count) -- 카테고리 중복 제거 후 평균
DISTINCT와 집계
COUNT(DISTINCT 컬럼)으로 중복 제거 후 집계할 수 있다.
장점
- DB 레벨에서 집계하므로 애플리케이션 메모리·네트워크 비용 절감
- 인덱스와 결합 시 대용량 데이터에서도 빠른 처리 가능
단점
- 복잡한 다중 그룹화는 쿼리 가독성 저하
- 집계 결과를 또 집계하려면 서브쿼리나 CTE 필요
주의할 점
WHERE vs HAVING 혼동: 집계 함수 조건은 반드시 HAVING 사용. WHERE COUNT(*) > 3은 문법 오류.