문서 읽는 데 44분 · E1

E-1: Oracle vs ANSI 방언 대조 + 기출 함정 — 어디서도 통하는 SQL 감각

목차 21
전체 24강 중 20강 · 데이터베이스
난이도 · 입문

ℹ️Oracle SQL로 SQLD 자격증을 대비하며 실전 SQL을 쌓는 트랙이에요. 프로그래밍 경험이 없어도 시작할 수 있고, 부트캠프에서는 자바 기초 다음·스프링 부트 직전에 배치하길 권해요.

안녕하세요, 홍순구 튜터입니다. 지난 시간엔 데이터를 "조회·분석" 하는 데서 한 걸음 나아가, 권한(GRANT/REVOKE)·뷰·시퀀스로 데이터를 안전하고 편하게 "운영" 하는 도구를 익혔어요. SQL 의 큰 줄기를 거의 다 훑은 셈이에요.

그런데 한 가지 짚고 넘어갈 게 있어요. 지난 시간 마무리에서 살짝 흘렸듯이, 지금까지 우리가 짠 SQL 은 전부 Oracle 문법이었어요. DECODE 로 조건을 나누고, NVL 로 빈칸을 메우고, (+) 로 외부 조인을 걸고, MINUS 로 차집합을 구하고, ROWNUM 으로 위에서 몇 줄만 떼어 냈죠.

그런데 같은 일을 다른 데이터베이스에서는 조금씩 다르게 적어요. Oracle 의 DECODE 를 표준 SQL 에선 CASE 로, NVLCOALESCE 로 쓰죠. 오늘은 지금까지 배운 SQL 을 표준(ANSI) 문법과 나란히 놓고 비교하면서, 어느 데이터베이스에서도 통하는 SQL 감각을 만들어요. SQLD 시험에서도 "이건 Oracle 고유 문법이냐, 표준이냐" 를 가르는 문제가 단골로 나와서, 오늘 정리를 잘해 두면 점수로 바로 이어져요.

텍스트
 오늘의 여정
   ① 왜 방언이 갈릴까 — 표준(ANSI)과 사투리
   ② NULL 메우기 — NVL vs COALESCE
   ③ 조건 분기 — DECODE vs CASE
   ④ 외부 조인 — (+) vs ANSI OUTER JOIN
   ⑤ 집합 빼기 — MINUS vs EXCEPT
   ⑥ 몇 줄만 — ROWNUM vs FETCH FIRST
   ⑦ 기출 빈출 함정 종합
   ⑧ 같은 인스타 쿼리를 두 방언으로

💡 오늘 수업의 핵심 — "Oracle 의 DECODE·NVL·(+)·MINUS·ROWNUM 을 표준(ANSI) CASE·COALESCE·OUTER JOIN·EXCEPT·FETCH FIRST 와 나란히 놓고, 방언 구분 문제와 빈출 함정을 한 번에 정리한다"

🎯 학습 목표

  • Oracle 고유 문법과 ANSI 표준 문법의 대응 관계를 안다. (SQLD 2과목, 방언 구분 ★빈출)
  • NULL 처리·조건 분기·외부 조인·집합·행 제한을 두 방언으로 자유롭게 옮긴다.
  • ROWNUM 정렬 함정 같은 기출 단골 함정을 분별하고, 왜 틀리는지 설명한다.

Step 1: "왜 방언이 갈릴까 — 표준(ANSI)과 사투리"

SQL 에는 국제 표준이 있어요. ANSI(미국 표준)와 ISO(국제 표준)가 정한 규격인데, SQL-92, SQL:1999, SQL:2008 처럼 연도를 붙여 부르죠. "SQL 은 이렇게 적자" 는 공통 약속이에요.

그런데 데이터베이스 제품들은 이 표준이 자리잡기 한참 전부터 각자 SQL 을 만들어 팔고 있었어요. Oracle 도 마찬가지예요. 표준이 나오기 전에 이미 DECODE·NVL·(+)·MINUS·ROWNUM 같은 자기만의 문법을 만들어 썼어요. 나중에 표준이 정해진 뒤에도, 기존 문법을 쓰던 수많은 코드를 깨뜨릴 수 없으니 고유 문법은 그대로 두고, 표준 문법도 추가로 지원하게 됐죠.

그래서 오늘날 Oracle 에서는 같은 일을 두 가지로 적을 수 있어요. 오래된 Oracle 사투리, 그리고 나중에 받아들인 표준어. 표준어는 다른 데이터베이스(MySQL·PostgreSQL·SQL Server)에서도 거의 그대로 통해요.

텍스트
 Oracle 이 표준(ANSI)을 받아들인 순서
   ~8i   고유 문법 시대 ─ DECODE · NVL · (+) · MINUS · ROWNUM
    9i   ANSI 표준 조인 ─ LEFT/RIGHT/FULL OUTER JOIN ... ON
   12c   FETCH FIRST n ROWS ONLY ─ 표준 행 제한
   21c   EXCEPT ─ MINUS 의 표준 이름

CASE·COALESCE 같은 표준 함수도 오래전부터 함께 지원해 왔어요. 정리하면, Oracle 고유 문법은 Oracle 안에서만, 표준 문법은 어디서나 통한다고 보면 돼요.

⚠️ SQLD 시험은 "이 문법이 Oracle 고유인지, 표준(ANSI)인지" 를 묻는 문제가 ★빈출이에요. 특히 DECODE·NVL·MINUS·(+)·ROWNUM 다섯은 Oracle 고유 라고 기억해 두면 분별 문제에서 막히지 않아요.

💡 한 줄 정리 — SQL 은 표준(ANSI)이 있지만, Oracle 은 표준 이전의 고유 문법과 나중에 받아들인 표준 문법을 함께 지원한다. 고유 문법은 Oracle 안에서만, 표준 문법은 어디서나 통한다.

🙋 학생 질문 — "튜터님, 그럼 둘 중 어느 쪽으로 외워야 하나요?"

