문서 읽는 데 54분 · C2

C-2: 단일행 함수 — 꺼낸 데이터를 가공하기

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

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

안녕하세요, 홍순구 튜터입니다. 지난 시간에 우리는 SELECT로 데이터를 '있는 그대로' 꺼냈어요. WHERE로 원하는 행만 고르고, BETWEEN·IN·LIKE로 범위와 패턴을 걸고, ORDER BY로 줄을 세웠죠. 그런데 꺼낸 값은 표에 저장된 모습 그대로였어요. 'jaehoon'은 'jaehoon'으로, '2026-06-01'은 '2026-06-01'으로요.

오늘은 그 값을 '가공'해요. 'jaehoon'을 'JAEHOON'으로 바꾸고, '2026-06-01'을 '2026년 06월 01일'로 보기 좋게 다듬고, 게시물 글에 섞여 있는 #해시태그를 똑 떼어내요. 이렇게 값을 넣으면 가공해서 돌려주는 도구를 함수(function)라고 해요. 지난 시간에 LIKE로 "여행이 들어간 글"까지 찾았다면, 오늘은 그 글에서 #여행만 정확히 뽑아내는 데까지 가요.

텍스트
 오늘의 여정
    함수란? — 값을 넣으면 가공해 돌려주는 작은 기계 (DUAL 연습대)
    문자 함수  — 대소문자(UPPER·INITCAP) · 길이(LENGTH)
    문자 함수  — 자르기(SUBSTR) · 위치 찾기(INSTR)
    문자 함수  — 바꾸기(REPLACE) · 채우기(LPAD) · 공백 정리(TRIM)
    숫자 함수 — 반올림(ROUND) · 버림(TRUNC) · 나머지(MOD)
    날짜 함수 — 지금(SYSDATE) · 날짜 계산 · 개월 수(MONTHS_BETWEEN)
    변환 함수 — 날짜·숫자를 보기 좋게 (TO_CHAR · TO_DATE)
    정규식 함수 — 글에서 #해시태그만 똑 (REGEXP_SUBSTR)

💡 오늘 수업의 핵심 — "한 행이 들어가면 한 결과가 나오는 단일행 함수로, 글자를 자르고 날짜를 바꾸고 글에서 해시태그를 뽑아낸다"

🎯 학습 목표

  • 단일행 함수의 개념(한 행마다 한 결과)을 이해하고, 문자·숫자·날짜 함수로 데이터를 가공한다. (SQLD 2과목 'SQL 기본 — 단일행 함수')
  • 변환 함수 TO_CHAR·TO_DATE와 포맷 모델로 날짜·숫자를 원하는 모양으로 바꾼다.
  • 정규식 함수 REGEXP_SUBSTR·REGEXP_LIKE로 글 속 해시태그를 정확히 추출한다. (SQLD 2과목 '정규표현식')

Step 1: "함수란 무엇인가 — 값을 가공하는 작은 기계"

본격적으로 글자를 자르기 전에, 함수가 뭔지부터 잡고 갈게요. 함수는 믹서기 같은 거예요. 과일을 넣으면 주스가 되어 나오죠. SQL 함수도 값을 넣으면 정해진 방식으로 가공해서 결과를 돌려줘요. 'hello'를 넣으면 대문자 'HELLO'가 나오는 식이에요.

함수는 크게 두 종류로 나뉘어요. 이 구분이 중요해요.

텍스트
 단일행 함수 — 한 행이 들어가면, 한 결과가 나온다
   UPPER('jaehoon')    'JAEHOON'
   LENGTH('김재훈')    3
   회원이 12명이면, 결과도 12개 (행마다 하나씩)

 다중행 함수 — 여러 행이 모여서, 한 결과가 나온다  (다음 시간)
   게시물 50개를 모두 모아서  전체 개수 '50' 하나로

오늘 배우는 건 전부 위쪽, 단일행 함수예요. 행 하나하나에 적용돼서 행 수만큼 결과가 나와요. 아래쪽 다중행 함수(여러 행을 모아 하나로 요약하는 집계)는 다음 시간에 따로 다뤄요. 지금은 "함수 하나가 값 하나를 가공한다"만 기억하면 돼요.

함수를 빠르게 시험해 보고 싶을 때 쓰는 표가 하나 있어요. DUAL이라는 1행짜리 연습용 표예요.

SQL
SELECT UPPER('hello') FROM dual;

dual은 Oracle이 기본으로 주는 칸 하나짜리 표예요. 진짜 데이터가 아니라 함수만 잠깐 시험할 때 써요. 위 쿼리를 실행하면 HELLO가 한 줄 나와요. 표가 필요 없는 계산이나 함수 결과를 확인할 때 FROM dual을 붙이면 돼요.

🌟 알아두면 편해요 — 단일행 함수는 SELECT 절뿐 아니라 WHERE·ORDER BY에서도 쓸 수 있어요. "닉네임 길이가 짧은 순으로 정렬", "대문자로 바꾼 아이디가 'J'로 시작하는 회원" 같은 조건에도 함수를 넣을 수 있다는 뜻이에요. ★빈출

💡 한 줄 정리

단일행 함수는 행 하나마다 값을 가공해 결과 하나를 돌려주며(행 수만큼 결과), DUAL 표로 함수를 빠르게 시험해 볼 수 있다.

🙋 학생 질문 — "튜터님, DUAL은 왜 필요해요? 그냥 함수만 쓰면 안 되나요?"

Oracle에서는 SELECT 뒤에 반드시 FROM 표가 있어야 문법이 완성돼요. 그런데 UPPER('hello')처럼 진짜 표가 필요 없는 계산을 하고 싶을 때가 있죠. 이때 둘 데가 없어서 만든 게 DUAL이에요. 칸 하나, 행 하나뿐인 가짜 표라서 함수 결과가 딱 한 줄만 나와요. "함수 시험용 받침대"라고 생각하면 돼요. 참고로 MySQL 같은 다른 DB는 FROM 없이도 SELECT UPPER('hello')가 되는데, 이 방언 차이는 나중에 다뤄요.


Step 2: "글자를 다루는 함수 ① — 대소문자와 길이"

가장 자주 쓰는 문자 함수부터 볼게요. 먼저 대소문자를 바꾸는 함수 세 개예요. UPPER는 전부 대문자로, LOWER는 전부 소문자로, INITCAP은 단어 첫 글자만 대문자로 바꿔요. 우리 회원 아이디(username)가 전부 소문자니까, 이걸로 시험해 볼게요.

SQL
SELECT username, UPPER(username), INITCAP(username)
FROM member
WHERE member_id <= 3;
username UPPER(username) INITCAP(username)
jaehoon JAEHOON Jaehoon
minji MINJI Minji
seungwoo SEUNGWOO Seungwoo

UPPER는 통째로 대문자, INITCAP은 첫 글자만 대문자가 됐죠. 이름을 화면에 예쁘게 보여 줄 때 INITCAP을 자주 써요.

