[SQL] GROUP BY
정의
GROUP BY 는 데이터를 키별로 그룹으로 나누고 각 그룹에 집계 함수 (aggregate) 를 적용해 그룹당 한 행으로 줄이는 연산. SQL 의 split-apply-combine.
Pandas groupby 와 1:1 대응. 시각화도 동일하게 사용한다.
시각화
기본 골격
SELECT 그룹키, 집계함수(컬럼)
FROM 테이블
[WHERE 조건]
GROUP BY 그룹키
[HAVING 그룹조건]
[ORDER BY ...]
실행 순서는 SQL 의 “쿼리 실행의 논리적 순서” 참고. WHERE → GROUP BY → HAVING → SELECT 임을 기억.
기본 예제
SELECT city, AVG(salary) AS avg_salary, COUNT(*) AS cnt
FROM users
GROUP BY city; city | avg_salary | cnt
---------+------------+-----
Seoul | 3833.33 | 3
Busan | 4250.00 | 2city 가 같은 행끼리 모이고 각 그룹에서 AVG(salary), COUNT(*) 가 계산된다.
집계 함수 (Aggregate Functions)
표준 SQL 의 핵심 집계.
| 함수 | 의미 | NULL 처리 |
|---|---|---|
COUNT(*) | 행 수 (NULL 포함) | NULL 행도 셈 |
COUNT(col) | non-NULL 행 수 | NULL 제외 |
COUNT(DISTINCT col) | 고유값 수 | NULL 제외 |
SUM(col), AVG(col) | 합, 평균 | NULL 무시 |
MIN(col), MAX(col) | 극값 | NULL 무시 |
STDDEV(col), VARIANCE(col) | 표준편차, 분산 | NULL 무시 |
BOOL_AND, BOOL_OR (PG) | 모두 참, 하나라도 참 | NULL 무시 |
STRING_AGG(col, sep) (PG) / GROUP_CONCAT(col SEPARATOR sep) (MySQL) | 문자열 합 | NULL 무시 |
ARRAY_AGG(col) (PG) | 배열 합 | NULL 포함 |
JSON_AGG(col) (PG) / JSON_ARRAYAGG(col) (MySQL 8+) | JSON 배열 | dialect 따라 |
NULL 의 묵시적 무시 함정
-- score 컬럼에 NULL 이 섞여 있을 때
SELECT
COUNT(*) AS total, -- NULL 포함
COUNT(score) AS scored, -- NULL 제외
AVG(score) AS avg_score, -- NULL 제외하고 평균
SUM(score) / COUNT(*) AS my_avg -- NULL 도 0 으로 친 평균 (보통 의도와 다름)
FROM exams;
IMPORTANT
AVG 는 분모가 non-NULL 개수. 결측치가 많을 때 의미가 변한다. 의도하면 COALESCE(score, 0) 으로 명시.
HAVING, 집계 조건
WHERE 는 집계 전, HAVING 은 집계 후.
-- ❌ WHERE 에 집계 함수 (에러)
SELECT city, COUNT(*) FROM users
WHERE COUNT(*) > 2
GROUP BY city;
-- ✓ HAVING 이 정답
SELECT city, COUNT(*) AS cnt
FROM users
GROUP BY city
HAVING COUNT(*) > 2;-- 첫 번째: ERROR: aggregate functions are not allowed in WHERE
-- 두 번째:
city | cnt
---------+-----
Seoul | 3-- WHERE + HAVING 같이 쓸 수도
SELECT city, AVG(salary)
FROM users
WHERE active = TRUE -- 그룹 전, active 사용자만
GROUP BY city
HAVING AVG(salary) > 3000; -- 그룹 후, 평균이 큰 city
여러 컬럼 그룹
SELECT city, dept, AVG(salary) AS avg_sal
FROM users
GROUP BY city, dept;
| city | dept | avg_sal |
|---|---|---|
| Seoul | A | 3250.0 |
| Seoul | B | 5000.0 |
| Busan | A | 4000.0 |
| Busan | B | 4500.0 |
(city, dept) 의 모든 고유 조합당 한 행.
ROLLUP / CUBE / GROUPING SETS
여러 수준의 소계/총계를 한 쿼리로.
SELECT city, dept, SUM(salary)
FROM users
GROUP BY ROLLUP (city, dept);
| city | dept | sum |
|---|---|---|
| Seoul | A | 6500 |
| Seoul | B | 5000 |
| Seoul | NULL | 11500 (city 소계) |
| Busan | A | 4000 |
| Busan | B | 4500 |
| Busan | NULL | 8500 |
| NULL | NULL | 20000 (총계) |
| 구문 | 의미 |
|---|---|
ROLLUP (a, b) | (a, b), (a), () |
CUBE (a, b) | (a, b), (a), (b), () |
GROUPING SETS ((a, b), (a)) | 사용자 정의 |
PG/MySQL 8 모두 지원. 분석/OLAP 보고서에 빠르고 깔끔.
GROUP BY 의 SELECT 제약
표준 SQL: SELECT 절에 나오는 비집계 컬럼은 모두 GROUP BY 에 있어야 한다.
-- ❌ 표준 위반: name 이 GROUP BY 에 없음
SELECT city, name, AVG(salary)
FROM users
GROUP BY city;
dialect 차이:
- PostgreSQL: 엄격, 에러.
- MySQL: 기본은 허용 (조용히 임의의 값).
sql_mode=ONLY_FULL_GROUP_BY면 엄격. MySQL 5.7+ 부터 ONLY_FULL_GROUP_BY 가 기본.
해법: 명시적 집계 또는 GROUP BY 에 포함.
SELECT city, MAX(name), AVG(salary) FROM users GROUP BY city; -- 명시적 집계
SELECT city, name, AVG(salary) FROM users GROUP BY city, name; -- 그룹키 추가
DISTINCT vs GROUP BY
같은 결과지만 의미가 다르다.
SELECT DISTINCT city, dept FROM users;
SELECT city, dept FROM users GROUP BY city, dept;
둘 다 같은 결과셋. 옵티마이저는 보통 같은 plan 으로 처리. GROUP BY 는 집계가 따라올 때, DISTINCT 는 단순 중복 제거 의도일 때.
윈도 함수 (Window Functions)
그룹별 집계를 행 단위로 보존하며 계산. GROUP BY 가 그룹당 한 행, 윈도는 모든 행 유지.
SELECT
name,
city,
salary,
AVG(salary) OVER (PARTITION BY city) AS city_avg,
RANK() OVER (PARTITION BY city ORDER BY salary DESC) AS city_rank,
ROW_NUMBER() OVER (ORDER BY salary DESC) AS overall_rank
FROM users; name | city | salary | city_avg | city_rank | overall_rank
---------+-------+--------+----------+-----------+--------------
Alice | Seoul | 3000 | 3833.33 | 3 | 5
Bob | Busan | 4000 | 4250.00 | 2 | 2
Charlie | Seoul | 3500 | 3833.33 | 2 | 3윈도 함수 종류
| 카테고리 | 함수 | 의미 |
|---|---|---|
| 집계 | SUM, AVG, COUNT, … | OVER 가 있으면 윈도 |
| 순위 | ROW_NUMBER() | 1, 2, 3, … (동률 무시) |
| 순위 | RANK() | 1, 2, 2, 4 (동률 공유, 다음 점프) |
| 순위 | DENSE_RANK() | 1, 2, 2, 3 (동률 공유, 점프 없음) |
| 순위 | NTILE(n) | n 분위수 |
| 오프셋 | LAG(col, n), LEAD(col, n) | n 행 전/후 값 |
| 경계 | FIRST_VALUE, LAST_VALUE, NTH_VALUE | 윈도 내 위치 값 |
| 통계 | PERCENT_RANK, CUME_DIST | 누적 분포 |
윈도 프레임 (Frame)
OVER (PARTITION BY ... ORDER BY ... ROWS/RANGE BETWEEN ... AND ...).
-- 7 일 이동평균
SELECT
date,
amount,
AVG(amount) OVER (
ORDER BY date
ROWS BETWEEN 6 PRECEDING AND CURRENT ROW
) AS ma7
FROM daily_sales;
-- 누적합
SELECT
date,
amount,
SUM(amount) OVER (ORDER BY date) AS cumsum
FROM daily_sales;
Pandas rolling / Pandas expanding 와 같은 개념.
pandas 와 GROUP BY
기본 대응
| SQL | pandas |
|---|---|
GROUP BY city | df.groupby('city') |
AVG(salary) | .mean() |
COUNT(*) | .size() |
COUNT(col) | .count() |
COUNT(DISTINCT col) | .nunique() |
SUM(col), MIN(col), MAX(col) | .sum(), .min(), .max() |
HAVING ... | .filter(lambda g: ...) 또는 .agg(...).query(...) |
GROUP BY a, b | df.groupby(['a', 'b']) |
ROLLUP/CUBE | crosstab(margins=True), Pandas pivot_table margins=True |
ROW_NUMBER() OVER (PARTITION BY x ORDER BY y) | df.sort_values('y').groupby('x').cumcount() + 1 |
LAG(col) | df.groupby('x')['col'].shift(1) (Pandas shift) |
AVG(...) OVER (ROWS 6 PRECEDING) | df.rolling(7).mean() |
예시 비교
SELECT
city,
COUNT(*) AS cnt,
AVG(salary) AS avg_sal,
COUNT(DISTINCT dept) AS dept_cnt
FROM users
WHERE active = TRUE
GROUP BY city
HAVING COUNT(*) >= 2
ORDER BY avg_sal DESC;-- 양쪽 모두 동일한 의미와 결과
-- city 별로 active 사용자만 집계, 2명 이상인 도시만, 평균 급여 내림차순함정
1. WHERE 와 HAVING 헷갈림
-- WHERE 는 집계 전, HAVING 은 집계 후
WHERE salary > 3000 -- 개별 행 필터
HAVING AVG(salary) > 3000 -- 그룹 필터
2. COUNT(*) vs COUNT(col)
-- 행 수
SELECT COUNT(*) FROM t;
-- non-NULL 행 수, 종종 의도와 다름
SELECT COUNT(active) FROM t; -- active 가 NULL 인 행은 안 셈
3. COUNT 와 LEFT JOIN
-- 주문 없는 사용자 cnt 가 0 이 아니라 1 이 나오는 함정
SELECT u.name, COUNT(*) AS order_count
FROM users u
LEFT JOIN orders o ON o.user_id = u.id
GROUP BY u.name;
-- COUNT(*) 는 LEFT JOIN 의 NULL 행도 셈 (1)
-- ✓ COUNT(o.id) 가 정답 (NULL 무시)
SELECT u.name, COUNT(o.id) AS order_count
FROM users u
LEFT JOIN orders o ON o.user_id = u.id
GROUP BY u.name;
4. GROUP BY 후 정렬 컬럼
-- SELECT 의 별칭을 ORDER BY 에 쓸 수 있는지는 dialect 마다 다름
SELECT city, AVG(salary) AS avg_sal
FROM users GROUP BY city
ORDER BY avg_sal DESC; -- PG/MySQL 모두 OK
5. 큰 카디널리티 GROUP BY
수억 행 × 그룹키가 수백만 → hash table 폭발. EXPLAIN ANALYZE 로 work_mem (PG) 또는 tmp_table_size (MySQL) 확인. External Merge Sort / Grace Hash 처럼 디스크 spill 발생.
관련 위키
이 글의 용어 (13개)
- [DB] MySQL / InnoDB: clustered index, redo log, MVCCdatabase-internals
- 정의 MySQL + InnoDB (기본 스토리지 엔진) 의 clustered index + redo log 기반 행 기반 RDBMS. 웹 / SaaS 의 가장 흔한 backbon…
- [DB] PostgreSQL: 프로세스 모델, MVCC, WAL, 확장성database-internals
- 정의 PostgreSQL 은 오픈소스 ORDBMS. 1986 UC Berkeley POSTGRES 의 후예. MVCC, 확장 가능 타입, JSONB, full-text searc…
- [Pandas] agg / aggregatepandas
- 정의 는 여러 집계 함수를 한 번에 적용 한다. , , 모두에서 사용 가능. 는 같은 함수의 별칭. 다양한 형태 단일 함수 함수 리스트 <CodeWithOutput languag…
- [Pandas] expanding / ewmpandas
- 정의 외의 윈도우 연산. - : 시작부터 현재까지 누적 윈도우 - : exponentially weighted (지수 가중) expanding <CodeWithOutput lan…
- [Pandas] groupbypandas
- 정의 는 데이터를 그룹으로 나누고 (split), 각 그룹에 함수를 적용 (apply), 결과를 합쳐 (combine) 새 DataFrame 으로 만드는 split-apply-c…
- [Pandas] pivot_tablepandas
- 정의 는 의 확장. 집계 함수를 동반 해 중복 (index, columns) 도 처리. Excel 의 피벗 테이블과 가장 가까운 기능. 기본 <CodeWithOutput lang…
- [Pandas] rolling / expandingpandas
- 정의 - : 고정 크기 슬라이딩 윈도우 (이동 평균 등) - : 시작부터 누적 윈도우 (cumulative) - : 지수 가중 이동 평균 시각화 rolling 기본 <CodeWi…
- [Pandas] shift / diff / pct_changepandas
- 정의 - : 값을 n 칸 밀기 (lag/lead) - : n 칸 차이 (= ) - : 변화율 (= ) 시계열 분석의 기본 도구. 전일 대비 / 전월 대비 / 누적 증가율 등. s…
- [SQL] Data Typessql
- 정의 SQL 의 컬럼은 타입 을 가진다. 가능한 값의 도메인, 가능한 연산, 정렬 순서, 인덱스 종류, 저장 크기가 타입에 따라 결정된다. 같은 의미의 타입도 dialect 마다…
- [SQL] DMLsql
- 정의 DML (Data Manipulation Language) 은 데이터 자체를 조회/삽입/수정/삭제 하는 SQL 의 하위 집합. 일상 쿼리의 99% 가 여기에 속한다. | 명…
- [SQL] JOINsql
- 정의 JOIN 은 두 개 이상의 테이블을 공통 컬럼 (보통 키) 으로 결합해 한 결과셋으로 만드는 연산. SQL 의 관계 모델 핵심. 종류는 5 가지: , , , , . 같은 테…
- External Merge Sortalgorithm
- 정의 External Merge Sort (외부 머지 정렬) 는 데이터가 메모리에 들어가지 않을 때 사용하는 변형. 두 단계로 동작한다. 1. Run 생성: 메모리 크기만큼 청크…
- SQLsql
- 정의 SQL (Structured Query Language) 은 관계형 데이터베이스 (RDBMS) 와 대화하기 위한 선언형 (declarative) 질의 언어. 1970 년 E…
💬 댓글