둘 다 알아 두는 게 맞아요. 이유가 나뉘어요. 시험 에서는 "이건 Oracle 고유" 같은 분별을 묻기 때문에 양쪽을 다 알아야 풀 수 있어요. 실무 에서는 나중에 다른 데이터베이스로 옮길 일을 대비해 가능하면 표준 문법을 쓰는 게 좋아요. 그래야 코드를 거의 그대로 옮길 수 있거든요. 그래서 "표준을 기본으로 쓰되, Oracle 고유 문법도 읽고 분별할 줄 안다" 가 목표예요.


Step 2: "NULL 메우기 — NVL vs COALESCE"

C-3 에서 빈 값(NULL)을 기본값으로 메우는 NVL 을 배웠죠. 우리 회원 12명 중 자기소개(bio)가 비어 있는 사람이 4명(회원 3·5·8·10)이었어요. 이 빈 소개를 "소개 없음" 으로 채워 보면, Oracle 방식과 표준 방식이 이렇게 갈려요.

SQL
-- Oracle 고유: NVL(값, 대체값)
SELECT member_id, NVL(bio, '소개 없음') AS profile FROM member;

-- ANSI 표준: COALESCE(값, 대체값, ...)
SELECT member_id, COALESCE(bio, '소개 없음') AS profile FROM member;

두 쿼리는 완전히 같은 결과 를 내요. 빈 소개를 가진 네 사람이 똑같이 "소개 없음" 으로 채워져요.

member_id profile
3 소개 없음
5 소개 없음
8 소개 없음
10 소개 없음

그럼 둘은 뭐가 다를까요? NVL 은 인자를 딱 두 개 받아요. "이 값이 NULL 이면 저 값으로." COALESCE 는 인자를 여러 개 받아서, 앞에서부터 훑어 처음으로 NULL 이 아닌 값 을 돌려줘요. 그러니까 COALESCENVL 의 상위 호환인 셈이에요.

SQL
-- bio 가 없으면 nickname, 그것도 없으면 '익명' — 폴백을 여러 단계로
SELECT member_id, COALESCE(bio, nickname, '익명') AS profile FROM member;

NVL 로는 이렇게 세 단계 폴백을 한 번에 못 적어요(중첩해야 해요). SQL Server 에는 ISNULL(값, 대체값) 이라는 비슷한 함수가 있는데, 이것도 인자 두 개짜리라 NVL 과 닮았어요. MySQL 의 IFNULL 도 마찬가지인데, 이건 MySQL 로 옮기는 다음 과목에서 따로 다뤄요.

⚠️ NVLCOALESCE 모두 대체값의 타입이 원래 컬럼과 맞아야 해요. 숫자 컬럼에 문자 기본값을 넣으려다 형변환 에러가 나는 게 SQLD 단골 함정이에요. COALESCE 가 인자를 여러 개 받는다는 점도 자주 출제돼요.

💡 한 줄 정리NVL(Oracle, 2인자)과 COALESCE(ANSI, N인자)는 NULL 을 기본값으로 메운다. 같은 일이면 결과가 동일하고, 폴백을 여러 단계로 줄 땐 COALESCE 가 유리하다.

🙋 학생 질문 — "튜터님, C-3 에서 배운 NVL2 나 NULLIF 도 방언이 다른가요?"

NVL2(값이 NULL이냐 아니냐로 두 갈래)는 Oracle 고유예요. 표준에는 같은 함수가 없어서, 옮길 땐 CASE WHEN col IS NULL THEN ... ELSE ... END 로 풀어요. 반대로 NULLIF(두 값이 같으면 NULL)는 표준 함수 라서 Oracle·표준 양쪽에서 같은 이름으로 통해요. 그래서 C-3 에서 본 네 형제 중 COALESCE·NULLIF 는 표준, NVL·NVL2 는 Oracle 고유로 갈린다고 기억하면 깔끔해요.


Step 3: "조건 분기 — DECODE vs CASE"

값에 따라 갈래를 나누는 일도 두 방언이 달라요. C-3 에서 회원 번호가 짝수냐 홀수냐를 DECODECASE 로 나눠 봤죠. 회원 12명을 짝/홀로 가르면 각각 6명씩이에요.

SQL
-- Oracle 고유: DECODE(기준, 값1, 결과1, 값2, 결과2, ..., 기본값)
SELECT member_id, DECODE(MOD(member_id, 2), 0, '짝수', '홀수') AS parity
FROM member;

-- ANSI 표준: CASE WHEN 조건 THEN 결과 ... ELSE 기본값 END
SELECT member_id,
       CASE WHEN MOD(member_id, 2) = 0 THEN '짝수' ELSE '홀수' END AS parity
FROM member;

이 둘도 결과가 똑같아요. 그런데 둘의 성격은 꽤 달라요.

구분 DECODE (Oracle 고유) CASE (ANSI 표준)
비교 방식 동등(=) 비교만 동등·범위(<,BETWEEN)·복합 조건 다 됨
형태 함수 식(expression)
이식성 Oracle 전용 표준 — 어디서나
가독성 짧지만 갈래 많아지면 헷갈림 조건이 한눈에 보임

DECODE 는 "이 값이면 이것, 저 값이면 저것" 처럼 딱 떨어지는 동등 비교 만 돼요. "4 이하면 1기, 5~8이면 2기" 같은 범위 조건은 DECODE 로 못 적고 CASE 로만 적을 수 있어요.

여기서 시험 단골 함정이 하나 나와요. DECODENULL 과 NULL 을 "같다" 로 봐요. 그런데 일반 비교 연산자 = 는 절대 그러지 않아요(NULL 비교는 늘 "알 수 없음" 이라 거짓 취급). B-5 에서 배운 NULL 의 3치 논리, 기억나시죠?

SQL
-- DECODE 는 NULL=NULL 을 매칭 / CASE 의 = 는 매칭 안 됨
SELECT DECODE(NULL, NULL, 'matched', 'no')                  AS decode_result,
       CASE WHEN NULL = NULL THEN 'matched' ELSE 'else' END AS case_result
FROM dual;

실행하면 decode_resultmatched, case_resultelse 가 나와요. 같은 의도를 DECODE 에서 CASE 로 옮길 때, NULL 을 다루는 부분은 CASE WHEN col IS NULL THEN ... 처럼 명시해 줘야 결과가 안 어긋나요.

