AI 시대 DB 모델링 & SQL 완벽 마스터6-2 JOIN 뻥튀기 심화 ⭐
테크타니 LEARN — 섹션 06 · AI 결과 의심하기 ★

AI가 가장 자주 틀리는 그 SQL,
3단계 검산으로 평생 잡는다

3-7에서 만났던 JOIN 뻥튀기 함정 — AI가 가장 자주 만드는 잘못된 SQL이 정확히 이 함정입니다. COUNT가 부풀고 평균이 3배 작아지는 두 사례를 해부하고, 의심되는 결과를 1분 만에 판별하는 3단계 검산 습관을 손에 넣습니다.

난이도심화 ⭐소요10분 + 실습준비물3-7 수강 · shop DB도구DBeaver + AI 과외 선생님

이번 섹션의 여정

6-1 AI 틀리는 패턴 TOP 5

완료 ✓

6-2 JOIN 뻥튀기 심화 ⭐

지금 여기

6-3 ~ 6-4

NULL · GROUP BY

6-5 검증 체크리스트 ★

디버깅 루틴

섹션 7 — 마무리

강의 완주

다시 그 함정 — AI가 가장 자주 빠집니다

3-7에서 봤던 JOIN 뻥튀기 함정, 기억나시죠? 주문상세까지 JOIN했더니 김철수의 주문 수가 6건에서 10건으로 부풀어 올랐던 그 사례입니다. 이번 강의에서 그 함정을 다시 꺼내는 이유는 하나예요 — AI가 가장 자주 만드는 잘못된 SQL이 정확히 이 패턴이기 때문입니다.

이 강의가 섹션 6의 ⭐인 이유

문법 에러는 차라리 다행입니다 — 실행이 안 되니 바로 알아채요. JOIN 뻥튀기는 다릅니다. 실행도 잘 되고, 숫자도 그럴듯하게 나오는데, 틀렸습니다. 이런 SQL을 잡는 눈이 이 강의의 목표입니다.

시작 전 체크
3-7 JOIN 뻥튀기 함정 수강 (복습 베이스)
shop DB 적재 완료 — 2-3
AI 과외 선생님 세션 — 2-4

사례 1 — COUNT 뻥튀기 (복습)

요구사항은 “회원별 주문 횟수”. 아무 컨텍스트 없이 AI에게 던졌더니 이런 SQL이 돌아왔습니다.

-- AI가 만든 SQL — 문법은 깔끔하지만…
SELECT
  m.member_name,
  COUNT(*) AS 주문수
FROM
  members m
JOIN orders o
  ON m.member_id = o.member_id
JOIN order_items oi
  ON o.order_id = oi.order_id
JOIN products p
  ON oi.product_id = p.product_id
GROUP BY
  m.member_id,
  m.member_name
ORDER BY
  주문수 DESC;

문법적으로 깔끔하고, 실행도 잘 됩니다. 근데 — 주문 수가 부풀어 옵니다.

DBeaver — AI가 만든 SQL 실행 결과
SELECT m.member_name, COUNT(*) AS 주문수 FROM members m JOIN … ;
member_name주문수
김철수10
조성민8
오지은7
이영희6
김철수의 진짜 주문은 6건 → 10으로 뻥튀기 (조성민도 4건 → 8)

원인은 3-7 그대로입니다. order_items까지 JOIN하는 순간, 한 주문이 그 안에 든 상품 가짓수만큼 행으로 펼쳐지고, COUNT(*)는 주문이 아니라 이 펼쳐진 행을 셉니다.

주문 1건이 3행으로 펼쳐지는 순간
orders order_items 와 JOIN 하면
┌──────────────────┐ order_id │ product_name
│ 14번 주문 (김철수) │ ──▶ ─────────┼─────────────────────
└──────────────────┘ 14 │ SQL 입문서
책 3권을 담은 주문 14 │ 데이터 모델링 실전
14 │ AI 시대 개발자
 
주문은 1건 — 행은 3개. COUNT(*)는 3을 셉니다.

여기까지는 복습입니다. 원리를 다시 보고 싶으면 3-7 JOIN 뻥튀기 함정으로. 오늘은 한 발 더 들어갑니다 — 집계값 자체가 틀어지는, 더 위험한 사례.

사례 2 — 평균이 3배 작게 나오는 AVG 함정 ⭐

이번 요구사항은 “회원별 평균 주문액” — 한 주문당 평균 얼마를 쓰는지. AI의 답은 이랬습니다.

-- AI가 만든 (잘못된) SQL — 숫자가 그럴듯해서 더 위험합니다
SELECT
  m.member_name,
  AVG(p.price * oi.quantity) AS 평균_주문액
FROM
  members m
JOIN orders o
  ON m.member_id = o.member_id
JOIN order_items oi
  ON o.order_id = oi.order_id
JOIN products p
  ON oi.product_id = p.product_id