다음은 글자 길이를 재는 LENGTH예요. 한글 닉네임으로 재 볼게요.

SQL
SELECT nickname, LENGTH(nickname), LENGTHB(nickname)
FROM member
WHERE member_id = 1;
nickname LENGTH(nickname) LENGTHB(nickname)
김재훈 3 9

LENGTH는 글자 수를 세요. '김재훈'은 세 글자라 3이에요. 그런데 LENGTHB는 9가 나왔죠? LENGTHB는 글자 수가 아니라 바이트(byte) 수를 세요. 우리 데이터베이스는 한글 한 글자를 3바이트로 저장하거든요. 그래서 세 글자 × 3바이트 = 9가 나온 거예요.

⚠️ 함정 — 영어는 한 글자가 1바이트라서 LENGTHLENGTHB가 같아요. 하지만 한글은 한 글자가 여러 바이트라서 둘이 달라요. "글자 수는 LENGTH, 저장 크기는 LENGTHB"로 구분하세요. SQLD에서 한글 길이를 묻는 함정으로 자주 나와요. ★빈출

지난 시간에 이름 뒤에 '님'을 붙일 때 이어 붙이기 기호 ||를 썼던 거 기억하시죠. 그것도 두 값을 합쳐 새 값을 만드는 가공이었어요.

SQL
SELECT nickname || '님' FROM member WHERE member_id = 1;

결과는 '김재훈님'이에요. ||로 글자를 잇고, UPPER로 모양을 바꾸고, LENGTH로 크기를 재는 것까지 — 전부 글자를 다루는 도구예요. 이제 글자를 '자르는' 함수로 가 볼게요.

💡 한 줄 정리

UPPER/LOWER/INITCAP은 대소문자를 바꾸고, LENGTH는 글자 수를, LENGTHB는 바이트 수를 세며, 한글은 한 글자가 여러 바이트라 둘이 다르다.

🙋 학생 질문 — "튜터님, 한글이 왜 한 글자에 3바이트나 차지해요?"

컴퓨터는 글자를 숫자로 바꿔 저장하는데, 영어 알파벳은 종류가 적어서 1바이트(0~255)로 충분해요. 하지만 한글·한자·이모지처럼 전 세계 글자를 다 담으려면 숫자 범위가 훨씬 커야 해요. 그래서 우리 DB가 쓰는 방식(UTF-8)에서는 한글 한 글자를 3바이트로 저장해요. 화면에 보이는 글자 수와 실제 저장 크기가 다를 수 있다는 점만 기억하면 돼요. 컬럼 길이를 VARCHAR2로 정할 때 이 차이가 중요해지는데, 지금은 "한글은 글자보다 바이트가 크다" 정도로 충분해요.


Step 3: "글자를 다루는 함수 ② — 자르고 위치 찾기"

이제 글자를 자르고, 특정 글자가 어디 있는지 찾는 함수예요. 오늘의 주인공인 해시태그 추출이 여기서 시작돼요.

먼저 SUBSTR(서브스트링)은 글자의 일부를 잘라내요. SUBSTR(문자열, 시작위치, 길이) 형태예요. 한글 닉네임에서 성(姓) 한 글자만 잘라 볼게요.

SQL
SELECT nickname, SUBSTR(nickname, 1, 1) AS 성
FROM member
WHERE member_id <= 5;
nickname
김재훈
이민지
박승우
정하루
최도연

SUBSTR(nickname, 1, 1)은 "1번째 글자부터 1글자"라는 뜻이에요. 그래서 성씨 한 글자만 똑 떨어져 나왔죠.

다음은 INSTR(인스트링)이에요. 특정 글자가 몇 번째 위치에 있는지 알려 줘요. 게시물 글에서 #이 어디 있는지 찾아볼게요.

SQL
SELECT post_id, caption, INSTR(caption, '#') AS 샵위치
FROM post
WHERE post_id IN (1, 3, 12);
post_id caption 샵위치
1 첫 게시물입니다 #안녕 #일상 #첫게시물 10
3 주말 산책 0
12 제주도 여행 #여행 #제주 #바다 8

1번 글은 #이 10번째 글자에 있고, 12번 글은 8번째에 있어요. 그런데 3번 글('주말 산책')은 #이 없어서 0이 나왔죠. 이게 중요해요.

⚠️ 함정INSTR은 찾는 글자가 없으면 NULL이 아니라 0을 돌려줘요. "없으면 빈칸이겠지" 하고 NULL로 생각하면 틀려요. 이 0을 어떻게 처리할지(예: 해시태그 없는 글은 따로 표시하기)는 다음 시간 NULL 처리 함수에서 이어 가요.

이제 두 함수를 합쳐 볼게요. # 위치를 INSTR로 찾고, 그 위치부터 끝까지를 SUBSTR로 자르면 해시태그 부분만 뽑을 수 있어요.

SQL
SELECT post_id, SUBSTR(caption, INSTR(caption, '#')) AS 해시태그부분
FROM post
WHERE post_id IN (1, 12);
post_id 해시태그부분
1 #안녕 #일상 #첫게시물
12 #여행 #제주 #바다

# 위치부터 끝까지 잘랐으니, 해시태그가 여러 개면 전부 딸려 나와요. "첫 번째 해시태그 하나만 깔끔하게"는 이 방법으론 어려워요. 글자를 손으로 자르는 이 방식은 한계가 있어서, 정확히 한 개만 뽑는 더 좋은 도구를 Step 8에서 정규식으로 만나요.

참고로 INSTR은 "몇 번째 #인지"도 찾을 수 있어요. INSTR(문자열, 찾을것, 시작위치, 몇번째) 형태예요.

SQL
SELECT INSTR(caption, '#', 1, 2) AS 두번째샵
FROM post
WHERE post_id = 2;

2번 글('오늘도 코딩 #개발 #코딩 #개발자')에서 1번째 글자부터 세어 2번째 #의 위치를 찾으면 12가 나와요.

💡 한 줄 정리

SUBSTR(문자열, 시작, 길이)는 글자를 잘라내고, INSTR(문자열, 찾을것)은 위치를 알려 주되 없으면 0을 돌려주며, 둘을 합치면 글에서 원하는 부분을 뽑을 수 있다.

🙋 학생 질문 — "튜터님, SUBSTR 시작 위치는 0부터예요, 1부터예요?"

SQL의 SUBSTR1부터 세요. SUBSTR(nickname, 1, 1)이 첫 글자예요. 프로그래밍 언어 중에는 0부터 세는 경우가 많아서('첫 글자가 0번') 헷갈리기 쉬운데, SQL은 사람이 세는 방식처럼 1부터예요. 재미있는 건 시작 위치에 음수도 쓸 수 있다는 거예요. SUBSTR(caption, -2)라고 하면 "뒤에서 2번째 글자부터 끝까지"가 돼요. 끝부분을 자를 때 유용하니 기억해 두세요.


Step 4: "글자를 다루는 함수 ③ — 바꾸고 채우기"