⚠️ DECODE(a, b, ...) 에서 ab 가 둘 다 NULL 이면 "같다" 로 매칭돼요. CASE WHEN a = b 는 둘이 NULL 이어도 절대 참이 안 돼요. 이 차이를 묻는 문제가 ★빈출이에요.

💡 한 줄 정리DECODE(Oracle)는 동등 비교 전용이고 NULL=NULL 을 같다고 본다. CASE(ANSI)는 범위·복합 조건까지 되고 어디서나 통한다. 옮길 땐 NULL 처리를 IS NULL 로 명시한다.

🙋 학생 질문 — "튜터님, 그럼 DECODE 는 이제 안 써도 되나요?"

실무에서 다른 데이터베이스로 옮길 가능성이 있거나, 범위 조건이 끼면 CASE 가 나아요. 그래도 DECODE 를 알아 둬야 하는 이유가 있어요. 첫째, 오래된 Oracle 코드에는 DECODE 가 셀 수 없이 많아서 읽을 줄 알아야 유지보수를 해요. 둘째, 단순한 동등 분기라면 DECODE 가 더 짧아 손이 갈 때가 있어요. 그러니 "새로 짤 땐 CASE, 읽을 땐 DECODE 도 척척" 으로 가면 돼요.


Step 4: "외부 조인 — Oracle (+) vs ANSI OUTER JOIN"

C-5 에서 조인을 배울 때, 게시물이 한 개도 없는 회원(7·8·10번)도 빠뜨리지 않으려면 외부 조인(OUTER JOIN)이 필요하다고 했죠. 이 외부 조인에서 Oracle 사투리와 표준어가 가장 크게 갈려요.

SQL
-- ANSI 표준: 회원을 다 남기고, 게시물이 없으면 NULL 로
SELECT m.member_id, p.post_id
FROM member m LEFT JOIN post p ON m.member_id = p.member_id;

-- Oracle 고유: 비어 있을 수 있는 쪽에 (+) 를 붙인다
SELECT m.member_id, p.post_id
FROM member m, post p
WHERE m.member_id = p.member_id(+);

두 쿼리 모두 53행 을 돌려줘요. 게시물 50건에, 게시물이 0개라 평소 조인에서 사라지던 회원 3명(7·8·10)이 NULL 을 달고 살아남아 더해진 결과예요.

텍스트
 LEFT OUTER JOIN (회원 기준)
   · 게시물 있는 회원   자기 게시물과 짝지어짐 (50건)
   · 게시물 0개 회원    post 를 NULL 로 채워 살아남음 (회원 7·8·10, +3건)
   · 합계 = 53행

 Oracle 표기:  WHERE m.member_id = p.member_id(+)
    (+) 는 "비어 있을 수 있는" post 쪽에 붙인다 (= 부족하면 NULL 로 채워 줄 쪽)

(+) 를 어느 쪽에 붙이는지가 헷갈리는 함정인데, "없을 수 있어서 NULL 로 채워 줄 쪽" 에 붙인다고 기억하세요. 회원은 다 남기고 게시물이 없을 수 있으니, post 쪽 조건에 (+) 가 붙어요.

문제는 Oracle 의 (+)한계가 많다 는 거예요. 그래서 Oracle 도 공식적으로 ANSI 조인을 권장해요.

(+) 가 못 하는 것 ANSI OUTER JOIN
FULL OUTER(양쪽 다 남기기) 불가 FULL OUTER JOIN 으로 가능
한 쿼리에서 양쪽에 동시 (+) 불가 자유롭게 가능
IN·OR 과 함께 쓰면 에러 제약 없음

⚠️ (+) 는 Oracle 고유예요. 표준 LEFT/RIGHT/FULL OUTER JOIN 과 짝을 맞춰 외워 두세요. "왼쪽을 다 남기면 LEFT, 그건 Oracle 로 오른쪽에 (+)" 같은 대응을 묻는 문제가 ★빈출이에요.

💡 한 줄 정리 — Oracle (+) 와 ANSI OUTER JOIN 은 같은 외부 조인이지만, (+) 는 FULL·양쪽·IN 에서 막힌다. 표준 OUTER JOIN 이 더 강력하고 어디서나 통한다.

🙋 학생 질문 — "튜터님, (+) 를 양쪽에 다 붙이면 FULL OUTER 가 되지 않나요?"

안 돼요. Oracle 은 한 조인 조건의 양쪽에 동시에 (+) 를 붙이는 걸 허용하지 않아요(에러가 나요). 그래서 "회원도 다 남기고 게시물도 다 남기는" FULL OUTER JOIN 은 옛날 Oracle 에선 UNION 으로 두 쿼리를 이어 붙여 흉내 내야 했어요. 표준 FULL OUTER JOIN 한 줄이면 끝나는 일을요. 이럴 때 표준 문법의 위력이 확 드러나요.


Step 5: "집합 빼기 — MINUS vs EXCEPT"

D-1 에서 집합 연산자를 배웠죠. 두 결과의 차집합을 구하는 게 Oracle 에선 MINUS 였어요. "짝사랑" — 내가 팔로우하지만 나를 팔로우하지 않는 사람을 구할 때 썼죠.

예시로 회원 4번을 보면, 4번은 1번과 2번을 팔로우해요. 그런데 4번을 팔로우하는 건 2번뿐이에요. 그러니까 4번에게 2번은 맞팔, 1번은 짝사랑인 거예요.

SQL
-- Oracle 고유: 내가 팔로우 - 나를 팔로우 = 짝사랑
SELECT following_id FROM follow WHERE follower_id = 4
MINUS
SELECT follower_id FROM follow WHERE following_id = 4;

-- ANSI 표준 (Oracle 21c+): EXCEPT — 결과 동일
SELECT following_id FROM follow WHERE follower_id = 4
EXCEPT
SELECT follower_id FROM follow WHERE following_id = 4;

두 쿼리 모두 짝사랑 상대로 1번 한 명을 돌려줘요. MINUSEXCEPT 는 이름만 다르고 하는 일이 똑같아요.

집합 연산자 네 갈래를 방언으로 정리하면 이래요.

연산 Oracle ANSI 표준 다른 DBMS
합집합 UNION / UNION ALL UNION / UNION ALL 동일
교집합 INTERSECT INTERSECT 동일
차집합 MINUS EXCEPT SQL Server·PostgreSQL = EXCEPT

