AI 시대 DB 모델링 & SQL 완벽 마스터6-3 NULL 처리 실수
테크타니 LEARN — 섹션 06 · AI 결과 의심하기

NULL은 0이 아닙니다 —
"값이 없다"가 만드는 3가지 함정

LEFT JOIN 결과의 NULL, COUNT(*)와 COUNT(컬럼)의 차이, 그리고 NULL과의 비교 — AI가 만든 SQL에서 NULL 함정이 숨어드는 3곳을 shop 데이터로 직접 재현합니다. NULL은 어려운 개념이 아니에요. 규칙만 알면 COALESCE와 IS NULL로 전부 잡힙니다.

난이도보통소요6분준비물6-2 완료 · shop DB핵심 도구COALESCE · IS NULL · COUNT(컬럼)

이번 섹션의 여정

6-1 ~ 6-2

완료 ✓

6-3 NULL 처리 실수

지금 여기

6-4 GROUP BY 단위 오류

집계 단위 함정

6-5 검증 체크리스트 ★

의심 루틴 완성

섹션 7 — 마무리

다음 마일스톤

NULL — 0도 아니고 빈 문자열도 아닙니다

3-2 WHERE에서 한 번 다뤘죠 — NULL은 “값이 없다”는 특수 상태라서 = NULL로는 못 찾고 IS NULL 을 써야 한다고요. 오늘은 그 NULL이 AI가 만든 SQL에서 어떻게 사고를 치는지 — 가장 흔한 3가지 사례를 직접 재현하고, 잡는 법까지 챙깁니다.

NULL의 정체 — 이 한 줄이 오늘의 전부

NULL = “값이 없다, 모른다”. 0도 아니고 빈 문자열도 아닙니다. 그래서 NULL이 낀 연산의 결과는 — 항상 “모름(UNKNOWN)“이 됩니다.

시작 전 체크
6-2 완료 — JOIN 뻥튀기 3단계 검산 습관
DBeaver + shop DB (2-3에서 적재)
AI 과외 선생님 세션 (2-4에서 셋업)

함정 ① — LEFT JOIN이 만드는 NULL ⭐

요구사항: “모든 회원의 매출을 보여줘 — 주문 안 한 회원도 포함해서.” AI는 배운 대로 LEFT JOIN을 정확히 썼습니다. 주문 없는 회원도 안 잘리고요. 그런데 —

-- AI가 만든 SQL — LEFT JOIN 자체는 정확합니다
SELECT
  m.member_name,
  SUM(p.price * oi.quantity) AS 매출
FROM
  members m
LEFT JOIN orders o
  ON m.member_id = o.member_id
LEFT JOIN order_items oi
  ON o.order_id = oi.order_id
LEFT JOIN products p
  ON oi.product_id = p.product_id
GROUP BY
  m.member_id,
  m.member_name
ORDER BY
  매출 DESC;
DBeaver — AI SQL 실행 결과
member_name매출
조성민600000
김철수498000
박민수356000
황태웅NULL
안소영NULL
류재민NULL
백서윤NULL
20 rows fetched — 주문 없는 4명의 매출이 NULL

황태웅·안소영처럼 주문 안 한 회원의 매출이 NULL로 나옵니다. 0이 아니에요 — “값이 없다, 모른다”입니다. 이대로 두면 이런 문제가 생깁니다.

  • 보고서에 NULL 글자가 그대로 찍힘 — 비전공자는 “오류인가?” 하고 헷갈립니다.
  • 정렬하면 NULL이 맨 끝(또는 맨 앞)으로 — 의도와 다른 위치로 밀립니다.
  • 이 결과 위에 평균·합계 같은 2차 계산을 얹으면 — NULL 회원이 통째로 빠집니다.
  • NULL이 낀 산술식(예: 매출 + 100)은 — 결과가 통째로 NULL이 됩니다.
평균이 정확히 25% 부풀어요 — 진짜 숫자로

위 결과로 회원 평균 매출을 내면 AVG(매출)은 NULL인 4명을 무시하고 3,013,000 ÷ 16 ≈ 188,313원. 주문 안 한 회원까지 포함한 진짜 평균은 3,013,000 ÷ 20 = 150,650원입니다. NULL을 그대로 두면 평균이 25% 부풀어 보고됩니다.

처방은 함수 하나입니다 — COALESCE(값1, 값2), “값1이 NULL이면 값2를 대신 써.”