GROUP BY
  m.member_id,
  m.member_name;

이게 이 강의에서 가장 위험한 사례입니다. 사례 1은 그래도 “10건? 좀 많은데?” 하고 걸러낼 여지가 있는데, 이번엔 결과 숫자가 그럴듯하게 나오거든요. 무엇이 어긋났는지 김철수의 14번 주문(책 3권)으로 뜯어봅니다.

✕ AI가 계산한 것 — 행 단위 평균

주문상세 행별 금액의 평균

14번 주문은 JOIN 후 3행으로 펼쳐집니다. AVG(price × quantity)는 24,000 / 32,000 / 28,000 — 이 행 3개를 평균 내서 28,000원이라고 답합니다.

✓ 질문의 진짜 의미 — 주문 단위 평균

주문별 총액을 구한 뒤, 그 평균

14번 주문의 진짜 총액은 24,000 + 32,000 + 28,000 = 84,000원. “한 주문당 얼마”를 물었으니 — 평균의 재료는 이 주문 총액이어야 합니다.

같은 주문인데 28,000원 vs 84,000원 — 3배 작게 나옵니다. 결과 표만 보면 “오, 김철수가 한 주문에 평균 3만원도 안 쓰는구나” 하고 그대로 보고서에 올라가요. 사고입니다. 실제 shop DB에서 두 방식을 돌려 김철수 행을 비교하면 —

같은 질문, 다른 답 — 김철수 행 비교
-- ✕ AI의 SQL: 펼쳐진 행 10개로 나눈 평균
김철수 │ 49800.0000
 
-- ✓ 진짜 답: 주문 6건으로 나눈 평균
김철수 │ 83000.0000
 
분자(총 구매액 498,000원)는 같은데 — 분모가 행 10개 vs 주문 6건

진짜 답은 단계를 나누는 것입니다. 먼저 주문별 총액을 구하고 → 그 위에 평균. 5-4의 CTE 사고법 그대로예요.

-- ✓ 진짜 답 — 먼저 주문별 총액, 그 위에 평균
WITH order_totals AS (
  SELECT
    o.order_id,
    o.member_id,
    SUM(p.price * oi.quantity) AS order_total
  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.order_id,
    o.member_id
)
SELECT
  m.member_name,
  AVG(ot.order_total) AS 평균_주문액
FROM
  members m
JOIN order_totals ot
  ON m.member_id = ot.member_id
GROUP BY
  m.member_id,
  m.member_name;
틀린 숫자보다 무서운 건 — 그럴듯한 숫자

에러가 나면 누구나 알아챕니다. 하지만 49,800원은 표만 봐서는 절대 못 잡아요. JOIN과 집계가 한 SQL에 같이 들어가면, 집계의 단위가 무엇인지부터 의심 — 그래서 다음의 검산 습관이 필요합니다.

3단계 검산 습관 — 이것만 외우세요 ⭐

이런 사고를 막는 체계적인 검산법입니다. 순서대로 세 번 — 그게 전부예요.

JOIN 결과 행 수 확인01 / 03

집계를 붙이기 전, JOIN만 한 결과의 행 수부터 셉니다. 가장 자식 테이블(우리 DB에선 order_items 64행)과 같으면 정상 — 다르면 어딘가 N:M이 풀려 카테시안 곱이 생긴 겁니다.

COUNT(*) = 가장 자식 테이블 행 수?
DISTINCT 로 집계 단위 확인02 / 03

결과가 “회원별” 이라면 회원 수와 맞아야죠. 주문 있는 회원은 16명 — COUNT(DISTINCT m.member_id)16이 나와야 합니다.

COUNT(DISTINCT …) = 의도한 단위 수?
단순 집계로 교차 검증03 / 03

JOIN 없이, 한 명만 콕 집어 다시 구해봅니다. WHERE member_id = 1 한 줄짜리 SQL의 답(6건)과 메인 쿼리의 김철수 행이 일치해야 정상 — 다르면 그 차이가 곧 뻥튀기의 증거.

가장 단순한 SQL로 같은 값을 다시

세 검산을 직접 쳐보세요. 전부 한 줄짜리입니다.

-- 검산 1) JOIN 결과 행 수 — 가장 자식 테이블(order_items 64행)과 같은가
SELECT
  COUNT(*)
FROM
  members m
JOIN orders o
  ON m.member_id = o.member_id
JOIN order_items oi
  ON o.order_id = oi.order_id
JOIN products p
  ON oi.product_id = p.product_id;
 
-- 검산 2) 집계 단위 — 주문 있는 회원이 정말 16명인가
SELECT
  COUNT(DISTINCT m.member_id)
FROM
  members m
JOIN orders o
  ON m.member_id = o.member_id;
 
-- 검산 3) 단순 집계 교차 검증 — 김철수의 진짜 주문 수
SELECT
  COUNT(*)
FROM
  orders
WHERE
  member_id = 1;