글자를 자르는 걸 봤으니, 이번엔 바꾸고 채우는 함수예요.

먼저 REPLACE는 글자를 찾아서 다른 글자로 바꿔요. #을 빈 문자열로 바꾸면 해시태그 기호를 다 떼어낼 수 있어요.

SQL
SELECT caption, REPLACE(caption, '#', '') AS 샵제거
FROM post
WHERE post_id = 1;
caption 샵제거
첫 게시물입니다 #안녕 #일상 #첫게시물 첫 게시물입니다 안녕 일상 첫게시물

REPLACE(caption, '#', '')은 "#을 찾아서 아무것도 없는 것('')으로 바꿔라", 즉 지우라는 뜻이에요. 세 개의 #이 한 번에 다 사라졌죠.

다음은 LPADRPAD예요. 글자가 정해진 길이가 될 때까지 빈 곳을 특정 문자로 채워요. LPAD는 왼쪽을, RPAD는 오른쪽을 채워요. 게시물 번호를 다섯 자리로 맞춰 볼게요.

SQL
SELECT post_id,
       LPAD(post_id, 5, '0') AS 다섯자리,
       'P' || LPAD(post_id, 4, '0') AS 게시물코드
FROM post
WHERE post_id IN (1, 12, 50);
post_id 다섯자리 게시물코드
1 00001 P0001
12 00012 P0012
50 00050 P0050

LPAD(post_id, 5, '0')은 "전체 다섯 자리가 되도록 왼쪽을 0으로 채워라"예요. 1이 '00001'이 됐죠. 여기에 ||로 'P'를 앞에 붙이면 'P0001' 같은 코드가 만들어져요. 번호 자리를 맞추면 정렬도 깔끔해지고 코드 모양도 통일돼요.

RPAD는 반대로 오른쪽을 채워요.

SQL
SELECT RPAD(username, 10, '.') FROM member WHERE member_id = 2;

결과는 'minji.....'예요. 'minji'(5글자) 뒤에 점을 채워 10글자를 만든 거예요.

마지막으로 TRIM은 양쪽 공백을 잘라내요. 결과를 눈으로 보려고 대괄호로 감싸 볼게요.

SQL
SELECT '[' || TRIM('  hi  ') || ']' FROM dual;

결과는 '[hi]'예요. 앞뒤 공백이 사라져서 대괄호가 글자에 딱 붙었죠. 사용자가 입력한 값 앞뒤에 실수로 들어간 공백을 정리할 때 자주 써요. 왼쪽만 자르는 LTRIM, 오른쪽만 자르는 RTRIM도 있어요.

💡 한 줄 정리

REPLACE는 글자를 찾아 바꾸거나 지우고, LPAD/RPAD는 정해진 길이까지 빈 곳을 채우며, TRIM은 양쪽 공백을 잘라낸다.

🙋 학생 질문 — "튜터님, LPAD로 0을 채우는 건 어디에 써요?"

번호나 코드의 자릿수를 맞출 때 자주 써요. 예를 들어 주문번호를 '1', '2', ... '100'처럼 두면 길이가 제각각이라 정렬도 어긋나고 보기에도 들쭉날쭉해요. LPAD로 '00001', '00002', ... '00100'처럼 자리를 맞추면 항상 같은 길이라 정렬이 깔끔하고, 'P0001' 같은 코드 모양도 통일돼요. 영수증 번호, 사번, 상품 코드처럼 "자릿수가 고정된 번호"를 만들 때 떠올리면 돼요.


Step 5: "숫자를 다루는 함수 — 반올림·버림·나머지"

이번엔 숫자를 가공하는 함수예요. 먼저 ROUND(반올림)와 TRUNC(버림)부터 볼게요. 둘 다 두 번째 인자로 "어느 자리까지 남길지"를 정해요. DUAL로 시험해 볼게요.

SQL
SELECT ROUND(123.456, 2), ROUND(123.456, 0), ROUND(123.456, -1) FROM dual;
결과
ROUND(123.456, 2) 123.46
ROUND(123.456, 0) 123
ROUND(123.456, -1) 120

두 번째 숫자가 자릿수예요. 양수면 소수점 아래, 0이면 정수, 음수면 정수 위쪽 자리에서 반올림해요. ROUND(123.456, 2)는 소수 둘째 자리까지 남겨 123.46, -1은 10의 자리에서 반올림해 120이 됐어요. 음수 자릿수가 SQLD 단골 함정이에요. ★빈출

TRUNC는 같은 자리에서 반올림 없이 그냥 잘라 버려요.

SQL
SELECT TRUNC(123.456, 2), TRUNC(123.456, -1) FROM dual;
결과
TRUNC(123.456, 2) 123.45
TRUNC(123.456, -1) 120

같은 소수 둘째 자리인데 ROUND는 123.46, TRUNC는 123.45죠. ROUND는 반올림해서 올라갔고, TRUNC는 뒤를 그냥 버린 거예요.

⚠️ 함정ROUND(반올림)와 TRUNC(버림)를 헷갈리면 안 돼요. 돈 계산에서 특히 중요해요. 소수점 처리를 반올림할지 버림할지에 따라 1원씩 차이가 나거든요. "ROUND는 올림질, TRUNC는 싹둑"으로 기억하세요.

다음은 MOD예요. 나눗셈의 나머지를 구해요. MOD(숫자, 2)가 0이면 짝수, 1이면 홀수죠. 회원 번호가 짝수인 회원만 골라볼게요.

SQL
SELECT member_id FROM member
WHERE MOD(member_id, 2) = 0
ORDER BY member_id;

결과는 2, 4, 6, 8, 10, 12 — 짝수 번호 회원 6명이 나와요. MOD로 짝수·홀수를 가르거나, "3으로 나눠 떨어지는 것만" 같은 조건을 만들 수 있어요.

마지막으로 CEIL(올림)과 FLOOR(내림)예요. CEIL은 무조건 위 정수로, FLOOR는 무조건 아래 정수로 보내요.

SQL
SELECT CEIL(4.1), FLOOR(4.9), CEIL(-4.1), FLOOR(-4.9) FROM dual;
결과
CEIL(4.1) 5
FLOOR(4.9) 4
CEIL(-4.1) -4
FLOOR(-4.9) -5

CEIL(4.1)은 4.1보다 큰 가장 가까운 정수 5, FLOOR(4.9)는 4.9보다 작은 4예요. 음수에서 헷갈리기 쉬워요. CEIL(-4.1)은 -4.1보다 큰 정수라 -4예요(-4가 -5보다 크니까요).

💡 한 줄 정리

ROUND는 반올림, TRUNC는 버림이고 자릿수에 음수를 주면 정수 위 자리에서 처리하며, MOD는 나머지, CEIL/FLOOR는 올림/내림이다.

🙋 학생 질문 — "튜터님, ROUND랑 TRUNC에 음수 자릿수를 주는 게 잘 이해가 안 돼요."