-- 처방 — COALESCE로 NULL을 0으로 명시
SELECT
  m.member_name,
  COALESCE(SUM(p.price * oi.quantity), 0) AS 매출
FROM
  members m
LEFT JOIN orders o
  ON m.member_id = o.member_id
LEFT JOIN order_items oi
  ON o.order_id = oi.order_id
LEFT JOIN products p
  ON oi.product_id = p.product_id
GROUP BY
  m.member_id,
  m.member_name
ORDER BY
  매출 DESC;
DBeaver — COALESCE 적용 후
member_name매출
조성민600000
김철수498000
황태웅0
안소영0
류재민0
백서윤0
NULL → 0 — 보고서에 바로 써도 되는 결과
IFNULL도 같은 일을 합니다

IFNULL(값, 대체값)은 같은 의미의 MySQL 전용 함수예요. COALESCE는 표준 SQL인 데다 인수를 여러 개 받아 앞에서부터 NULL이 아닌 첫 값을 고를 수도 있어서 — 이 강의에서는 COALESCE로 통일합니다.

함정 ① 잡는 규칙

LEFT JOIN + 집계 결과는 — 항상 COALESCE(..., 0)로 NULL → 0을 명시합니다.

함정 ② — COUNT(*) vs COUNT(컬럼)

둘의 차이를 정확히 알고 계세요? 모르면 큰 사고가 나는 차이입니다.

✕ COUNT(*) — 행을 셉니다

NULL이어도 행이 있으면 1

LEFT JOIN에서 매칭이 없어도 행 자체는 1행 존재합니다 — 오른쪽 컬럼이 전부 NULL인 채로요. 이 버전은 그 행도 셉니다. 주문 0건인 황태웅이 1로 나오는 이유.

✓ COUNT(컬럼) — 값을 셉니다

그 컬럼이 NULL이 아닌 행만

COUNT(o.order_id)order_id가 NULL인 행을 빼고 셉니다. 주문 0건인 황태웅이 정확히 0 — 우리가 원한 답입니다.

shop에서 두 버전을 나란히 놓고 비교해보세요.

-- 모든 회원 + 주문 LEFT JOIN — 두 COUNT를 나란히
SELECT
  m.member_name,
  COUNT(*) AS count_all, -- 행 수 (NULL 포함)
  COUNT(o.order_id) AS count_order_id -- 값 수 (NULL 제외)
FROM
  members m
LEFT JOIN orders o
  ON m.member_id = o.member_id
GROUP BY
  m.member_id,
  m.member_name;
DBeaver — 두 COUNT 비교
member_namecount_allcount_order_id
김철수66
이영희44
조성민44
황태웅10
안소영10
20 rows fetched — 진짜 주문 수는 count_order_id 쪽

주문이 있는 회원(김철수 6건)은 두 숫자가 같지만, 주문 0건인 황태웅에서 갈립니다. COUNT(*) = 1, COUNT(o.order_id) = 0 — 우리가 원한 답은 0이죠.

AI도 자주 틀립니다

AI는 습관처럼 COUNT(*)만 쓰는 경우가 많아요. 그러면 주문 0건 회원이 “1건 주문한 것처럼” 보고됩니다. LEFT JOIN 뒤의 COUNT는 무조건 한 번 의심하세요.

함정 ② 잡는 규칙

주문 안 한 회원의 주문 수를 정확히 0으로 세려면 — COUNT(*)가 아니라 COUNT(자식 테이블의 컬럼).

함정 ③ — NULL과 비교하면 UNKNOWN

-- AI가 만든 SQL — 언뜻 보면 "30살이 아닌 회원"
SELECT
  *
FROM
  members
WHERE
  age != 30;

멀쩡해 보이지만 — age가 NULL인 회원은 이 결과에 안 나옵니다. 30살이 “아닌”데도요. SQL에서 NULL과 어떤 값을 비교하면 결과는 TRUE도 FALSE도 아닌 UNKNOWN이기 때문입니다.

WHERE age != 30 — 3가지 결과
age = 35 → age != 30 → TRUE → 결과에 포함
age = 30 → age != 30 → FALSE → 제외 (의도대로)
age = NULL → age != 30 → UNKNOWN → 제외 (함정!)
 
WHERE는 TRUE인 행만 통과 — FALSE도 UNKNOWN도 버립니다

