C-3: NULL과 조건 처리 — 빈 값을 길들이고, 조건으로 값을 바꾸기
목차 29
안녕하세요, 홍순구 튜터입니다. 지난 시간에 우리는 단일행 함수로 값을 가공했어요. 글자를 자르고(SUBSTR), 날짜를 바꾸고(TO_CHAR), 글에서 해시태그만 똑 떼어냈죠(REGEXP_SUBSTR). 그런데 그러다 보니 자꾸 마주친 게 하나 있어요. 바로 빈 값이에요.
해시태그 없는 글에서 REGEXP_SUBSTR은 빈칸(NULL)을 돌려줬고, INSTR은 0을 돌려줬어요. 자기소개(bio)를 안 적은 회원도 있었고요. 오늘은 이 빈 값, NULL을 길들여요. NULL을 다른 값으로 바꿔 채우고(NVL·COALESCE), "값이 이러면 저렇게, 저러면 이렇게" 조건에 따라 값을 갈래로 나눠요(DECODE·CASE). 지난 시간에 'NULL이 나와서 곤란했던' 그 빈칸들을 오늘 깔끔하게 채워 넣을 거예요.
오늘의 여정
① NULL 다시 보기 — '값이 없음'의 정체와 연산의 함정
② NVL — 빈칸을 기본값으로 채우기
③ NVL2 — 값이 있냐 없냐로 두 갈래
④ COALESCE — 여러 후보 중 첫 비어있지 않은 값
⑤ NULLIF — 두 값이 같으면 일부러 NULL 만들기
⑥ DECODE — Oracle식 값 분기
⑦ CASE — 표준 조건 분기 (단순 · 검색)
⑧ NULL과 정렬 — NULLS FIRST / NULLS LAST
💡 오늘 수업의 핵심 — "NULL을 기본값으로 채우고(NVL·COALESCE), 조건에 따라 값을 여러 갈래로 바꾼다(DECODE·CASE)"
🎯 학습 목표
- NULL의 정체('값 없음')와 연산·비교에서의 함정을 이해하고,
NVL·NVL2·NULLIF·COALESCE로 NULL을 다룬다. (SQLD 2과목 'SQL 기본 — NULL 관련 함수') DECODE(Oracle)와CASE(표준)로 값을 조건에 따라 여러 갈래로 분기한다. (SQLD 2과목 'CASE/DECODE')- 정렬에서 NULL이 어디에 놓이는지 이해하고
NULLS FIRST/NULLS LAST로 위치를 직접 정한다.
Step 1: "NULL 다시 보기 — 값이 없다는 것"
함수로 들어가기 전에 NULL이 정확히 뭔지부터 잡고 갈게요. 지난 시간들에서 빈칸을 여러 번 만났지만, 오늘은 이걸 본격적으로 다루니까요.
NULL은 '값이 없음'이에요. 여기서 중요한 건, NULL은 0도 아니고 빈 문자열('')도 아니라는 점이에요. 0은 '0이라는 값'이 있는 거고, 빈 문자열도 '비어 있다는 값'이 있는 거예요. NULL은 아예 아무것도 없는, 칸 자체가 비어 있는 상태예요. 설문지에서 나이 칸을 0으로 적은 것과, 칸을 통째로 비워 둔 것은 다르잖아요. 0은 "나이가 0살"이고, 빈칸은 "나이를 모름"이에요. NULL은 후자예요.
0 / 빈 문자열 / NULL — 셋은 다르다
값 0 → '0' 이라는 값이 있다 (예: 좋아요 0개)
값 '' → '비었다' 라는 값이 있다 (예: 한 글자도 안 쓴 메모)
NULL → 값이 아예 없다 (모름·미입력) (예: 자기소개를 안 적음)
우리 회원 표에서 자기소개(bio)를 안 적은 회원을 찾아볼게요. 지난 모듈에서 봤던 IS NULL을 다시 써요.
-- sql/queries/C3_null_conditional.sql
SELECT member_id, nickname, bio
FROM member
WHERE bio IS NULL;
| member_id | nickname | bio |
|---|---|---|
| 3 | 박승우 | (없음) |
| 5 | 최도연 | (없음) |
| 8 | 한나래 | (없음) |
| 10 | 이민수 | (없음) |
자기소개가 비어 있는 회원 4명이 나왔어요. 그런데 여기서 "그럼 bio = NULL로 찾으면 되겠네" 하면 함정에 빠져요.
SELECT member_id, nickname FROM member WHERE bio = NULL;
이건 한 행도 안 나와요. 방금 IS NULL로 찾았을 땐 분명 4명이 나왔는데, = NULL로는 0건이에요. NULL은 '모름'이라서 = NULL로 비교하면 "모르는 값과 같은가?"라는 질문이 되고, 그 답도 "모름"이 돼요. 참(true)이 아니니 한 행도 안 걸리는 거예요. 그래서 NULL은 반드시 IS NULL·IS NOT NULL로만 가려내요. B 모듈에서 짚었던 3가지 논리값(참·거짓·모름) 이야기가 바로 여기서 살아나요.
연산도 마찬가지예요. NULL이 끼면 산술 결과가 통째로 NULL이 돼요.
SELECT 100 + NULL AS 더하기, 100 * NULL AS 곱하기 FROM dual;
둘 다 NULL이 나와요. '모르는 값'에 100을 더해도 여전히 모르니까요. 재미있는 건 문자 결합(||)은 좀 달라요. 결합에서는 NULL이 빈 글자처럼 취급돼 사라져요. 자기소개가 없는 회원에게 인사말을 붙여 볼게요.
SELECT member_id, 'Hi ' || bio AS 인사
FROM member
WHERE member_id = 3;
결과는 'Hi '예요. bio가 NULL이라 뒤가 비어서 'Hi '만 남았어요. 산술에서는 전체를 NULL로 만들고, 문자 결합에서는 빈칸처럼 빠지는 이 차이가 처음엔 헷갈려요. 그래서 NULL은 그냥 두지 말고 처리해 줘야 해요. 그 도구가 오늘 배울 함수들이에요.
⚠️ 함정 — NULL은
= NULL로 비교할 수 없어요. 항상IS NULL·IS NOT NULL을 써요. SQLD에서WHERE 컬럼 = NULL을 정답처럼 섞어 놓고 "이 조건으로 몇 건이 나오나" 묻는 함정이 단골이에요. 답은 언제나 0건이에요. ★빈출
💡 한 줄 정리
NULL은 '값이 없음'이라 0이나 빈 문자열과 다르고, = NULL로는 비교할 수 없어 IS NULL로만 가려내며, 산술에 끼면 결과가 통째로 NULL이 된다.
🙋 학생 질문 — "튜터님, NULL이 0이랑 다른 게 실제로 무슨 차이를 만들어요?"
평균을 낼 때 확 갈려요. 예를 들어 시험을 안 본 학생의 점수를 0으로 적으면, 그 0이 평균을 끌어내려요. 안 본 건데 0점 취급을 받는 거죠. 반대로 NULL(미응시)로 두면 평균 계산에서 아예 빠져서, 시험을 본 학생들만의 평균이 나와요. "값이 0"과 "값이 없음"은 이렇게 결과를 바꿔요. 그래서 "아직 모르는 값"은 0으로 채우지 말고 NULL로 두는 게 정직한 설계예요. 이 NULL이 집계에서 어떻게 처리되는지는 다음 시간에 자세히 봐요.
Step 2: "NVL — 빈칸을 기본값으로 채우기"
이제 NULL을 길들이는 첫 함수예요. NVL은 "값이 NULL이면 다른 값으로 바꿔라"예요. 이름은 'Null VaLue'에서 왔어요. NVL(값, NULL일때쓸값) 형태예요.
자기소개가 비어 있는 회원에게 '소개 없음'을 채워 볼게요.
-- sql/queries/C3_null_conditional.sql
SELECT member_id, nickname, NVL(bio, '소개 없음') AS 소개
FROM member
WHERE member_id IN (1, 2, 3, 5);
| member_id | nickname | 소개 |
|---|---|---|
| 1 | 김재훈 | 백엔드 공부 중 |
| 2 | 이민지 | 사진과 커피 |
| 3 | 박승우 | 소개 없음 |
| 5 | 최도연 | 소개 없음 |
bio가 있는 1·2번은 원래 값 그대로, 비어 있던 3·5번은 '소개 없음'으로 채워졌죠. 화면에 빈칸 대신 보기 좋은 안내 문구가 들어가요.
지난 시간에 못다 한 숙제도 오늘 해결해요. 해시태그가 없는 글에서 REGEXP_SUBSTR이 NULL을 돌려줬던 거 기억하시죠. 거기에 NVL을 씌우면 '태그 없음'으로 채울 수 있어요.
SELECT post_id,
NVL(REGEXP_SUBSTR(caption, '#[^ ]+'), '태그 없음') AS 첫태그
FROM post
WHERE post_id IN (1, 3, 5, 29);
| post_id | caption | 첫태그 |
|---|---|---|
| 1 | 첫 게시물입니다 #안녕 #일상 #첫게시물 | #안녕 |
| 3 | 주말 산책 | 태그 없음 |
| 5 | 필름 카메라 #사진 | #사진 |
| 29 | 여행 다녀왔어요 | 태그 없음 |
지난 시간에 NULL이라 빈칸으로 남았던 3번·29번이 이제 '태그 없음'으로 또렷하게 채워졌어요. 함수 안에 함수를 넣고(REGEXP_SUBSTR의 결과를 NVL이 받음), 그 빈 결과를 한 번에 정리한 거예요.
⚠️ 함정 —
NVL의 두 인자는 형(type)이 맞아야 해요.NVL(bio, '소개 없음')은 둘 다 문자라 괜찮지만, 숫자 컬럼에NVL(좋아요수, '없음')처럼 문자를 섞으면 오류가 나요. 숫자엔 숫자로(NVL(좋아요수, 0)), 문자엔 문자로 채워야 해요. ★빈출
💡 한 줄 정리
NVL(값, 대체값)은 값이 NULL일 때만 대체값으로 바꿔 주며, 두 인자의 형은 서로 맞아야 한다.
🙋 학생 질문 — "튜터님, NVL로 채우면 진짜 데이터도 바뀌나요?"
아니에요. NVL은 조회할 때 화면에 보여 주는 모습만 바꿔요. 표에 저장된 bio는 여전히 NULL 그대로예요. SELECT는 데이터를 '읽어서 가공해 보여 주는' 일이라, 원본은 손대지 않아요. 그래서 같은 회원을 NVL 없이 다시 조회하면 또 빈칸으로 나와요. 저장된 값을 진짜로 바꾸려면 나중에 배울 UPDATE가 필요해요. 지금은 "보여 줄 때만 채운다"로 충분해요.
Step 3: "NVL2 — 값이 있냐 없냐로 두 갈래"
NVL은 NULL일 때만 바꿨죠. NVL2는 한 발 더 나가요. "값이 있으면 이걸, 없으면 저걸"처럼 두 갈래로 나눠요. 인자가 셋이에요. NVL2(값, 값이있을때, 값이없을때).
회원이 자기소개를 적었는지 아닌지로 갈라 볼게요.
-- sql/queries/C3_null_conditional.sql
SELECT member_id, nickname,
NVL2(bio, '소개 있음', '소개 없음') AS 프로필상태
FROM member
WHERE member_id IN (1, 3, 8, 12);
| member_id | nickname | 프로필상태 |
|---|---|---|
| 1 | 김재훈 | 소개 있음 |
| 3 | 박승우 | 소개 없음 |
| 8 | 한나래 | 소개 없음 |
| 12 | 박지훈 | 소개 있음 |
bio가 있는 1·12번은 '소개 있음', 비어 있는 3·8번은 '소개 없음'으로 갈렸어요. 전체로 보면 자기소개를 적은 회원 8명은 '소개 있음', 비운 4명은 '소개 없음'으로 나뉘어요.
NVL과 NVL2의 차이를 짚고 갈게요. 핵심은 NVL2는 원래 값을 결과로 쓰지 않는다는 점이에요. NVL은 값이 있으면 그 값을 그대로 돌려주지만, NVL2는 값이 있든 없든 우리가 정한 문구로 바꿔요. "값의 내용"이 아니라 "값이 있느냐 없느냐"만 보고 두 갈래로 보내는 거예요.
NVL : 값이 있으면 → 그 값 그대로 / 없으면 → '소개 없음'
NVL2 : 값이 있으면 → '소개 있음' / 없으면 → '소개 없음' (원래 내용은 안 씀)
⚠️ 함정 —
NVL2의 인자 순서를 헷갈리면 안 돼요. 두 번째가 'NOT NULL일 때', 세 번째가 'NULL일 때'예요.NVL은 'NULL일 때'가 두 번째였는데,NVL2는 'NOT NULL일 때'가 먼저 와요. 순서가 반대로 느껴져서 SQLD에서 자주 틀려요. ★빈출
💡 한 줄 정리
NVL2(값, A, B)는 값이 있으면 A, 없으면 B를 돌려주며, 원래 값의 내용이 아니라 '있느냐 없느냐'로 두 갈래를 나눈다.
🙋 학생 질문 — "튜터님, NVL2는 언제 NVL보다 유용해요?"
"값이 있다는 사실 자체"를 표시하고 싶을 때예요. 예를 들어 전화번호를 등록한 회원에게는 '인증 완료', 안 한 회원에게는 '미인증'을 보여 주고 싶다고 해요. 실제 전화번호를 화면에 노출하긴 싫고요. 이때 NVL2(전화번호, '인증 완료', '미인증')이면, 번호 내용은 감추고 등록 여부만 깔끔하게 보여 줘요. NVL은 원래 값을 그대로 드러내니 이런 일엔 안 맞아요. "내용은 숨기고 유무만"일 때 NVL2를 떠올리면 돼요.
Step 4: "COALESCE — 여러 후보 중 첫 비어있지 않은 값"
NVL은 후보가 하나였어요. NULL이면 그거 하나로 바꿨죠. 그런데 "1순위가 비었으면 2순위, 그것도 비었으면 3순위"처럼 여러 후보를 차례로 시도하고 싶을 때가 있어요. 그게 COALESCE예요. 이름은 '합쳐진다'는 뜻인데, "왼쪽부터 보다가 처음으로 비어있지 않은 값을 고른다"로 기억하면 돼요.
표시 이름을 만들어 볼게요. 자기소개가 있으면 그걸, 없으면 닉네임을, 그것도 없으면 '익명'을 쓰기로 해요.
-- sql/queries/C3_null_conditional.sql
SELECT member_id, bio, nickname,
COALESCE(bio, nickname, '익명') AS 표시이름
FROM member
WHERE bio IS NULL;
| member_id | bio | nickname | 표시이름 |
|---|---|---|---|
| 3 | (없음) | 박승우 | 박승우 |
| 5 | (없음) | 최도연 | 최도연 |
| 8 | (없음) | 한나래 | 한나래 |
| 10 | (없음) | 이민수 | 이민수 |
bio가 비어 있으니 다음 후보인 nickname으로 내려갔어요. 만약 닉네임까지 비어 있었다면 마지막 '익명'이 쓰였을 거예요. 왼쪽부터 차례로 보다가 처음 만난 값에서 멈추는 거예요.
NVL과 비교해 둘게요. 둘 다 NULL을 대체하지만 차이가 있어요.
| 함수 | 후보 개수 | 출신 |
|---|---|---|
NVL(a, b) |
딱 2개 (a가 NULL이면 b) | Oracle 고유 |
COALESCE(a, b, c, …) |
여러 개 (왼쪽부터 첫 비-NULL) | ANSI 표준 |
COALESCE는 후보를 여러 개 줄 수 있고, 여러 데이터베이스에서 똑같이 동작하는 표준 함수예요. 그래서 나중에 다른 DBMS로 옮길 때도 그대로 써요. Oracle만 쓸 거고 후보가 둘뿐이면 NVL이 간결하고, 후보가 여럿이거나 표준을 따르고 싶으면 COALESCE예요. 이 방언 차이는 마무리 모듈에서 다시 정리해요.
🌟 알아두면 좋아요 —
COALESCE는 후보가 둘일 때NVL과 결과가 같아요.COALESCE(bio, '소개 없음')은NVL(bio, '소개 없음')과 똑같이 동작해요. 다만NVL은 두 번째 값을 항상 계산하고,COALESCE는 필요할 때만 계산한다는 미세한 차이가 있는데, 지금은 "후보가 여럿이면 COALESCE" 정도로 충분해요.
💡 한 줄 정리
COALESCE(a, b, c, …)는 왼쪽부터 보다가 처음으로 비어있지 않은 값을 돌려주는 ANSI 표준 함수로, 후보를 여러 개 줄 수 있다.
🙋 학생 질문 — "튜터님, COALESCE는 후보를 몇 개까지 쓸 수 있어요?"
실무에서 쓰는 수준이면 개수 걱정은 안 해도 돼요. 두세 개가 가장 흔하고, 필요하면 더 많이 나열할 수 있어요. 다만 후보가 너무 많아지면 "이 값이 대체 어디서 오는 거지?" 하고 읽기 어려워져요. 보통은 "1순위 → 2순위 → 최후의 기본값" 정도, 길어도 서너 개 안에서 끝내는 게 읽기 좋아요. 후보가 다섯 개를 넘어가면, 그건 데이터 설계를 다시 볼 신호인 경우가 많아요.
Step 5: "NULLIF — 두 값이 같으면 일부러 NULL 만들기"
지금까지는 NULL을 다른 값으로 바꿨어요. NULLIF는 거꾸로예요. 특정 조건일 때 일부러 NULL을 만들어요. NULLIF(a, b)는 "a와 b가 같으면 NULL, 다르면 a"를 돌려줘요. 이름 그대로 'NULL IF (같으면)'이에요.
기본 동작부터 DUAL로 볼게요.
-- sql/queries/C3_null_conditional.sql
SELECT NULLIF(10, 10) AS 같을때, NULLIF(10, 20) AS 다를때 FROM dual;
| 같을때 | 다를때 |
|---|---|
| (NULL) | 10 |
10과 10은 같으니 NULL, 10과 20은 다르니 첫 값 10이 그대로 나왔어요. "같으면 지운다"가 핵심이에요.
이게 언제 쓸모 있을까요? 지난 시간에 남겨 둔 숙제가 여기서 풀려요. INSTR은 찾는 글자가 없으면 NULL이 아니라 0을 돌려줬죠. 이 0이 어정쩡했어요. '진짜 0번째'가 아니라 '없다'는 뜻인데 숫자 0으로 섞여 있으니까요. NULLIF로 이 0을 진짜 NULL로 바꿔 줄 수 있어요.
SELECT post_id,
INSTR(caption, '#') AS 샵위치,
NULLIF(INSTR(caption, '#'), 0) AS 정리한위치
FROM post
WHERE post_id IN (1, 3);
| post_id | caption | 샵위치 | 정리한위치 |
|---|---|---|---|
| 1 | 첫 게시물입니다 #안녕 #일상 #첫게시물 | 10 | 10 |
| 3 | 주말 산책 | 0 | (NULL) |
1번 글은 #이 10번째에 있으니 그대로 10이고, 3번 글은 #이 없어 0이었는데 NULLIF(…, 0)이 그 0을 NULL로 바꿨어요. "없음을 뜻하는 0"을 "진짜 없음(NULL)"으로 승격시킨 거예요.
이렇게 NULL로 바꾸고 나면, 앞서 배운 NVL로 깔끔하게 마무리할 수 있어요. 함수들이 이어져요.
SELECT post_id,
NVL(NULLIF(INSTR(caption, '#'), 0), -1) AS 위치또는없음
FROM post
WHERE post_id IN (1, 3);
1번은 10, 3번은 -1이 나와요. 0을 NULL로 바꾼 뒤(NULLIF), 그 NULL을 '-1(없음 표시)'로 채운 거예요(NVL). 어정쩡했던 0이 두 함수를 거쳐 또렷한 신호로 정리됐어요.
⚠️ 함정 —
NULLIF(a, b)는 같으면 NULL, 다르면 첫 번째 값 a를 돌려줘요. 다를 때 b가 나온다고 착각하기 쉬워요. "같으면 지우고, 다르면 앞엣것을 남긴다"로 외우세요.
💡 한 줄 정리
NULLIF(a, b)는 두 값이 같으면 NULL을, 다르면 첫 값 a를 돌려주며, '없음을 뜻하는 특정 값(예: 0)'을 진짜 NULL로 바꿀 때 쓴다.
🙋 학생 질문 — "튜터님, 멀쩡한 값을 일부러 NULL로 만드는 게 왜 필요해요?"
"이 값은 사실 없는 거나 마찬가지"인 경우를 정리할 때 써요. 가장 유명한 예가 나눗셈이에요. 어떤 수를 0으로 나누면 오류가 나는데, 나누는 수에 NULLIF(나누는수, 0)을 씌우면 0일 때 NULL이 되고, 'NULL로 나누기'는 오류 대신 그냥 NULL이 돼요. 오류로 멈추는 대신 "결과 없음"으로 부드럽게 넘어가는 거죠. 오늘 본 INSTR의 0처럼, "특정 값이 사실은 없음을 뜻할 때" 그걸 NULL로 통일해 두면 뒤처리가 깔끔해져요.
Step 6: "DECODE — Oracle식 값 분기"
지금까지는 NULL을 다뤘어요. 이제 방향을 틀어, 값을 조건에 따라 여러 갈래로 바꾸는 도구로 가요. 첫 번째는 DECODE예요. Oracle이 만든 함수로, "이 값이 A면 이걸, B면 저걸"처럼 값을 갈아 끼워요.
형태는 DECODE(기준값, 조건1, 결과1, 조건2, 결과2, …, 기본값)이에요. 회원 번호의 홀짝을 갈라 볼게요. 지난 시간에 배운 MOD(나머지)로 짝홀을 나눴던 거 기억하시죠.
-- sql/queries/C3_null_conditional.sql
SELECT member_id,
DECODE(MOD(member_id, 2), 0, '짝수회원', 1, '홀수회원') AS 구분
FROM member
WHERE member_id <= 6;
| member_id | 구분 |
|---|---|
| 1 | 홀수회원 |
| 2 | 짝수회원 |
| 3 | 홀수회원 |
| 4 | 짝수회원 |
| 5 | 홀수회원 |
| 6 | 짝수회원 |
MOD(member_id, 2)가 0이면 '짝수회원', 1이면 '홀수회원'으로 갈아 끼웠어요. DECODE가 기준값을 조건들과 차례로 맞춰 보다가, 일치하는 걸 만나면 그 결과를 돌려줘요.
맨 마지막에 기본값을 하나 둘 수 있어요. 어느 조건에도 안 맞으면 그 기본값이 나와요.
SELECT DECODE('B', 'A', '우수', 'B', '보통', '기타') FROM dual;
결과는 '보통'이에요. 'B'가 두 번째 조건과 맞았거든요. 만약 'C'를 넣었다면 어느 조건에도 안 맞아 마지막 '기타'가 나와요. 이 마지막 한 칸이 "그 외 전부"를 받는 기본값이에요.
DECODE의 한계도 알아 둬야 해요. 값이 정확히 같은지(동등)만 따질 수 있어요. "10보다 큰가", "1부터 4 사이인가" 같은 범위 비교는 못 해요. 그건 다음에 배울 CASE의 몫이에요.
🌟 알아두면 좋아요 —
DECODE는 Oracle에만 있는 함수예요. 다른 데이터베이스(MySQL 등)에는 없어요. 그래서 여러 DB에서 두루 쓰려면 표준인CASE를 써야 하는데, Oracle 환경에서 짧게 분기할 땐DECODE가 간결해서 실무에서도 자주 보여요. 이 둘의 관계는 바로 다음 Step에서 정리해요.
💡 한 줄 정리
DECODE(기준값, 조건, 결과, …, 기본값)은 값이 정확히 같은지로 분기하는 Oracle 고유 함수이며, 범위 비교는 못 하고 마지막 인자가 기본값이 된다.
🙋 학생 질문 — "튜터님, DECODE는 조건을 몇 개까지 늘어놓을 수 있어요?"
조건·결과 쌍을 필요한 만큼 이어 붙일 수 있어요. 등급이 다섯 단계면 다섯 쌍을 나열하고 마지막에 기본값을 두면 돼요. 다만 쌍이 많아지면 한 줄이 길어져서 읽기 어려워져요. 그리고 DECODE는 동등 비교만 되니, 조건이 "이 값들 중 하나"가 아니라 "어떤 범위"로 가기 시작하면 DECODE로는 표현이 안 돼요. 그럴 땐 미련 없이 CASE로 갈아타는 게 좋아요. 짧고 단순한 값 치환은 DECODE, 복잡해지면 CASE로 기억해 두세요.
Step 7: "CASE — 표준 조건 분기 (단순 · 검색)"
CASE는 DECODE의 모든 걸 하면서 범위·복합 조건까지 해내는 표준 분기 구문이에요. 모든 데이터베이스가 지원해요. 두 가지 모양이 있어요.
먼저 단순 CASE예요. DECODE처럼 한 값을 여러 후보와 동등 비교해요. CASE 기준값 WHEN 후보 THEN 결과 … ELSE 기본값 END 형태예요.
-- sql/queries/C3_null_conditional.sql
SELECT member_id,
CASE MOD(member_id, 2)
WHEN 0 THEN '짝수'
ELSE '홀수'
END AS 홀짝
FROM member
WHERE member_id <= 4;
| member_id | 홀짝 |
|---|---|
| 1 | 홀수 |
| 2 | 짝수 |
| 3 | 홀수 |
| 4 | 짝수 |
방금 DECODE로 했던 홀짝 가르기를 단순 CASE로 똑같이 한 거예요. WHEN 0 THEN '짝수', 나머지는 ELSE '홀수'로요.
이제 검색 CASE예요. 여기서 CASE의 진짜 힘이 나와요. WHEN 뒤에 조건식을 통째로 쓸 수 있어서, 범위 비교도 복합 조건도 돼요. CASE WHEN 조건 THEN 결과 … ELSE 기본값 END 형태예요. 가입 번호로 기수를 나눠 볼게요.
SELECT member_id,
CASE WHEN member_id <= 4 THEN '1기'
WHEN member_id <= 8 THEN '2기'
ELSE '3기'
END AS 가입기수
FROM member
WHERE member_id IN (2, 6, 11);
| member_id | 가입기수 |
|---|---|
| 2 | 1기 |
| 6 | 2기 |
| 11 | 3기 |
member_id <= 4 같은 범위 조건을 썼죠. DECODE로는 못 하던 일이에요. 위에서부터 차례로 조건을 보다가 처음 참이 되는 데서 멈춰요. 그래서 6번은 <= 4는 거짓, <= 8은 참이라 '2기'가 됐어요. 1기·2기·3기에 각각 4명씩 고르게 나뉘어요.
NULL도 조건으로 가를 수 있어요. 자기소개 유무로 프로필 완성도를 나눠 볼게요.
SELECT member_id, nickname,
CASE WHEN bio IS NULL THEN '미완성' ELSE '완성' END AS 프로필
FROM member
WHERE member_id IN (1, 3);
1번(자기소개 있음)은 '완성', 3번(없음)은 '미완성'이 나와요. IS NULL을 조건에 그대로 쓸 수 있어서, Step 3의 NVL2가 하던 일을 CASE로도 할 수 있어요.
이제 둘을 다 봤으니 DECODE와 CASE를 정리해요.
| 구분 | DECODE | CASE |
|---|---|---|
| 출신 | Oracle 고유 | ANSI 표준(모든 DB) |
| 비교 방식 | 동등 비교만 | 동등 + 범위 + 복합 조건 |
| 읽기 | 짧지만 쌍이 많으면 빡빡 | 길지만 조건이 또렷 |
| 추천 | 단순 값 치환 | 범위·복합 조건, 표준 필요 시 |
⚠️ 함정 —
CASE는 반드시END로 닫아야 해요.END를 빠뜨리면 문법 오류가 나요. 그리고ELSE를 생략하면, 어느 조건에도 안 맞는 행은 NULL이 돼요. "기타"를 따로 처리하고 싶으면ELSE를 꼭 넣어야 해요. ★빈출
💡 한 줄 정리
CASE는 동등 비교(단순)부터 범위·복합 조건(검색)까지 처리하는 표준 분기 구문으로, DECODE가 못 하는 범위 조건을 WHEN 조건 THEN 으로 해내며 반드시 END로 닫는다.
🙋 학생 질문 — "튜터님, 그럼 DECODE는 안 배워도 CASE만 쓰면 되는 거 아니에요?"
기능만 보면 CASE가 DECODE를 다 덮어요. 실제로 표준을 따르려면 CASE만 써도 돼요. 그런데 DECODE를 알아야 하는 이유가 둘 있어요. 하나는 SQLD 시험에 DECODE가 자주 나와요. Oracle 고유 문법이라 "이 DECODE를 CASE로 바꾸면?" 같은 문제가 단골이에요. 다른 하나는 실무에서 남이 짠 Oracle 쿼리를 읽을 때 DECODE가 흔하게 나와요. 내가 새로 짤 땐 CASE를 권하지만, DECODE를 읽고 이해하는 능력은 꼭 필요해요. "쓰는 건 CASE, 읽는 건 둘 다"로 생각하면 돼요.
Step 8: "NULL과 정렬 — NULLS FIRST / NULLS LAST"
오늘의 마지막이에요. NULL은 값을 채우거나 가를 때뿐 아니라 정렬에서도 한 번 더 짚어야 해요. 지난 모듈에서 ORDER BY로 줄을 세울 때, NULL이 섞인 컬럼은 어디로 갈지 헷갈린다고 예고했었죠. 오늘 그걸 풀어요.
자기소개로 정렬해 볼게요. 먼저 기본 오름차순이에요.
-- sql/queries/C3_null_conditional.sql
SELECT member_id, bio
FROM member
ORDER BY bio;
자기소개가 있는 8명이 가나다순으로 먼저 나오고, 그 뒤에 자기소개가 NULL인 4명(박승우·최도연·한나래·이민수)이 맨 아래에 모여요. 즉 오름차순(ASC)에서 Oracle은 NULL을 맨 뒤에 둬요.
ORDER BY bio (오름차순) — 값이 있는 회원이 먼저, NULL이 맨 뒤
┌ 자기소개 있는 8명 (가나다순)
│ 디자인하는 사람 · 맛집 탐험가 · 백엔드 공부 중 · …
│
└ 자기소개 없는 4명 (NULL) — 맨 아래로 모인다
박승우 · 최도연 · 한나래 · 이민수
내림차순으로 바꾸면 반대가 돼요.
SELECT member_id, bio
FROM member
ORDER BY bio DESC;
이번엔 자기소개가 NULL인 4명이 맨 위로 올라오고, 그 아래로 자기소개가 가나다 역순으로 나와요. 내림차순(DESC)에서 Oracle은 NULL을 맨 앞에 둬요. 정리하면, Oracle 기본값은 오름차순이면 NULL이 뒤, 내림차순이면 NULL이 앞이에요.
이 위치가 마음에 안 들면 직접 정할 수 있어요. NULLS FIRST(맨 앞)·NULLS LAST(맨 뒤)를 붙여요.
SELECT member_id, bio
FROM member
ORDER BY bio ASC NULLS FIRST;
오름차순인데도 NULL인 4명을 맨 위로 끌어올렸어요. 기본 오름차순은 NULL을 뒤로 보내지만, NULLS FIRST로 "비어 있는 회원부터 보여 줘"라고 강제한 거예요. 반대로 NULLS LAST를 붙이면 어느 정렬이든 NULL을 맨 뒤로 보내요.
⚠️ 함정 — Oracle의 기본 NULL 위치는 정렬 방향에 따라 달라요. 오름차순은 NULL이 뒤(
NULLS LAST가 기본), 내림차순은 NULL이 앞(NULLS FIRST가 기본)이에요. "오름차순이니 NULL이 위겠지" 하고 거꾸로 외우면 틀려요. 위치를 확실히 하려면NULLS FIRST·NULLS LAST를 직접 붙이는 게 안전해요. ★빈출
💡 한 줄 정리
Oracle은 오름차순에서 NULL을 맨 뒤, 내림차순에서 맨 앞에 두며, NULLS FIRST·NULLS LAST로 정렬 방향과 무관하게 NULL 위치를 직접 지정할 수 있다.
🙋 학생 질문 — "튜터님, 데이터베이스마다 NULL 정렬 위치가 다른가요?"
네, 달라요. Oracle은 오름차순일 때 NULL을 맨 뒤에 두지만, 어떤 데이터베이스(MySQL 등)는 오름차순일 때 NULL을 맨 앞에 둬요. 기본 동작이 반대인 거죠. 그래서 NULL이 섞인 컬럼으로 정렬할 때 "어느 DB에서나 똑같이" 동작하게 하려면, 기본값에 기대지 말고 NULLS FIRST·NULLS LAST를 명시하는 게 안전해요. 이런 DB마다 다른 기본 동작 차이는 마무리 모듈에서 방언을 비교할 때 다시 모아서 정리해요.
마무리
오늘은 빈 값 NULL을 길들이고, 조건에 따라 값을 갈래로 나눴어요. NULL이 0이나 빈 문자열과 다르다는 것부터 시작해서, NVL로 빈칸을 채우고, NVL2로 유무를 가르고, COALESCE로 여러 후보를 차례로 시도하고, NULLIF로 일부러 NULL을 만들었죠. 그다음 DECODE와 CASE로 값을 조건에 따라 바꾸고, 마지막엔 정렬에서 NULL이 어디 놓이는지까지 정했어요. 지난 시간에 '빈칸이라 곤란했던' 부분들을 오늘 거의 다 메운 셈이에요.
오늘 배운 핵심 세 가지
- 💡 하나 — NULL은 '값 없음'이라 0이나 빈 문자열과 다르고,
= NULL이 아니라IS NULL로 가려내며, 연산에 끼면 결과가 통째로 NULL이 된다. - 💡 둘 — NULL을 다루는 함수는
NVL(기본값으로),NVL2(유무로 두 갈래),COALESCE(여러 후보 중 첫 비-NULL),NULLIF(같으면 NULL)가 있다. - 💡 셋 — 값을 조건으로 분기할 땐 Oracle 고유
DECODE(동등 비교)와 표준CASE(범위·복합 조건)를 쓰고, 정렬의 NULL 위치는NULLS FIRST·NULLS LAST로 정한다.
다음 시간 예고
오늘 한 행씩 값을 가공하고 갈래를 나눴죠. 다음 시간엔 시야를 넓혀, 여러 행을 한꺼번에 모아 요약해요. "회원이 전부 몇 명인지", "게시물 좋아요를 다 더하면 얼마인지" 같은 집계예요. COUNT·SUM·AVG 같은 함수를 만나요. 여기서 오늘 배운 NULL이 또 한 번 중요해져요. COUNT(*)와 COUNT(컬럼)이 NULL을 다르게 센다는, 시험에 단골로 나오는 함정이 기다리고 있거든요. 오늘 NULL의 정체를 단단히 잡아 둔 게 다음 시간 집계에서 그대로 힘이 돼요.
과제
오늘 배운 NULL 처리와 조건 분기를 직접 써 보는 과제예요. 결과가 어떻게 나올지 먼저 예상한 뒤 실행해 확인해 보세요.
[기초] 빈 자기소개 채우기
회원 표에서 다음을 조회하는 SQL을 써 보세요. (가) NVL로 bio가 비어 있으면 '소개를 작성해 주세요'로 채워 보여 주세요. (나) NVL2로 자기소개가 있으면 '작성함', 없으면 '미작성'으로 갈라 보여 주세요. (다) bio가 NULL인 회원만 찾는 조건을 WHERE에 써 보세요. bio = NULL로 쓰면 왜 안 되는지도 한 줄로 적어 보세요.
[응용] 게시물 등급 매기기
게시물 표에서 다음을 해 보세요. (가) CASE로 post_id가 1부터 10이면 '초창기', 11부터 30이면 '성장기', 그 외는 '최근'으로 나눠 보여 주세요. (나) 같은 분류를 DECODE로도 시도해 보고, 왜 DECODE로는 깔끔하게 안 되는지 설명해 보세요. (다) 해시태그 없는 글에서 INSTR(caption, '#')이 0을 돌려주는데, NULLIF로 이 0을 NULL로 바꾼 뒤 NVL로 '해시태그 없음'을 표시하는 SQL을 완성해 보세요.
[심화] 표시 이름과 정렬 다루기
회원 표에서 다음을 해 보세요. (가) COALESCE로 표시 이름을 만들되, bio가 있으면 bio를, 없으면 nickname을, 그것도 없으면 '익명'을 쓰도록 해 보세요. (나) bio를 기준으로 오름차순 정렬하되, 자기소개가 없는 회원이 맨 위에 오도록 NULLS FIRST를 붙여 보세요. (다) NULLS FIRST를 빼면 자기소개 없는 회원이 어디에 놓이는지, 그리고 내림차순(DESC)으로 바꾸면 또 어떻게 달라지는지 예상해 보고 실행으로 확인해 보세요.
생각해볼 주제
1. NULL을 0으로 채우는 게 항상 좋을까
빈 값을 보면 NVL(값, 0)으로 0을 채우고 싶은 유혹이 들어요. 화면도 깔끔해지고요. 그런데 '값이 없음(NULL)'과 '값이 0'은 의미가 달라요. 자기소개가 없는 것과 자기소개에 '0'을 적은 것이 다르듯이요. 어떤 경우에 NULL을 0으로 채워도 괜찮고, 어떤 경우엔 그 0이 나중에 잘못된 계산이나 해석을 부를 수 있는지 생각해 보세요.
2. DECODE와 CASE, 무엇을 기준으로 고를까
같은 분기를 DECODE로도 CASE로도 할 수 있었어요. DECODE는 짧지만 Oracle 전용이고 동등 비교만 되며, CASE는 길지만 표준이고 범위까지 다뤄요. 짧음과 표준 준수, 단순함과 표현력 사이에서 무엇을 우선해야 할지, 그리고 "나중에 다른 데이터베이스로 옮길 수도 있다"는 가능성이 이 선택에 어떤 영향을 주는지 생각해 보세요.
3. NULL 정렬 위치를 명시해야 하는 이유
Oracle은 오름차순이면 NULL을 뒤에, 내림차순이면 앞에 두는 기본값이 있었어요. 편하지만, 다른 데이터베이스는 기본 동작이 다를 수 있어요. 같은 쿼리를 다른 DB에서 돌렸을 때 NULL 위치가 달라지면 어떤 문제가 생길지, 그리고 NULLS FIRST·NULLS LAST를 늘 명시하는 습관이 왜 안전한지 생각해 보세요.
✅ 예시 답안정답 보기
과제와 생각해볼 주제의 예시답안이에요. 정답을 그대로 베끼기보다, 왜 그 함수를 그렇게 쓰는지 흐름을 따라와 주세요. "빈 값을 어떻게 채울까", "이 조건을 어떻게 갈래로 나눌까"를 떠올리는 감각을 기르는 게 목표예요.
🎯 [과제 1 예시답안] 빈 자기소개 채우기
채점 포인트
| 항목 | 배점 | 핵심 |
|---|---|---|
(가) NVL로 빈칸 채우기 |
35% | NVL(bio, '소개를 작성해 주세요') |
(나) NVL2로 유무 가르기 |
35% | NVL2(bio, '작성함', '미작성') |
(다) IS NULL 조건 + 이유 |
30% | WHERE bio IS NULL · = NULL은 항상 거짓 |
풀이 예시
(가)·(나) 한 쿼리로
SELECT member_id, nickname,
NVL(bio, '소개를 작성해 주세요') AS 소개,
NVL2(bio, '작성함', '미작성') AS 상태
FROM member
WHERE member_id IN (1, 3, 8);
| member_id | nickname | 소개 | 상태 |
|---|---|---|---|
| 1 | 김재훈 | 백엔드 공부 중 | 작성함 |
| 3 | 박승우 | 소개를 작성해 주세요 | 미작성 |
| 8 | 한나래 | 소개를 작성해 주세요 | 미작성 |
NVL은 자기소개가 있으면 그 값을, 없으면 안내 문구를 보여 줘요. NVL2는 내용은 감추고 '작성함'·'미작성'으로만 갈라요. 전체로 보면 8명이 '작성함', 4명이 '미작성'으로 나뉘어요.
(다) NULL인 회원만 찾기
SELECT member_id, nickname FROM member WHERE bio IS NULL;
자기소개가 없는 4명(박승우·최도연·한나래·이민수)이 나와요. bio = NULL로 쓰면 안 되는 이유는, NULL은 '모름'이라 = NULL 비교가 참이 될 수 없어 한 행도 안 걸리기 때문이에요. NULL은 반드시 IS NULL로 가려내요.
💡 튜터의 한마디 — NVL과 NVL2를 한 줄에 나란히 써서, 같은 bio 하나로 '채운 값'과 '유무 표시'를 동시에 뽑았죠. 두 함수의 차이가 결과에서 또렷이 보여요. NVL은 원래 값을 살리고, NVL2는 유무만 남겨요. "내용을 살릴지, 유무만 볼지"로 둘을 구분하세요.
🎯 [과제 2 예시답안] 게시물 등급 매기기
채점 포인트
| 항목 | 배점 | 핵심 |
|---|---|---|
(가) 검색 CASE로 범위 분류 |
35% | WHEN post_id <= 10 THEN '초창기' |
(나) DECODE로는 안 되는 이유 |
30% | DECODE는 동등 비교만, 범위 불가 |
(다) NULLIF + NVL 체인 |
35% | 0 → NULL → 표시값, 형 맞추기 |
풀이 예시
(가) 검색 CASE로 등급 매기기
SELECT post_id,
CASE WHEN post_id <= 10 THEN '초창기'
WHEN post_id <= 30 THEN '성장기'
ELSE '최근'
END AS 등급
FROM post
WHERE post_id IN (5, 20, 45);
| post_id | 등급 |
|---|---|
| 5 | 초창기 |
| 20 | 성장기 |
| 45 | 최근 |
전체로 보면 초창기 10개, 성장기 20개, 최근 20개로 나뉘어요. 위에서부터 조건을 보다가 처음 참이 되는 데서 멈추니, 20번은 <= 10은 거짓, <= 30은 참이라 '성장기'가 돼요.
(나) DECODE로는 안 되는 이유
DECODE는 값이 정확히 같은지(동등)만 따질 수 있어요. "post_id가 1부터 10 사이"처럼 범위로 묶는 조건은 표현할 방법이 없어요. DECODE(post_id, 1, '초창기', 2, '초창기', …)처럼 1번부터 10번까지 일일이 나열해야 하는데, 게시물이 50개면 감당이 안 돼요. 범위 분류는 CASE의 몫이에요.
(다) NULLIF로 0을 NULL로, NVL로 표시
SELECT post_id,
NVL(TO_CHAR(NULLIF(INSTR(caption, '#'), 0)), '해시태그 없음') AS 샵위치
FROM post
WHERE post_id IN (1, 3, 12);
| post_id | 샵위치 |
|---|---|
| 1 | 10 |
| 3 | 해시태그 없음 |
| 12 | 8 |
INSTR이 돌려준 0을 NULLIF(…, 0)이 NULL로 바꾸고, 그 NULL을 NVL이 '해시태그 없음'으로 채웠어요. 여기서 TO_CHAR가 한 번 끼었죠. INSTR 결과는 숫자라서, 문자 '해시태그 없음'과 형을 맞추려면 숫자를 문자로 바꿔 줘야 하거든요. 안 그러면 NVL의 두 인자 형이 안 맞아 오류가 나요.
💡 튜터의 한마디 — (다)에서 함수 세 개가 이어진 게 핵심이에요. INSTR(위치 찾기) → NULLIF(0을 NULL로) → NVL(NULL을 표시값으로), 거기에 형을 맞추는 TO_CHAR까지요. 함수는 이렇게 결과를 다음 함수에 넘기며 사슬처럼 엮여요. 그리고 NVL은 두 인자의 형이 같아야 한다는 규칙을 잊으면, 멀쩡해 보이는 쿼리가 형 오류로 멈춰요.
🎯 [과제 3 예시답안] 표시 이름과 정렬 다루기
채점 포인트
| 항목 | 배점 | 핵심 |
|---|---|---|
(가) COALESCE 후보 체인 |
35% | COALESCE(bio, nickname, '익명') |
(나) NULLS FIRST 정렬 |
35% | ORDER BY bio NULLS FIRST |
| (다) 기본 위치 + DESC 변화 예측 | 30% | 기본 ASC는 NULL 뒤, DESC는 NULL 앞 |
풀이 예시
(가) COALESCE로 표시 이름 만들기
SELECT member_id, bio, nickname,
COALESCE(bio, nickname, '익명') AS 표시이름
FROM member
WHERE member_id IN (1, 3);
| member_id | bio | nickname | 표시이름 |
|---|---|---|---|
| 1 | 백엔드 공부 중 | 김재훈 | 백엔드 공부 중 |
| 3 | (없음) | 박승우 | 박승우 |
1번은 bio가 있으니 그걸 쓰고, 3번은 bio가 비어 다음 후보 nickname으로 내려갔어요. 둘 다 비었다면 마지막 '익명'이 쓰였을 거예요.
(나) NULLS FIRST로 빈 회원을 위로
SELECT member_id, bio
FROM member
ORDER BY bio ASC NULLS FIRST;
자기소개가 없는 4명이 맨 위로 올라오고, 그 아래로 자기소개가 있는 회원이 가나다순으로 나와요.
(다) NULLS FIRST를 빼면 · DESC로 바꾸면
NULLS FIRST를 빼면 기본 오름차순이라 자기소개 없는 4명이 맨 아래로 가요(Oracle 오름차순 기본은 NULL을 뒤로). 내림차순(ORDER BY bio DESC)으로 바꾸면, 이번엔 NULL인 4명이 맨 위로 올라와요(내림차순 기본은 NULL을 앞으로). 같은 데이터인데 정렬 방향만으로 NULL 위치가 정반대가 돼요.
💡 튜터의 한마디 — (다)가 오늘의 함정을 정면으로 묻는 문제예요. "오름차순이니 NULL이 앞이겠지"라고 생각하기 쉬운데, Oracle 기본은 오름차순일 때 NULL이 뒤예요. 방향에 따라 기본 위치가 뒤집히니, 헷갈리지 않으려면 NULLS FIRST·NULLS LAST를 직접 붙이는 습관이 가장 안전해요.
🤔 [생각해볼 주제 1] NULL을 0으로 채우는 게 항상 좋을까
문제 상황 요약
빈 값을 보면 NVL(값, 0)으로 0을 채우고 싶어져요. 화면도 깔끔해지고요. 그런데 '값이 없음(NULL)'과 '값이 0'은 의미가 달라요. 언제 0으로 채워도 되고, 언제 그 0이 나중에 문제를 부를까요?
튜터의 가이드 및 해설
기준은 "그 0이 계산이나 해석에 끼어들 여지가 있느냐"예요. 단순히 화면에 보여 주기만 한다면 0이든 '없음'이든 큰 문제는 없어요. 문제는 그 값으로 무언가를 계산할 때예요.
예를 들어 '아직 점수가 없는 학생'의 NULL을 0으로 채우면, 그 0이 평균을 끌어내려요. 시험을 안 본 건데 0점으로 취급돼 반 평균이 왜곡되죠. 반대로 NULL로 두면 그 학생은 평균 계산에서 빠져서, 시험을 본 학생들만의 평균이 정직하게 나와요. 그래서 "값이 없다"는 사실 자체가 의미를 가지는 곳에서는 함부로 0으로 덮으면 안 돼요.
반대로 "좋아요 수가 NULL이면 사실상 0개"처럼 NULL과 0이 같은 뜻인 경우엔 NVL(좋아요수, 0)이 자연스럽고 안전해요. 핵심은 "이 빈칸이 진짜 0인가, 아니면 모름인가"를 먼저 따지는 거예요.
🎯 SQLD는 이렇게 나온다
NVL의 결과를 예측하는 문제, 그리고 NULL이 연산에 끼면 결과가 NULL이 된다는 점이 단골이에요. NULL + 100의 결과(NULL), NVL(NULL, 0) + 100의 결과(100)를 비교하는 보기가 자주 나와요. NULL을 0으로 바꾸면 연산이 정상적으로 된다는 흐름을 묻는 거죠.
💡 실무에선
"빈칸의 의미를 먼저 정의한다"가 원칙이에요. 설계 단계에서 "이 컬럼의 NULL은 무슨 뜻인가(미입력인가, 0인가)"를 분명히 해 두면, 나중에 NVL로 채울지 NULL로 둘지가 자연스럽게 정해져요. 의미를 안 정하고 일단 0으로 덮으면, 그 0이 집계에 섞여 조용히 숫자를 틀리게 만드는 사고가 나요.
🤔 [생각해볼 주제 2] DECODE와 CASE, 무엇을 기준으로 고를까
문제 상황 요약
같은 분기를 DECODE로도 CASE로도 할 수 있었어요. DECODE는 짧지만 Oracle 전용이고 동등 비교만 되며, CASE는 길지만 표준이고 범위까지 다뤄요. 무엇을 기준으로 골라야 할까요?
튜터의 가이드 및 해설
첫 번째 기준은 "조건의 모양"이에요. "이 값이 정확히 A, B, C 중 하나"처럼 동등 비교로 끝나면 DECODE가 짧고 깔끔해요. 반면 "10보다 큰가", "1부터 30 사이인가"처럼 범위가 들어오면 DECODE로는 표현이 안 되니 CASE를 써야 해요. 조건이 복합적이 될수록 CASE로 기울어요.
두 번째 기준은 "이 쿼리가 어디서 돌 것인가"예요. DECODE는 Oracle에만 있어서, 나중에 다른 데이터베이스로 옮기면 그대로는 안 돌아가요. 여러 DBMS에서 두루 쓰일 가능성이 있다면 표준인 CASE가 안전해요. 반대로 Oracle 한곳에서만 쓰고 분기가 단순하면 DECODE의 간결함이 빛나요.
정리하면, 단순한 값 치환이고 Oracle 전용이면 DECODE, 범위·복합 조건이거나 표준이 필요하면 CASE예요. 새로 짤 땐 CASE를 기본으로 두되, DECODE를 읽고 이해하는 능력은 갖춰 두는 게 좋아요.
🎯 SQLD는 이렇게 나온다
DECODE를 동등한 CASE로 바꾸는 문제, 그 반대로 바꾸는 문제가 단골이에요. DECODE는 동등 비교만 가능하다는 점, 마지막 인자가 기본값이라는 점, CASE는 END로 닫아야 하고 ELSE가 없으면 NULL이 된다는 점이 자주 나와요. 특히 DECODE로 범위 조건을 표현할 수 있느냐(없다)를 묻는 보기를 조심하세요.
💡 실무에선
가독성과 이식성을 우선해요. 표준 CASE로 통일해 두면, 나중에 데이터베이스를 바꾸거나 다른 팀원이 읽을 때 마찰이 적어요. 다만 기존 Oracle 코드엔 DECODE가 워낙 많아서, 읽고 해석하는 능력은 실무 필수예요. "새로 쓰는 건 CASE, 읽는 건 둘 다"가 현실적인 균형이에요.
🤔 [생각해볼 주제 3] NULL 정렬 위치를 명시해야 하는 이유
문제 상황 요약
Oracle은 오름차순이면 NULL을 뒤에, 내림차순이면 앞에 두는 기본값이 있었어요. 편하지만, 다른 데이터베이스는 기본 동작이 다를 수 있어요. 기본값에 기대면 어떤 문제가 생길까요?
튜터의 가이드 및 해설
문제는 "환경이 바뀌면 결과가 조용히 달라진다"는 데 있어요. 같은 ORDER BY bio 쿼리를 Oracle에서 돌리면 NULL이 맨 뒤에 오는데, 어떤 데이터베이스에서는 같은 오름차순인데 NULL이 맨 앞에 와요. 쿼리는 한 글자도 안 바뀌었는데 화면에 보이는 순서가 뒤집히는 거예요.
이게 위험한 이유는 "에러가 안 나서"예요. 문법 오류면 바로 알아채는데, NULL 위치가 바뀌는 건 조용히 일어나요. 첫 페이지에 보여 줄 목록의 맨 위가 달라지거나, "상위 10건"의 구성이 바뀌어도 아무 경고가 없어요. 그래서 옮긴 뒤 한참 지나서야 "왜 순서가 이상하지" 하고 발견하게 돼요.
해결은 단순해요. NULL이 섞인 컬럼으로 정렬할 땐 기본값에 기대지 말고 NULLS FIRST·NULLS LAST를 직접 붙이는 거예요. 그러면 어느 데이터베이스에서 돌리든 NULL 위치가 똑같이 고정돼요. "방향만 정하지 말고 NULL 위치까지 정한다"가 안전한 습관이에요.
🎯 SQLD는 이렇게 나온다
ORDER BY에서 Oracle의 기본 NULL 위치(오름차순은 뒤, 내림차순은 앞)를 묻는 문제, NULLS FIRST·NULLS LAST의 동작을 묻는 문제가 나와요. "오름차순이면 NULL이 앞"처럼 거꾸로 적은 보기를 정답인 척 섞어 놓는 함정이 단골이에요. 방향별 기본 위치를 정확히 기억해 두세요.
💡 실무에선
"기본값에 기대지 않는다"가 좋은 습관이에요. 정렬뿐 아니라, 환경마다 다를 수 있는 동작은 코드에 명시해 두면 옮겨도 흔들리지 않아요. NULLS LAST 한 마디를 붙이는 작은 수고가, 데이터베이스를 옮기거나 버전을 올렸을 때 "왜 순서가 달라졌지"를 찾아 헤매는 큰 수고를 막아 줘요.