자릿수를 0을 기준으로 좌우로 생각하면 쉬워요. 소수점이 0번 자리고, 오른쪽(소수점 아래)이 양수, 왼쪽(정수 위)이 음수예요. ROUND(123.456, 2)는 소수 둘째 자리까지(양수 2), ROUND(123.456, 0)은 정수까지, ROUND(123.456, -1)은 10의 자리에서(음수 1) 처리해요. 그래서 -1은 일의 자리를 반올림해 120, -2라면 십의 자리를 반올림해 100이 돼요. "양수는 소수점 오른쪽, 음수는 왼쪽"만 기억하면 돼요.


Step 6: "날짜를 다루는 함수 — 지금, 그리고 날짜 계산"

날짜 함수로 가기 전에 한 가지 짚고 갈게요. Oracle에서 날짜는 사실 속으로는 숫자처럼 다뤄져요. 그래서 날짜끼리 빼면 며칠 차이인지 숫자가 나오고, 날짜에 1을 더하면 하루 뒤가 돼요. 이 성질을 알면 날짜 함수가 훨씬 쉬워요.

먼저 SYSDATE는 지금 이 순간의 날짜와 시각이에요.

SQL
SELECT SYSDATE FROM dual;

실행하는 바로 그 순간의 날짜가 나와요. 오늘 실행하면 오늘 날짜, 다음 시간에 실행하면 그날 날짜가 나오죠. 그래서 결과가 매번 달라져요. 이 점은 뒤에서 다시 짚을게요.

날짜끼리 빼면 며칠 차이인지 나와요. 매번 바뀌는 SYSDATE 대신, 기준일을 6월 30일로 고정해서 "그 글이 며칠 전에 올라왔는지" 계산해 볼게요.

SQL
SELECT post_id, created_at,
       TO_DATE('2026-06-30', 'YYYY-MM-DD') - created_at AS 경과일
FROM post
WHERE post_id IN (1, 33);
post_id created_at 경과일
1 2026-06-01 29
33 2026-05-26 35

6월 30일에서 6월 1일을 빼니 29일, 5월 26일을 빼니 35일이 나왔어요. 날짜 빼기는 이렇게 '일 수'를 돌려줘요.

'며칠'이 아니라 '몇 개월'을 알고 싶으면 MONTHS_BETWEEN을 써요.

SQL
SELECT post_id, MONTHS_BETWEEN(DATE '2026-06-30', created_at) AS 개월수
FROM post
WHERE post_id IN (1, 13);
post_id created_at 개월수
1 2026-06-01 0.9354...
13 2026-05-02 1.9032...

결과가 소수로 나오죠. 1번 글은 약 0.94개월(한 달이 채 안 됨), 13번 글은 약 1.9개월 전이에요. 딱 떨어지지 않는 날수까지 소수로 표현해 줘요.

날짜에 개월을 더하는 ADD_MONTHS, 그 달의 마지막 날을 주는 LAST_DAY도 있어요.

SQL
SELECT ADD_MONTHS(created_at, 1) AS 한달후,
       LAST_DAY(created_at)       AS 그달말일
FROM post
WHERE post_id = 1;

1번 글(2026-06-01)에 한 달을 더하면 2026-07-01, 그 달의 마지막 날은 2026-06-30이 나와요. LAST_DAY는 윤년도 알아서 처리해요.

SQL
SELECT LAST_DAY(DATE '2024-02-10') FROM dual;

2024년은 윤년이라 2월 마지막 날이 2024-02-29로 나와요. 직접 며칠인지 계산할 필요 없이 함수가 맞춰 줘요.

⚠️ 함정SYSDATE를 쿼리에 직접 넣으면 결과가 실행할 때마다 달라져요. "오늘로부터 30일 이내" 같은 조건엔 자연스럽지만, 예제나 시험 문제처럼 답이 정해져 있어야 할 땐 고정 날짜(DATE '2026-06-30')로 계산해야 결과가 흔들리지 않아요.

💡 한 줄 정리

날짜끼리 빼면 일 수가, MONTHS_BETWEEN은 개월 수(소수)가 나오며, ADD_MONTHS·LAST_DAY로 개월을 더하거나 그 달 말일을 구하고, SYSDATE는 실행 순간의 시각이라 결과가 매번 바뀐다.

🙋 학생 질문 — "튜터님, 날짜에서 날짜를 빼면 왜 숫자가 나와요?"

Oracle은 날짜를 속으로 "기준일로부터 며칠 지났나"라는 숫자로 저장하기 때문이에요. 그래서 두 날짜를 빼면 두 숫자의 차이, 즉 며칠 차이인지가 자연스럽게 나와요. 6월 30일에서 6월 1일을 빼면 29가 나오는 식이죠. 같은 이유로 날짜에 1을 더하면 하루 뒤, 7을 더하면 일주일 뒤가 돼요. "날짜는 속으로 숫자"라고 생각하면 날짜 계산이 훨씬 직관적으로 잡혀요.


Step 7: "형을 바꾸는 함수 — TO_CHAR · TO_DATE"

값에는 종류(형, type)가 있어요. 숫자, 문자, 날짜죠. 변환 함수는 이 형을 바꿔요. 지난 시간에 예고한 "가입 날짜를 2026년 6월처럼 보기 좋게"를 드디어 오늘 해결해요. 단, 우리 회원 표엔 가입일 칸이 없으니 게시 날짜(created_at)로 보여 드릴게요.

TO_CHAR(날짜, 포맷)은 날짜를 원하는 모양의 문자로 바꿔요.

SQL
SELECT created_at,
       TO_CHAR(created_at, 'YYYY"년" MM"월" DD"일"') AS 한글날짜
FROM post
WHERE post_id = 1;
created_at 한글날짜
2026-06-01 2026년 06월 01일

'YYYY"년" MM"월" DD"일"'이 포맷이에요. 자주 쓰는 기호는 이래요.

기호 예 (2026-06-01)
YYYY 연도 네 자리 2026
MM 월 두 자리 06
DD 일 두 자리 01
DAY 요일 월요일
HH24 / MI 24시간 시 / 분 시각

큰따옴표("년") 안의 글자는 포맷 기호가 아니라 그대로 출력돼요. 그래서 '2026년 06월 01일'처럼 사이에 한글을 끼울 수 있어요.

요일도 뽑을 수 있어요. 한글 요일은 언어를 한국어로 지정해 줘요.

SQL
SELECT TO_CHAR(created_at, 'DAY', 'NLS_DATE_LANGUAGE=KOREAN') AS 요일
FROM post
WHERE post_id = 1;

2026년 6월 1일은 '월요일'이 나와요. 세 번째 인자 NLS_DATE_LANGUAGE=KOREAN이 "요일을 한국어로 표시해 달라"는 옵션이에요. 안 주면 'MONDAY'처럼 영어로 나와요.

TO_CHAR는 숫자도 다뤄요. 큰 숫자에 천 단위 콤마를 넣어 볼게요.

SQL
SELECT TO_CHAR(1234567, '999,999,999') FROM dual;