NOT (age = 30)으로 바꿔 써도 똑같이 당합니다 — NOT UNKNOWN도 여전히 UNKNOWN이거든요. 해결은 NULL 분기를 명시하는 것.

-- NULL인 회원도 결과에 포함하려면
SELECT
  *
FROM
  members
WHERE
  age != 30 OR age IS NULL;
shop에서는 이 함정이 안 보입니다

우리 members.age는 NOT NULL 컬럼이라 빠지는 행이 없어요 — age != 30의 결과는 19명(30살 노수아 1명만 제외)으로 정상입니다. 하지만 실무 DB의 나이·전화번호·탈퇴일 같은 NULL 허용 컬럼에서는 정말 자주 터지는 함정입니다.

함정 ③ 잡는 규칙

비교 연산 결과가 이상할 땐 — 그 컬럼의 NULL 가능성부터 점검하고, 필요하면 OR ... IS NULL 분기를 추가합니다.

AI에게 NULL 점검을 시키세요

6-2의 검산 협업과 같은 패턴입니다. AI한테 SQL을 받자마자 — 아래 후속 메시지 하나로 NULL 처리를 자체 점검시키세요.

방금 만든 SQL을 NULL 처리 관점에서 점검해줘.

1. LEFT JOIN 결과에 NULL이 들어가는 컬럼이 있어?
   있다면 COALESCE로 처리해야 하지 않아?
2. COUNT(*)와 COUNT(컬럼) 중 어느 쪽을 썼어? 의도에 맞아?
3. WHERE 조건에 쓴 컬럼이 NULL일 가능성은 없어?
   IS NULL 분기가 필요하지 않아?

위 3가지를 점검하고, 필요하면 SQL을 수정해줘.

이 한 메시지로 AI가 본인이 만든 SQL의 NULL 처리를 스스로 점검하고 고쳐줍니다. 단 — 이 질문은 검증 포인트를 아는 사람만 던질 수 있어요. 그 포인트가 바로 오늘 배운 3가지입니다.

한눈에 정리

#함정증상잡는 법
1LEFT JOIN 결과의 NULL매출이 0 대신 NULLCOALESCE(SUM(...), 0)
2COUNT(*) vs COUNT(컬럼)0건이 1건으로 둔갑COUNT(자식 테이블 컬럼)
3비교 연산의 UNKNOWNNULL 행이 조용히 증발OR ... IS NULL 추가
오늘의 규칙

NULL = “값이 없다”. 0도 아니고 빈 문자열도 아니며, NULL이 낀 연산은 항상 “모름(UNKNOWN)“입니다. SQL의 모든 NULL 처리는 — COALESCE / IS NULL / COUNT(컬럼) 셋 중 하나로 명시하세요.

자주 막히는 지점

① WHERE 매출 = NULL 로 걸었더니 0행이 나와요

3-2에서 본 규칙 그대로입니다 — NULL과의 비교는 TRUE가 아니라 UNKNOWN이에요.

FIX ▸NULL은 =로 못 찾습니다 — IS NULL로. = NULL은 항상 UNKNOWN이라 모든 행이 버려집니다.
② COALESCE와 IFNULL, 뭘 써야 하나요?

IFNULL은 MySQL 전용이고 인수가 2개로 고정입니다. 나중에 다른 DB로 옮겨도 그대로 도는 건 COALESCE 쪽이에요.

FIX ▸아무거나 — 결과는 같습니다. 표준 SQL이고 인수도 여러 개 받는 COALESCE 권장.
③ DBeaver 결과에 [NULL] 이라고 떠요 — 문자열인가요?

결과 그리드에서 NULL은 연한 글씨의 [NULL]로 표시됩니다. ‘NULL’이라는 글자가 저장된 게 아니에요.

FIX ▸문자열이 아닙니다 — DBeaver가 "값 없음"을 표시하는 방식이에요.

NULL 함정 3가지 — 결국 전부 “값이 없다”는 한 가지 성질에서 나옵니다. 연습 문제로 손에 익히고 나면, 다음 강의에서는 AI가 집계 단위를 잘못 잡는 GROUP BY 함정으로 넘어갑니다.

NEXT LESSON6-3 연습 문제

LEFT JOIN의 NULL · COUNT 차이 · UNKNOWN 함정 — 3가지를 shop DB에서 직접 잡아봅니다.

GO ▸ 연습 문제 풀기