UNION·INTERSECT 는 Oracle 과 표준이 같은 이름을 써요. 오직 차집합만 Oracle 은 MINUS, 표준은 EXCEPT 로 갈려요. 이 하나만 콕 집어 기억하면 돼요.

⚠️ EXCEPT 는 Oracle 에서 21c 부터 쓸 수 있어요. 그 전 버전(19c 등)에서는 EXCEPT 가 없어서 MINUS 만 통했어요. 우리 실습 환경(26ai)에서는 둘 다 동작해요. 표준 이름은 EXCEPT 라는 점이 시험에 자주 나와요.

💡 한 줄 정리 — 차집합은 Oracle MINUS, 표준 EXCEPT 다(이름만 다르고 결과는 같다). UNION·INTERSECT 는 양쪽 이름이 같다. 차집합 하나만 갈린다고 기억한다.

🙋 학생 질문 — "튜터님, 시험엔 MINUS 가 나오나요 EXCEPT 가 나오나요?"

둘 다 나올 수 있어요. SQLD 는 Oracle 기준이 강해서 MINUS 가 보기에 자주 등장하지만, "표준 SQL 의 차집합 연산자는?" 처럼 EXCEPT 를 정답으로 묻는 문제도 나와요. 그러니 "Oracle = MINUS, 표준 = EXCEPT, 결과는 같다" 를 묶어서 외워 두면 어느 쪽으로 물어도 흔들리지 않아요. 참고로 SQL Server·PostgreSQL 같은 다른 데이터베이스는 EXCEPT 만 있고 MINUS 는 없어요.


Step 6: "몇 줄만 — ROWNUM vs FETCH FIRST vs TOP/LIMIT"

D-6 에서 인기글 Top-N 을 뽑을 때 ROWNUMFETCH FIRST 를 썼죠. 좋아요가 가장 많은 게시물 3개를 뽑으면 post 12(6개)·1(5개)·40(4개)이 나와요. 이 "위에서 몇 줄만" 도 방언이 갈려요.

SQL
-- Oracle 고유: ROWNUM — 반드시 '정렬된 인라인 뷰' 바깥에서 건다
SELECT post_id, likes FROM (
  SELECT post_id, COUNT(*) AS likes
  FROM post_like GROUP BY post_id
  ORDER BY COUNT(*) DESC, post_id
) WHERE ROWNUM <= 3;

-- ANSI 표준 (Oracle 12c+): FETCH FIRST — 훨씬 직관적
SELECT post_id, COUNT(*) AS likes
FROM post_like GROUP BY post_id
ORDER BY COUNT(*) DESC, post_id
FETCH FIRST 3 ROWS ONLY;

두 쿼리 모두 post 12·1·40 을 돌려줘요. 그런데 FETCH FIRST 쪽이 훨씬 읽기 쉽죠? ORDER BY 뒤에 "앞 3줄만" 이라고 붙이면 끝이에요. ROWNUM 은 반드시 정렬을 먼저 끝낸 인라인 뷰(괄호 안)를 만들고, 그 바깥에서 줄 수를 잘라야 해요. 왜 그래야 하는지는 다음 Step 함정에서 자세히 봐요.

다른 데이터베이스는 또 다르게 적어요.

방식 문법 DBMS
ROWNUM 정렬된 인라인 뷰 바깥에서 WHERE ROWNUM <= 3 Oracle 고유
FETCH FIRST ORDER BY ... FETCH FIRST 3 ROWS ONLY Oracle 12c+ · ANSI 표준
TOP SELECT TOP 3 ... SQL Server
LIMIT ... LIMIT 3 MySQL · PostgreSQL

TOPLIMIT 은 개념만 알아 두세요. 특히 LIMIT 은 MySQL 로 쿼리를 옮기는 다음 과목에서 페이지네이션과 함께 자세히 다뤄요. 오늘은 Oracle 과 표준(FETCH FIRST)의 대조가 중심이에요.

⚠️ ROWNUM 은 Oracle 고유 의사컬럼(pseudo-column)이에요. 표준 FETCH FIRST 와 짝을 이뤄요. "행을 몇 개로 제한" 을 묻는데 보기에 ROWNUM·FETCH FIRST·TOP·LIMIT 이 섞여 나오면 어느 DBMS 것인지 분별하는 문제예요.

💡 한 줄 정리 — 행 제한은 Oracle ROWNUM(정렬 뷰 바깥에서), 표준 FETCH FIRST(ORDER BY 뒤에 붙임)다. 표준 쪽이 직관적이고, TOP(SQL Server)·LIMIT(MySQL/PG)은 또 다른 방언이다.

🙋 학생 질문 — "튜터님, LIMIT 을 왜 여기서 깊게 안 다루나요?"

LIMIT 은 MySQL·PostgreSQL 의 방언이라, Oracle 과 표준을 정리하는 오늘 수업의 중심에서는 살짝 비켜나 있어요. 다음 과목에서 우리가 짠 Oracle 쿼리를 MySQL 로 옮길 때, ROWNUM/FETCH FIRSTLIMIT 변환과 함께 피드 페이지네이션(무한 스크롤)을 본격적으로 다뤄요. 오늘은 "표준은 FETCH FIRST, 다른 방언도 있다" 정도로 큰 지도만 잡아 두면 충분해요.


Step 7: "기출 빈출 함정 종합"

이제 방언을 옮길 때 발을 헛디디기 쉬운 함정들을 모아 볼게요. 오늘 배운 것 중 시험에 가장 자주 나오는 함정은 단연 ROWNUMORDER BY 의 순서 예요.

좋아요 Top-3 를 뽑겠다고 이렇게 적으면 어떻게 될까요?

SQL
-- 틀린 쿼리 — 정렬 '전에' ROWNUM 으로 잘라 버린다
SELECT post_id, likes FROM (
  SELECT post_id, COUNT(*) AS likes
  FROM post_like GROUP BY post_id
) WHERE ROWNUM <= 3
ORDER BY likes DESC;

언뜻 맞아 보이지만, 결과는 post 1(5개)·5(3개)·2(1개) 가 나와요. 진짜 1등인 post 12(좋아요 6개)가 통째로 사라졌어요! 왜 이런 일이 벌어질까요?