결과는 '1,234,567'이에요. 9는 자릿수 한 칸을 뜻하고, 콤마는 그 위치에 콤마를 찍으라는 표시예요. 금액을 화면에 보여 줄 때 자주 써요.

반대 방향도 있어요. TO_DATE(문자, 포맷)는 문자를 날짜로 바꿔요.

SQL
SELECT TO_DATE('2026-06-01', 'YYYY-MM-DD') FROM dual;

'2026-06-01'이라는 글자를 진짜 날짜 값으로 바꿔 줘요. 사용자가 입력한 날짜 문자열을 날짜로 비교할 때 필요해요.

⚠️ 함정 — Oracle은 형이 안 맞으면 알아서 바꿔 주기도 해요. '10' + 5를 실행하면 문자 '10'을 숫자로 바꿔 15가 나와요. 편하지만 위험해요. '10원'처럼 숫자가 아닌 글자가 섞이면 그 순간 오류가 나거든요. 그래서 실무에선 TO_NUMBER·TO_DATE로 직접 바꾸는 명시적 변환을 권해요. "자동 변환에 기대지 말자"가 핵심이에요. ★빈출

💡 한 줄 정리

TO_CHAR(날짜/숫자, 포맷)은 원하는 모양의 문자로 바꾸고(YYYY·MM·DD·콤마 등), TO_DATE는 문자를 날짜로 되돌리며, 자동 형변환에 기대면 위험하니 직접 변환을 권한다.

🙋 학생 질문 — "튜터님, 포맷에서 MM은 월인데 분은 왜 MM이 아니에요?"

좋은 질문이에요. 월과 분이 둘 다 'M'으로 시작해서 헷갈리기 쉬운데, 월은 MM, 분은 MI로 구분해요. MI는 'MInute(분)'의 앞 글자라고 생각하면 외우기 쉬워요. 비슷하게 시는 HH(또는 24시간제는 HH24), 초는 SS예요. TO_CHAR(SYSDATE, 'HH24:MI:SS')라고 하면 '14:30:05'처럼 시각이 나와요. "월은 MM, 분은 MI"만 확실히 구분하면 시각 포맷은 거의 다 잡혀요.


Step 8: "패턴을 다루는 함수 — 정규식으로 해시태그를 똑"

드디어 오늘의 목표예요. 지난 시간에 LIKE로 "여행이 들어간 글"까지는 찾았지만, "글에서 #해시태그만 똑 떼어내기"는 못 했죠. Step 3에서 SUBSTRINSTR로 시도했지만 해시태그가 여러 개면 다 딸려 나왔고요. 이걸 깔끔하게 해결하는 게 정규식 함수예요.

정규식(정규표현식)은 "이런 모양의 글자"를 한 줄 패턴으로 적는 작은 언어예요. 우리가 쓸 패턴 #[^ ]+을 한 조각씩 읽어 볼게요.

텍스트
 #[^ ]+     해시태그 하나의 모양

   #        샵 글자 그대로
   [^ ]     공백이 아닌 글자 하나   (^ 는 '아닌', 대괄호 안 빈칸은 공백)
   +        앞의 것이 한 개 이상

   합치면:  샵으로 시작해서 공백 직전까지 이어지는 덩어리 = 해시태그 한 개

이 패턴으로 REGEXP_SUBSTR을 쓰면 해시태그를 정확히 뽑아요.

SQL
SELECT post_id, caption, REGEXP_SUBSTR(caption, '#[^ ]+') AS 첫해시태그
FROM post
WHERE post_id IN (1, 12, 3, 29);
post_id caption 첫해시태그
1 첫 게시물입니다 #안녕 #일상 #첫게시물 #안녕
12 제주도 여행 #여행 #제주 #바다 #여행
3 주말 산책 (없음)
29 여행 다녀왔어요 (없음)

Step 3의 손으로 자르던 방식은 12번 글에서 '#여행 #제주 #바다'가 다 딸려 나왔는데, REGEXP_SUBSTR은 첫 번째 해시태그 '#여행'만 깔끔하게 뽑았어요. 같은 게시물인데 결과가 확 달라졌죠. 이게 정규식의 힘이에요.

여기서 눈여겨볼 게 3번과 29번이에요. #이 없는 글은 결과가 NULL(빈칸)로 나와요. 특히 29번 '여행 다녀왔어요'는 '여행'이라는 단어는 있지만 #여행 태그는 없어서 NULL이에요. 이 빈 결과를 어떻게 채울지(예: '태그 없음'으로 표시)는 다음 시간 NULL 처리 함수에서 이어 가요.

해시태그가 여러 개면 몇 번째 것인지도 고를 수 있어요.

SQL
SELECT REGEXP_SUBSTR(caption, '#[^ ]+', 1, 2) FROM post WHERE post_id = 1;

1번 글에서 2번째 해시태그를 뽑으면 '#일상'이 나와요. 네 번째 인자 2가 "2번째 것"이라는 뜻이에요.

해시태그가 몇 개인지 세는 REGEXP_COUNT도 있어요.

SQL
SELECT post_id, REGEXP_COUNT(caption, '#') AS 태그수
FROM post
WHERE post_id IN (1, 5, 3);
post_id caption 태그수
1 첫 게시물입니다 #안녕 #일상 #첫게시물 3
5 필름 카메라 #사진 1
3 주말 산책 0

해시태그 3개, 1개, 0개가 또렷이 갈리죠. 반대로 해시태그를 다 떼고 본문만 남기려면 REGEXP_REPLACE를 써요.

SQL
SELECT '[' || REGEXP_REPLACE(caption, '#[^ ]+', '') || ']' AS 본문만
FROM post
WHERE post_id = 1;

결과는 '[첫 게시물입니다 ]'예요. 해시태그 세 개가 다 사라졌어요. 떼어낸 곳에 공백이 남는데, 이건 Step 4에서 배운 TRIM으로 정리하면 돼요. 함수들이 이렇게 이어져요.

마지막으로 REGEXP_LIKE예요. 지난 시간 LIKE의 강화판이라고 보면 돼요. LIKE와 비교해 볼게요. 지난 시간에 '여행'이 들어간 글은 4건이었죠. 그런데 진짜 #여행 태그를 단 글만 찾으면 어떨까요?

SQL
SELECT post_id, caption FROM post
WHERE REGEXP_LIKE(caption, '#여행');
post_id caption
12 제주도 여행 #여행 #제주 #바다
25 주말 여행 준비 #여행
39 여행 사진 정리 #여행 #추억 #필름

3건이 나와요. '여행'이라는 단어가 든 글은 4건인데, 진짜 #여행 태그를 단 글은 3건이에요. 차이는 29번 '여행 다녀왔어요'예요. 단어는 있지만 태그가 없어서 빠진 거죠. LIKE는 "글자가 들어있나"만 보지만, REGEXP_LIKE는 "#이 붙은 태그인가"까지 정교하게 가려내요.

