AI가 가장 자주 틀리는 그 SQL,
3단계 검산으로 평생 잡는다
3-7에서 만났던 JOIN 뻥튀기 함정 — AI가 가장 자주 만드는 잘못된 SQL이 정확히 이 함정입니다. COUNT가 부풀고 평균이 3배 작아지는 두 사례를 해부하고, 의심되는 결과를 1분 만에 판별하는 3단계 검산 습관을 손에 넣습니다.
이번 섹션의 여정
완료 ✓
지금 여기
NULL · GROUP BY
디버깅 루틴
강의 완주
다시 그 함정 — AI가 가장 자주 빠집니다
3-7에서 봤던 JOIN 뻥튀기 함정, 기억나시죠? 주문상세까지 JOIN했더니 김철수의 주문 수가 6건에서 10건으로 부풀어 올랐던 그 사례입니다. 이번 강의에서 그 함정을 다시 꺼내는 이유는 하나예요 — AI가 가장 자주 만드는 잘못된 SQL이 정확히 이 패턴이기 때문입니다.
문법 에러는 차라리 다행입니다 — 실행이 안 되니 바로 알아채요. JOIN 뻥튀기는 다릅니다. 실행도 잘 되고, 숫자도 그럴듯하게 나오는데, 틀렸습니다. 이런 SQL을 잡는 눈이 이 강의의 목표입니다.
사례 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;문법적으로 깔끔하고, 실행도 잘 됩니다. 근데 — 주문 수가 부풀어 옵니다.
| member_name | 주문수 |
|---|---|
| 김철수 | 10 |
| 조성민 | 8 |
| 오지은 | 7 |
| 이영희 | 6 |
| ⋮ | |
원인은 3-7 그대로입니다. order_items까지 JOIN하는 순간, 한 주문이 그 안에 든 상품 가짓수만큼 행으로 펼쳐지고, COUNT(*)는 주문이 아니라 이 펼쳐진 행을 셉니다.
여기까지는 복습입니다. 원리를 다시 보고 싶으면 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권)으로 뜯어봅니다.
주문상세 행별 금액의 평균
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에서 두 방식을 돌려 김철수 행을 비교하면 —
진짜 답은 단계를 나누는 것입니다. 먼저 주문별 총액을 구하고 → 그 위에 평균. 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만 한 결과의 행 수부터 셉니다. 가장 자식
테이블(우리 DB에선 order_items 64행)과 같으면 정상 — 다르면 어딘가 N:M이
풀려 카테시안 곱이 생긴 겁니다.
결과가 “회원별” 이라면 회원 수와 맞아야죠. 주문 있는 회원은 16명 —
COUNT(DISTINCT m.member_id)가 16이 나와야 합니다.
JOIN 없이, 한 명만 콕 집어 다시 구해봅니다. WHERE member_id = 1 한
줄짜리 SQL의 답(6건)과 메인 쿼리의 김철수 행이 일치해야 정상 — 다르면 그
차이가 곧 뻥튀기의 증거.
세 검산을 직접 쳐보세요. 전부 한 줄짜리입니다.
-- 검산 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;모든 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 전체를 관통하는 협업 패턴입니다.
한눈에 정리
| 단계 | 무엇을 | 어떻게 |
|---|---|---|
| 1 | JOIN 결과 행 수 점검 | COUNT(*) 가 가장 자식 테이블 행 수와 같은지 |
| 2 | 집계 단위 검증 | COUNT(DISTINCT 의도한_단위) 가 기대값과 같은지 |
| 3 ⭐ | 단순 집계 교차 검증 | 가장 단순한 SQL로 같은 값을 다시 — 한 명만 콕 집어 |
| + | AI와 분업 | ”검산 SQL도 같이 짜줘” 후속 메시지 |
JOIN + 집계 = 일단 의심. 의심 = 3단계 검산. 이 두 문장만 외우면 — AI가 가장 자주 틀리는 패턴 1번은 평생 잡을 수 있습니다.
자주 막히는 지점
64의 몇 배씩 나온다면 JOIN 조건 문제입니다. AI가 ON 절 컬럼을 잘못 짚는 일도 흔해요 — 6-1의 패턴 4(환각 컬럼)와 같이 나타나기도 합니다.
JOIN … ON 절이 부모 PK = 자식 FK 쌍인지 확인 — ON 조건이 빠졌거나 엉뚱한 컬럼이면 카테시안 곱이 폭발합니다.JOIN 체인에서 행을 가장 잘게 쪼개는 테이블이 기준입니다. JOIN 결과 행 수는 이 테이블의 행 수를 따라가는 게 정상이에요.
order_items 입니다.INNER JOIN은 주문 있는 회원만(16명), LEFT JOIN은 전체 회원(20명)을 셉니다. 어느 쪽도 문법 오류는 아니고 — 질문의 의도가 무엇이냐의 문제. 이 NULL 이야기는 바로 다음 6-3에서 본격적으로 다룹니다.
LEFT JOIN 을 썼다면 주문 없는 회원 4명까지 포함돼 20 — 먼저 의도한 단위가 무엇인지부터 정하세요.뻥튀기 사례 두 개와 3단계 검산 — 오늘 배운 건 이게 전부지만, 섹션 6에서 가장 자주 써먹게 될 무기입니다. 연습 문제로 손에 익히고 넘어가세요.