텍스트
 ROWNUM 함정 — ROWNUM 은 ORDER BY '전에' 매겨진다
    정렬 안 된 그룹 결과:  post 1 · 2 · 5 · 12 · 40 · ...
    WHERE ROWNUM <= 3   :  앞 3행만 싹둑       post 1 · 2 · 5
    ORDER BY likes DESC :  그 3행만 정렬        post 1(5) · 5(3) · 2(1)

   진짜 1등 post 12(6개) 는 에서 이미 잘려 나가 사라졌다.

ROWNUMWHERE 단계에서 결과 행에 1, 2, 3... 번호를 매기는데, 이게 ORDER BY 보다 먼저 일어나요. 그래서 정렬되지 않은 임의의 앞 3행을 먼저 잘라 버리고, 그 3행만 정렬하니 진짜 Top-3 와 무관한 결과가 나와요. 그래서 Step 6 처럼 정렬을 끝낸 인라인 뷰를 먼저 만들고, 그 바깥에서 ROWNUM 을 걸어야 하는 거예요. FETCH FIRST 는 이 함정이 아예 없어서 더 안전하죠.

함정은 이것 말고도 오늘 곳곳에 숨어 있었어요. 한곳에 모아 둘게요.

함정 무엇이 함정인가 안전한 길
ROWNUM + ORDER BY 정렬 전에 잘려 엉뚱한 Top-N 정렬된 인라인 뷰 바깥에서 ROWNUM, 또는 FETCH FIRST
DECODE 의 NULL NULL=NULL 을 "같다" 로 매칭 CASE WHEN col IS NULL 로 명시
(+) 한계 FULL·양쪽·IN 에서 막힘 ANSI OUTER JOIN
NOT IN + NULL 목록에 NULL 끼면 결과가 공집합 NOT EXISTS 또는 IS NOT NULL (D-2 회수)
암시적 형변환 '10' + 5 처럼 문자↔숫자 자동 변환 타입을 명시해 비교(TO_NUMBER 등)

⚠️ ROWNUM 정렬 함정은 SQLD 에서 "이 쿼리의 결과는?" 으로 자주 물어요. ROWNUMORDER BY 보다 먼저 매겨진다는 한 가지 원리만 잡으면, 보기에서 함정 쿼리를 골라낼 수 있어요.

💡 한 줄 정리ROWNUMORDER BY 보다 먼저 매겨지므로 정렬 전에 자르면 엉뚱한 Top-N 이 나온다. 정렬된 인라인 뷰 바깥에서 걸거나 FETCH FIRST 를 쓴다. DECODE NULL·(+) 한계·NOT IN+NULL 도 단골 함정이다.


Step 8: "같은 인스타 쿼리를 두 방언으로"

오늘 배운 대조를 한 장으로 정리할게요. 같은 일을 Oracle 사투리와 표준어로 어떻게 적는지 나란히 놓은 표예요. 시험 직전에 이 표만 떠올려도 방언 문제는 거의 풀려요.

하는 일 Oracle 고유 ANSI 표준
NULL 메우기 NVL(a, b) COALESCE(a, b, ...)
조건 분기 DECODE(x, ...) CASE WHEN ... END
외부 조인 (+) LEFT/RIGHT/FULL OUTER JOIN
차집합 MINUS EXCEPT
행 제한 ROWNUM FETCH FIRST ... ROWS ONLY

마지막으로, 실제 인스타 쿼리 하나를 두 방언으로 끝까지 적어 볼게요. "좋아요가 가장 많은 인기글 3개" 를 뽑는 같은 리포트예요.

SQL
-- Oracle 사투리 (ROWNUM)
SELECT post_id, likes FROM (
  SELECT post_id, COUNT(*) AS likes
  FROM post_like GROUP BY post_id
  ORDER BY COUNT(*) DESC, post_id
) WHERE ROWNUM <= 3;

-- 표준어 (FETCH FIRST) — 같은 결과 post 12·1·40
SELECT post_id, COUNT(*) AS likes
FROM post_like GROUP BY post_id
ORDER BY COUNT(*) DESC, post_id
FETCH FIRST 3 ROWS ONLY;

둘 다 post 12·1·40 을 돌려주는, 결과가 완전히 같은 쿼리예요. 다만 표준어 쪽이 짧고 함정도 없죠. 이렇게 "Oracle 로 짠 쿼리를 표준으로 옮길 수 있고, 그 반대도 읽을 수 있는" 감각이 오늘의 수확이에요.

💡 한 줄 정리 — 같은 일을 Oracle 고유와 ANSI 표준 두 방언으로 적을 수 있다. 표준 쪽이 더 짧고 이식성이 좋지만, Oracle 고유 문법도 읽고 분별할 줄 알아야 한다.


마무리

오늘은 지금까지 Oracle 문법으로만 짜 온 SQL 을, 표준(ANSI) 문법과 나란히 놓고 비교했어요. NULL 을 메우는 NVLCOALESCE, 갈래를 나누는 DECODECASE, 외부 조인의 (+)OUTER JOIN, 차집합의 MINUSEXCEPT, 행 제한의 ROWNUMFETCH FIRST 까지, 다섯 쌍을 한 번에 정리했죠. 그리고 ROWNUM 정렬 함정 같은 기출 단골을 분별하는 눈도 길렀어요.

오늘 배운 핵심 세 가지

  • 💡 방언의 갈래 — Oracle 고유(DECODE·NVL·(+)·MINUS·ROWNUM)는 Oracle 안에서만, 표준(CASE·COALESCE·OUTER JOIN·EXCEPT·FETCH FIRST)은 어디서나 통한다.
  • 💡 결과는 같다 — 같은 일을 두 방언으로 적어도 결과는 동일하다. 표준 쪽이 더 짧고 이식성이 좋다.
  • 💡 함정 분별ROWNUMORDER BY 보다 먼저 매겨진다. DECODE 의 NULL=NULL, (+) 의 한계도 단골 함정이다.

다음 시간 예고

