에러 없이 조용히 틀리는,
GROUP BY 세 가지 함정
결과는 멀쩡히 나오는데 의미가 틀린 SQL — GROUP BY 함정은 에러를 안 내서 더 위험합니다. 컬럼 누락, 집계 단위 오류, 비중 분모 실수. AI가 자주 빠지는 세 가지를 직접 진단하고 고치는 법을 익힙니다.
이번 섹션의 여정
완료 ✓
JOIN 뻥튀기 · NULL ✓
지금 여기
섹션 6 종합
실무 주의사항
MySQL의 너그러움이 함정이 됩니다
3-4 GROUP BY에서 이런 규칙을 배웠죠 — “GROUP BY에 쓴 컬럼 외에는 SELECT에 올릴 수 없다.” 그런데 사실, MySQL은 설정에 따라 이걸 허용합니다. 다른 DB들은 전부 에러를 내는데 말이죠.
| DB | 규칙 위반 시 동작 |
|---|---|
MySQL — ONLY_FULL_GROUP_BY 꺼짐 | 허용 — 그룹 안 임의 값 반환 ⚠️ |
| MySQL 5.7+ 기본값 — 모드 켜짐 | 에러 |
| PostgreSQL · Oracle · SQL Server | 에러 |
MySQL만 너그럽고 — 그 너그러움이 위험합니다. 에러가 났다면 바로 고쳤을 텐데, 결과가 그냥 나와버리니까요. 결과는 나오는데 의미가 모호한 SQL — AI가 만든 쿼리에서 이 함정이 어떻게 들어오는지, 오늘 세 가지 패턴으로 봅니다.
2-1에서 설치한 MySQL 8은 기본적으로 ONLY_FULL_GROUP_BY 모드가 켜져
있어서, 함정 1의 SQL을 치면 Error 1055가 납니다. DB가 함정을 막아주는
거예요. 문제는 실무 — 이 모드를 꺼둔 서버가 아직 많고, 그런 곳에서 AI의
SQL은 에러 없이 조용히 틀립니다.
함정 1 — GROUP BY에 없는 컬럼이 SELECT에 ⭐
요구사항은 이랬습니다 — “카테고리별 매출이랑, 그 카테고리의 대표 상품 1개를 같이 보여줘.” AI가 만들어준 SQL이 이겁니다.
-- ❌ AI가 만든 SQL
SELECT
c.category_name,
p.product_name, -- ⚠️ GROUP BY에 안 들어감
SUM(p.price * oi.quantity) AS 매출
FROM
categories c
JOIN products p
ON c.category_id = p.category_id
JOIN order_items oi
ON p.product_id = oi.product_id
GROUP BY
c.category_id,
c.category_name;SELECT에는 p.product_name이 있는데 — GROUP BY에는 없습니다. 카테고리로 묶어놓고, 묶이지 않은 상품 이름을 꺼내달라는 모순이죠. 이걸 너그러운 서버에서 실행하면 —
| category_name | product_name | 매출 |
|---|---|---|
| 의류 | 라운드 티셔츠 | 1107000 |
| 전자기기 | 무선 이어폰 | 857000 |
| 식품 | 프리미엄 그래놀라 | 60000 |
| 도서 | SQL 입문서 | 479000 |
| 뷰티 | 수분 크림 | 510000 |
MySQL은 이때 그 카테고리 안 임의의 한 상품 이름을 반환합니다. “대표 상품” 같은 보장은 전혀 없어요. 더 고약한 건 — 매출 숫자는 정확하다는 것. 숫자가 맞으니 상품 이름도 맞겠거니 하고 넘어가게 됩니다.
에러가 안 납니다. 결과도 그럴듯합니다. 그래서 틀린 줄 모르고 — 그대로 보고서에 들어갑니다.
고치는 방법은 두 가지입니다. 첫째, 대표 상품이 굳이 필요 없다면 빼는 것 — 의미가 단번에 명확해집니다.
-- ✅ 옵션 1: 카테고리별 매출만 — 의미 명확
SELECT
c.category_name,
SUM(p.price * oi.quantity) AS 매출
FROM
categories c
JOIN products p
ON c.category_id = p.category_id
JOIN order_items oi
ON p.product_id = oi.product_id
GROUP BY
c.category_id,
c.category_name;둘째, 진짜 대표 상품(매출 1위)이 필요하다면 — CTE로 단계를 나누고, ROW_NUMBER로 그룹 안 순위를 매깁니다.
-- ✅ 옵션 2: 카테고리별 "진짜 매출 1위 상품" — CTE 단계 분리
WITH category_top AS (
SELECT
c.category_id,
c.category_name,
p.product_name,
SUM(p.price * oi.quantity) AS sales,
ROW_NUMBER() OVER (
PARTITION BY c.category_id
ORDER BY SUM(p.price * oi.quantity) DESC
) AS rn
FROM
categories c
JOIN products p
ON c.category_id = p.category_id
JOIN order_items oi
ON p.product_id = oi.product_id
GROUP BY
c.category_id,
c.category_name,
p.product_id,
p.product_name
)
SELECT
category_name,
product_name AS 대표상품,
sales AS 매출
FROM
category_top
WHERE
rn = 1;| category_name | 대표상품 | 매출 |
|---|---|---|
| 의류 | 패딩 점퍼 | 645000 |
| 전자기기 | 무선 이어폰 | 534000 |
| 식품 | 프리미엄 그래놀라 | 60000 |
| 도서 | 데이터 모델링 실전 | 192000 |
| 뷰티 | 수분 크림 | 256000 |
ROW_NUMBER는 처음 보죠? 그룹 안에서 순위를 매기는 윈도우 함수입니다. 깊이 파는 건 나중으로 미루고 — 지금은 공식 하나만 기억하세요. *“각 그룹의 대표 한 행”*이 필요하면, 윈도우 함수 또는 단계 분리(CTE). 5-4의 단계별 사고법 그대로입니다.
함정 2 — 집계 단위가 의도와 다릅니다
이번 요구사항 — “월별 매출을 보여줘.” AI가 이렇게 답했습니다.
-- ❌ AI가 만든 SQL — "월별"이라고 했는데
SELECT
o.ordered_at AS 일자,
SUM(p.price * oi.quantity) AS 매출
FROM
orders o
JOIN order_items oi
ON o.order_id = oi.order_id
JOIN products p
ON oi.product_id = p.product_id
GROUP BY
o.ordered_at;| 일자 | 매출 |
|---|---|
| 2025-02-01 | 43000 |
| 2025-02-15 | 24000 |
| 2025-03-01 | 114000 |
| ⋮ | |
에러도 없고 숫자도 다 맞습니다. 그런데 단위가 틀렸어요 — ordered_at은 하루하루의 날짜라서, 이걸로 묶으면 일별입니다.
GROUP BY ordered_at — 날짜 단위
ordered_at은 하루 단위의 날짜 값입니다. 날짜가 다르면
다른 그룹 — 그래서 주문 날짜 수만큼 40행이 나옵니다. 요구는
“월별”이었는데요.
DATE_FORMAT — 월 단위로 변환
DATE_FORMAT(ordered_at, ‘%Y-%m’)은 날짜를
2025-02 형태로 바꿉니다. 같은 달의 모든 날짜가 같은 값
이 되어 — 한 그룹으로 묶입니다.
-- ✅ 월별 매출
SELECT
DATE_FORMAT(o.ordered_at, '%Y-%m') AS 월,
SUM(p.price * oi.quantity) AS 매출
FROM
orders o
JOIN order_items oi
ON o.order_id = oi.order_id
JOIN products p
ON oi.product_id = p.product_id
GROUP BY
월
ORDER BY
월;| 월 | 매출 |
|---|---|
| 2025-02 | 67000 |
| 2025-03 | 171000 |
| 2025-04 | 207000 |
| ⋮ | |
| 2026-04 | 56000 |
같은 요령으로, 의도한 단위에 맞는 표현을 GROUP BY에 넣으면 됩니다.
| 의도한 단위 | GROUP BY에 쓸 표현 |
|---|---|
| 연도별 | YEAR(ordered_at) 또는 DATE_FORMAT(.., '%Y') |
| 월별 | DATE_FORMAT(.., '%Y-%m') |
| 주별 | YEARWEEK(ordered_at) |
| 일별 | DATE(ordered_at) — DATETIME 타입일 때. DATE 타입이면 그대로 |
| 요일별 | DAYNAME(ordered_at) |
| 시간대별 | HOUR(ordered_at) — DATETIME 타입일 때 |
결과를 받으면 행 수부터 세어보세요. 우리 데이터에서 “월별”이면 15행 안팎이어야 정상인데 40행이 나왔다 — 그 순간 단위 오류를 잡은 겁니다. AI에게 한국어로 *“월별로 묶어줘”*라고 해도 종종 일별로 묶어옵니다. 숫자가 아니라 단위를 의심하세요.
함정 3 — 비중 계산, 분모가 자기 자신
마지막은 한 단계 미묘한 함정입니다 — “카테고리별 매출이랑, 전체 매출 대비 비중도 같이” 같은 비율 계산에서 자주 나옵니다.
-- ❌ 잘못된 시도 — 비중이 전부 1.0
SELECT
c.category_name,
SUM(p.price * oi.quantity) AS 매출,
SUM(p.price * oi.quantity) / SUM(p.price * oi.quantity) AS 비중
FROM
categories c
JOIN products p
ON c.category_id = p.category_id
JOIN order_items oi
ON p.product_id = oi.product_id
GROUP BY
c.category_id,
c.category_name;| category_name | 매출 | 비중 |
|---|---|---|
| 의류 | 1107000 | 1.0000 |
| 전자기기 | 857000 | 1.0000 |
| 식품 | 60000 | 1.0000 |
| 도서 | 479000 | 1.0000 |
| 뷰티 | 510000 | 1.0000 |
왜 이럴까요? GROUP BY가 적용된 SQL에서 SUM(...)은 항상 그 그룹 안에서 계산됩니다. 분자도 “이 그룹의 합”, 분모도 “이 그룹의 합” — 자기 자신을 자기 자신으로 나누니 전부 1.0이죠. 분모는 그룹 밖의 전체 합이어야 합니다. 서브쿼리로 명시하면 —
-- ✅ 분모 = 그룹 밖의 "전체 매출" (서브쿼리로 명시)
SELECT
c.category_name,
SUM(p.price * oi.quantity) AS 매출,
SUM(p.price * oi.quantity)
/ (
SELECT
SUM(p2.price * oi2.quantity)
FROM
products p2
JOIN order_items oi2
ON p2.product_id = oi2.product_id
) AS 비중
FROM
categories c
JOIN products p
ON c.category_id = p.category_id
JOIN order_items oi
ON p.product_id = oi.product_id
GROUP BY
c.category_id,
c.category_name
ORDER BY
매출 DESC;| category_name | 매출 | 비중 |
|---|---|---|
| 의류 | 1107000 | 0.3674 |
| 전자기기 | 857000 | 0.2844 |
| 뷰티 | 510000 | 0.1693 |
| 도서 | 479000 | 0.1590 |
| 식품 | 60000 | 0.0199 |
이제 *“카테고리 매출 ÷ 전체 매출”*이라는 의도대로 계산됩니다. 검증법도 간단해요 — 비중을 전부 더해서 1.0이 되는지 보면 됩니다.
같은 계산을 서브쿼리 없이 SUM(SUM(...)) OVER () 같은 윈도우 함수로도 쓸
수 있습니다. 함정 1의 ROW_NUMBER와 같은 가족이에요. 깊이는 나중에 — 지금은
*“그룹 밖의 값이 필요하면 서브쿼리 또는 윈도우 함수”*라는 방향만 기억하면
충분합니다.
AI에게 GROUP BY 검증 시키기
세 함정 모두 — 사람이 매번 눈으로 잡기는 어렵습니다. 6-3에서 했던 것처럼, AI에게 자기 SQL을 자체 점검시키는 프롬프트를 받자마자 던지세요. 그대로 복사해서 쓰면 됩니다.
방금 만든 SQL — GROUP BY 관점에서 점검해줘.
1. SELECT 절의 모든 비집계 컬럼이 GROUP BY에 다 들어있어?
2. GROUP BY의 단위가 내가 의도한 단위(월별, 카테고리별 등)와 같아?
(월별을 원했는데 일별로 묶지 않았어?)
3. 비율·비중 계산이 있다면 — 분모가 "전체 합"으로 정확히 들어갔어?
위 3가지 점검 후, 문제가 있으면 SQL을 수정해줘.1번이 함정 1(컬럼 누락), 2번이 함정 2(집계 단위), 3번이 함정 3(비중 분모)입니다. 이 한 메시지로 — 오늘 배운 검증을 AI가 대신 수행합니다.
한눈에 정리
| # | 함정 | 증상 | 잡는 법 |
|---|---|---|---|
| 1 | GROUP BY에 없는 컬럼이 SELECT에 | 임의 값 반환 — 에러 없음 ⚠️ | GROUP BY에 추가 · 집계 함수로 감싸기 · CTE 단계 분리 |
| 2 | 집계 단위 오류 | 월별인데 일별 행 수 | DATE_FORMAT · YEAR 등으로 단위 명시 |
| 3 | 비중·비율 분모 오류 | 모든 비중이 1.0 | 서브쿼리로 전체 합 명시 (또는 윈도우 함수) |
GROUP BY의 결과는 “한 그룹 = 한 행”. SELECT에 올리는 모든 것이 그 그룹의 정체와 모호함 없이 맞아떨어져야 합니다.
자주 막히는 지점
MySQL 8 기본 설치는 ONLY_FULL_GROUP_BY 모드가 켜져 있어서, GROUP BY에 없는
컬럼을 SELECT하면 에러를 냅니다.
함정 2의 수정 SQL이 GROUP BY 월처럼 SELECT의 별칭을 재사용했죠 — MySQL의
편의 기능입니다.
DATE_FORMAT(...) 식을 GROUP BY에 그대로 쓰는 게 안전합니다.이번 강의의 옵션 2는 미리보기에 가깝습니다 — 못 따라가도 진도에 지장 없습니다.
GROUP BY 함정 셋 — 직접 잡아봐야 진짜 내 것이 됩니다. 연습 문제로 점검하고 나면, 섹션 6의 마지막이자 종합인 6-5 검증 체크리스트 + 디버깅 실습이 기다립니다.