학습 목표
- 문자열 함수(CONCAT/SUBSTRING/REPLACE/TRIM/LPAD/LOCATE/REGEXP_REPLACE)를 다룬다
- 숫자 함수(ROUND/CEIL/FLOOR/TRUNCATE/MOD)의 미묘한 차이를 안다
- 날짜 함수(DATE_ADD/DATEDIFF/TIMESTAMPDIFF/DATE_FORMAT/STR_TO_DATE/LAST_DAY/주차)를 자유자재로 쓴다
- 조건 함수(IF/IFNULL/NULLIF/COALESCE/CASE)와 형변환(CAST/CONVERT)을 익힌다
- 컬럼에 함수를 씌우면 인덱스를 못 탄다는 원칙을 EXPLAIN 으로 확인한다
선행 스텝: Step 11 (DML) 예상 소요: 55분
결과
⚠️ 함정:
CONCAT은 인자 중 하나라도 NULL 이면 결과 전체가 NULL 입니다.CONCAT_WS(With Separator)는 NULL 인자를 건너뜁니다.이름 뒤에 붙인 별명 하나가 NULL 이라 전체 문자열이 통째로 사라지는 사고가 흔합니다. NULL 가능성이 있으면
CONCAT_WS를 쓰거나COALESCE(phone,'')로 감싸세요.
문자열 인덱스는 1부터 시작합니다(0이 아닙니다).
결과
결과
⚠️ 함정: utf8mb4 에서 한글 한 글자는 3바이트입니다.
LENGTH('가')는 3,CHAR_LENGTH('가')는 1. 글자 수를 세려면 반드시CHAR_LENGTH를 쓰세요.VARCHAR(50)은 50 "글자"이지 50 바이트가 아닙니다.
정규식 함수는 8.0에서 새로 들어왔습니다(5.7 에는 REGEXP 매칭만 있고 치환/추출이 없었습니다).
결과
넷 다 "소수를 정리"하지만 방식이 다릅니다.
결과
ROUND 와 TRUNCATE 의 결정적 차이는 반올림 vs 버림입니다. 또 둘 다 음수 자릿수를 받아 정수부를 정리할 수 있습니다.
결과
💡 실무 팁: 금액을 다룰 때
ROUND와TRUNCATE를 혼동하면 회계상 1원씩 어긋납니다. "부가세는 버림"처럼 업무 규칙이 반올림인지 버림인지 먼저 확인하세요.
결과
결과
결과
DATEDIFF(a, b) 는 일(day) 수만 돌려줍니다. 시·분·연 단위가 필요하면 TIMESTAMPDIFF(단위, 시작, 끝) 을 씁니다. 인자 순서가 반대라는 점에 주의하세요.
결과
나이 계산은 TIMESTAMPDIFF(YEAR, ...) 가 정석입니다(윤년·생일 지남 여부까지 알아서 처리).
결과
DATE_FORMAT 은 날짜 → 문자열, STR_TO_DATE 는 그 반대입니다.
결과
결과
⚠️ 함정:
d4_null은 "일(日)"이 없어2025-07-00이 되는데, 서버가NO_ZERO_DATE모드라 NULL + 경고로 처리합니다. 파싱 포맷에 필수 요소가 빠지면 조용히 NULL 이 되니, 변환 후IS NULL검사를 잊지 마세요.
결과
💡 실무 팁:
WEEK()는 모드에 따라 결과가 달라집니다(일요일 시작/월요일 시작, 1주차 정의). 리포트에서 주차를 쓸 땐 모드를 명시(보통 ISO 8601 인 모드 3)하지 않으면 서버 설정에 따라 값이 흔들립니다.LAST_DAY는 월말 구하기와 "그 달의 일수" 계산에 유용합니다.
결과
IFNULL 은 인자 2개 전용, COALESCE 는 인자 여러 개 중 첫 번째 NULL 아닌 값을 돌려줍니다(표준 SQL). 셋 이상을 순서대로 시도하려면 COALESCE 를 쓰세요.
NULLIF(a, b) 는 a = b 면 NULL, 아니면 a. 0으로 나누기 방지의 정석입니다.
결과
결과
결과
💡 실무 팁:
SUM(CASE WHEN 조건 THEN 1 ELSE 0 END)는 행을 열로 돌리는 수동 피벗의 핵심 패턴입니다.COUNT(CASE WHEN 조건 THEN 1 END)(ELSE 없이 NULL) 로도 같은 결과를 얻습니다 — COUNT 는 NULL 을 세지 않으니까요.
결과
MySQL 은 타입이 다르면 알아서 변환해 비교합니다. 편리하지만 위험합니다.
결과
⚠️ 함정:
'abc' = 0이 참입니다. 문자열이 숫자로 변환되다 실패하면 0이 되기 때문입니다.WHERE code = 0처럼 숫자 리터럴을 쓰면 문자열 코드가 전부 걸려들 수 있습니다. 타입을 맞춰서 비교하세요(문자열은 따옴표로).
이 절이 이 스텝에서 성능상 가장 중요합니다. 함수 자체는 잘못이 없지만, 인덱스가 걸린 컬럼을 함수로 감싸는 순간 그 인덱스는 무력화됩니다. 인덱스는 "컬럼의 원본 값"으로 정렬돼 있는데, 함수를 씌우면 값이 바뀌어 정렬이 소용없어지기 때문입니다.
orders.customer_id 에는 인덱스(idx_orders_customer)가 있습니다. 여기에 연산을 씌워 봅시다.
결과
type=ALL — 풀 테이블 스캔 600행 전부를 읽습니다. key=NULL, 인덱스를 안 씁니다.
결과
type=ref, key=idx_orders_customer — 인덱스로 20행만 콕 집어 읽습니다. + 0 하나 붙였을 뿐인데 600행 vs 20행입니다.
YEAR(order_date) = 2025 는 모든 행의 order_date 에 YEAR() 를 적용해 봐야 하므로 인덱스를 못 씁니다. 아래처럼 컬럼은 건드리지 않고 값을 범위로 주면, order_date 에 인덱스가 있을 경우 그 인덱스를 탈 수 있습니다.
참고: 이 스키마의
orders.order_date에는 인덱스가 없어서 두 쿼리 모두 EXPLAIN 상 풀 스캔으로 나옵니다. 하지만 "컬럼을 함수로 감싸지 않는다"는 원칙은 인덱스 유무와 무관하게 지켜야 합니다. 지금은 인덱스가 없지만, 나중에 누군가order_date에 인덱스를 추가하면 범위 조건 쿼리만 이득을 봅니다.
문자열도 마찬가지입니다.
email 에는 UNIQUE 인덱스가 있습니다. SUBSTRING(...)='kim' 은 풀 스캔이지만, 접두(prefix) LIKE 'kim%' 는 인덱스 범위 스캔을 씁니다. 단, LIKE '%kim'(앞에 %)은 접두를 특정할 수 없어 인덱스를 못 탑니다.
MySQL 8.0.13 부터는 표현식에 인덱스를 걸 수 있습니다. 그러면 그 함수를 쓴 조건도 인덱스를 탑니다.
💡 실무 팁: 원칙은 "컬럼을 감싸지 말고 값을 가공하라" 입니다. 그게 불가능한 경우(예: 대소문자 무시 검색, 특정 표현식 반복 조회)에만 함수 기반 인덱스나 생성 컬럼(generated column)을 고려하세요. 인덱스를 늘리면 쓰기가 느려지므로 공짜가 아닙니다.
| 분류 | 함수 | 핵심 / 함정 |
|---|---|---|
| 문자열 | CONCAT vs CONCAT_WS | CONCAT 은 NULL 하나면 전체 NULL; WS 는 건너뜀 |
SUBSTRING_INDEX | @ 앞/뒤 자르기, 음수=뒤에서 | |
LENGTH vs CHAR_LENGTH | 바이트 vs 글자(한글=3바이트) | |
REGEXP_REPLACE 등 | 8.0+ 정규식 치환/추출 | |
| 숫자 | ROUND vs TRUNCATE | 반올림 vs 버림, 음수 자릿수 가능 |
NULLIF(x,0) | 0으로 나누기 방지 | |
| 날짜 | DATEDIFF vs TIMESTAMPDIFF | 일 수 vs 단위지정(인자 순서 반대) |
DATE_FORMAT/STR_TO_DATE | 상호 변환. 필수 요소 빠지면 NULL | |
LAST_DAY/WEEK(d,3) | 월말/ISO 주차(모드 명시) | |
| 조건 | COALESCE | 여러 인자 중 첫 non-NULL |
CASE+SUM | 조건부 카운트 = 수동 피벗 | |
| 형변환 | CAST/CONVERT | 'abc'=0 이 참 → 타입 맞춰 비교 |
| 성능 | 컬럼에 함수 금지 | 인덱스 무력화. 값을 가공하거나 함수 기반 인덱스(8.0.13+) |
exercise.sql 을 푸세요. 정답은 solution.sql.
YYYY년 MM월 형식 라벨로 집계COALESCE 로 phone 이 없으면 '연락처없음' 표시YEAR(order_date)=2025 를 인덱스 친화적인 범위 조건으로 다시 쓰기→ Step 13 — 윈도우 함수 (예정)
이 스텝은 SQL 파일 세 개로 진행합니다. 먼저 practice.sql 을 실행해 본문 12-1 ~ 12-6 의 예제(문자열 → 숫자 → 날짜 → 조건 → 형변환 → EXPLAIN)를 순서대로 눈으로 확인하고, 그다음 exercise.sql 의 빈칸을 스스로 채워 8문제를 풉니다. 막히거나 다 풀었으면 solution.sql 로 답과 해설을 대조합니다. 세 파일 모두 USE shop; 으로 시작하며 SELECT / EXPLAIN 만 수행하므로 원본 데이터를 건드리지 않습니다.
본문에 나온 예제를 한 파일에 모아 둔 따라 하기용 스크립트입니다. [12-1] 부터 [12-35] 까지 번호가 붙어 있어 본문 절 번호와 나란히 읽을 수 있습니다.
mysql -h127.0.0.1 -P3307 -ulearner -plearn1234 shop < practice.sql. 포트가 3307 인 점(로컬 학습용 컨테이너)에 주의하세요.[12-5] LOCATE/INSTR(못 찾으면 0), [12-6] UPPER/LOWER, [12-8] REGEXP_LIKE/REGEXP_SUBSTR, [12-11] ABS/MOD/POWER/SQRT, [12-21] DATE_FORMAT 을 이용한 월별 집계, [12-22] IF 는 본문에 표로만 언급된 것들이니 파일에서 실제 결과를 확인해 보세요.[12-2] 는 WHERE phone IS NULL 로 골라낸 행에 CONCAT('전화: ', phone) 과 CONCAT_WS(' ', '전화:', phone) 를 나란히 찍어, 전자가 통째로 NULL 이 되는 것을 눈으로 확인시킵니다. 12-1 절의 "함정" 박스와 같은 내용이되, customer_id, phone 컬럼을 함께 보여 주고 3행까지 출력합니다.[12-18] 의 d4_null 은 STR_TO_DATE('2025년 07월', '%Y년 %m월') 로 일(日)이 빠져 NULL 이 되는 예입니다. 바로 뒤에 SHOW WARNINGS; 를 붙여 두었으니 경고 메시지까지 함께 보세요. [12-28] 의 암묵적 형변환('abc' = 0 → 1) 뒤에도 같은 이유로 SHOW WARNINGS; 가 있습니다.[12-29] ~ [12-34] 가 이 스텝의 핵심입니다. customer_id + 0 = 5(type=ALL) 와 customer_id = 5(type=ref, key=idx_orders_customer) 를 EXPLAIN 으로 비교합니다. + 0 하나 차이로 600행 풀 스캔과 20행 인덱스 조회가 갈립니다.[12-35] 의 함수 기반 인덱스 ALTER TABLE ... ADD INDEX idx_order_year ((YEAR(order_date))) 는 주석 처리되어 있습니다. 원본 테이블을 변경하지 않기 위한 의도이니 주석을 풀지 마세요.연습문제 8개가 주석만 있고 답이 비어 있는 스켈레톤 파일입니다. 본문 "연습문제" 절의 1~8번과 그대로 대응하며, 각 문제 블록 아래 빈 줄에 직접 SQL 을 써 넣고 실행해 보는 방식입니다.
SUBSTRING_INDEX, UPPER, REGEXP_REPLACE, ROUND vs TRUNCATE)를, Q4TIMESTAMPDIFF, DATE_FORMAT)와 CASE 를, Q7 은 COALESCE 를, Q8 은 인덱스 친화적 조건 작성을 다룹니다.010-1000-0001 → ***-****-0001 처럼 하이픈은 남기고 앞쪽 숫자만 가려야 해서 REGEXP_REPLACE 한 번으로는 잘 풀리지 않습니다. 의도된 난관이며, 해법은 solution.sql 의 A2 를 보세요.exercise.sql 의 정답과 해설 주석입니다. 먼저 스스로 풀어 본 뒤에 여세요.
LEFT(phone, CHAR_LENGTH(phone) - 4) 로 앞부분을 떼어 [0-9] 를 * 로 치환하고 RIGHT(phone, 4) 를 CONCAT 으로 다시 붙입니다. 여기서 LENGTH 가 아니라 CHAR_LENGTH 를 쓴 이유는 12-1 절의 바이트 vs 글자 함정과 같습니다.paid_at 이 order_date + (5 + order_id%120) 분이라 소요 시간이 최대 124분까지만 나옵니다. 결과가 그 이상이면 뭔가 잘못 짠 것입니다.GROUP BY ym_label 로 묶되 ORDER BY MIN(order_date) 로 정렬합니다. 라벨 문자열이 아니라 실제 날짜로 정렬해야 안전하다는 점('%Y년 %m월' 은 우연히 사전순=시간순일 뿐)을 짚어 줍니다.YEAR(order_date) = 2025)을 실행하지 않고 주석으로만 두고, 범위 조건 버전과 customer_id 기반 EXPLAIN 두 줄로 원리를 확인시킵니다. orders.order_date 에는 인덱스가 없어 둘 다 풀 스캔이지만 원칙은 동일하다는 설명이 주석에 담겨 있습니다.