오늘 우리는 글에서 해시태그만 정확히 뽑아내는 데까지 왔어요. 지난 시간에 LIKE로 멈췄던 그 지점을 정규식으로 넘어선 거예요.

🌟 알아두면 좋아요 — 정규식은 강력하지만 패턴이 복잡해지면 읽기 어려워져요. SQLD에서는 REGEXP_SUBSTR·REGEXP_LIKE의 기본 형태 위주로 나오니, 오늘 본 #[^ ]+처럼 한 조각씩 읽는 연습을 해 두면 충분해요.

💡 한 줄 정리

REGEXP_SUBSTR은 패턴에 맞는 부분을 정확히 뽑고(여러 개 중 몇 번째도 지정), REGEXP_COUNT는 개수, REGEXP_REPLACE는 치환, REGEXP_LIKELIKE보다 정교한 패턴 검색을 한다.

🙋 학생 질문 — "튜터님, 정규식 #[^ ]+ 가 너무 어려워 보여요. 꼭 외워야 하나요?"

통째로 외우기보다 조각으로 읽는 연습을 권해요. 정규식은 글자 하나하나가 뜻을 가진 작은 부품이에요. #은 샵 그대로, [ ]는 "이 안의 글자 중 하나", ^는 "아닌", +는 "한 개 이상"이에요. [^ ]는 "공백이 아닌 글자 하나", 뒤에 +가 붙으니 "공백 아닌 글자가 한 개 이상". 그래서 "샵 뒤로 공백 전까지"가 돼요. SQLD에서는 이 정도 기본 부품만 알면 풀 수 있는 수준으로 나와요. 처음엔 패턴을 천천히 읽어 보고, 자주 쓰는 패턴 몇 개만 익숙해지면 돼요.


마무리

오늘은 표에서 꺼낸 값을 '가공'하는 단일행 함수들을 배웠어요. 글자를 자르고(SUBSTR) 바꾸고(REPLACE) 길이를 재고(LENGTH), 숫자를 반올림하고(ROUND) 나머지를 구하고(MOD), 날짜를 계산하고(MONTHS_BETWEEN) 보기 좋게 바꾸고(TO_CHAR), 마지막엔 정규식으로 글에서 해시태그만 똑 떼어냈죠(REGEXP_SUBSTR). 데이터를 '읽기만' 하던 단계에서 '내 마음대로 가공하는' 단계로 한 걸음 나아간 거예요.

오늘 배운 핵심 세 가지

  • 💡 하나 — 단일행 함수는 행 하나마다 값을 가공해 결과 하나를 돌려준다. 문자(UPPER·SUBSTR·INSTR·REPLACE·LPAD·TRIM), 숫자(ROUND·TRUNC·MOD·CEIL·FLOOR)가 대표적이다.
  • 💡 — 날짜는 속으로 숫자라 빼면 일 수가 나오고, MONTHS_BETWEEN·ADD_MONTHS·LAST_DAY로 계산하며, TO_CHAR·TO_DATE로 형을 바꾼다. 자동 형변환에 기대면 위험하다.
  • 💡 — 정규식 함수 REGEXP_SUBSTR·REGEXP_LIKESUBSTR+INSTR이나 LIKE로는 어려운 정교한 패턴 추출·검색을 해낸다. 글에서 #해시태그만 정확히 뽑는 게 대표 예다.

다음 시간 예고

오늘 함수를 쓰다가 빈 결과를 자주 만났죠. 해시태그 없는 글에서 INSTR은 0을, REGEXP_SUBSTR은 NULL을 돌려줬어요. 자기소개(bio)가 비어 있는 회원도 있었고요. 다음 시간엔 이 NULL을 길들여요. NULL을 다른 값으로 대체하는 NVL·COALESCE, 그리고 "값이 이러면 저렇게, 저러면 이렇게" 조건에 따라 값을 바꾸는 DECODE·CASE를 배워요. 오늘 'NULL이 나와서 곤란했던' 부분들을 다음 시간에 깔끔하게 채워 넣을 거예요.


과제

오늘 배운 단일행 함수를 직접 써 보는 과제예요. 결과가 어떻게 나올지 먼저 예상한 뒤 실행해 확인해 보세요.

[기초] 문자 함수로 회원 정보 가공하기

회원 표에서 다음을 조회하는 SQL을 써 보세요. (가) username을 전부 대문자로 바꿔 함께 보여 주세요(UPPER). (나) nickname에서 성(첫 글자)만 잘라 보여 주세요(SUBSTR). (다) nickname 뒤에 '님'을 붙이고(||), 그 글자 수를 함께 보여 주세요(LENGTH). 김재훈이면 '김재훈님'과 글자 수가 어떻게 나올지 예상해 보세요.

[응용] 날짜를 보기 좋게 바꾸기

게시물 표에서 다음을 조회하는 SQL을 써 보세요. (가) created_at을 '2026년 06월 01일' 모양의 문자로 바꿔 보여 주세요(TO_CHAR). (나) 기준일을 DATE '2026-06-30'으로 두고, 각 게시물이 그 기준일로부터 며칠 전에 올라왔는지 계산해 보여 주세요(날짜 빼기). (다) SYSDATE를 직접 쓰지 않고 기준일을 고정한 이유를 한 줄로 적어 보세요.

[심화] 정규식으로 해시태그 다루기

게시물 표에서 다음을 해 보세요. (가) REGEXP_SUBSTR로 각 게시물의 첫 번째 해시태그를 뽑아 보세요. (나) 같은 일을 Step 3의 SUBSTR+INSTR 방식으로도 해 보고, 해시태그가 여러 개인 게시물(예: 1번)에서 두 결과가 어떻게 다른지 비교해 설명해 보세요. (다) REGEXP_COUNT로 해시태그가 2개 이상인 게시물을 찾으려면 WHERE에 어떤 조건을 걸어야 할지 SQL로 완성해 보세요. 그리고 해시태그가 없는 글에서 REGEXP_SUBSTR 결과가 무엇이 되는지, 그 빈 값을 어떻게 처리하면 좋을지 생각을 적어 보세요.


생각해볼 주제

1. 손으로 자르기 vs 정규식, 언제 무엇을 쓸까

해시태그를 뽑는 방법이 둘 있었어요. SUBSTR+INSTR로 직접 자르는 방식과 REGEXP_SUBSTR 정규식 방식이죠. 정규식이 더 정확하고 짧았지만, 패턴이 복잡해지면 읽기 어려워진다는 단점도 있었어요. 단순한 자르기엔 어느 쪽이 나을지, 복잡한 패턴엔 어느 쪽이 나을지, 가독성과 정확함 사이에서 무엇을 기준으로 고를지 생각해 보세요.

2. 자동 형변환은 편한데 왜 위험할까

'10' + 5가 15로 나오는 것처럼 Oracle은 형이 안 맞으면 알아서 바꿔 줘요. 타이핑이 줄어 편하죠. 그런데 실무에선 TO_NUMBER·TO_DATE로 직접 바꾸라고 권해요. 자동 변환에 기댈 때 어떤 값이 들어오면 갑자기 오류가 날 수 있는지, 그리고 그 오류가 언제 터질지 예측하기 어려운 게 왜 위험한지 생각해 보세요.

