집계와 그룹
집계와 그룹¶
1. 집계 함수 (Aggregate Functions)¶
집계 함수는 여러 행의 값을 하나의 결과로 계산합니다.
| 함수 | 설명 |
|---|---|
COUNT() |
행 개수 |
SUM() |
합계 |
AVG() |
평균 |
MIN() |
최소값 |
MAX() |
최대값 |
2. 실습 테이블 준비¶
CREATE TABLE sales (
id SERIAL PRIMARY KEY,
product VARCHAR(100),
category VARCHAR(50),
amount NUMERIC(10, 2),
quantity INTEGER,
sale_date DATE,
region VARCHAR(50)
);
INSERT INTO sales (product, category, amount, quantity, sale_date, region) VALUES
('노트북', '전자기기', 1500000, 2, '2024-01-05', '서울'),
('마우스', '전자기기', 50000, 10, '2024-01-05', '서울'),
('키보드', '전자기기', 100000, 5, '2024-01-06', '부산'),
('모니터', '전자기기', 300000, 3, '2024-01-07', '서울'),
('책상', '가구', 250000, 2, '2024-01-08', '대전'),
('의자', '가구', 150000, 4, '2024-01-08', '서울'),
('노트북', '전자기기', 1800000, 1, '2024-01-10', '부산'),
('마우스', '전자기기', 45000, 20, '2024-01-12', '대전'),
('책상', '가구', 280000, 1, '2024-01-15', '서울'),
('의자', '가구', 180000, 3, '2024-01-15', '부산');
3. COUNT - 개수 세기¶
전체 행 수¶
SELECT COUNT(*) FROM sales;
-- 10
특정 컬럼 개수 (NULL 제외)¶
SELECT COUNT(region) FROM sales;
-- NULL이 아닌 region 개수
중복 제거 개수¶
SELECT COUNT(DISTINCT category) FROM sales;
-- 2 (전자기기, 가구)
SELECT COUNT(DISTINCT region) FROM sales;
-- 3 (서울, 부산, 대전)
4. SUM - 합계¶
-- 총 매출액
SELECT SUM(amount) FROM sales;
-- 4653000
-- 총 판매 수량
SELECT SUM(quantity) FROM sales;
-- 51
-- 조건부 합계
SELECT SUM(amount) FROM sales WHERE category = '전자기기';
5. AVG - 평균¶
-- 평균 매출액
SELECT AVG(amount) FROM sales;
-- 465300
-- 소수점 처리
SELECT ROUND(AVG(amount), 2) AS avg_amount FROM sales;
-- 조건부 평균
SELECT ROUND(AVG(amount), 2)
FROM sales
WHERE region = '서울';
6. MIN / MAX - 최소/최대¶
-- 최소 매출액
SELECT MIN(amount) FROM sales;
-- 45000
-- 최대 매출액
SELECT MAX(amount) FROM sales;
-- 1800000
-- 가장 최근 판매일
SELECT MAX(sale_date) FROM sales;
-- 가장 오래된 판매일
SELECT MIN(sale_date) FROM sales;
7. 여러 집계 함수 함께 사용¶
SELECT
COUNT(*) AS total_count,
SUM(amount) AS total_sales,
ROUND(AVG(amount), 2) AS avg_sales,
MIN(amount) AS min_sales,
MAX(amount) AS max_sales,
SUM(quantity) AS total_quantity
FROM sales;
8. GROUP BY - 그룹화¶
데이터를 특정 컬럼 기준으로 그룹화하여 집계합니다.
기본 GROUP BY¶
-- 카테고리별 매출
SELECT
category,
COUNT(*) AS count,
SUM(amount) AS total_amount
FROM sales
GROUP BY category;
결과:
category │ count │ total_amount
──────────┼───────┼──────────────
전자기기 │ 6 │ 3795000
가구 │ 4 │ 858000
지역별 매출¶
SELECT
region,
COUNT(*) AS sales_count,
SUM(amount) AS total_amount,
ROUND(AVG(amount), 2) AS avg_amount
FROM sales
GROUP BY region
ORDER BY total_amount DESC;
상품별 매출¶
SELECT
product,
SUM(quantity) AS total_qty,
SUM(amount) AS total_sales
FROM sales
GROUP BY product
ORDER BY total_sales DESC;
9. 다중 컬럼 GROUP BY¶
-- 카테고리 + 지역별 매출
SELECT
category,
region,
COUNT(*) AS count,
SUM(amount) AS total
FROM sales
GROUP BY category, region
ORDER BY category, region;
결과:
category │ region │ count │ total
──────────┼────────┼───────┼─────────
가구 │ 대전 │ 1 │ 250000
가구 │ 부산 │ 1 │ 180000
가구 │ 서울 │ 2 │ 430000
전자기기 │ 대전 │ 1 │ 45000
전자기기 │ 부산 │ 2 │ 1900000
전자기기 │ 서울 │ 3 │ 1850000
10. HAVING - 그룹 필터링¶
WHERE는 그룹화 전, HAVING은 그룹화 후 필터링합니다.
-- 총 매출 50만원 이상인 카테고리만
SELECT
category,
SUM(amount) AS total_amount
FROM sales
GROUP BY category
HAVING SUM(amount) >= 500000;
WHERE + HAVING¶
-- 서울, 부산 지역에서 총 매출 100만원 이상인 상품
SELECT
product,
SUM(amount) AS total_amount
FROM sales
WHERE region IN ('서울', '부산') -- 그룹화 전 필터
GROUP BY product
HAVING SUM(amount) >= 1000000 -- 그룹화 후 필터
ORDER BY total_amount DESC;
HAVING에서 별칭 사용 (PostgreSQL)¶
-- PostgreSQL은 HAVING에서 별칭 사용 가능
SELECT
product,
SUM(amount) AS total
FROM sales
GROUP BY product
HAVING SUM(amount) > 500000; -- 표준 방식
-- 또는 (PostgreSQL 확장)
-- HAVING total > 500000; -- 일부 버전에서만 동작
11. GROUP BY + JOIN¶
-- 준비: 카테고리 테이블
CREATE TABLE categories (
id SERIAL PRIMARY KEY,
name VARCHAR(50),
description TEXT
);
INSERT INTO categories (name, description) VALUES
('전자기기', '전자 제품'),
('가구', '가구 제품');
-- 카테고리 정보와 함께 집계
SELECT
c.name AS category,
c.description,
COUNT(s.id) AS sales_count,
SUM(s.amount) AS total_sales
FROM categories c
LEFT JOIN sales s ON c.name = s.category
GROUP BY c.id, c.name, c.description;
12. 날짜별 집계¶
일별 매출¶
SELECT
sale_date,
COUNT(*) AS count,
SUM(amount) AS daily_total
FROM sales
GROUP BY sale_date
ORDER BY sale_date;
월별 매출¶
SELECT
DATE_TRUNC('month', sale_date) AS month,
COUNT(*) AS count,
SUM(amount) AS monthly_total
FROM sales
GROUP BY DATE_TRUNC('month', sale_date)
ORDER BY month;
연도별 매출¶
SELECT
EXTRACT(YEAR FROM sale_date) AS year,
SUM(amount) AS yearly_total
FROM sales
GROUP BY EXTRACT(YEAR FROM sale_date);
13. 조건부 집계¶
CASE + SUM¶
SELECT
SUM(CASE WHEN category = '전자기기' THEN amount ELSE 0 END) AS electronics,
SUM(CASE WHEN category = '가구' THEN amount ELSE 0 END) AS furniture
FROM sales;
FILTER (PostgreSQL 9.4+)¶
SELECT
COUNT(*) FILTER (WHERE category = '전자기기') AS electronics_count,
COUNT(*) FILTER (WHERE category = '가구') AS furniture_count,
SUM(amount) FILTER (WHERE region = '서울') AS seoul_sales
FROM sales;
14. ROLLUP과 CUBE¶
ROLLUP - 소계 추가¶
SELECT
category,
region,
SUM(amount) AS total
FROM sales
GROUP BY ROLLUP (category, region)
ORDER BY category NULLS LAST, region NULLS LAST;
결과:
category │ region │ total
──────────┼────────┼──────────
가구 │ 대전 │ 250000
가구 │ 부산 │ 180000
가구 │ 서울 │ 430000
가구 │ NULL │ 860000 ← 가구 소계
전자기기 │ 대전 │ 45000
전자기기 │ 부산 │ 1900000
전자기기 │ 서울 │ 1850000
전자기기 │ NULL │ 3795000 ← 전자기기 소계
NULL │ NULL │ 4655000 ← 총계
CUBE - 모든 조합의 소계¶
SELECT
category,
region,
SUM(amount) AS total
FROM sales
GROUP BY CUBE (category, region)
ORDER BY category NULLS LAST, region NULLS LAST;
GROUPING - NULL 구분¶
SELECT
CASE WHEN GROUPING(category) = 1 THEN '전체' ELSE category END AS category,
CASE WHEN GROUPING(region) = 1 THEN '전체' ELSE region END AS region,
SUM(amount) AS total
FROM sales
GROUP BY ROLLUP (category, region);
15. 실습 예제¶
실습 1: 기본 집계¶
-- 1. 전체 매출 통계
SELECT
COUNT(*) AS 총_판매건수,
SUM(amount) AS 총_매출,
ROUND(AVG(amount), 0) AS 평균_매출,
MIN(amount) AS 최소_매출,
MAX(amount) AS 최대_매출
FROM sales;
-- 2. 카테고리별 판매 통계
SELECT
category AS 카테고리,
COUNT(*) AS 판매건수,
SUM(quantity) AS 총_수량,
SUM(amount) AS 총_매출,
ROUND(AVG(amount), 0) AS 평균_매출
FROM sales
GROUP BY category
ORDER BY 총_매출 DESC;
실습 2: 복합 조건¶
-- 1. 지역별 매출 (50만원 이상만)
SELECT
region,
SUM(amount) AS total
FROM sales
GROUP BY region
HAVING SUM(amount) >= 500000
ORDER BY total DESC;
-- 2. 상품별 판매 수량 랭킹
SELECT
product,
SUM(quantity) AS total_qty
FROM sales
GROUP BY product
ORDER BY total_qty DESC
LIMIT 5;
실습 3: 날짜 집계¶
-- 1. 일별 매출 추이
SELECT
sale_date,
SUM(amount) AS daily_sales,
SUM(SUM(amount)) OVER (ORDER BY sale_date) AS cumulative_sales
FROM sales
GROUP BY sale_date
ORDER BY sale_date;
-- 2. 최근 7일 일평균 매출
SELECT
ROUND(AVG(daily_total), 2) AS avg_daily_sales
FROM (
SELECT sale_date, SUM(amount) AS daily_total
FROM sales
WHERE sale_date >= CURRENT_DATE - INTERVAL '7 days'
GROUP BY sale_date
) daily;
실습 4: 크로스탭 (피벗)¶
-- 카테고리 × 지역 매출 크로스탭
SELECT
category,
SUM(amount) FILTER (WHERE region = '서울') AS 서울,
SUM(amount) FILTER (WHERE region = '부산') AS 부산,
SUM(amount) FILTER (WHERE region = '대전') AS 대전,
SUM(amount) AS 총계
FROM sales
GROUP BY category;
결과:
category │ 서울 │ 부산 │ 대전 │ 총계
──────────┼─────────┼─────────┼───────┼──────────
가구 │ 430000 │ 180000 │ 250000│ 860000
전자기기 │ 1850000 │ 1900000 │ 45000│ 3795000
16. 쿼리 실행 순서¶
FROM / JOIN ← 테이블 지정
↓
WHERE ← 행 필터링
↓
GROUP BY ← 그룹화
↓
HAVING ← 그룹 필터링
↓
SELECT ← 컬럼 선택
↓
DISTINCT ← 중복 제거
↓
ORDER BY ← 정렬
↓
LIMIT/OFFSET ← 결과 제한
다음 단계¶
08_Subqueries_and_CTE.md에서 서브쿼리와 WITH 절을 배워봅시다!