이제 SQL 문법은 모델링부터 윈도우 함수, 방언 대조까지 한 바퀴를 다 돌았어요. 다음 시간엔 그동안 배운 걸 총동원해서, SQLD 모의고사 로 실전 감각을 만들어요. 50문항을 정해진 시간 안에 푸는 호흡, 헷갈리는 오답 보기를 걸러내는 법, 과목별 시간 배분과 과락을 막는 전략까지 — 시험장에서 바로 통하는 마무리를 준비해요.


과제

오늘 배운 방언 대조를 직접 손으로 옮겨 보는 과제예요. 쿼리를 실행하기 전에 "두 방언의 결과가 같을지" 를 먼저 머릿속으로 그려 본 뒤, 실행해서 확인해 보세요.

[기초] Oracle 문법을 표준으로 옮기기

다음 Oracle 쿼리를 표준(ANSI) 문법으로 바꿔 보세요. (가) NVL(bio, '비공개') 를 쓰는 쿼리를 COALESCE 로 옮기세요. (나) DECODE(MOD(member_id, 2), 0, '짝', '홀')CASE 로 옮기세요. (다) 두 경우 모두 원래 쿼리와 결과가 같은지 실행해 확인하고, COALESCE/CASENVL/DECODE 보다 나은 점을 한 줄로 적어 보세요.

[응용] 외부 조인과 차집합을 두 방언으로

(가) "회원별 게시물 수(게시물 0개 회원도 포함)" 를 Oracle (+) 방식과 ANSI LEFT OUTER JOIN 방식 두 가지로 짜고, 행 수가 같은지 확인하세요. (나) "회원 4번의 짝사랑(내가 팔로우하지만 나를 안 따라준 사람)" 을 MINUSEXCEPT 두 방식으로 짜고 결과가 같은지 확인하세요. (다) (+)FULL OUTER JOIN 을 못 한다는 점을 떠올려, 표준 OUTER JOIN 이 나은 이유를 한 줄로 정리하세요.

[심화] 함정 쿼리를 찾아 고치기

아래 쿼리는 "좋아요 Top-3" 를 뽑으려 했지만 결과가 틀려요.

SQL
SELECT post_id, likes FROM (
  SELECT post_id, COUNT(*) AS likes
  FROM post_like GROUP BY post_id
) WHERE ROWNUM <= 3
ORDER BY likes DESC;

(가) 이 쿼리를 실행하면 진짜 1등 게시물이 빠지는데, 왜 그런지 ROWNUMORDER BY 의 순서로 설명하세요. (나) 같은 결과를 올바르게 내도록 ROWNUM 방식으로 고치세요. (다) 같은 일을 FETCH FIRST 로도 짜고, 어느 쪽이 함정이 없는지 한 줄로 적어 보세요.


생각해볼 주제

1. 표준이 있는데 왜 Oracle 고유 문법이 아직도 남아 있을까

SQL 에는 ANSI 표준이 있고, 표준으로 통일하면 어느 데이터베이스로든 옮기기 쉬워요. 그런데도 Oracle 은 DECODE·NVL·(+) 같은 고유 문법을 없애지 않고 그대로 둬요. 표준만 남기면 깔끔할 텐데, 왜 옛 문법을 계속 지원할까요? "이미 그 문법으로 짜인 수십 년치 코드" 라는 점과, "새 문법으로 바꾸려면 누가 그 비용을 치르나" 를 함께 떠올려 보세요.

2. DECODE 가 NULL=NULL 을 "같다" 로 보는 건 버그일까 설계일까

일반 비교 = 는 NULL=NULL 을 절대 참으로 안 보는데, DECODE 는 같다고 봐요. 같은 데이터베이스 안에서 NULL 을 다루는 규칙이 둘로 갈리는 셈이죠. 이게 일관성을 깨는 실수일까요, 아니면 의도된 편의일까요? DECODE 가 주로 "값을 분류해 라벨을 붙이는" 데 쓰인다는 점을 떠올려, NULL 도 하나의 분류로 잡아 주는 게 편한 상황을 그려 보세요.

3. 실무에서 쿼리를 표준으로 짜 둘지, 그 DB 에 맞춰 짤지

새 프로젝트에서 Oracle 을 쓴다면, ROWNUM·(+) 같은 Oracle 고유 문법으로 짧고 빠르게 짤 수도 있고, 나중을 대비해 FETCH FIRST·OUTER JOIN 같은 표준으로 짤 수도 있어요. "이 회사는 앞으로도 Oracle 만 쓸 것 같다" 와 "언젠가 다른 데이터베이스로 옮길지 모른다" 중 어느 쪽을 가정하느냐에 따라 선택이 갈려요. 두 경우에 각각 어떤 선택이 합리적일지, 그리고 그 가정이 틀렸을 때 치를 대가까지 견줘 보세요.

✅ 예시 답안정답 보기

과제와 생각해볼 주제의 예시답안이에요. 정답을 그대로 베끼기보다 "이건 Oracle 고유 문법인가 표준인가", "두 방언의 결과가 정말 같은가", "이 쿼리가 왜 틀렸는가" 를 떠올리는 감각을 기르는 게 목표예요.


🎯 [과제 1 예시답안] Oracle 문법을 표준으로 옮기기

채점 포인트

항목 배점 핵심
(가) NVLCOALESCE 30% 인자 두 개를 그대로 옮기고 결과 동일
(나) DECODECASE 35% WHEN ... THEN ... ELSE ... END 형태
(다) 결과 동일 확인 + 표준의 장점 35% 이식성·N개 인자·범위 조건

풀이 예시

(가) NVL 을 COALESCE 로

SQL
-- Oracle 고유
SELECT member_id, NVL(bio, '비공개') AS profile FROM member;
-- 표준
SELECT member_id, COALESCE(bio, '비공개') AS profile FROM member;

자기소개가 비어 있는 네 회원(3·5·8·10)이 두 쿼리 모두 똑같이 "비공개" 로 채워져요. 결과가 완전히 같아요.

(나) DECODE 를 CASE 로

SQL
-- Oracle 고유
SELECT member_id, DECODE(MOD(member_id, 2), 0, '짝', '홀') AS parity FROM member;
-- 표준
SELECT member_id,
       CASE WHEN MOD(member_id, 2) = 0 THEN '짝' ELSE '홀' END AS parity
FROM member;

회원 12명이 짝 6명·홀 6명으로 똑같이 나뉘어요.