3. SYSDATE를 쿼리에 직접 박으면 생기는 일

SYSDATE는 실행하는 순간의 시각이라 결과가 매번 달라져요. "최근 30일 글"처럼 항상 지금 기준이어야 할 땐 좋지만, 답이 정해져 있어야 하는 보고서나 예제에선 곤란할 수 있어요. 같은 쿼리인데 실행하는 날마다 결과가 다르면 어떤 문제가 생길지, 결과를 똑같이 재현해야 할 때는 날짜를 어떻게 다루면 좋을지 생각해 보세요.

✅ 예시 답안정답 보기

과제와 생각해볼 주제의 예시답안이에요. 정답을 그대로 베끼기보다, 왜 그 함수를 그렇게 쓰는지 흐름을 따라와 주세요. 함수 이름을 외우는 게 아니라, "이 가공을 하려면 어떤 함수가 필요하지?"를 떠올리는 감각을 기르는 게 목표예요.


🎯 [과제 1 예시답안] 문자 함수로 회원 정보 가공하기

채점 포인트

항목 배점 핵심
(가) UPPER로 대문자 30% UPPER(username)
(나) SUBSTR로 성 추출 35% SUBSTR(nickname, 1, 1) — 1번째부터 1글자
(다) ` 이어 붙이기 +LENGTH`

풀이 예시

SQL
SELECT username,
       UPPER(username)        AS 대문자아이디,
       SUBSTR(nickname, 1, 1) AS 성,
       nickname || '님'       AS 인사,
       LENGTH(nickname || '님') AS 글자수
FROM member
WHERE member_id <= 5;
username 대문자아이디 인사 글자수
jaehoon JAEHOON 김재훈님 4
minji MINJI 이민지님 4
seungwoo SEUNGWOO 박승우님 4
haru HARU 정하루님 4
doyeon DOYEON 최도연님 4

UPPER는 통째로 대문자, SUBSTR(nickname, 1, 1)은 첫 글자만 잘라 성을 만들어요. nickname || '님'으로 '김재훈님'을 만들고, 그 전체 글자 수를 LENGTH로 재면 4가 나와요(세 글자 + '님' 한 글자).

💡 튜터의 한마디 — 함수를 SELECT 자리에 여러 개 나란히 쓸 수 있다는 점이 포인트예요. 한 행에서 아이디·성·인사말·길이를 동시에 가공해 뽑았죠. 함수는 컬럼 하나를 통째로 가공하는 게 아니라, 행마다 그 칸의 값을 하나씩 가공한다는 걸 기억하세요.


🎯 [과제 2 예시답안] 날짜를 보기 좋게 바꾸기

채점 포인트

항목 배점 핵심
(가) TO_CHAR 날짜 포맷 35% 'YYYY"년" MM"월" DD"일"'
(나) 날짜 빼기 35% DATE '2026-06-30' - created_at
(다) 기준일 고정 이유 30% SYSDATE는 실행마다 결과가 바뀜

풀이 예시

(가)·(나) 한 쿼리로

SQL
SELECT post_id,
       TO_CHAR(created_at, 'YYYY"년" MM"월" DD"일"') AS 작성일,
       DATE '2026-06-30' - created_at                AS 경과일
FROM post
WHERE post_id IN (1, 33);
post_id 작성일 경과일
1 2026년 06월 01일 29
33 2026년 05월 26일 35

TO_CHAR의 포맷에서 큰따옴표 안의 '년'·'월'·'일'은 그대로 출력되고, YYYY·MM·DD만 실제 값으로 채워져요. 날짜 빼기는 두 날짜의 일 수 차이를 돌려주니, 6월 30일에서 빼면 며칠 전 글인지 나와요.

(다) 기준일을 고정한 이유

SYSDATE를 쓰면 실행하는 날마다 '경과일'이 달라져요. 오늘 실행하면 오늘 기준, 다음 시간에 실행하면 그날 기준이라 같은 게시물인데 숫자가 바뀌죠. 과제처럼 답이 정해져 있어야 할 때는 DATE '2026-06-30'으로 기준을 고정해야 결과가 흔들리지 않아요.

💡 튜터의 한마디 — 날짜 포맷에서 가장 자주 틀리는 게 월(MM)과 분(MI)을 헷갈리는 거예요. 'YYYY-MM-DD'는 날짜, 'HH24:MI:SS'는 시각이에요. 그리고 "예제·보고서는 고정 날짜, 실시간 조건은 SYSDATE"라는 구분을 몸에 익혀 두세요.


🎯 [과제 3 예시답안] 정규식으로 해시태그 다루기

채점 포인트

항목 배점 핵심
(가) REGEXP_SUBSTR 첫 태그 30% REGEXP_SUBSTR(caption, '#[^ ]+')
(나) 손 자르기와 비교 35% 여러 태그면 SUBSTR+INSTR은 다 딸려옴
(다) REGEXP_COUNT 조건 + 빈 값 처리 35% >= 2 → 8건 / 없으면 NULL

풀이 예시

(가) 첫 번째 해시태그 뽑기

SQL
SELECT post_id, caption, REGEXP_SUBSTR(caption, '#[^ ]+') AS 첫태그
FROM post
WHERE post_id IN (1, 12);
post_id caption 첫태그
1 첫 게시물입니다 #안녕 #일상 #첫게시물 #안녕
12 제주도 여행 #여행 #제주 #바다 #여행

(나) 손으로 자른 방식과 비교

SQL
SELECT SUBSTR(caption, INSTR(caption, '#'))  AS 손자르기,
       REGEXP_SUBSTR(caption, '#[^ ]+')      AS 정규식
FROM post
WHERE post_id = 1;
손자르기 정규식
#안녕 #일상 #첫게시물 #안녕

SUBSTR+INSTR은 첫 # 위치부터 끝까지를 자르니, 뒤의 해시태그까지 전부 딸려 나와요. 반면 REGEXP_SUBSTR은 '공백 전까지'라는 패턴 덕분에 첫 태그 하나만 정확히 떼어내요. 해시태그가 하나뿐인 글이면 둘 결과가 같지만, 여러 개면 이렇게 갈려요.

(다) 해시태그 2개 이상 게시물

SQL
SELECT post_id, caption, REGEXP_COUNT(caption, '#') AS 태그수
FROM post
WHERE REGEXP_COUNT(caption, '#') >= 2
ORDER BY post_id;

해시태그가 2개 이상인 게시물은 8건(1·2·11·12·13·18·39·40)이 나와요. 그리고 해시태그가 없는 글에서 REGEXP_SUBSTR은 NULL을 돌려줘요('주말 산책' 같은 글). 이 빈 값은 그대로 두면 화면에 아무것도 안 보이니, '태그 없음' 같은 기본값으로 바꿔 주면 좋아요. 그 방법(NVL 등)은 다음 시간에 배워요.

