본문으로 건너뛰기
김신건의 로그

[SQL] GROUP BY

· 수정 · 📖 약 3분 · 1,111자/단어 #sql #group-by #aggregate #having #window-function #database
SQL GROUP BY, GROUP BY, HAVING, Aggregation, SQL Aggregate, Window Function

정의

GROUP BY 는 데이터를 키별로 그룹으로 나누고 각 그룹에 집계 함수 (aggregate) 를 적용해 그룹당 한 행으로 줄이는 연산. SQL 의 split-apply-combine.

Pandas groupby 와 1:1 대응. 시각화도 동일하게 사용한다.

시각화

기본 골격

SELECT 그룹키, 집계함수(컬럼)
FROM 테이블
[WHERE 조건]
GROUP BY 그룹키
[HAVING 그룹조건]
[ORDER BY ...]

실행 순서는 SQL 의 “쿼리 실행의 논리적 순서” 참고. WHEREGROUP BYHAVINGSELECT 임을 기억.

기본 예제

sql
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 |   2

city 가 같은 행끼리 모이고 각 그룹에서 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 은 집계 후.

sql
-- ❌ 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;
citydeptavg_sal
SeoulA3250.0
SeoulB5000.0
BusanA4000.0
BusanB4500.0

(city, dept) 의 모든 고유 조합당 한 행.

ROLLUP / CUBE / GROUPING SETS

여러 수준의 소계/총계를 한 쿼리로.

SELECT city, dept, SUM(salary)
FROM users
GROUP BY ROLLUP (city, dept);
citydeptsum
SeoulA6500
SeoulB5000
SeoulNULL11500 (city 소계)
BusanA4000
BusanB4500
BusanNULL8500
NULLNULL20000 (총계)
구문의미
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 가 그룹당 한 행, 윈도는 모든 행 유지.

sql
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

기본 대응

SQLpandas
GROUP BY citydf.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, bdf.groupby(['a', 'b'])
ROLLUP/CUBEcrosstab(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 ANALYZEwork_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…

💬 댓글

사이트 검색 / 명령어

검색

스크롤 = 확대/축소 · 드래그 = 이동 · 0 = 원래 크기 · ESC = 닫기