(다) 표준이 나은 점

COALESCE 는 인자를 여러 개 받아 폴백을 여러 단계로 줄 수 있고, CASE 는 동등 비교뿐 아니라 범위·복합 조건까지 적을 수 있어요. 무엇보다 둘 다 표준이라 다른 데이터베이스로 옮겨도 거의 그대로 통해요.

💡 튜터의 한마디 — "옮겼더니 결과가 같더라" 를 직접 눈으로 확인하는 게 핵심이에요. 방언은 적는 법만 다를 뿐 하는 일은 같아요. 시험에서 "다음 Oracle 쿼리와 같은 표준 쿼리는?" 을 물으면, 이 대응(NVLCOALESCE, DECODECASE)을 떠올리면 바로 답이 보여요.


🎯 [과제 2 예시답안] 외부 조인과 차집합을 두 방언으로

채점 포인트

항목 배점 핵심
(가) 회원별 게시물 수 — (+) & LEFT JOIN 40% 0개 회원 포함, 두 방식 행 수 동일
(나) 짝사랑 — MINUS & EXCEPT 35% 두 방식 결과 동일
(다) FULL OUTER 로 본 표준의 장점 25% (+) 한계 설명

풀이 예시

(가) 회원별 게시물 수 (0개 회원도 포함)

SQL
-- ANSI LEFT OUTER JOIN
SELECT m.member_id, COUNT(p.post_id) AS cnt
FROM member m LEFT JOIN post p ON m.member_id = p.member_id
GROUP BY m.member_id ORDER BY m.member_id;

-- Oracle (+)
SELECT m.member_id, COUNT(p.post_id) AS cnt
FROM member m, post p
WHERE m.member_id = p.member_id(+)
GROUP BY m.member_id ORDER BY m.member_id;

두 쿼리 모두 회원 12명이 다 나오고, 게시물이 0개인 회원 7·8·10 도 cnt 가 0으로 빠짐없이 나와요. COUNT(p.post_id) 는 NULL 을 세지 않으니, 게시물이 없는 회원은 0이 돼요.

(나) 회원 4번의 짝사랑

SQL
-- Oracle
SELECT following_id FROM follow WHERE follower_id = 4
MINUS
SELECT follower_id FROM follow WHERE following_id = 4;

-- 표준 (Oracle 21c+)
SELECT following_id FROM follow WHERE follower_id = 4
EXCEPT
SELECT follower_id FROM follow WHERE following_id = 4;

두 방식 모두 1번 한 명을 돌려줘요. 4번은 1번·2번을 팔로우하지만, 4번을 팔로우하는 건 2번뿐이라 1번이 짝사랑 상대예요.

(다) FULL OUTER 로 본 표준의 장점

Oracle (+) 는 양쪽을 동시에 다 남기는 FULL OUTER JOIN 을 못 해요(양쪽에 (+) 를 같이 못 붙임). "게시물 없는 회원도, 작성자 없는 게시물도 다 보고 싶다" 같은 요구가 생기면, (+) 로는 UNION 으로 두 쿼리를 이어 붙여 흉내 내야 하지만 표준 FULL OUTER JOIN 은 한 줄로 끝나요.

💡 튜터의 한마디 — 외부 조인은 "없는 쪽을 NULL 로 채워 살려 두는" 조인이에요. (+)LEFT JOIN 이든 살아남는 회원과 행 수가 똑같다는 걸 확인했다면 핵심을 잡은 거예요. 다만 (+) 는 한계가 많아서, 실무에선 표준 OUTER JOIN 을 쓰는 게 안전해요.


🎯 [과제 3 예시답안] 함정 쿼리를 찾아 고치기

채점 포인트

항목 배점 핵심
(가) 틀린 이유 설명 40% ROWNUMORDER BY 보다 먼저 매겨짐
(나) ROWNUM 으로 올바르게 고치기 35% 정렬된 인라인 뷰 바깥에서 ROWNUM
(다) FETCH FIRST 로도 짜기 + 비교 25% 함정 없는 쪽 분별

풀이 예시

(가) 왜 틀렸나

SQL
-- 틀린 쿼리
SELECT post_id, likes FROM (
  SELECT post_id, COUNT(*) AS likes FROM post_like GROUP BY post_id
) WHERE ROWNUM <= 3
ORDER BY likes DESC;

이 쿼리는 post 1·5·2 를 돌려줘서, 진짜 1등 post 12(좋아요 6개)가 빠져요. ROWNUMWHERE 단계에서 매겨지는데, 이게 ORDER BY 보다 먼저 일어나요. 그래서 정렬되지 않은 임의의 앞 3행(post 1·2·5)을 먼저 잘라 버리고, 그 3행만 likes 로 정렬하니 진짜 Top-3 와 무관한 결과가 나와요.

(나) ROWNUM 으로 올바르게

SQL
SELECT post_id, likes FROM (
  SELECT post_id, COUNT(*) AS likes
  FROM post_like GROUP BY post_id
  ORDER BY COUNT(*) DESC, post_id     -- 먼저 정렬을 끝낸다
) WHERE ROWNUM <= 3;                  -- 정렬된 결과의 위에서 3줄

정렬을 끝낸 인라인 뷰(괄호 안)를 만들고, 그 바깥에서 ROWNUM 으로 자르면 post 12·1·40 이 제대로 나와요.

(다) FETCH FIRST 로도

SQL
SELECT post_id, COUNT(*) AS likes
FROM post_like GROUP BY post_id
ORDER BY COUNT(*) DESC, post_id
FETCH FIRST 3 ROWS ONLY;

같은 post 12·1·40 이 나와요. FETCH FIRSTORDER BY 뒤에 붙여서 정렬이 끝난 뒤에 자른다는 게 문법에 드러나 있어요. 그래서 ROWNUM 같은 순서 함정이 아예 없어요. 훨씬 안전하고 읽기 쉬워요.

💡 튜터의 한마디ROWNUM 함정의 핵심은 단 한 줄이에요. "ROWNUMORDER BY 보다 먼저 매겨진다." 이 원리만 잡으면, 시험에서 함정 쿼리를 골라내는 것도, 직접 올바르게 고치는 것도 어렵지 않아요.


생각해볼 주제

