realmysql
Real MySQL 시즌1 — 전체 지도 & 치트시트
원본: 이성욱·백은빈(당근마켓 DB팀) 강의 "Real MySQL 시즌1" 24개 에피소드 (Part1 12개 + Part2 12개). 에피소드 간 선후관계가 없는 옴니버스 구성이라, 지금 필요한 것부터 골라 읽어도 된다.
관련: realmysql-part1 · realmysql-part2 · isolation (격리 수준) · db
한 줄 결론(면접 오프닝):
MySQL 실무 사고의 90%는 "인덱스를 못 타는 쿼리", "필요 없는데 잡는 잠금", "한 번에 너무 많이 읽는 작업" 이 셋에서 나온다. 나머지는 이 셋의 변주다.
§0. 24개 에피소드 지도
| # | 주제 | 한 줄 요점 | 링크 |
|---|---|---|---|
| 1 | CHAR vs VARCHAR | 길이 변동폭 좁고 자주 바뀌면 CHAR가 유리 | realmysql-part1 |
| 2 | VARCHAR vs TEXT | TEXT는 버퍼 재사용 X, row size 제한 X · 오프페이지 주의 | realmysql-part1 |
| 3 | COUNT(*) 튜닝 | 최고의 튜닝은 쿼리 제거, 차선은 커버링 인덱스 | realmysql-part1 |
| 4 | 페이징 쿼리 | LIMIT ... OFFSET은 지양, 범위/개수 기반으로 |
realmysql-part1 |
| 5 | Stored Function | DETERMINISTIC 안 붙이면 풀스캔 |
realmysql-part1 |
| 6 | LATERAL 파생테이블 | 선행 테이블 컬럼 참조 가능 → Top-N·퍼널 분석 최적 | realmysql-part1 |
| 7 | SELECT FOR UPDATE | 격리수준 무관 최신 커밋 데이터를 읽는다 | realmysql-part1 |
| 8 | Generated Column / 함수기반 인덱스 | 표현식이 완전히 일치해야 인덱스 탄다 | realmysql-part1 |
| 9 | 에러 핸들링 | 에러 메시지 X, 에러번호 △, SQLSTATE ○ | realmysql-part1 |
| 10 | LEFT JOIN | 드리븐 테이블 조건은 ON절에 (WHERE면 INNER가 됨) | realmysql-part1 |
| 11 | Prepared Statement | MySQL은 커넥션 단위 캐시 → 기대만큼 안 빠르다 | realmysql-part1 |
| 12 | SQL 가독성 | 의도가 드러나는 쿼리 = 유지보수 비용 | realmysql-part1 |
| 13 | Collation | 기본 콜레이션에서 가 = ㄱㅏ 로 인식됨(!) |
realmysql-part2 |
| 14 | UUID | 랜덤 + 32byte → 인덱스 워킹셋 = 전체. 8byte 정수로 대체 | realmysql-part2 |
| 15 | 인덱스 못 타는 경우 | 컬럼 가공 / OR / 선행컬럼 누락 / %LIKE / 정규식 |
realmysql-part2 |
| 16 | COUNT(*) vs COUNT(col) | COUNT(*)가 거의 항상 빠르다 (커버링 인덱스) |
realmysql-part2 |
| 17 | NOWAIT / SKIP LOCKED | 선착순 쿠폰·잡 큐의 정석. 조인 시 OF 필수 |
realmysql-part2 |
| 18 | UNION | 그냥 UNION = UNION DISTINCT = 임시테이블 |
realmysql-part2 |
| 19 | JSON 타입 | 부분 업데이트 · 함수기반 인덱스 · binlog 설정까지 | realmysql-part2 |
| 20 | 데드락 | 완전 정복은 불가능. 재처리 로직도 정당한 해법 | realmysql-part2 |
| 21 | JOIN UPDATE/DELETE | VALUES 생성자로 다건 개별값 업데이트 가능 | realmysql-part2 |
| 22 | 커넥션 관리 | 풀 max는 20~30부터, min≠max로 두고 관측 | realmysql-part2 |
| 23 | 파티셔닝 | 파티션 프루닝이 전부. 기준 컬럼은 모든 고유키에 포함 | realmysql-part2 |
| 24 | 배치/롱 트랜잭션 | 굵고 짧게(개발자) vs 가늘고 길게(DBA) | realmysql-part2 |
§1. 5분 치트시트 — 쿼리 작성 전 체크리스트
인덱스를 못 타는 5가지 (Ep15)
-- ❌ 컬럼 가공 (산술연산 / 함수 / 형변환)
WHERE id * 1 = 100
WHERE DATE(joined_at) = '2024-01-01'
WHERE account_type = 7 -- account_type이 VARCHAR면 형변환 발생
-- ✅
WHERE id = 100
WHERE joined_at >= '2024-01-01' AND joined_at < '2024-01-02'
WHERE account_type = '7'
-- ❌ 인덱스 없는 컬럼과 OR → OR는 "모든" 조건 컬럼에 인덱스 필요
-- ❌ 복합인덱스 선행 컬럼 누락
-- ❌ LIKE '%esther%' (프리픽스 '%esther'는 OK)
-- ❌ REGEXP (항상 풀스캔)
예외:
!=와IS NULL은 항상 인덱스를 못 탄다는 건 틀린 말이다. 데이터 분포도에 따라 옵티마이저가 판단한다.
잠금 관련
-- 격리수준 무관, 항상 "최신 커밋 데이터"를 읽는다
SELECT ... FOR UPDATE; -- X락
SELECT ... FOR SHARE; -- S락 → 이후 UPDATE 하면 락 업그레이드 = 데드락 유발
-- 동시성 옵션
SELECT ... FOR UPDATE NOWAIT; -- 잠겨 있으면 즉시 에러
SELECT ... FOR UPDATE SKIP LOCKED; -- 잠긴 행은 건너뜀 (선착순 쿠폰/잡 큐)
SELECT ... FOR UPDATE OF coupon; -- 조인 시 특정 테이블만 잠금
SELECT FOR UPDATE를 아예 없애는 튜닝: 조건을 UPDATE의 WHERE로 옮기고
affectedRows로 판단.
COUNT
COUNT(*) -- 커버링 인덱스 가능, 가장 빠름. 기본으로 이것만 써라
COUNT(col) -- col IS NOT NULL 인 건수 → 데이터 파일을 읽어야 할 수 있음 (최대 수십 배 느림)
COUNT(DISTINCT x) -- 임시테이블 + 건별 SELECT/INSERT → 최소 2~3배 느림
UNION
UNION -- = UNION DISTINCT (임시테이블 생성, 전 컬럼 비교) ← 함정
UNION ALL -- 임시테이블 없음, 첫 결과부터 스트리밍
페이징
-- ❌ OFFSET이 커질수록 앞부분을 계속 다시 읽음
SELECT * FROM t WHERE user_id = 1 ORDER BY id LIMIT 20 OFFSET 10000;
-- ✅ 데이터 개수 기반 (동등 조건)
SELECT * FROM t WHERE user_id = 1 AND id > :lastId ORDER BY id LIMIT 20;
-- ✅ 범위 조건 + 식별자 순서가 다른 경우
WHERE (finished_at = :lastAt AND id > :lastId)
OR (finished_at > :lastAt AND finished_at < :endAt)
ORDER BY finished_at, id LIMIT 20;
§2. 자주 하는 오해 정리
| 흔한 믿음 | 실제 |
|---|---|
| "고정 길이면 CHAR, 가변이면 VARCHAR" | 무의미한 기준. 변동폭이 좁고 자주 UPDATE되면 CHAR (페이지 단편화 감소) |
| "COUNT(*)는 SELECT *보다 가볍다" | 대부분 비슷하거나 더 무겁다 (SELECT는 LIMIT이 붙지만 COUNT는 전부 읽음) |
| "MySQL은 낙관적 락/비관적 락 중 뭘 쓰나요?" | 질문 자체가 성립 안 됨. 트랜잭션 작성 방식의 문제지 서버 기능이 아니다 |
| "Prepared Statement 쓰면 무조건 빠르다" | MySQL은 파스트리만 캐시 + 커넥션 단위. 실행계획 재사용 안 함 |
| "UUID를 PK로 쓰면 편하다" | 랜덤 + 32byte → 인덱스 전체가 워킹셋. 비용이 월 수천 달러 차이로 번짐 |
| "!=, IS NULL은 인덱스를 못 탄다" | 분포도에 따라 탄다 |
| "데드락이 나면 내 코드가 잘못된 것" | MySQL 데드락은 완전 회피 불가능한 경우가 더 많다. 재시도 로직도 정당한 해법 |
| "유니크 인덱스는 성능에 좋다" | MySQL에선 성능 이점 전혀 없고 체인지 버퍼 못 씀 + 데드락 빈도만 올림 |
§3. 실무에 바로 적용할 것 (강사 권장사항 모음)
- ORM이 만드는 쿼리를 반드시 눈으로 확인하고 배포 (TypeORM이 불필요한
COUNT(DISTINCT)를 만드는 사례). 개발 서버에서 general log를 켜 두자. - SELECT 절에는 필요한 컬럼만. 대형 TEXT/JSON/BLOB을 습관적으로 조회하면 4배~200배 느려진다.
- Stored Function을 만들 땐
DETERMINISTIC,DEFINER,SQL SECURITY를 항상 명시. - DDL은 알고리즘을 직접 명시:
ALGORITHM=INSTANT→ 실패 시ALGORITHM=INPLACE, LOCK=NONE→ 그래도 안 되면 pt-online-schema-change. - 에러 핸들링은 SQLSTATE 기준 (단,
HY로 시작하면 미분류이므로 예외적으로 에러 번호 사용). - SQL 예외를 다른 예외로 감쌀 때 원본 SQLException을 버리지 말 것 (DBA가 원인 못 찾는 1순위 원인).
- 배치 작업은 DB 인스턴스 스펙(vCPU)을 보고 동시 스레드 수를 정하고, 주기적으로 커밋해 언두 로그를 쌓지 말 것.
§4. 이어서 볼 것
- 상세 노트: realmysql-part1 (Ep 1
12) / realmysql-part2 (Ep 1324) - 격리 수준과 MVCC의 이론 배경: isolation
- 원본 강의에서 다루지 않은 부분은 『Real MySQL 8.0』 개정판 참고