MySQL 8을 공부하며 꼭 이해해야 하는 개념 · 내장 함수 · 실전 함정 · 실무 팁을 한 장에 압축했습니다. 이 체크리스트만 외우면 실전에서 막히지 않는 것을 목표로 합니다. 깊은 설명·실행 결과는 reference/mysql8 코스(25 스텝)로 연결됩니다.
사용법
[ ]를 "설명할 수 있다 / 직접 칠 수 있다" 기준으로 채우세요. 읽어서 아는 것과 손이 아는 것은 다릅니다.= NULL은 항상 UNKNOWN. NULL 비교는 IS NULL / IS NOT NULL / <=>로만.NOT IN (서브쿼리)에 NULL이 하나라도 섞이면 결과가 통째로 0건. 부정 매칭은 NOT EXISTS를 기본값으로.WHERE DATE(created) = '..' ✗ → WHERE created >= '..' AND created < '..' ✓LEFT JOIN의 조건을 WHERE에 쓰면 INNER JOIN이 되어버린다. 오른쪽 테이블 조건은 ON에.COUNT(DISTINCT ...) / 조건부 집계 / 선집계 후 조인.ORDER BY 없는 LIMIT은 순서를 아무것도 보장하지 않는다. GROUP BY도 8.0부터 암묵 정렬 없음.DECIMAL, FLOAT/DOUBLE 금지. 0.1 + 0.2 ≠ 0.3으로 정산이 어긋난다.ONLY_FULL_GROUP_BY를 끄지 마라. 에러는 쿼리가 틀렸다는 신호다.SET sql_safe_updates=1 (WHERE 없는 UPDATE/DELETE 거부).INT / BIGINT / TINYINT, UNSIGNED의 의미와 범위DECIMAL(p,s) vs FLOAT/DOUBLE — 정확성 vs 근사CHAR vs VARCHAR — 고정 vs 가변, 정렬/임시테이블 메모리 영향DATE / DATETIME / TIMESTAMP / TIME / YEAR, TIMESTAMP의 타임존 자동 변환ENUM / SET — 내부 정수값 저장JSON 타입 (5부에서 상세)NULL 허용 여부 설계 (NOT NULL DEFAULT)utf8mb4 / utf8mb4_0900_ai_ci (대소문자·악센트 무시)DECIMAL(p,s). 원화면 DECIMAL(12,2) 정도.TIMESTAMP는 2038년 오버플로우. 먼 미래 날짜(구독 만료 등)는 DATETIME.ENUM 정렬은 사전순이 아니라 선언 순서. ORDER BY grade DESC가 등급순이 되는 이유.ENUM은 값 추가 시 ALTER TABLE 필요. 자주 늘어나는 도메인은 코드 테이블 + FK가 낫다.VARCHAR(255) 습관적으로 쓰지 마라. 길이 제한은 검증의 마지막 방어선이고, 정렬 시 선언 길이만큼 메모리를 잡는다.created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at ... ON UPDATE CURRENT_TIMESTAMPpt-online-schema-change(Percona) / gh-ost(GitHub).→ Step 02 데이터 타입, Step 13 제약·정규화
DISTINCT, 컬럼/테이블 별칭(AS)BETWEEN / IN / LIKE / REGEXPLIMIT offset, count 와 페이징SELECT name price (콤마 누락)는 에러가 안 난다. price를 별칭으로 해석 → 컬럼이 조용히 사라짐. AS를 항상 명시.FROM products p WHERE products.price → 에러.DISTINCT는 함수가 아니다. DISTINCT(city)의 괄호는 무시됨. SELECT 목록 전체에 걸린다.OR엔 무조건 괄호. 실무 버그 다수가 "OR 괄호 누락".BETWEEN 쓰지 마라. BETWEEN '2024-01-01' AND '2024-01-31'은 23:00:00 이후를 놓친다 → >= '2024-01-01' AND < '2024-02-01'.LIKE '%키워드%'는 인덱스를 못 탄다. 앞이 고정된 '키워드%'만 range scan. 대량이면 FULLTEXT/검색엔진.REGEXP는 절대 인덱스를 못 쓴다 — 항상 풀스캔. 좁혀진 결과의 추가 필터로만.<>, NOT IN, NOT LIKE) 쓸 땐 OR ... IS NULL 여부를 반드시 결정.ORDER BY 없는 LIMIT은 결과 순서 미보장.LIMIT 100000, 20 깊은 OFFSET은 재앙 — 100,020행 읽고 100,000행 버림. → 커서(keyset) 페이징: WHERE id > 마지막본id ORDER BY id LIMIT 20.NULLS LAST 문법이 없다 → ORDER BY (col IS NULL), col.<=> (NULL-safe equal): 파라미터가 NULL일 수 있는 검색/변경 감지에 유용.→ Step 04 SELECT, Step 05 연산자·NULL
함수 이름만 봐도 "무엇을 하는지 + 함정"이 떠올라야 합니다.
CONCAT(a,b,...) — 이어붙이기. 인자 하나라도 NULL이면 결과 NULL (주의)CONCAT_WS(sep, ...) — 구분자로 이어붙이기, NULL 인자는 건너뜀LENGTH() (바이트) vs CHAR_LENGTH() (문자 수) — 한글은 utf8mb4에서 3바이트, 글자 수는 CHAR_LENGTHUPPER() / LOWER()SUBSTRING(str, pos, len) (1-based), LEFT() / RIGHT()TRIM() / LTRIM() / RTRIM()REPLACE(str, from, to)LOCATE(sub, str) / INSTR() — 위치 찾기(없으면 0)LPAD() / RPAD()FORMAT(x, d) — 천단위 콤마. ⚠️ 결과는 문자열 → 정렬/계산 금지, 표시 전용REGEXP_REPLACE() / REGEXP_SUBSTR() / REGEXP_LIKE() (8.0)ROUND(x, d) / TRUNCATE(x, d) (반올림 vs 버림)CEIL() / FLOOR()ABS(), MOD(n,m) 또는 %POWER() / SQRT()RAND() — ⚠️ 재현 불가, 인덱스로 정렬 불가(ORDER BY RAND()는 풀스캔+파일소트)GREATEST() / LEAST() — 여러 값 중 최대/최소 (행 단위, 집계 아님)NOW() / CURDATE() / CURTIME() / SYSDATE() — ⚠️ NOW()는 문 시작 고정, SYSDATE()는 호출 시각DATE() / TIME() / YEAR() / MONTH() / DAY() / HOUR()DATE_ADD(d, INTERVAL n UNIT) / DATE_SUB() — INTERVAL 7 DAYDATEDIFF(a,b) (일수), TIMESTAMPDIFF(UNIT, a, b) (단위 지정)DATE_FORMAT(d, '%Y-%m-%d') / STR_TO_DATE()LAST_DAY(), WEEKDAY() / DAYOFWEEK(), EXTRACT(UNIT FROM d)UNIX_TIMESTAMP() / FROM_UNIXTIME()DATE()/DATE_FORMAT() 씌우면 인덱스 사망 → 범위 조건으로.IF(cond, a, b) — 3항IFNULL(a, b) vs 💡 COALESCE(a, b, c...) — COALESCE가 표준이고 확장 쉬움NULLIF(a, b) — 같으면 NULL (0으로 나누기 방지: x / NULLIF(y,0))CASE WHEN ... THEN ... ELSE ... END — 단순형/검색형 둘 다COALESCE로 여러 컬럼 중 첫 비-NULL 뽑기COUNT(*) vs COUNT(col) — ⚠️ COUNT(col)은 NULL을 안 센다COUNT(DISTINCT col) — "몇 명/몇 종류"는 거의 항상 이것SUM() / AVG() / MIN() / MAX() — ⚠️ 모두 NULL을 무시(0이 아님)GROUP_CONCAT(col ORDER BY .. SEPARATOR ..) — ⚠️ group_concat_max_len 넘으면 조용히 잘림SUM(조건) = 조건부 카운트 (SUM(status='X')), AVG(조건) = 비율COUNT(1) == COUNT(*) — "별표가 느리다"는 미신CAST(x AS type) / CONVERT() — DECIMAL, CHAR, DATE, UNSIGNED 등JSON_EXTRACT() / -> / ->> (5부)CONCAT은 NULL 전파 / CONCAT_WS는 NULL 스킵 — 상황 따라 골라 쓰기FORMAT/DATE_FORMAT 결과는 문자열 — 정렬·재계산 금지 (표시는 마지막에)COALESCE, NULLIF(y,0)으로 0 나눗셈 방지GROUP BY의 의미와 HAVING(그룹 필터) vs WHERE(행 필터)ONLY_FULL_GROUP_BY — SELECT의 비집계 컬럼은 GROUP BY에 있어야WITH ROLLUP — 소계/총계, GROUPING()SUM(CASE WHEN ...), SUM(조건))로 피벗WHERE에. HAVING city IN (..)은 다 그룹핑 후 버려서 느리고 인덱스도 못 탐.ONLY_FULL_GROUP_BY가 켜져서 옛 리포트 쿼리가 무더기 에러. 끄지 말 것 — 버그 방지.GROUP BY는 암묵 정렬을 안 한다. 순서 필요하면 ORDER BY 명시.COUNT(*)는 고객 수가 아니라 주문 수 — COUNT(DISTINCT customer_id).WITH ROLLUP + ORDER BY/LIMIT/DISTINCT 조합은 주의(NULL 정렬).INNER / LEFT / RIGHT / CROSS / SELF JOINON (조인 조건) vs WHERE (조인 후 필터)의 차이NOT EXISTS / NOT IN / LEFT JOIN ... WHERE 오른쪽 IS NULLEXISTS, IN)LEFT JOIN + 오른쪽 조건을 WHERE에 두면 INNER JOIN이 된다. 오른쪽 조건은 ON에.SUM()은 중복 합산. 조인 전 단위 확인, 선집계 후 조인.LEFT JOIN 뒤 COUNT(*)는 없는 행도 1로 센다. → COUNT(오른쪽테이블컬럼)로 0을 얻어라.SELECT DISTINCT가 보이면 잘못된 조인의 반창고인지 의심.c.name) — ambiguous 에러 방지.IN vs EXISTS, = ANY(=IN) / <> ALL(=NOT IN)WITH (CTE), WITH RECURSIVE (조직도 전개, 날짜 채우기)UNION vs UNION ALL (중복 제거 여부), INTERSECT / EXCEPT (8.0.31+)LATERAL 조인 (그룹별 Top-N)NOT IN (서브쿼리) + NULL = 항상 0건. → NOT EXISTS.> ALL은 항상 참, > ANY는 항상 거짓 (직관 반대).CAST(x AS CHAR(200)).UNION은 중복 제거로 정렬 비용 발생 — 중복 없음이 확실하면 UNION ALL.LEFT JOIN + GROUP BY로.→ Step 08 서브쿼리, Step 09 CTE·재귀, Step 10 집합 연산
함수() OVER (PARTITION BY ... ORDER BY ... 프레임절) 3부품ROW_NUMBER() (1,2,3,4) / RANK() (1,1,1,4) / DENSE_RANK() (1,1,1,2) / NTILE(n)LAG() / LEAD() (전월 대비 증감), FIRST_VALUE() / LAST_VALUE() / NTH_VALUE()PERCENT_RANK() / CUME_DIST()ROWS (물리 행) vs RANGE (동점 peer 묶음)WHERE/HAVING에 못 쓴다 — 서브쿼리/CTE로 감싸서 바깥에서 필터.ORDER BY를 쓰는 순간 기본 프레임 RANGE UNBOUNDED PRECEDING ~ CURRENT ROW가 붙는다 → 누적합이 됨.LAST_VALUE()가 마지막 값을 안 준다 — 기본 프레임이 "현재 행까지"라서. ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING 명시.ROW_NUMBER()는 동점 시 순서 비결정. ROWS 프레임 쓸 땐 ORDER BY를 유일하게(ORDER BY qty DESC, name).ROW_NUMBER, 순위표(공동 3등 다음 5등)→RANK, 상위 N등급→DENSE_RANK, 4/10분위→NTILE.INSERT ... VALUES, INSERT ... SELECT, 다중행 INSERTINSERT ... ON DUPLICATE KEY UPDATEUPDATE ... JOIN, DELETE ... JOINREPLACE (= DELETE + INSERT)WHERE 없는 UPDATE/DELETE는 전체 테이블을 날린다. → sql_safe_updates=1.REPLACE는 DELETE+INSERT — 기존 행이 사라지며 FK/AUTO_INCREMENT/트리거 부작용. UPSERT는 대개 ON DUPLICATE KEY UPDATE가 정답.LIMIT) — 긴 트랜잭션/락/binlog 폭증 방지.INSERT IGNORE는 에러를 조용히 삼킨다 — 의도 확인.PRIMARY KEY / UNIQUE / NOT NULL / CHECK / FOREIGN KEY / DEFAULTON DELETE / ON UPDATE: CASCADE / SET NULL / RESTRICT / NO ACTIONAUTO_INCREMENT 동작과 갭UNIQUE 제약 컬럼의 NULL은 여러 개 허용(NULL끼리 중복 아님).Using index)WHERE DATE(c)=.., WHERE col+1=.., WHERE LIKE '%x%'.(a,b,c)는 a부터 순서대로 써야 탄다. WHERE b=..만으론 못 탐.WHERE phone = 010...(문자열 컬럼에 숫자), 콜레이션 불일치 조인.index_mb > data_mb면 과다. 쓰기 느려짐.ORDER BY RAND(), 깊은 OFFSET 회피. 인덱스로 정렬·범위를 태워라.EXPLAIN 읽는 법: type(system>const>eq_ref>ref>range>index>ALL), key, rows, filtered, ExtraExtra의 신호: Using index(커버링👍), Using where, Using temporary⚠️, Using filesort⚠️EXPLAIN ANALYZE (실제 실행 시간/행수)ANALYZE TABLE ... UPDATE HISTOGRAM)type: ALL = 풀 테이블 스캔. 대형 테이블에서 보이면 인덱스 점검.Using temporary + Using filesort = 메모리 임시테이블→디스크로 떨어질 수 있음. VARCHAR 과다 선언·불필요 정렬 의심.rows는 추정값. table_rows(information_schema)도 추정 — 정확한 개수는 COUNT(*).SELECT NOW(); SELECT VERSION(); SELECT @@sql_mode;BEGIN / COMMIT / ROLLBACK, SAVEPOINT, 오토커밋READ UNCOMMITTED (dirty read)READ COMMITTED (non-repeatable read)REPEATABLE READ (MySQL 기본, phantom은 갭락으로 대부분 방지)SERIALIZABLESELECT ... FOR UPDATE / FOR SHAREREPEATABLE READ에서 같은 SELECT는 스냅샷 고정 — 중간에 커밋된 남의 변경이 안 보인다(의도이자 함정).FOR UPDATE) 로스트 업데이트 방지.SHOW ENGINE INNODB STATUS의 LATEST DETECTED DEADLOCK.VIEW (갱신 가능한 뷰 조건), 생성 컬럼 VIRTUAL vs STOREDBEFORE/AFTER INSERT/UPDATE/DELETE), 이벤트 스케줄러→ Step 14 뷰·생성컬럼, Step 20 저장 프로그램
JSON_EXTRACT(doc, '$.path') = doc->'$.path', ->>(따옴표 제거)JSON_TABLE() (JSON→관계형 행), JSON_ARRAYAGG() / JSON_OBJECTAGG()RANGE / LIST / HASH / KEY 파티셔닝, 파티션 프루닝DROP PARTITION으로 오래된 데이터 즉시 삭제(대량 DELETE 회피) — 시계열 데이터에 강력.CREATE USER / GRANT / REVOKE, ROLE(8.0), 최소 권한 원칙DROP 권한을 주지 마라 — DROP DATABASE엔 확인 절차가 없다.mysqldump, PITR(시점 복구), binlog, GTID, 복제(replication)performance_schema, sys 스키마SET PERSIST로 설정을 재시작 후에도 유지(8.0). 공용 DB 설정 변경은 SET SESSION만.→ Step 22 계정·보안, Step 23 백업·복제, Step 24 모니터링·튜닝
TRUE / FALSE / UNKNOWN. NULL과의 비교는 UNKNOWN.WHERE : UNKNOWN인 행은 제외GROUP BY : NULL끼리 한 그룹ORDER BY : NULL이 가장 작은 값 취급(기본 ASC면 맨 앞)COUNT(*) 제외)UNIQUE : NULL 여러 개 허용DISTINCT : NULL끼리 하나로 취급ON vs WHERE · NOT INIS NULL, <=>, COALESCE, IFNULL, NULLIF, GROUPING()NOT IN 서브쿼리에 NULL이 들어오면? → 결과 0건. NOT EXISTS 써라.LEFT JOIN 오른쪽 조건을 WHERE에 두면? → INNER JOIN 됨. ON에 둬라.WHERE DATE(created)='2024-01-01'의 문제? → 인덱스 못 탐. 범위 조건으로.FLOAT로 저장하면? → 정산 오차. DECIMAL 써라.LAST_VALUE()가 마지막 값을 안 주는 이유? → 기본 프레임이 현재 행까지. 프레임 명시.COUNT(DISTINCT customer_id).LIMIT 100000, 20이 느린 이유와 해법? → 앞 행 다 읽고 버림. 커서(keyset) 페이징.COUNT(*) vs COUNT(col)? → 후자는 NULL 제외.ONLY_FULL_GROUP_BY 에러가 나면? → 끄지 말고 쿼리를 고쳐라.더 깊게: 각 섹션의 링크를 따라 reference/mysql8에서 100만 행 테이블로 직접 측정하며 확인하세요. 이 문서는 암기용 인덱스, reference는 검증된 교재입니다.