집계 함수는 데이터를 그룹화하고 요약할 때 사용하는 SQL이다. 통계, 리포트, 대시보드, 매출 분석, 사용자 활동 분석 등 거의 모든 실무 쿼리에 사용된다.
집계 함수
여러 행의 값을 하나의 결과로 요약하는 함수이다.
| 함수 | 설명 |
|---|
COUNT() | 행의 개수 |
SUM() | 합계 |
AVG() | 평균 |
MAX() | 최대값 |
MIN() | 최소값 |
이 함수들은 일반적으로 GROUP BY와 함께 사용하여 카테고리별 집계를 수행한다.
예제
CREATE TABLE orders
(
id INT PRIMARY KEY,
user_id INT,
product VARCHAR(100),
amount DECIMAL(10, 2),
order_date DATE
);
INSERT INTO orders
VALUES (1, 1, 'Keyboard', 50.00, '2024-01-01'),
(2, 1, 'Mouse', 20.00, '2024-01-03'),
(3, 2, 'Monitor', 200.00, '2024-01-05'),
(4, 1, 'Chair', 150.00, '2024-02-10'),
(5, 2, 'Desk', 300.00, '2024-02-15');
COUNT() - 행 개수 세기
SELECT user_id, COUNT(*) AS order_count
FROM orders
GROUP BY user_id;
사용자 별 주문 횟수를 검색하는 쿼리문이다.
SUM() - 총합 구하기
SELECT user_id, SUM(amount) AS total_spent
FROM orders
GROUP BY user_id;
사용자 별 지출액을 집계하는 쿼리문이다.
| user_id | total_spent |
|---|
| 1 | 220.00 |
| 2 | 500.00 |
AVG() - 평균 구하기
SELECT user_id, AVG(amount) AS avg_order
FROM orders
GROUP BY user_id;
사용자별 평균 주문 금액을 집계하는 쿼리문이다.
| user_id | avg_order |
|---|
| 1 | 73.33 |
| 2 | 250.00 |
MAX(), MIN()
SELECT user_id, MAX(amount) AS max_order, MIN(amount) AS min_order
FROM orders
GROUP BY user_id;
사용자별 최고 및 최소 주문 금액을 집계하는 쿼리문이다.
| user_id | max_order | min_order |
|---|
| 1 | 150.00 | 20.00 |
| 2 | 300.00 | 200.00 |
날짜 기준 그룹화
SELECT MONTH(order_date) AS month, SUM(amount) AS monthly_sales
FROM orders
GROUP BY MONTH(order_date);
월별 매출을 집계하는 쿼리문이다.
| month | monthly_sales |
|---|
| 1 | 270.00 |
| 2 | 450.00 |
조건부 집계
SELECT user_id, COUNT(*) AS jan_orders
FROM orders
WHERE MONTH(order_date) = 1
GROUP BY user_id;
1월에 주문한 사용자만 집계하는 쿼리문이다.
HAVING으로 그룹 조건 필터링
SELECT user_id, SUM(amount) AS total_spent
FROM orders
GROUP BY user_id
HAVING total_spent > 300;
총 주문액이 300을 초과한 사용자를 집계하는 쿼리문이다.
| user_id | total_spent |
|---|
| 2 | 500.00 |
- WHERE은 행 필터링
- HAVING은 그룹 필터링
상황 별 알맞은 집계 방식
| 상황 | 추천 |
|---|
| 전체 건수 확인 | SELECT COUNT(*) FROM table; |
| 필드의 null 제외 카운트 | COUNT(column) |
| 월/연도별 요약 | GROUP BY MONTH(date) or YEAR(date) |
| 다중 그룹핑 | GROUP BY user_id, MONTH(order_date) |
| 정렬 | ORDER BY SUM(amount) DESC 등과 함께 사용 |
고급 집계
SQL에서는 ROLLUP과 CUBE라는 고급 집계 보고서가 존재한다.
| 기능 | 설명 | MySQL 지원 여부 |
|---|
ROLLUP | 계층적 요약 (소계, 총계) | 지원 |
CUBE | 다차원 조합 요약 | MySQL은 직접 미지원 (우회 가능) |
예제
CREATE TABLE cafe_orders
(
주문월 VARCHAR(7),
지점명 VARCHAR(20),
메뉴 VARCHAR(50),
수량 INT
);
INSERT INTO cafe_orders
VALUES ('2024-01', '강남점', '아메리카노', 100),
('2024-01', '강남점', '라떼', 50),
('2024-01', '홍대점', '아메리카노', 80),
('2024-02', '강남점', '아메리카노', 120),
('2024-02', '홍대점', '라떼', 60);
ROLLUP
SELECT 주문월, 지점명, SUM(수량) AS 총수량
FROM cafe_orders
GROUP BY 주문월, 지점명
WITH ROLLUP;
이처럼 쿼리문을 날리면 아래와 같은 결과가 나온다.
| 주문월 | 지점명 | 총수량 |
|---|
| 2024-01 | 강남점 | 150 |
| 2024-01 | 홍대점 | 80 |
| 2024-01 | NULL | 230 ← 1월 총합 |
| 2024-02 | 강남점 | 120 |
| 2024-02 | 홍대점 | 60 |
| 2024-02 | NULL | 180 ← 2월 총합 |
| NULL | NULL | 410 ← 전체 총합 |
NULL은 요약 행을 의미한다.
ROLLUP은 왼쪽부터 순차적으로 소계하며 층계를 자동으로 생성한다.
CUBE
MySQL에서는 CUBE를 지원하지 않기 때문에 우회해야 한다. 주로 UNION ALL + GROUP BY조합으로 구현한다.
SELECT 주문월, 지점명, SUM(수량) AS 총수량
FROM cafe_orders
GROUP BY 주문월, 지점명
UNION ALL
SELECT 주문월, NULL AS 지점명, SUM(수량) AS 총수량
FROM cafe_orders
GROUP BY 주문월
UNION ALL
SELECT NULL AS 주문월, 지점명, SUM(수량) AS 총수량
FROM cafe_orders
GROUP BY 지점명
UNION ALL
SELECT NULL AS 주문월, NULL AS 지점명, SUM(수량) AS 총수량
FROM cafe_orders;
이렇게 조회하면 마지막 전체 합계의 경우 아래와 같은 결과가 나온다.
| 주문월 | 지점명 | 총수량 |
|---|
| 2024-01 | 강남점 | 150 |
| 2024-01 | 홍대점 | 80 |
| 2024-02 | 강남점 | 120 |
| 2024-02 | 홍대점 | 60 |
| 2024-01 | NULL | 230 (1월 소계) |
| 2024-02 | NULL | 180 (2월 소계) |
| NULL | 강남점 | 270 (강남점 소계) |
| NULL | 홍대점 | 140 (홍대점 소계) |
| NULL | NULL | 410 (전체 총합) |
각 행의 의미는 아래와 같다.
| 조합 | 의미 |
|---|
| (주문월, 지점명) | 기본 상세 집계 |
| (주문월, NULL) | 해당 월 전체 소계 |
| (NULL, 지점명) | 해당 지점 전체 소계 |
| (NULL, NULL) | 전체 총합 |
윈도우 함수
윈도우 함수는 집계 함수처럼 작동하지만 그룹핑 없이 각 행에 결과를 붙인다. 이는 GROUP BY없이도 누적합, 순위, 이동 평균 등을 계산할 수 있다는 의미이다.
| 함수 | 설명 |
|---|
ROW_NUMBER() | 순번 (중복 없음) |
RANK() | 순위 (동점 허용) |
DENSE_RANK() | 밀집 순위 (순위 건너뛰지 않음) |
SUM() OVER() | 누적합 |
AVG() OVER() | 누적 평균 |
LAG() / LEAD() | 이전값 / 다음값 참조 |
예제
CREATE TABLE sales
(
판매일자 DATE,
직원명 VARCHAR(20),
매출액 INT
);
INSERT INTO sales
VALUES ('2024-06-01', '홍길동', 200),
('2024-06-02', '홍길동', 300),
('2024-06-03', '홍길동', 250),
('2024-06-01', '김철수', 150),
('2024-06-02', '김철수', 400),
('2024-06-03', '김철수', 350);
누적합, SUM() OVER
SELECT 직원명,
판매일자,
매출액,
SUM(매출액) OVER (PARTITION BY 직원명 ORDER BY 판매일자) AS 누적매출
FROM sales;
| 직원명 | 판매일자 | 매출액 | 누적매출 |
|---|
| 김철수 | 2024-06-01 | 150 | 150 |
| 김철수 | 2024-06-02 | 400 | 550 |
| 김철수 | 2024-06-03 | 350 | 900 |
| 홍길동 | 2024-06-01 | 200 | 200 |
| 홍길동 | 2024-06-02 | 300 | 500 |
| 홍길동 | 2024-06-03 | 250 | 750 |
PARTITION BY는 직원 별로 누적된다.
ORGER BY는 날짜순으로 누적된다.
순위 함수, RANK
SELECT 직원명,
판매일자,
매출액,
RANK() OVER (PARTITION BY 직원명 ORDER BY 매출액 DESC) AS 순위
FROM sales;
| 직원명 | 매출액 | 순위 |
|---|
| 김철수 | 400 | 1 |
| 김철수 | 350 | 2 |
| 김철수 | 150 | 3 |
| 홍길동 | 300 | 1 |
| 홍길동 | 250 | 2 |
| 홍길동 | 200 | 3 |
이전 행 참조, LAG()
SELECT 직원명,
판매일자,
매출액,
LAG(매출액) OVER (PARTITION BY 직원명 ORDER BY 판매일자) AS 전날매출
FROM sales;
| 직원명 | 판매일자 | 매출액 | 전날매출 |
|---|
| 김철수 | 2024-06-01 | 150 | NULL |
| 김철수 | 2024-06-02 | 400 | 150 |
| 김철수 | 2024-06-03 | 350 | 400 |
| 홍길동 | 2024-06-01 | 200 | NULL |
| 홍길동 | 2024-06-02 | 300 | 200 |
| 홍길동 | 2024-06-03 | 250 | 300 |