DBeaver — 3단계 검산 실행
SELECT
COUNT(*)
FROM
members m
JOIN orders o
JOIN order_items oi
JOIN products p …;
→ 64 (order_items 행 수와 일치 — 카테시안 곱 없음)
 
SELECT
COUNT(DISTINCT m.member_id)
FROM
members m
JOIN orders o …;
→ 16 (주문 있는 회원 수와 일치 — 집계 단위 정상)
 
SELECT
COUNT(*)
FROM
orders
WHERE
member_id = 1;
→ 6 (김철수의 진짜 주문 수)
 
메인 쿼리의 김철수 행이 6과 다르다? — 그 차이가 뻥튀기의 증거
3단계 모두 통과 → 이제 결과를 믿어도 됩니다
매번 다 할 필요는 없습니다

모든 SQL마다 3단계를 도는 게 아니에요. JOIN과 집계가 같이 들어간, 의심되는 결과만. 1분 투자로 보고서 사고 1시간을 막는 겁니다.

검산도 AI에게 시키세요

AI에게 SQL을 받고 결과를 확인했다면 — 바로 후속 메시지 하나를 보냅니다. 검산 SQL을 본인이 짜는 게 아니라, AI에게 시키는 거예요.

방금 만든 SQL — 결과를 검산하고 싶어.

1. 이 JOIN 결과의 총 행 수가, 가장 자식 테이블(order_items 64행)과
   같은지 확인하는 SQL도 짜줘.

2. 김철수(member_id=1)의 진짜 주문 수를 — JOIN 없이
   단순 SELECT 만으로 구하는 SQL도 줘.

이 두 결과를 비교해서, 메인 쿼리가 뻥튀기 없는지 확인하고 싶어.

이 한 메시지면 AI가 검산 SQL 2개를 정리해 줍니다. 여러분이 할 일은 두 숫자를 비교해서 판단하는 것뿐이에요.

검산 분업의 원칙

검산 SQL은 AI가 짜고 — 판단은 사람이 한다. AI가 만든 SQL을 AI가 만든 검산으로 교차 확인하는 것, 이게 섹션 6 전체를 관통하는 협업 패턴입니다.

한눈에 정리

단계무엇을어떻게
1JOIN 결과 행 수 점검COUNT(*) 가 가장 자식 테이블 행 수와 같은지
2집계 단위 검증COUNT(DISTINCT 의도한_단위) 가 기대값과 같은지
3 ⭐단순 집계 교차 검증가장 단순한 SQL로 같은 값을 다시 — 한 명만 콕 집어
+AI와 분업”검산 SQL도 같이 짜줘” 후속 메시지
섹션 6의 제 1 원칙

JOIN + 집계 = 일단 의심. 의심 = 3단계 검산. 이 두 문장만 외우면 — AI가 가장 자주 틀리는 패턴 1번은 평생 잡을 수 있습니다.

자주 막히는 지점

① 검산 1의 COUNT가 64보다 훨씬 크게 나와요

64의 몇 배씩 나온다면 JOIN 조건 문제입니다. AI가 ON 절 컬럼을 잘못 짚는 일도 흔해요 — 6-1의 패턴 4(환각 컬럼)와 같이 나타나기도 합니다.

FIX ▸JOIN … ON 절이 부모 PK = 자식 FK 쌍인지 확인 — ON 조건이 빠졌거나 엉뚱한 컬럼이면 카테시안 곱이 폭발합니다.
② 어떤 게 '가장 자식 테이블'인지 모르겠어요

JOIN 체인에서 행을 가장 잘게 쪼개는 테이블이 기준입니다. JOIN 결과 행 수는 이 테이블의 행 수를 따라가는 게 정상이에요.

FIX ▸FK 화살표를 따라 1:N의 N쪽 끝까지 — 우리 DB에선 members → orders → order_items 라인의 끝, order_items 입니다.
③ 검산 2의 DISTINCT가 16이 아니라 20이 나와요

INNER JOIN은 주문 있는 회원만(16명), LEFT JOIN은 전체 회원(20명)을 셉니다. 어느 쪽도 문법 오류는 아니고 — 질문의 의도가 무엇이냐의 문제. 이 NULL 이야기는 바로 다음 6-3에서 본격적으로 다룹니다.

FIX ▸LEFT JOIN 을 썼다면 주문 없는 회원 4명까지 포함돼 20 — 먼저 의도한 단위가 무엇인지부터 정하세요.

뻥튀기 사례 두 개와 3단계 검산 — 오늘 배운 건 이게 전부지만, 섹션 6에서 가장 자주 써먹게 될 무기입니다. 연습 문제로 손에 익히고 넘어가세요.

NEXT LESSON6-2 연습 문제

뻥튀기가 숨은 SQL을 직접 찾아내고, 3단계 검산으로 증거를 잡아봅니다.

GO ▸ 연습 문제 풀기