F-1: MySQL 환경 + 방언 기초 — 같은 스키마를 또 다른 방언으로
목차 20
안녕하세요, 홍순구 튜터입니다. 지난 시간엔 그동안 배운 SQL 을 총동원해서 SQLD 모의고사 50문항을 풀며 실전 감각을 만들었어요. 모델링부터 함수·집계·조인·서브쿼리·계층형·윈도우·방언까지, 인스타그램 한 스키마 위에서 한 바퀴를 다 돌았죠. SQLD 대비는 이걸로 든든해졌어요.
그런데 한 가지 더 짚고 갈 게 있어요. 지난 시간(E-1)에 우리는 Oracle 문법을 표준(ANSI)과 나란히 놓고 비교했죠. NVL 옆에 COALESCE, ROWNUM 옆에 FETCH FIRST 를 두고 "같은 일을 다른 방언으로" 적는 눈을 길렀어요. 오늘은 그 눈으로 이번엔 Oracle 과 MySQL 을 견줘요.
왜 하필 MySQL 일까요? 우리가 SQLD 를 Oracle 로 공부한 건 시험이 Oracle 문법을 주축으로 출제되기 때문이에요. 그런데 실무 백엔드, 특히 다음에 이어질 과목에서 마주칠 환경은 MySQL 이 아주 흔해요. 그래서 지금까지 Oracle 로 짠 인스타그램 스키마를 통째로 MySQL 8.4 로 옮겨 보면서, 시퀀스가 AUTO_INCREMENT 로, ROWNUM 이 LIMIT 으로, NVL 이 IFNULL 로 바뀌는 감각을 만들어요.
오늘의 여정
① 왜 MySQL 로 옮길까 — 두 번째 방언
② 환경 띄우기 — 컨테이너와 문자셋(utf8mb4)
③ 자료형 옮기기 — VARCHAR2 · NUMBER · DATE
④ 자동 채번 — 시퀀스 vs AUTO_INCREMENT
⑤ 피드 페이지네이션 — ROWNUM vs LIMIT
⑥ NULL 메우기 — NVL vs IFNULL
⑦ 같은 스키마를 통째로 — 변환 한 장 정리
💡 오늘 수업의 핵심 — "같은 인스타그램 스키마를 MySQL 8.4 로 옮기며, 자료형 매핑·시퀀스→AUTO_INCREMENT·ROWNUM→LIMIT·NVL→IFNULL 이라는 방언 기초를 정리한다"
🎯 학습 목표
- MySQL 8.4 환경을 띄우고, Oracle 스키마를 MySQL DDL 로 옮긴다(자료형 매핑·문자셋).
- 시퀀스↔
AUTO_INCREMENT,ROWNUM/FETCH FIRST↔LIMIT변환을 안다. (SQLD 방언 구분 ★빈출) NVL↔IFNULL/COALESCE등 NULL 처리의 방언 차이를 안다.
Step 1: "왜 MySQL 로 옮길까 — 두 번째 방언"
지난 시간(E-1)에 우리는 Oracle 사투리와 표준어(ANSI)를 나란히 놓고 봤어요. 표준어는 다른 데이터베이스에서도 거의 그대로 통한다고 했죠. 그런데 데이터베이스 제품은 표준어만 쓰는 게 아니라, 저마다 자기 사투리도 함께 써요. MySQL 도 그래요. 표준에 가까운 부분도 있고, MySQL 만의 방언도 있어요.
그래서 오늘은 Oracle 로 짠 쿼리를 MySQL 로 옮기면서 세 갈래의 말 을 함께 보게 돼요. Oracle 사투리, 표준어(ANSI), 그리고 MySQL 사투리예요. 다행히 E-1 에서 표준어를 한 번 짚어 둔 덕분에, MySQL 방언도 표준어에 빗대면 금방 익혀져요.
같은 일을 적는 세 갈래
Oracle 사투리 NVL · ROWNUM · 시퀀스 · DECODE · (+)
표준어(ANSI) COALESCE · FETCH FIRST · CASE · OUTER JOIN
MySQL 사투리 IFNULL · LIMIT · AUTO_INCREMENT
그럼 왜 굳이 MySQL 일까요? MySQL 은 오픈소스(무료)라 스타트업부터 대기업까지 널리 쓰고, 다음에 이어질 백엔드 과목에서 다룰 환경도 MySQL 8.4 예요. 즉 "시험은 Oracle 로, 실무는 MySQL 로" 두 발을 걸치게 되는 셈이에요.
버전 이야기를 잠깐 할게요. MySQL 은 장기 지원(LTS) 버전을 따로 내는데, 지금 두 개가 함께 쓰여요. 오래 안정적으로 쓰이는 8.4 LTS, 그리고 최신 기능이 들어간 9.7 LTS 예요. 우리는 다음 과목과 환경을 맞추려고 8.4 를 골라요. 참고로 윈도우 함수(D-4~D-5 에서 배운 RANK·OVER)나 CTE(WITH)는 MySQL 8.0 부터 들어왔어요. 아주 오래된 5.x 시절엔 없었지만, 지금 우리가 쓰는 8.4 에는 다 있으니 걱정 안 해도 돼요.
⚠️ "MySQL 은 윈도우 함수·CTE 가 없다" 는 말은 옛 5.7 시절 이야기예요. 8.0 부터 둘 다 지원하고, 우리가 쓰는 8.4 도 당연히 돼요. 버전을 묶어서 "8.0+ 부터 가능" 으로 기억해 두세요.
💡 한 줄 정리 — MySQL 은 Oracle·표준어와 또 다른 사투리를 쓴다. 시험은 Oracle, 실무는 MySQL 이 흔해서 둘을 옮기는 감각이 자산이 된다. 우리는 8.4 LTS 로 실습한다.
🙋 학생 질문 — "튜터님, Oracle 로 다 배웠는데 왜 또 MySQL 을 배워요?"
방언을 하나 더 외우는 게 아니라, "옮기는 능력" 을 기르는 거예요. 실무에선 회사마다 쓰는 데이터베이스가 달라요. 어떤 곳은 Oracle, 어떤 곳은 MySQL, 또 어떤 곳은 PostgreSQL 을 써요. 그때마다 SQL 을 처음부터 새로 배우는 게 아니라, "아, 이건 Oracle 의 NVL 에 해당하는 거구나" 하고 빠르게 갈아타는 사람이 강해요. 오늘 Oracle→MySQL 을 한 번 옮겨 보면, 나중에 또 다른 데이터베이스를 만나도 같은 방식으로 금방 적응할 수 있어요.
Step 2: "환경 띄우기 — 컨테이너와 문자셋(utf8mb4)"
이제 MySQL 8.4 를 실제로 띄워 볼게요. A-0 에서 Oracle 컨테이너를 띄웠던 것과 똑같은 방식이에요. 도커(Docker)로 MySQL 컨테이너 하나를 더 올려서, Oracle 옆에 나란히 두고 같은 인스타 데이터를 두 방언으로 견줘요.
# docker-compose.yml — Oracle 옆에 MySQL 8.4 서비스를 하나 더 둔다
mysql:
image: mysql:8.4
container_name: instagram-mysql
ports:
- "3306:3306" # MySQL 표준 포트 (Oracle 은 1521)
environment:
MYSQL_ROOT_PASSWORD: root1234
MYSQL_DATABASE: instagram # 앱 스키마(데이터베이스)를 자동 생성
MYSQL_USER: insta
MYSQL_PASSWORD: insta1234
command: --character-set-server=utf8mb4 --collation-server=utf8mb4_0900_ai_ci
띄우는 명령은 Oracle 때와 같아요.
docker compose up -d mysql
여기서 한 가지 꼭 짚을 게 있어요. 바로 문자셋(character set) 이에요. 우리 데이터엔 "김재훈", "제주도 여행" 같은 한글이 가득하죠. MySQL 에서 한글이나 이모지를 안전하게 담으려면 utf8mb4 라는 문자셋을 써야 해요. 위 설정의 --character-set-server=utf8mb4 가 그 약속이에요.
이름이 좀 헷갈리는데, MySQL 의 옛 utf8 은 사실 한 글자를 최대 3바이트까지만 담아서 이모지(4바이트)가 깨져요. 진짜 완전한 UTF-8 은 utf8mb4(mb4 = 최대 4바이트) 예요. 그래서 한글·이모지를 쓰려면 무조건 utf8mb4 라고 외워 두세요.
데이터를 넣을 때도 연결의 문자셋을 맞춰 줘야 해요. 그래서 스키마·시드 파일 맨 위에 이 한 줄을 둬요.
-- sql/migration/oracle_to_mysql/01_schema_mysql.sql
SET NAMES utf8mb4;
SET NAMES utf8mb4 는 "지금 이 연결에서 주고받는 글자는 utf8mb4 로 다루자" 는 선언이에요. 이걸 빠뜨리고 한글을 넣으면, 글자가 엉뚱한 문자셋으로 해석돼서 "김재훈" 이 "김재훈" 처럼 깨져 저장돼요. 한번 깨져서 들어가면 되돌리기 까다로우니, 한글 데이터를 넣기 전에 문자셋부터 맞추는 습관을 들이세요.
이렇게 컨테이너를 띄우고 접속하면, 잠시 뒤 우리 인스타 회원 12명과 게시물 50개가 MySQL 안에서 그대로 조회돼요. Oracle 에 있던 것과 똑같은 데이터가 이제 MySQL 위에서도 살아 있는 모습을 확인할 수 있어요.
⚠️ MySQL 의
utf8은 가짜(3바이트)예요. 한글·이모지를 안전하게 담으려면utf8mb4를 써야 하고, 데이터를 넣기 전SET NAMES utf8mb4로 연결 문자셋을 맞춰야 한글이 안 깨져요.
💡 한 줄 정리 — MySQL 8.4 컨테이너를 Oracle 옆에 띄워 같은 데이터를 두 방언으로 본다. 한글은
utf8mb4문자셋으로 담고, 넣기 전SET NAMES utf8mb4로 연결 문자셋을 맞춘다.
🙋 학생 질문 — "튜터님, 그냥 utf8 쓰면 안 되고 꼭 utf8mb4 여야 하나요?"
네, 한글·이모지를 쓴다면 utf8mb4 가 맞아요. MySQL 의 utf8 은 역사적인 사정으로 한 글자를 3바이트까지만 담도록 만들어졌어요. 한글은 3바이트라 운 좋게 들어가긴 하지만, 이모지(😀 같은 4바이트 글자)는 담다가 에러가 나거나 깨져요. utf8mb4 는 이름 그대로 한 글자를 최대 4바이트까지 담아서 이모지까지 안전해요. 그래서 요즘 MySQL 프로젝트는 거의 다 utf8mb4 를 기본으로 써요. "MySQL 에서 utf8 은 함정, utf8mb4 가 진짜" 라고 기억하면 돼요.
Step 3: "자료형 옮기기 — VARCHAR2 · NUMBER · DATE"
이제 본격적으로 테이블을 옮겨 볼게요. C-1 에서 만든 member·post 테이블의 CREATE TABLE 을 MySQL 로 바꾸는 거예요. 가장 먼저 손볼 건 자료형(데이터 타입) 이에요. 이름이 살짝씩 달라요.
| 하는 일 | Oracle | MySQL | 메모 |
|---|---|---|---|
| 가변 길이 문자 | VARCHAR2(n) |
VARCHAR(n) |
이름만 다르고 의미 같음 |
| 정수·숫자 | NUMBER |
INT / BIGINT / DECIMAL |
쓰임에 맞게 골라 씀 |
| 날짜+시각 | DATE |
DATETIME |
⚠️ MySQL DATE 는 시각이 없음 |
| 긴 글 | CLOB |
TEXT |
수천 자 이상 본문 |
VARCHAR2 는 MySQL 에선 그냥 VARCHAR 예요. 끝의 2 만 떼면 돼요. 숫자는 좀 갈라져요. Oracle 은 정수든 소수든 전부 NUMBER 하나로 받았지만, MySQL 은 용도에 따라 골라 써요. 회원 번호·게시물 번호처럼 큰 정수에는 BIGINT, 작은 정수면 INT, 소수점이 필요하면 DECIMAL 을 써요.
가장 조심할 건 날짜 예요. Oracle 의 DATE 는 사실 날짜와 시각(시·분·초)을 함께 담아요(C-2 에서 봤죠). 그런데 MySQL 의 DATE 는 날짜만 담고 시각이 없어요. 그래서 Oracle DATE 를 MySQL 로 옮길 땐 시각까지 살리려고 DATETIME 을 써요.
-- sql/migration/oracle_to_mysql/01_schema_mysql.sql
-- Before(Oracle): created_at DATE DEFAULT SYSDATE NOT NULL
CREATE TABLE post (
post_id BIGINT NOT NULL AUTO_INCREMENT,
member_id BIGINT NOT NULL,
caption VARCHAR(500),
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (post_id),
FOREIGN KEY (member_id) REFERENCES member(member_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
기본값도 방언이 갈려요. Oracle 은 "입력 안 하면 지금 시각" 을 DEFAULT SYSDATE 로 적었는데, MySQL 은 DEFAULT CURRENT_TIMESTAMP 로 적어요. 둘 다 "값을 안 주면 현재 시각을 자동으로 채운다" 는 같은 뜻이에요.
옮긴 뒤 게시물의 작성 시각을 보면, 날짜만 넣었는데도 시각 자리가 00:00:00 으로 함께 들어와 있어요. 첫 게시물의 created_at 은 2026-06-01 00:00:00 으로 조회돼요. DATETIME 이라 시각 칸까지 챙긴 모습이에요.
⚠️ Oracle 의
DATE는 시각을 포함하지만, MySQL 의DATE는 날짜만이고 시각이 없어요. OracleDATE→ MySQLDATETIME으로 옮겨야 시·분·초가 보존돼요. 이걸DATE→DATE로 잘못 옮기면 시각이 통째로 날아가는 게 단골 실수예요.
💡 한 줄 정리 —
VARCHAR2→VARCHAR,NUMBER→INT/BIGINT/DECIMAL,DATE→DATETIME(시각 보존),DEFAULT SYSDATE→DEFAULT CURRENT_TIMESTAMP로 옮긴다.
🙋 학생 질문 — "튜터님, NUMBER 를 INT 로 할지 BIGINT 로 할지 어떻게 정해요?"
담을 수 있는 숫자의 크기로 정해요. INT 는 약 21억까지, BIGINT 는 그보다 훨씬 큰 수(900경 단위)까지 담아요. 회원 번호처럼 "언젠가 21억을 넘을 수도 있나?" 싶은 식별자는 넉넉하게 BIGINT 로 잡는 게 안전해요. 인스타그램 게시물처럼 수십억 건이 쌓이는 서비스를 생각하면, 식별자는 처음부터 BIGINT 로 두는 게 마음 편해요. 반대로 "좋아요 수" 처럼 21억을 넘을 일이 거의 없는 값은 INT 로도 충분하고요. 우리 스키마에선 번호(식별자)는 전부 BIGINT 로 통일했어요.
Step 4: "자동 채번 — 시퀀스 vs AUTO_INCREMENT"
게시물을 새로 올릴 때마다 post_id 가 1, 2, 3... 하고 자동으로 매겨져야겠죠? 이 "번호 자동 매기기" 를 두 방언이 아주 다르게 해요.
D-7 에서 Oracle 의 시퀀스(sequence) 를 배웠어요. CREATE SEQUENCE 로 번호 발급기를 따로 만들고, 시퀀스.NEXTVAL 로 다음 번호를 꺼내 INSERT 에 넣었죠. 그런데 MySQL 엔 시퀀스가 없어요. 대신 컬럼에 AUTO_INCREMENT 라는 속성을 붙여요.
시퀀스 (Oracle) AUTO_INCREMENT (MySQL)
CREATE SEQUENCE 로 따로 만듦 컬럼에 속성으로 붙임
post_id_seq.NEXTVAL 로 꺼냄 INSERT 때 알아서 +1
여러 표가 한 발급기를 공유 가능 한 표(컬럼)에 전속
AUTO_INCREMENT 가 붙은 컬럼은, INSERT 할 때 그 칸을 아예 비워 두면 데이터베이스가 다음 번호를 알아서 채워 줘요. 시퀀스에서 NEXTVAL 을 꺼내 넣던 일을, MySQL 은 컬럼이 스스로 해 주는 셈이에요.
-- sql/migration/oracle_to_mysql/01_schema_mysql.sql
CREATE TABLE member (
member_id BIGINT NOT NULL AUTO_INCREMENT, -- 시퀀스 대신 자동 채번
username VARCHAR(30) NOT NULL UNIQUE,
nickname VARCHAR(30) NOT NULL,
bio VARCHAR(150),
PRIMARY KEY (member_id)
);
우리 회원은 이미 1~12 번까지 들어 있어요. 여기에 새 회원을 member_id 없이 넣어 볼게요.
-- Before(Oracle): INSERT INTO member(member_id, ...) VALUES (member_id_seq.NEXTVAL, ...);
-- MySQL: member_id 를 아예 빼고 넣으면 자동으로 다음 번호가 채워진다
INSERT INTO member (username, nickname, bio) VALUES ('newbie', '새내기', NULL);
SELECT LAST_INSERT_ID(); -- 방금 채번된 번호 확인
새 회원에게 13 번 이 자동으로 붙어요. 직전 최대 번호가 12 였으니 그다음인 13 이 매겨진 거예요. 방금 채번된 번호는 LAST_INSERT_ID() 라는 함수로 바로 확인할 수 있어요(Oracle 의 시퀀스.CURRVAL 에 해당해요). 같은 식으로 게시물을 post_id 없이 넣으면, 직전 최대였던 50 다음인 51 번 이 붙어요.
여기서 시험에도 나오고 실무에서도 자주 부딪히는 함정이 하나 있어요. AUTO_INCREMENT 로 매겨진 번호는, 그 입력을 취소(롤백)해도 번호가 되돌아오지 않아요. 13 번을 만들었다가 취소하면, 13 은 비어 버리고 다음 회원은 14 번을 받아요. 그래서 자동 채번 번호엔 중간중간 빈 번호(갭)가 생길 수 있어요. "번호가 1, 2, 4..." 처럼 건너뛰어도 고장이 아니라 정상이에요.
이 "자동으로 번호를 매기는 방식" 의 차이는 그냥 문법 차이로 끝나지 않아요. 다음에 이어질 백엔드 과목에서 테이블의 자동 채번을 프로그램 객체와 연결할 때, 시퀀스 방식이냐 AUTO_INCREMENT 방식이냐에 따라 설정이 갈려요. 오늘 이 차이를 잡아 두면 다음 과목에서 그 선택이 한결 쉬워져요.
⚠️
AUTO_INCREMENT번호는 롤백해도 되돌아오지 않아서 중간에 빈 번호(갭)가 생길 수 있어요. "번호가 연속이어야 한다" 고 가정하면 안 돼요. Oracle 시퀀스도 마찬가지로 갭이 생겨요.
💡 한 줄 정리 — Oracle 은 시퀀스(
NEXTVAL)로, MySQL 은 컬럼의AUTO_INCREMENT로 번호를 자동 매긴다.INSERT때 그 칸을 비우면 다음 번호가 채워지고,LAST_INSERT_ID()로 확인한다.
🙋 학생 질문 — "튜터님, 시퀀스가 여러 표를 공유할 수 있다면 더 좋은 거 아닌가요?"
상황마다 달라요. Oracle 시퀀스는 발급기를 따로 만들어서 여러 표가 한 발급기를 공유할 수도 있어요. "회원이든 게시물이든 전체에서 유일한 번호" 가 필요할 땐 이게 편해요. 반면 MySQL 의 AUTO_INCREMENT 는 컬럼에 딱 붙어서 그 표 전용이에요. 대신 "이 표의 다음 번호" 라는 의도가 컬럼에 바로 드러나서 단순하고 직관적이죠. 대부분의 서비스는 "한 표 안에서만 유일하면 충분" 해서 AUTO_INCREMENT 의 단순함이 잘 맞아요. 둘 중 뭐가 낫다기보다, MySQL 은 단순함을, Oracle 은 유연함을 택한 거라고 보면 돼요.
Step 5: "피드 페이지네이션 — ROWNUM vs LIMIT"
이제 인스타그램의 핵심, 피드(feed) 를 만들어 볼게요. 피드는 최신 게시물을 위에서부터 몇 개씩 보여주고, 스크롤을 내리면 다음 몇 개를 더 불러오죠. 이렇게 "전체에서 일부만 잘라서 페이지 단위로 보여주는" 걸 페이지네이션(pagination) 이라고 해요.
E-1·E-2 에서 "위에서 몇 줄만" 뽑는 걸 Oracle ROWNUM 과 FETCH FIRST 로 했죠. MySQL 은 이걸 LIMIT 으로 해요. 훨씬 짧고 직관적이에요.
ROWNUM (Oracle) LIMIT (MySQL)
정렬 '전에' 번호가 매겨짐 ORDER BY '뒤에' 잘라냄
정렬 인라인뷰 바깥에서 잘라야 함 정렬 끝난 결과에서 n 건
순서 잘못 짜면 엉뚱한 결과(함정) 정렬 후 자르니 함정 없음
피드 첫 페이지, 즉 가장 최신 게시물 5개를 뽑아 볼게요.
-- sql/migration/oracle_to_mysql/03_queries_mysql.sql
-- Before(Oracle): ... ORDER BY created_at DESC FETCH FIRST 5 ROWS ONLY;
SELECT post_id, member_id, created_at
FROM post
ORDER BY created_at DESC, post_id DESC
LIMIT 5;
결과는 게시물 50 · 49 · 48 · 47 · 46 이에요. 작성일이 가장 최근인 5개죠. ORDER BY 로 최신순 정렬을 끝낸 다음에 LIMIT 5 가 앞 5개를 잘라내요. 순서가 자연스럽죠? "정렬하고 → 자른다" 예요.
두 번째 페이지는 어떻게 할까요? 앞 5개를 건너뛰고 다음 5개를 가져오면 돼요. 이때 OFFSET(건너뛰기)을 써요.
-- 2 페이지: 앞 5건을 건너뛰고(OFFSET 5) 다음 5건(LIMIT 5)
SELECT post_id, member_id, created_at
FROM post
ORDER BY created_at DESC, post_id DESC
LIMIT 5 OFFSET 5;
결과는 게시물 45 · 12 · 44 · 10 · 43 이에요. 여기서 작은 볼거리가 하나 있어요. 게시물 45 와 12 는 작성일이 둘 다 2026-06-10 으로 같아요. 작성일만으로는 순서를 못 가리니, 우리는 ORDER BY 에 post_id DESC 를 더해서 "같은 날이면 번호 큰 것 먼저" 로 못을 박았어요. 그래서 45 가 12 보다 앞에 와요. 페이지네이션에서 이렇게 순서를 확실히 고정하는 두 번째 기준 을 넣는 건 아주 중요해요. 안 그러면 페이지를 넘길 때 같은 게시물이 또 나오거나 빠지는 사고가 나거든요.
이제 E-1 에서 봤던 ROWNUM 함정을 떠올려 볼게요. Oracle 의 ROWNUM 은 번호가 ORDER BY 보다 먼저 매겨져서, 정렬하기 전에 잘라 버리면 엉뚱한 결과가 나왔죠. 그래서 정렬을 끝낸 인라인 뷰를 만들고 그 바깥에서 잘라야 했어요. 그런데 MySQL 의 LIMIT 은 늘 ORDER BY 뒤에 붙어서, 정렬이 끝난 결과를 자르기 때문에 그 함정이 아예 없어요. 이 점이 LIMIT 의 큰 장점이에요.
다른 데이터베이스도 "몇 줄만" 을 저마다 다르게 적어요. 한 표로 정리할게요.
| 방식 | 문법 | DBMS |
|---|---|---|
ROWNUM |
정렬된 인라인 뷰 바깥에서 WHERE ROWNUM <= n |
Oracle 고유 |
FETCH FIRST |
ORDER BY ... FETCH FIRST n ROWS ONLY |
Oracle 12c+ · ANSI 표준 |
LIMIT |
ORDER BY ... LIMIT n OFFSET m |
MySQL · PostgreSQL |
TOP |
SELECT TOP n ... |
SQL Server |
⚠️
LIMIT은 반드시ORDER BY와 함께 써야 결과가 일정해요. 정렬 기준 없이LIMIT만 쓰면 "아무 5개" 가 나와서 페이지마다 순서가 흔들려요. 같은 값이 있을 때를 대비해post_id같은 두 번째 정렬 기준까지 넣는 게 안전해요.
💡 한 줄 정리 — MySQL 은
LIMIT n OFFSET m으로 페이지를 자른다.ORDER BY뒤에 붙어 정렬 끝난 결과를 자르니ROWNUM같은 정렬 함정이 없다. 순서 고정을 위해 두 번째 정렬 기준을 넣는다.
🙋 학생 질문 — "튜터님, 페이지가 깊어지면 OFFSET 이 느려진다던데 사실인가요?"
맞아요. OFFSET 은 "앞의 N 개를 일단 다 읽고 나서 버리는" 방식이에요. 그래서 1000 페이지처럼 깊이 들어가면, 데이터베이스가 앞의 수만 건을 읽어 버린 뒤에야 원하는 5개를 돌려줘서 점점 느려져요. 그래서 인스타그램 같은 무한 스크롤은 보통 OFFSET 대신 "마지막으로 본 게시물의 기준값보다 이전 것" 을 WHERE 로 거르는 방식(흔히 커서 방식이라고 불러요)을 써요. 오늘은 LIMIT/OFFSET 의 기본기를 잡는 게 목표라 여기까지만 짚고, 깊은 페이지 최적화는 다음 과목에서 더 다뤄요.
Step 6: "NULL 메우기 — NVL vs IFNULL"
빈 값(NULL)을 기본값으로 메우는 일도 방언이 갈려요. C-3 에서 Oracle 의 NVL 을 배웠고, E-1 에서 그 표준어가 COALESCE 라는 것도 봤죠. 우리 회원 12명 중 자기소개(bio)가 비어 있는 사람이 4명(회원 3·5·8·10)이었어요. 이 빈 소개를 "소개 없음" 으로 채워 볼게요.
-- sql/migration/oracle_to_mysql/03_queries_mysql.sql
-- Before(Oracle): SELECT member_id, NVL(bio, '소개 없음') FROM member ...
-- MySQL 사투리: IFNULL(값, 대체값) — 인자 두 개
SELECT member_id, nickname, IFNULL(bio, '소개 없음') AS bio_display
FROM member
WHERE member_id IN (1, 3, 5);
-- 표준어: COALESCE(값, 대체값, ...) — MySQL 에서도 그대로 통함
SELECT member_id, nickname, COALESCE(bio, '소개 없음') AS bio_display
FROM member
WHERE member_id IN (1, 3, 5);
두 쿼리는 완전히 같은 결과 를 내요. 자기소개가 있는 회원 1(김재훈)은 원래 소개가 그대로 나오고, 비어 있는 회원 3(박승우)·5(최도연)는 "소개 없음" 으로 채워져요.
| member_id | nickname | bio_display |
|---|---|---|
| 1 | 김재훈 | 백엔드 공부 중 |
| 3 | 박승우 | 소개 없음 |
| 5 | 최도연 | 소개 없음 |
IFNULL 은 MySQL 의 사투리예요. NVL 처럼 인자를 딱 두 개 받아요. "이 값이 NULL 이면 저 값으로" 죠. 반면 COALESCE 는 E-1 에서 봤듯이 표준어 라 Oracle·MySQL 어디서나 통하고, 인자를 여러 개 받아 앞에서부터 처음으로 NULL 이 아닌 값을 골라요. NULL 처리 함수를 방언별로 한 표에 모으면 이래요.
| 하는 일 | Oracle | MySQL | SQL Server | 표준(ANSI) |
|---|---|---|---|---|
| 2인자 NULL 대체 | NVL |
IFNULL |
ISNULL |
— |
| N인자 NULL 대체 | COALESCE |
COALESCE |
COALESCE |
COALESCE |
재밌죠? NVL(Oracle)·IFNULL(MySQL)·ISNULL(SQL Server)이 다 같은 일을 하는 사투리이고, COALESCE 만 어디서나 통하는 표준어예요. 그래서 실무에선 옮길 일을 대비해 COALESCE 를 기본으로 쓰는 사람이 많아요.
그리고 D-2·E-2 에서 못 박았던 NULL 의 철칙은 MySQL 에서도 똑같아요. NULL 인지 확인할 땐 = NULL 이 아니라 IS NULL 을 써야 해요. 자기소개가 빈 회원이 몇 명인지 세면, MySQL 에서도 그대로 4명이 나와요.
SELECT COUNT(*) AS no_bio FROM member WHERE bio IS NULL; -- 4
⚠️
IFNULL은 MySQL 전용,COALESCE는 어디서나 통하는 표준이에요. 둘이 결과는 같지만, "표준 NULL 대체 함수는?" 을 물으면 답은COALESCE예요. NULL 확인은 어느 방언에서든IS NULL이라는 점도 변하지 않아요.
💡 한 줄 정리 — NULL 대체는 Oracle
NVL, MySQLIFNULL(둘 다 2인자 사투리), 표준COALESCE(N인자, 어디서나)다. NULL 확인은 모든 방언에서IS NULL을 쓴다.
🙋 학생 질문 — "튜터님, MySQL 에선 IFNULL 이랑 COALESCE 중 뭘 써야 하나요?"
규칙을 하나 정한다면 COALESCE 를 권해요. 이유가 둘이에요. 첫째, COALESCE 는 표준이라 나중에 다른 데이터베이스로 쿼리를 옮겨도 그대로 통해요. IFNULL 은 MySQL 에서만 통해서, 옮길 때 일일이 바꿔야 해요. 둘째, COALESCE 는 폴백을 여러 단계로 줄 수 있어요(소개가 없으면 닉네임, 그것도 없으면 '익명' 처럼). 다만 IFNULL 은 이름이 직관적이고 인자가 둘뿐이라, "딱 한 가지 대체값만 필요한" 단순한 경우엔 읽기 쉬운 장점이 있어요. 그래서 "기본은 COALESCE, 단순할 땐 IFNULL 도 무방" 정도로 가면 돼요.
Step 7: "같은 스키마를 통째로 — 변환 한 장 정리"
오늘 옮긴 방언들을 한 장으로 모아 볼게요. Oracle 로 짠 인스타 스키마를 MySQL 로 옮길 때 손보는 자리는 결국 이 표로 정리돼요. 시험 직전이든 실무에서 변환할 때든, 이 표만 떠올리면 길을 잃지 않아요.
| 하는 일 | Oracle | MySQL |
|---|---|---|
| 가변 문자 | VARCHAR2(n) |
VARCHAR(n) |
| 정수·숫자 | NUMBER |
INT / BIGINT / DECIMAL |
| 날짜+시각 | DATE |
DATETIME |
| 현재 시각 기본값 | DEFAULT SYSDATE |
DEFAULT CURRENT_TIMESTAMP |
| 자동 채번 | 시퀀스 NEXTVAL |
AUTO_INCREMENT |
| 행 제한 | ROWNUM / FETCH FIRST |
LIMIT n OFFSET m |
| NULL 대체 | NVL |
IFNULL / COALESCE |
이 규칙으로 인스타 핵심 다섯 표(회원·게시물·댓글·좋아요·팔로우)를 통째로 MySQL 로 옮겼어요. 회원 테이블 하나를 두 방언으로 나란히 보면 변환의 결이 한눈에 들어와요.
-- Before (Oracle) After (MySQL)
-- CREATE TABLE member ( CREATE TABLE member (
-- member_id NUMBER PRIMARY KEY, member_id BIGINT NOT NULL AUTO_INCREMENT,
-- username VARCHAR2(30) NOT NULL, username VARCHAR(30) NOT NULL UNIQUE,
-- nickname VARCHAR2(30) NOT NULL, nickname VARCHAR(30) NOT NULL,
-- bio VARCHAR2(150) bio VARCHAR(150),
-- ); PRIMARY KEY (member_id)
-- ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
번호 채번이 AUTO_INCREMENT 로, 문자형이 VARCHAR 로, 그리고 맨 끝에 저장 엔진(InnoDB)과 문자셋(utf8mb4)이 붙은 게 보이죠? 이렇게 옮긴 뒤 데이터를 넣으면, Oracle 에 있던 것과 똑같이 회원 12명과 게시물 50개가 MySQL 위에서 그대로 살아나요. 같은 인스타그램이, 이제 두 데이터베이스 위에서 나란히 돌아가는 거예요.
💡 한 줄 정리 — 자료형·기본값·채번·행 제한·NULL 대체, 이 다섯 자리만 방언에 맞게 손보면 Oracle 스키마를 MySQL 로 통째로 옮길 수 있다. 같은 인스타 데이터(회원 12·게시물 50)가 두 데이터베이스에서 똑같이 돈다.
마무리
오늘은 그동안 Oracle 로만 짜 온 인스타그램 스키마를, 통째로 MySQL 8.4 로 옮겨 봤어요. 컨테이너를 띄우고 utf8mb4 로 한글을 안전하게 담는 환경부터 시작해서, 자료형(VARCHAR2→VARCHAR·DATE→DATETIME), 자동 채번(시퀀스→AUTO_INCREMENT), 피드 페이지네이션(ROWNUM→LIMIT), NULL 메우기(NVL→IFNULL/COALESCE)까지 방언의 기초를 한 바퀴 돌았죠. E-1 에서 Oracle 과 표준어를 견줬던 그 눈으로, 이번엔 Oracle 과 MySQL 을 견준 거예요.
오늘 배운 핵심 세 가지
- 💡 두 번째 방언 — MySQL 은 Oracle·표준어와 또 다른 사투리(
IFNULL·LIMIT·AUTO_INCREMENT)를 쓴다. 시험은 Oracle, 실무는 MySQL 이 흔하니 옮기는 감각이 자산이다. - 💡 자료형과 채번 —
VARCHAR2→VARCHAR,DATE→DATETIME(시각 보존), 시퀀스→AUTO_INCREMENT. 자동 채번 번호는 롤백해도 갭이 생긴다. - 💡 행 제한과 NULL —
ROWNUM은LIMIT n OFFSET m으로,NVL은IFNULL/COALESCE로.LIMIT은 정렬 뒤에 잘라 함정이 없고, NULL 확인은 어디서나IS NULL이다.
다음 시간 예고
오늘은 자료형과 기본 방언을 옮겼어요. 그런데 함수와 복잡한 쿼리는 더 많이 갈려요. 다음 시간엔 날짜를 예쁘게 출력하는 TO_CHAR 가 MySQL 의 DATE_FORMAT 으로, 대댓글 트리를 따라가던 CONNECT BY(D-3)가 WITH RECURSIVE 로 바뀌는, 함수·조인·계층형 변환을 본격적으로 다뤄요. 그리고 Oracle 에선 빈 문자열을 NULL 로 보는데 MySQL 은 그렇지 않은, 헷갈리는 함정도 짚어요. 그렇게 인스타 쿼리를 MySQL 로 완전히 옮긴 뒤, 이어지는 백엔드 과목으로 매끄럽게 건너갈 준비를 마쳐요.
과제
오늘 옮긴 방언을 직접 손으로 다뤄 보는 과제예요. 쿼리를 실행하기 전에 결과를 먼저 예측한 뒤, MySQL 에서 실행해 맞혀 보세요.
[기초] 자료형과 NULL 대체 옮기기
(가) Oracle 의 member 테이블 CREATE TABLE 을 MySQL DDL 로 바꿔 적어 보세요. NUMBER·VARCHAR2·시퀀스 채번을 각각 어떤 MySQL 문법으로 옮겼는지 한 줄씩 설명을 다세요. (나) NVL(bio, '비공개') 를 쓰는 Oracle 쿼리를 MySQL 의 IFNULL 과 COALESCE 두 가지로 각각 옮기고, 결과가 같은지 확인하세요. (다) 자기소개가 비어 있는 회원이 몇 명인지 IS NULL 로 세어 보고, 왜 = NULL 로는 안 되는지 한 줄로 적으세요.
[응용] 피드 페이지네이션 만들기
(가) 최신순으로 게시물 1페이지(5건)와 2페이지(다음 5건)를 LIMIT/OFFSET 으로 짜 보세요. 정렬 기준은 작성일 최신순으로 하되, 같은 날짜가 있을 때를 대비해 두 번째 기준(post_id)까지 넣으세요. (나) 만약 ORDER BY 를 빼고 LIMIT 5 만 쓰면 어떤 문제가 생길지, 페이지를 넘길 때의 상황으로 한 줄 설명하세요. (다) 같은 "최신 5건" 을 Oracle 이라면 FETCH FIRST 로 어떻게 적을지 비교해 보고, MySQL LIMIT 이 ROWNUM 보다 함정이 적은 이유를 한 줄로 정리하세요.
[심화] 자동 채번 직접 확인하기
(가) member_id 를 비운 채 새 회원을 하나 넣고, 자동으로 몇 번이 붙는지 LAST_INSERT_ID() 로 확인하세요. (나) 그 회원을 지운 뒤 또 새 회원을 넣어, 번호가 지운 자리로 되돌아오는지 아니면 다음 번호로 건너뛰는지 직접 확인하고 결과를 한 줄로 적으세요. (다) 이 "번호가 되돌아오지 않는" 성질이 왜 문제가 안 되는지, 식별자의 역할(중복만 없으면 됨)과 엮어 생각해 보세요.
생각해볼 주제
1. 왜 데이터베이스마다 방언이 다를까
SQL 에는 표준(ANSI)이 있는데도 Oracle·MySQL·SQL Server 가 저마다 사투리를 써요. 표준만 따르면 어디서나 똑같이 쓸 수 있을 텐데, 왜 제품마다 NVL·IFNULL·ISNULL 처럼 다른 이름을 만들었을까요? 각 제품이 표준이 자리잡기 전부터 따로 발전해 온 역사와, "이미 그 문법으로 짜인 코드를 깰 수 없다" 는 현실을 함께 떠올려 보세요.
2. 실무에서 쿼리를 표준으로 짤지, 그 DB 에 맞춰 짤지
같은 NULL 대체를 MySQL 전용 IFNULL 로 짧게 짤 수도 있고, 표준 COALESCE 로 짤 수도 있어요. 지금 쓰는 데이터베이스에만 맞춰 짜면 당장은 편하지만, 나중에 다른 데이터베이스로 옮길 땐 전부 고쳐야 해요. "이 서비스는 앞으로도 MySQL 만 쓸 것 같다" 와 "언젠가 옮길지 모른다" 중 어느 쪽을 가정하느냐에 따라 선택이 갈려요. 두 경우에 각각 어떤 선택이 합리적일지, 그리고 그 가정이 틀렸을 때 치를 대가까지 견줘 보세요.
3. 자동으로 매긴 번호에 빈칸(갭)이 생겨도 괜찮을까
AUTO_INCREMENT 든 시퀀스든, 입력을 취소하면 번호에 빈칸이 생겨요. 게시물 번호가 1, 2, 4, 5... 처럼 3이 비어 있을 수 있죠. 만약 "우리 서비스 게시물은 빈 번호 없이 1, 2, 3 으로 딱 떨어져야 한다" 는 요구가 들어온다면 어떻게 대응해야 할까요? 식별자의 본래 역할이 "순서를 세는 것" 인지 "겹치지 않게 구분하는 것" 인지 떠올려 보고, 빈칸을 억지로 메우려 할 때 생기는 문제(동시에 여러 명이 글을 올리는 상황 등)도 함께 생각해 보세요.
✅ 예시 답안정답 보기
과제와 생각해볼 주제의 예시답안이에요. 정답을 그대로 베끼기보다 "이건 Oracle 문법을 MySQL 로 어떻게 옮기는가", "두 방언의 결과가 정말 같은가", "왜 이렇게 동작하는가" 를 떠올리는 감각을 기르는 게 목표예요.
🎯 [과제 1 예시답안] 자료형과 NULL 대체 옮기기
채점 포인트
| 항목 | 배점 | 핵심 |
|---|---|---|
(가) member DDL 을 MySQL 로 |
40% | NUMBER→BIGINT, VARCHAR2→VARCHAR, 시퀀스→AUTO_INCREMENT |
(나) NVL→IFNULL/COALESCE |
35% | 두 방식 결과 동일 |
(다) IS NULL 카운트 + = NULL 설명 |
25% | NULL 은 = 로 비교 불가 |
풀이 예시
(가) member 테이블을 MySQL DDL 로
CREATE TABLE member (
member_id BIGINT NOT NULL AUTO_INCREMENT, -- NUMBER + 시퀀스 → BIGINT + AUTO_INCREMENT
username VARCHAR(30) NOT NULL UNIQUE, -- VARCHAR2(30) → VARCHAR(30)
nickname VARCHAR(30) NOT NULL,
bio VARCHAR(150),
PRIMARY KEY (member_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
NUMBER 는 식별자라 BIGINT 로, VARCHAR2(n) 은 VARCHAR(n) 으로 옮겨요. Oracle 에선 시퀀스로 번호를 채웠지만 MySQL 은 컬럼에 AUTO_INCREMENT 를 붙여 자동으로 채번해요. 한글을 담으려고 끝에 CHARSET=utf8mb4 도 붙였어요.
(나) NVL 을 IFNULL / COALESCE 로
-- Oracle: NVL(bio, '비공개')
-- MySQL 사투리
SELECT member_id, IFNULL(bio, '비공개') AS profile FROM member;
-- 표준 (MySQL 에서도 통함)
SELECT member_id, COALESCE(bio, '비공개') AS profile FROM member;
자기소개가 있는 회원 1은 "백엔드 공부 중" 이 그대로 나오고, 비어 있는 회원 3·8은 두 쿼리 모두 "비공개" 로 똑같이 채워져요. IFNULL 과 COALESCE 의 결과가 완전히 같아요.
(다) 빈 소개 회원 세기
SELECT COUNT(*) AS no_bio FROM member WHERE bio IS NULL; -- 4
자기소개가 빈 회원은 3·5·8·10 으로 4명이에요. WHERE bio = NULL 로 쓰면 0건이 나와요. NULL 은 "값이 없음" 이라 = 로는 같은지 알 수 없기(늘 "알 수 없음") 때문이에요. 그래서 빈 값은 반드시 IS NULL 로 확인해요.
💡 튜터의 한마디 — 자료형 변환은 "이름만 살짝 다른 같은 그릇" 으로 보면 쉬워요. VARCHAR2→VARCHAR, NUMBER→BIGINT 처럼요. NULL 처리는 방언이 갈려도 "= NULL 금지, IS NULL" 이라는 철칙은 어디서나 똑같다는 걸 꼭 기억하세요.
🎯 [과제 2 예시답안] 피드 페이지네이션 만들기
채점 포인트
| 항목 | 배점 | 핵심 |
|---|---|---|
(가) 1·2페이지 LIMIT/OFFSET |
40% | 두 번째 정렬 기준(post_id)까지 |
(나) ORDER BY 없는 LIMIT 의 문제 |
30% | 페이지마다 순서 흔들림 |
(다) FETCH FIRST 비교 + 함정 |
30% | LIMIT 은 정렬 후 자름 |
풀이 예시
(가) 1페이지와 2페이지
-- 1페이지 (최신 5건)
SELECT post_id, member_id, created_at
FROM post
ORDER BY created_at DESC, post_id DESC
LIMIT 5;
-- 2페이지 (앞 5건 건너뛰고 다음 5건)
SELECT post_id, member_id, created_at
FROM post
ORDER BY created_at DESC, post_id DESC
LIMIT 5 OFFSET 5;
1페이지는 게시물 50·49·48·47·46, 2페이지는 45·12·44·10·43 이 나와요. 게시물 45와 12는 작성일이 둘 다 2026-06-10 으로 같은데, 두 번째 기준 post_id DESC 덕분에 번호가 큰 45가 먼저 와요.
(나) ORDER BY 를 빼면
ORDER BY 없이 LIMIT 5 만 쓰면 "아무 5개" 가 나와요. 데이터베이스가 순서를 보장하지 않으니, 1페이지에서 본 게시물이 2페이지에 또 나오거나, 어떤 게시물은 아예 안 보이는 사고가 생겨요. 페이지네이션엔 ORDER BY 가 필수예요.
(다) Oracle FETCH FIRST 와 비교
-- Oracle 표준 (같은 "최신 5건")
SELECT post_id, member_id, created_at
FROM post
ORDER BY created_at DESC, post_id DESC
FETCH FIRST 5 ROWS ONLY;
LIMIT 과 FETCH FIRST 는 둘 다 ORDER BY 뒤에 붙어서, 정렬이 끝난 결과를 자른다는 게 문법에 드러나 있어요. 그래서 Oracle ROWNUM 처럼 "정렬 전에 잘라 엉뚱한 결과가 나오는" 함정이 없어요. LIMIT 이 안전하고 읽기 쉬운 이유예요.
💡 튜터의 한마디 — 페이지네이션의 핵심은 "정렬을 확실히 고정" 하는 거예요. 같은 값이 있을 때를 대비해 post_id 같은 두 번째 기준까지 넣어야 페이지를 넘겨도 순서가 안 흔들려요. 이건 MySQL 이든 Oracle 이든 똑같이 중요해요.
🎯 [과제 3 예시답안] 자동 채번 직접 확인하기
채점 포인트
| 항목 | 배점 | 핵심 |
|---|---|---|
(가) id 생략 삽입 + LAST_INSERT_ID() |
35% | 다음 번호 자동 채번 |
| (나) 삭제 뒤 재삽입 — 갭 확인 | 40% | 번호가 되돌아오지 않음 |
| (다) 갭이 문제 안 되는 이유 | 25% | 식별자는 "구분" 이 역할 |
풀이 예시
(가) 자동 채번 확인
INSERT INTO member (username, nickname, bio) VALUES ('newbie', '새내기', NULL);
SELECT LAST_INSERT_ID(); -- 13
member_id 를 비운 채 넣으면, 직전 최대였던 12 다음인 13 이 자동으로 붙어요. 방금 채번된 번호는 LAST_INSERT_ID() 로 확인해요.
(나) 삭제 뒤 재삽입 — 번호가 되돌아올까
DELETE FROM member WHERE member_id = 13; -- 방금 만든 13 삭제
INSERT INTO member (username, nickname, bio) VALUES ('again', '다시', NULL);
SELECT LAST_INSERT_ID(); -- 14 (13 으로 되돌아오지 않음!)
13을 지운 뒤 또 넣으면, 비어 버린 13 자리로 되돌아가는 게 아니라 14 로 건너뛰어요. AUTO_INCREMENT 카운터는 한 번 올라가면 삭제해도 내려오지 않아서, 13은 빈 번호(갭)로 남아요.
(다) 갭이 왜 문제가 안 되나
식별자(member_id)의 본래 역할은 "회원을 겹치지 않게 구분" 하는 거예요. "1, 2, 3 으로 빈틈없이 세는" 게 역할이 아니에요. 13이 비고 14로 넘어가도, 14는 여전히 유일한 번호라 누구와도 겹치지 않아요. 그래서 갭은 식별엔 아무 문제가 없어요. 오히려 "갭을 메우겠다" 고 번호를 억지로 재사용하면, 여러 명이 동시에 글을 올릴 때 같은 번호가 두 번 발급되는 더 큰 사고가 날 수 있어요.
💡 튜터의 한마디 — "번호가 연속이어야 한다" 는 건 사람의 착각이에요. 데이터베이스의 식별자는 순번이 아니라 "겹치지 않는 이름표" 예요. 갭이 생기는 걸 자연스럽게 받아들이는 게, 자동 채번을 제대로 이해한 거예요.
생각해볼 주제
1. 왜 데이터베이스마다 방언이 다를까
문제 상황 요약
SQL 에는 표준(ANSI)이 있는데도 Oracle·MySQL·SQL Server 가 저마다 NVL·IFNULL·ISNULL 처럼 다른 사투리를 써요. 표준만 따르면 어디서나 똑같을 텐데, 왜 제품마다 다른 이름을 만들었을까요?
튜터의 가이드 및 해설
핵심은 역사와 하위 호환 이에요. 데이터베이스 제품들은 SQL 표준이 자리잡기 한참 전부터 각자 SQL 을 만들어 팔고 있었어요. Oracle 은 NVL 을, 나중에 나온 MySQL 은 IFNULL 을, SQL Server 는 ISNULL 을 각자 만들었죠. 그때는 "다 같은 이름을 쓰자" 는 약속(표준)이 아직 없었어요.
나중에 표준이 정해졌지만, 이미 그 사투리로 짜인 코드가 세상에 가득했어요. 그걸 다 깨뜨릴 수는 없으니, 제품들은 "기존 사투리는 그대로 두고, 표준(COALESCE)도 추가로 지원" 하는 길을 택했어요. 그래서 오늘날 MySQL 에선 IFNULL(자기 사투리)과 COALESCE(표준)가 둘 다 통해요. 방언이 사라지지 않고 남아 있는 건, 그 방언으로 짜인 코드를 지키기 위한 선택이에요.
🎯 SQLD는 이렇게 나온다
"
NVL(Oracle)·IFNULL(MySQL)·ISNULL(SQL Server)은 각 제품이 표준 이전부터 만들어 쓴 고유 함수이고,COALESCE만 ANSI 표준입니다. 제품들이 사투리를 없애지 않는 이유는 하위 호환, 즉 기존 코드를 깨지 않기 위해서입니다. 시험은 '표준 NULL 대체 함수는?'(답:COALESCE)처럼 사투리와 표준을 분별하는 능력을 묻습니다."
💡 실무에선
옮길 가능성이 조금이라도 있으면 표준 COALESCE 를 기본으로 쓰고, 사투리는 "읽고 분별할 줄만" 알아도 충분해요.
2. 실무에서 쿼리를 표준으로 짤지, 그 DB 에 맞춰 짤지
문제 상황 요약
같은 NULL 대체를 MySQL 전용 IFNULL 로 짧게 짤 수도 있고, 표준 COALESCE 로 짤 수도 있어요. 지금 쓰는 데이터베이스에만 맞추면 당장은 편하지만, 나중에 옮길 땐 전부 고쳐야 해요. 어느 쪽이 합리적일까요?
튜터의 가이드 및 해설
정답은 가정에 따라 다르다 예요. "이 서비스는 앞으로도 MySQL 만 쓸 것 같다" 면, 표준에 얽매일 필요가 적어요. 팀이 익숙한 사투리로 빠르게 짜고, MySQL 에 최적화된 기능도 마음껏 쓰는 게 생산적일 수 있어요. 반대로 "언젠가 다른 데이터베이스로 옮길지 모른다" 면, 처음부터 표준으로 짜 두는 게 나중의 이전 비용을 크게 줄여 줘요.
무게추는 그 가정이 틀렸을 때 치를 대가 예요. "MySQL 만 쓸 줄 알았는데" 갑자기 옮기게 되면, IFNULL·LIMIT 으로 도배된 쿼리를 전부 뜯어고쳐야 해요. 반대로 표준으로 짜 뒀는데 끝까지 MySQL 만 썼다면, 잃은 건 약간의 편의뿐이에요. 한쪽의 대가가 훨씬 크다는 점이 선택의 기울기를 만들어요.
🎯 SQLD는 이렇게 나온다
"단일 DBMS 환경에서 그 제품의 기능을 최대한 쓰고 싶다면 고유 문법이, 이식성·미래의 전환 가능성을 중시한다면 ANSI 표준이 유리합니다. 핵심은 '가정이 틀렸을 때의 비용' 입니다. 사투리로 짰다가 이전하면 전면 재작성 비용이 들지만, 표준으로 짰다가 이전하지 않으면 잃는 건 사소합니다. 불확실하면 표준이 안전한 기본값입니다."
💡 실무에선
대부분의 팀은 "표준을 기본으로, 성능이 꼭 필요한 일부 쿼리만 그 DB 의 사투리로" 라는 절충을 택해요.
3. 자동으로 매긴 번호에 빈칸(갭)이 생겨도 괜찮을까
문제 상황 요약
AUTO_INCREMENT 든 시퀀스든, 입력을 취소하거나 행을 지우면 번호에 빈칸이 생겨요. 게시물 번호가 1, 2, 4... 처럼 3이 빌 수 있죠. "우리 서비스 번호는 빈틈없이 떨어져야 한다" 는 요구가 들어오면 어떻게 대응할까요?
튜터의 가이드 및 해설
먼저 식별자의 역할을 다시 떠올려야 해요. member_id·post_id 같은 자동 채번 번호의 역할은 "각 행을 겹치지 않게 구분" 하는 거예요. "1, 2, 3 으로 순서를 세는" 게 아니에요. 그래서 3이 비고 4로 넘어가도, 4는 여전히 유일해서 식별엔 아무 문제가 없어요.
"빈틈없는 번호" 가 정말 필요한 경우(예: 영수증 일련번호, 세금 계산서 번호)는 분명히 있어요. 하지만 이건 식별자와 다른 문제 예요. 그럴 땐 자동 채번 식별자는 그대로 두고, "표시용 순번" 을 따로 계산해서 보여줘요(예: 조회할 때 행 순서대로 1, 2, 3 번호를 매김 — D-4 의 ROW_NUMBER 를 떠올려 보세요).
자동 채번 자체로 빈칸을 없애려고 하면 오히려 위험해요. 여러 명이 동시에 글을 올리는 상황에서 "빈 번호를 찾아 메우는" 로직은 같은 번호를 두 명에게 발급하는 사고로 이어지기 쉬워요. 그래서 식별자는 갭을 허용하고, 표시용 순번은 따로 다루는 게 안전한 설계예요.
🎯 SQLD는 이렇게 나온다
"
AUTO_INCREMENT·시퀀스는 롤백·삭제 시 번호가 되돌아오지 않아 갭이 생깁니다. 이는 식별자의 역할이 '순번 세기' 가 아니라 '유일하게 구분하기' 이기 때문에 정상 동작입니다. 연속 번호가 필요하면 식별자와 분리해ROW_NUMBER같은 표시용 순번을 따로 매깁니다."
💡 실무에선
식별자는 갭을 신경 쓰지 않고, "사용자에게 보이는 번호" 가 필요하면 조회 시점에 순번을 따로 계산해서 내려줘요.