💡 튜터의 한마디REGEXP_COUNTWHERE에 넣어 "태그 2개 이상" 같은 조건을 만들 수 있다는 게 핵심이에요. 함수는 SELECT 자리뿐 아니라 조건 자리에서도 똑같이 동작해요. 정규식의 진짜 힘은 "있나 없나"를 넘어 "몇 개인가, 어떤 모양인가"까지 따질 수 있다는 데 있어요.


🤔 [생각해볼 주제 1] 손으로 자르기 vs 정규식, 언제 무엇을 쓸까

문제 상황 요약

해시태그를 뽑는 방법이 둘이었어요. SUBSTR+INSTR로 직접 자르는 방식과 REGEXP_SUBSTR 정규식 방식. 정규식이 더 정확하고 짧았지만, 패턴이 복잡해지면 읽기 어려워진다는 단점도 있었죠. 어느 쪽을 언제 골라야 할까요?

튜터의 가이드 및 해설

기준은 "패턴의 복잡도"와 "읽는 사람"이에요. 고정된 위치를 자르는 단순한 일(예: 항상 앞 6자리가 날짜)이라면 SUBSTR이 더 직관적이에요. 누가 봐도 "앞 6글자"라는 게 한눈에 보이거든요.

반대로 "공백 전까지", "숫자만", "이메일 모양"처럼 규칙이 있는 패턴이라면 정규식이 압도적으로 짧고 정확해요. SUBSTR+INSTR로 이걸 흉내 내려면 조건이 복잡하게 얽혀서 오히려 읽기 어려워져요. 다만 정규식도 과하면 암호처럼 보이니, 한 줄에 욕심내지 말고 "한 조각씩 읽히는" 수준으로 유지하는 게 좋아요.

🎯 SQLD는 이렇게 나온다

함수의 결과를 예측하는 문제로 나와요. INSTR(caption, '#')의 결과(위치 숫자, 없으면 0), SUBSTR(문자열, 시작, 길이)의 잘린 결과, REGEXP_SUBSTR이 패턴에 맞는 첫 부분을 돌려준다는 점이 단골이에요. 특히 SUBSTR의 시작 위치가 1부터라는 것과 음수 위치(뒤에서부터)를 묻는 보기가 자주 섞여요.

💡 실무에선

가독성과 유지보수를 우선해요. 정규식 한 줄이 멋있어 보여도, 6개월 뒤의 내가 못 읽으면 짐이 돼요. 복잡한 패턴엔 정규식을 쓰되 주석으로 "무엇을 뽑는 패턴인지" 한 줄 남기고, 단순한 자르기엔 SUBSTR을 써서 의도를 드러내는 편이 좋아요.


🤔 [생각해볼 주제 2] 자동 형변환은 편한데 왜 위험할까

문제 상황 요약

'10' + 5가 15로 나오는 것처럼 Oracle은 형이 안 맞으면 알아서 바꿔 줘요. 타이핑이 줄어 편하죠. 그런데 실무에선 TO_NUMBER·TO_DATE로 직접 바꾸라고 권해요. 자동 변환의 무엇이 위험할까요?

튜터의 가이드 및 해설

문제는 "언제 터질지 모른다"는 데 있어요. '10' + 5는 잘 되지만, 데이터에 '10원'이나 '십' 같은 값이 하나라도 섞이면 그 행을 만나는 순간 변환 오류가 나요. 테스트할 땐 멀쩡하다가 실제 데이터에서 갑자기 깨지는 거죠.

또 자동 변환은 DB가 어떤 규칙으로 바꿀지(날짜 포맷, 숫자 형식)가 환경 설정에 따라 달라질 수 있어요. 같은 쿼리가 이 서버에선 되고 저 서버에선 안 되는 일이 생겨요. 그래서 TO_NUMBER·TO_DATE로 "내가 이 형으로 바꾸겠다"를 명시하면, 변환 규칙이 코드에 분명히 드러나고 예상치 못한 오류를 입구에서 막을 수 있어요.

🎯 SQLD는 이렇게 나온다

암시적(자동) 형변환과 명시적 형변환의 차이를 묻거나, '10' + 5 같은 식의 결과를 예측하는 문제로 나와요. 문자와 숫자를 비교·연산할 때 Oracle이 문자를 숫자로 바꾼다는 점, 그리고 변환이 불가능한 값에서 오류가 난다는 점이 포인트예요.

💡 실무에선

"명시적 변환을 기본으로"가 원칙이에요. 특히 날짜는 TO_DATE로 포맷을 정해 두지 않으면, 서버의 날짜 표시 설정이 바뀌었을 때 조용히 다른 날짜로 해석되는 사고가 나요. 편함보다 예측 가능함을 택하는 게 데이터를 지키는 길이에요.


🤔 [생각해볼 주제 3] SYSDATE를 쿼리에 직접 박으면 생기는 일

문제 상황 요약

SYSDATE는 실행하는 순간의 시각이라 결과가 매번 달라져요. "최근 30일 글"처럼 항상 지금 기준이어야 할 땐 좋지만, 답이 정해져 있어야 하는 보고서나 예제에선 곤란할 수 있어요. 어떤 문제가 생길까요?

튜터의 가이드 및 해설

가장 큰 문제는 "재현이 안 된다"는 거예요. 같은 보고서를 다른 날 뽑았는데 숫자가 다르면, 둘 중 뭐가 맞는지 따질 수가 없어요. "경과일"처럼 SYSDATE에 기댄 값은 실행 시점에 따라 계속 바뀌니까요.

해결의 핵심은 "기준 시점을 밖에서 정해 주는" 거예요. 보고서라면 "2026-06-30 기준" 같은 날짜를 입력값으로 받아 DATE '2026-06-30'으로 고정하면, 언제 실행해도 같은 결과가 나와요. 실시간 조건(최근 며칠)에만 SYSDATE를 쓰고, 결과를 똑같이 재현해야 하는 곳에는 고정 날짜를 쓰는 식으로 나눠요.

🎯 SQLD는 이렇게 나온다

SYSDATE가 현재 날짜·시각을 돌려준다는 정의, 날짜끼리 빼면 일 수가 나온다는 점, SYSDATE - created_at 같은 식의 결과 해석이 나와요. SYSTIMESTAMP(더 정밀한 시각)와의 차이를 묻기도 해요. 날짜 연산의 결과 단위가 '일(day)'이라는 게 자주 나오는 포인트예요.

💡 실무에선

"시간에 의존하는 값은 기준 시점을 입력으로 받는다"가 좋은 습관이에요. 그래야 같은 쿼리를 언제 돌려도 결과가 같아 검증이 되고, 문제가 생겼을 때 "그때 그 시점"으로 똑같이 재현해 원인을 찾을 수 있어요. 테스트하기 어려운 코드의 흔한 원인 하나가 바로 코드 속에 박힌 현재 시각이에요.

전체 목록 데이터베이스