1. 표준이 있는데 왜 Oracle 고유 문법이 아직도 남아 있을까

문제 상황 요약

ANSI 표준으로 통일하면 이식성이 좋아질 텐데, Oracle 은 DECODE·NVL·(+) 같은 고유 문법을 없애지 않고 계속 지원해요. 왜 옛 문법을 버리지 않을까요?

튜터의 가이드 및 해설

핵심은 하위 호환(backward compatibility) 이에요. Oracle 은 수십 년간 기업·금융·공공의 핵심 시스템에 쓰여 왔어요. 그 시스템들 안에는 DECODE·(+) 로 짜인 쿼리가 셀 수 없이 많아요. 만약 Oracle 이 어느 날 "고유 문법을 없앤다" 고 하면, 그 모든 코드가 한순간에 멈춰요. 데이터베이스 회사 입장에서 이건 고객을 잃는 최악의 선택이에요.

그래서 방향은 늘 "기존 문법은 그대로 두고, 표준 문법을 추가로 지원" 이에요. 옛 코드는 계속 돌아가고, 새 코드는 표준으로 짤 수 있죠. 새 문법으로 바꾸는 비용(코드 수정·테스트·검증)은 결국 고객이 치르는데, 잘 돌아가는 코드를 굳이 바꿀 이유가 없으니 옛 문법은 계속 살아남아요.

🎯 SQLD는 이렇게 나온다

"Oracle 의 DECODE, (+), MINUS, ROWNUM 은 표준 이전부터 있던 고유 문법입니다. Oracle 은 하위 호환을 위해 이들을 없애지 않고, 나중에 ANSI 표준(CASE, OUTER JOIN, EXCEPT, FETCH FIRST)을 추가로 지원했습니다. 그래서 같은 일을 두 방언으로 적을 수 있고, 시험은 이 둘을 분별하는 능력을 묻습니다."


2. DECODE 가 NULL=NULL 을 "같다" 로 보는 건 버그일까 설계일까

문제 상황 요약

일반 비교 = 는 NULL=NULL 을 절대 참으로 보지 않는데, DECODE 는 같다고 봐요. 같은 데이터베이스 안에서 NULL 규칙이 둘로 갈리는 셈인데, 이게 실수일까요 의도일까요?

튜터의 가이드 및 해설

이건 의도된 설계 예요. DECODE 의 쓰임새를 떠올리면 이유가 보여요. DECODE 는 주로 "이 값은 이 라벨, 저 값은 저 라벨" 처럼 값을 분류해 라벨을 붙이는 데 써요. 이때 "값이 비어 있는 경우(NULL)" 도 하나의 분류로 잡아 주면 편해요. 예를 들어 "등급이 NULL 이면 '미정' 으로" 같은 처리를 DECODE(grade, NULL, '미정', ...) 한 줄로 할 수 있죠.

만약 DECODE= 처럼 NULL 을 안 맞춰 줬다면, NULL 분류를 매번 따로 처리해야 해서 번거로웠을 거예요. 그래서 DECODE 는 일부러 NULL=NULL 을 매칭하게 만들었어요. 반대로 CASEWHEN col = val 은 표준 비교 규칙(NULL 은 늘 알 수 없음)을 그대로 따르기 때문에, NULL 을 잡으려면 WHEN col IS NULL 로 명시해야 해요. 두 방식의 철학이 다른 거지, 어느 한쪽이 버그인 건 아니에요.

🎯 SQLD는 이렇게 나온다

"DECODE 는 NULL 과 NULL 을 '같다' 로 매칭합니다. 이는 값을 분류하는 함수의 특성에 맞춘 의도된 설계입니다. 반면 CASE WHEN col = NULL 은 표준 3치 논리에 따라 절대 참이 되지 않으므로, NULL 을 잡으려면 WHEN col IS NULL 로 적어야 합니다. DECODECASE 로 옮길 때 이 차이를 놓치면 결과가 어긋납니다."


3. 실무에서 쿼리를 표준으로 짤지, 그 DB 에 맞춰 짤지

문제 상황 요약

Oracle 을 쓰는 새 프로젝트에서, ROWNUM·(+) 같은 고유 문법으로 짧게 짤 수도 있고, 나중을 대비해 FETCH FIRST·OUTER JOIN 같은 표준으로 짤 수도 있어요. 어느 쪽이 합리적일까요?

튜터의 가이드 및 해설

정답은 상황에 따라 다르다 예요. 두 가정으로 나눠 보면 선택이 갈려요.

"이 회사는 앞으로도 Oracle 만 쓸 것 같다" 면, 표준에 얽매일 필요가 적어요. 팀이 익숙한 문법으로 빠르게 짜고, Oracle 에 최적화된 기능(고유 힌트 등)도 마음껏 쓰는 게 생산적일 수 있어요. 반대로 "언젠가 다른 데이터베이스로 옮길지 모른다"(클라우드 이전, 비용 절감, 오픈소스 전환 등) 면, 처음부터 표준으로 짜 두는 게 나중의 이전 비용을 크게 줄여 줘요.

중요한 건 그 가정이 틀렸을 때 치를 대가 예요. "Oracle 만 쓸 줄 알았는데" 갑자기 MySQL 로 옮기게 되면, (+)·DECODE·ROWNUM 으로 도배된 쿼리를 전부 뜯어고쳐야 해요. 반대로 표준으로 짜 뒀는데 끝까지 Oracle 만 썼다면, 잃은 건 약간의 편의뿐이에요. 둘 중 한쪽의 대가가 훨씬 크다는 점이 선택의 무게추가 돼요.

🎯 SQLD는 이렇게 나온다

"단일 DBMS 환경이고 그 제품의 기능을 최대한 쓰고 싶다면 고유 문법이 유리하고, 이식성·다중 DBMS·미래의 전환 가능성을 중시한다면 ANSI 표준이 유리합니다. 핵심은 '가정이 틀렸을 때의 비용' 입니다. 고유 문법으로 짰다가 이전하게 되면 전면 재작성 비용이 들지만, 표준으로 짰다가 이전하지 않으면 잃는 건 사소합니다. 그래서 불확실하면 표준 쪽이 안전한 기본값입니다."

전체 목록 데이터베이스