AI 시대 DB 모델링 & SQL 완벽 마스터6-4 GROUP BY 단위 오류
테크타니 LEARN — 섹션 06 · AI 결과 의심하기

에러 없이 조용히 틀리는,
GROUP BY 세 가지 함정

결과는 멀쩡히 나오는데 의미가 틀린 SQL — GROUP BY 함정은 에러를 안 내서 더 위험합니다. 컬럼 누락, 집계 단위 오류, 비중 분모 실수. AI가 자주 빠지는 세 가지를 직접 진단하고 고치는 법을 익힙니다.

난이도보통소요6분준비물6-3 완료 (NULL 처리)도구DBeaver + AI 과외 선생님

이번 섹션의 여정

6-1 틀리는 패턴 TOP 5

완료 ✓

6-2 ~ 6-3

JOIN 뻥튀기 · NULL ✓

6-4 GROUP BY 단위 오류

지금 여기

6-5 검증 체크리스트 ★

섹션 6 종합

섹션 7 — 마무리

실무 주의사항

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가 만든 쿼리에서 이 함정이 어떻게 들어오는지, 오늘 세 가지 패턴으로 봅니다.

여러분의 MySQL 8에서는 에러가 납니다 — 그게 다행입니다

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에는 없습니다. 카테고리로 묶어놓고, 묶이지 않은 상품 이름을 꺼내달라는 모순이죠. 이걸 너그러운 서버에서 실행하면 —

DBeaver — ONLY_FULL_GROUP_BY가 꺼진 서버라면
-- 위의 AI SQL을 그대로 실행
category_nameproduct_name매출
의류라운드 티셔츠1107000
전자기기무선 이어폰857000
식품프리미엄 그래놀라60000
도서SQL 입문서479000
뷰티수분 크림510000
product_name = 그 카테고리 안 "임의의" 상품 — 실행마다 달라질 수 있음

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;
DBeaver — 옵션 2 실행 결과
category_name대표상품매출
의류패딩 점퍼645000
전자기기무선 이어폰534000
식품프리미엄 그래놀라60000
도서데이터 모델링 실전192000
뷰티수분 크림256000
도서의 진짜 1위는 "데이터 모델링 실전" — 함정 1의 임의 값과 다릅니다
5 rows — 그룹마다 정확히 한 행, 의미도 명확

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;
DBeaver — 실행 결과
일자매출
2025-02-0143000
2025-02-1524000
2025-03-01114000
40 rows — "월별"을 원했는데 주문 날짜별로 40행

에러도 없고 숫자도 다 맞습니다. 그런데 단위가 틀렸어요 — ordered_at은 하루하루의 날짜라서, 이걸로 묶으면 일별입니다.

✕ AI의 SQL

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
  월;
DBeaver — 수정 후 실행 결과
매출
2025-0267000
2025-03171000
2025-04207000
2026-0456000
15 rows — 2025-02부터 2026-04까지 매월 한 행

같은 요령으로, 의도한 단위에 맞는 표현을 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;
DBeaver — 실행 결과
category_name매출비중
의류11070001.0000
전자기기8570001.0000
식품600001.0000
도서4790001.0000
뷰티5100001.0000
모든 비중이 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;
DBeaver — 수정 후 실행 결과
category_name매출비중
의류11070000.3674
전자기기8570000.2844
뷰티5100000.1693
도서4790000.1590
식품600000.0199
비중 합계 ≈ 1.0 — 전체 매출 3,013,000원 대비

이제 *“카테고리 매출 ÷ 전체 매출”*이라는 의도대로 계산됩니다. 검증법도 간단해요 — 비중을 전부 더해서 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가 대신 수행합니다.

한눈에 정리

#함정증상잡는 법
1GROUP BY에 없는 컬럼이 SELECT에임의 값 반환 — 에러 없음 ⚠️GROUP BY에 추가 · 집계 함수로 감싸기 · CTE 단계 분리
2집계 단위 오류월별인데 일별 행 수DATE_FORMAT · YEAR 등으로 단위 명시
3비중·비율 분모 오류모든 비중이 1.0서브쿼리로 전체 합 명시 (또는 윈도우 함수)
✓ 오늘의 한 줄

GROUP BY의 결과는 “한 그룹 = 한 행”. SELECT에 올리는 모든 것이 그 그룹의 정체와 모호함 없이 맞아떨어져야 합니다.

자주 막히는 지점

① 함정 1의 ❌ SQL을 따라 쳤더니 Error 1055가 나요

MySQL 8 기본 설치는 ONLY_FULL_GROUP_BY 모드가 켜져 있어서, GROUP BY에 없는 컬럼을 SELECT하면 에러를 냅니다.

FIX ▸정상입니다 — 여러분의 DB가 함정을 막아준 것. 모드를 일부러 끌 필요 없이, 에러 메시지로 함정을 이해하면 충분합니다.
② GROUP BY 월 — 별칭(alias)으로 묶어도 되나요?

함정 2의 수정 SQL이 GROUP BY 월처럼 SELECT의 별칭을 재사용했죠 — MySQL의 편의 기능입니다.

FIX ▸MySQL에서는 OK. 다만 표준이 아니라 일부 DB에서는 에러 — 다른 DB로 옮길 일이 있으면 DATE_FORMAT(...) 식을 GROUP BY에 그대로 쓰는 게 안전합니다.
③ ROW_NUMBER·CTE가 아직 낯설어요

이번 강의의 옵션 2는 미리보기에 가깝습니다 — 못 따라가도 진도에 지장 없습니다.

FIX ▸지금은 "그룹별 대표 1행 = 윈도우 함수 또는 CTE 단계 분리" 공식만. 옵션 2를 AI 과외 선생님에게 붙여넣고 "한 단계씩 설명해줘"라고 해보세요.

GROUP BY 함정 셋 — 직접 잡아봐야 진짜 내 것이 됩니다. 연습 문제로 점검하고 나면, 섹션 6의 마지막이자 종합인 6-5 검증 체크리스트 + 디버깅 실습이 기다립니다.

NEXT LESSON6-4 연습 문제

오늘 배운 세 함정 — 틀린 SQL을 직접 진단하고 고쳐봅니다.

GO ▸ 연습 문제 풀기