콘텐츠로 이동

그룹과 집계 (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) 또는 임의의 값이 반환된다.

-- 잘못된 예: name이 GROUP BY에 없음
SELECT department, name, COUNT(*) FROM employee GROUP BY department; -- ❌

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;

실행 순서

FROM → WHERE → GROUP BY → HAVING → SELECT → ORDER BY → LIMIT

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 컬럼)으로 중복 제거 후 집계할 수 있다.

-- 실제 주문한 회원 수 (중복 제거)
SELECT COUNT(DISTINCT member_id) AS 주문회원수
FROM orders;

장점

  • DB 레벨에서 집계하므로 애플리케이션 메모리·네트워크 비용 절감
  • 인덱스와 결합 시 대용량 데이터에서도 빠른 처리 가능

단점

  • 복잡한 다중 그룹화는 쿼리 가독성 저하
  • 집계 결과를 또 집계하려면 서브쿼리나 CTE 필요

주의할 점

WHERE vs HAVING 혼동: 집계 함수 조건은 반드시 HAVING 사용. WHERE COUNT(*) > 3은 문법 오류.

: GROUP BY 없이 집계 함수만 쓰면 전체 테이블을 하나의 그룹으로 취급한다.

SELECT COUNT(*), AVG(salary) FROM employee; -- 전체 집계