raw
Database

Real MySQL 8.0 정리 (1) — 아키텍처부터 인덱스까지

2026.03.08·28분

회사에서 팀장님이 Real MySQL 8.0을 읽어보라고 책을 주셔서 읽었습니다.

두 권짜리 책인데 1권만 해도 500페이지가 넘거든요. 평소에 ORM만 쓰다 보니 "MySQL이 알아서 잘 하겠지"라고 생각해왔는데, 프로덕션에서 이상한 에러를 마주한 뒤로는 그냥 넘어갈 수가 없었어요. 락 타임아웃, 예상보다 10배 느린 쿼리, 인덱스 걸었는데도 느린 상황들.

이 글은 1권 챕터 1~8까지 공부하면서 정리한 학습 노트입니다. 완전한 요약이 아니라 "이게 이런 거였어?" 싶었던 것들 위주로 골랐어요.


MySQL 아키텍처 — 다른 DBMS와 결정적으로 다른 점

MySQL을 처음 공부할 때 가장 중요하게 이해해야 할 구조가 있어요. 바로 2계층 분리 구조입니다.

text
1┌─────────────────────────────────┐
2│ MySQL Server Layer │
3│ SQL 파싱 · 쿼리 옵티마이저 │
4│ 접속 관리 · 권한 · 캐시 │
5├─────────────────────────────────┤
6│ Storage Engine Layer │
7│ ┌─────────┐ ┌──────┐ ┌──────┐ │
8│ │ InnoDB │ │MyISAM│ │Memory│ │
9│ └─────────┘ └──────┘ └──────┘ │
10└─────────────────────────────────┘

MySQL Server는 쿼리를 받아서 "어떻게 처리할지" 결정하는 두뇌다. 스토리지 엔진은 실제로 데이터를 디스크에 읽고 쓰는 손발이다.

Oracle이나 PostgreSQL은 이 둘이 통합된 구조인데, MySQL은 분리되어 있어서 테이블마다 다른 엔진을 쓸 수 있다. 덕분에 유연하지만, 트랜잭션 같은 핵심 기능이 엔진 레벨에 구현된다는 걸 항상 의식해야 한다.

[!info] 왜 InnoDB만 써야 하나? MyISAM은 트랜잭션을 지원하지 않는다. 트랜잭션이 없으면 부분 업데이트가 실패했을 때 롤백이 안 된다. 실무에서 MyISAM을 쓸 이유는 거의 없다. InnoDB가 기본값인 이유가 있다.


InnoDB 내부 구조 — 성능의 비밀이 여기 있다

페이지(Page): 읽기/쓰기의 최소 단위

InnoDB는 데이터를 페이지(Page) 단위로 디스크에서 읽고 메모리에 올린다. 기본 크기는 16KB다.

여기서 중요한 포인트가 있어요. MySQL은 행 하나를 따로 읽어올 수 없다. 무조건 그 행이 속한 페이지 전체(16KB)를 읽어온다. 그래서 필요 이상으로 큰 데이터가 한 행에 있으면 I/O가 낭비된다.

테이블 데이터뿐 아니라 인덱스도 별도의 페이지로 저장된다. 이 개념은 뒤에서 인덱스 구조를 이해할 때 다시 나온다.

버퍼 풀(Buffer Pool): 디스크 I/O를 줄이는 캐시

버퍼 풀은 InnoDB가 사용하는 핵심 메모리 공간이다. 디스크에서 읽어온 페이지를 여기 올려두고 재사용한다.

text
1쿼리 요청 → 버퍼 풀에 있으면? → 바로 반환 (빠름)
2 ↓ 없으면
3 디스크에서 읽어옴 → 버퍼 풀에 올림 → 반환 (느림)

버퍼 풀 크기가 클수록 디스크 I/O가 줄어 성능이 좋아진다. 운영 서버에서는 전체 메모리의 50~80%를 버퍼 풀에 할당하는 게 일반적이다.

sql
1SHOW VARIABLES LIKE 'innodb_buffer_pool_size';

Undo Log & Redo Log — 데이터 안전의 두 기둥

이 두 로그를 이해하면 MVCC가 어떻게 동작하는지도 같이 이해된다.

Undo Log는 변경 전 데이터를 저장한다.

text
1UPDATE accounts SET balance = 5000 WHERE id = 1;
2-- 실행 전: balance = 10000
3-- Undo Log에 "id=1, balance=10000" 저장
4-- 실제 데이터는 5000으로 변경
5-- ROLLBACK 시 Undo Log의 10000으로 복구

Undo Log는 롤백뿐 아니라 MVCC의 구버전 데이터로도 쓰인다. 다른 트랜잭션이 예전 버전 데이터를 읽어야 할 때 여기서 가져온다.

Redo Log는 변경 후 데이터를 기록한다.

InnoDB는 성능을 위해 데이터를 메모리(버퍼 풀)에서 먼저 수정하고, 디스크에는 나중에 쓴다. 만약 그 사이에 서버가 다운되면 메모리 내용이 사라진다. Redo Log가 있으면 재시작 시 이 로그를 재실행(Redo)해서 데이터를 복구할 수 있다.

text
1쓰기 요청 → 버퍼 풀(메모리) 수정 → Redo Log 기록 → 응답 반환
2
3 나중에 디스크에 반영 (비동기)

[!tip] 왜 Redo Log에 먼저 쓸까? 디스크 랜덤 I/O는 느리고, Redo Log 쓰기는 순차 I/O라 빠르다. Redo Log에 빠르게 기록하고 응답을 돌려주는 방식이 성능상 유리하다.

MVCC — 락 없이 읽기 일관성을 보장하는 방법

MVCC(Multi-Version Concurrency Control) 는 읽기 트랜잭션과 쓰기 트랜잭션이 서로를 블로킹하지 않게 해주는 핵심 메커니즘이다.

InnoDB는 데이터를 수정할 때 덮어쓰지 않는다. 버전을 쌓아둔다.

text
1accounts 행의 버전 히스토리 (Undo Log):
2 버전 1: balance=10000 ← 트랜잭션 100번이 만들었음
3 버전 2: balance=5000 ← 트랜잭션 101번이 만들었음 (최신)
4
5트랜잭션 B가 100번 시점에 BEGIN했다면:
6 → "101번 이후 버전은 내 스냅샷에 없음"
7 → 항상 버전 1(10000)을 읽음
8 → 락 없이 일관된 읽기!

이 덕분에 SELECTUPDATE를 블로킹하지 않는다. InnoDB가 높은 동시성을 유지하는 핵심 이유다.


트랜잭션과 잠금 — 핵심 3줄 요약

이 주제는 별도 글에서 더 자세히 다뤘어요. 여기서는 핵심만 짚고 넘어간다.

  1. InnoDB의 락은 인덱스에 걸린다. 인덱스가 없으면 FOR UPDATE 하나가 테이블 전체를 잠근다.
  2. REPEATABLE READ + MVCC가 MySQL의 기본 격리 수준이다. 트랜잭션 시작 시점의 스냅샷을 읽는다.
  3. 데드락은 방지보다 감지 + 재시도가 현실적이다. InnoDB가 자동으로 감지하고 롤백하지만, 애플리케이션에서 재시도 로직을 항상 넣어야 한다.

인덱스 — 가장 중요한 챕터

솔직히 이 책에서 가장 많이 공부한 챕터가 여기예요. 인덱스를 "그냥 걸면 빨라지는 것" 정도로만 알고 있었는데, 내부 구조를 이해하고 나니 왜 어떤 쿼리는 인덱스를 써도 느린지, 왜 순서가 중요한지가 보이기 시작했어요.

B-Tree 인덱스 — 인덱스의 기본 구조

MySQL 인덱스의 기본은 B-Tree(Balanced Tree) 다. 항상 균형을 유지하는 트리 구조다.

text
1// 정확히는 B+tree 다
2 [15]
3 / \
4 [7] [23]
5 / \ / \
6 [3] [11] [18] [27]
7 ↓ ↓ ↓ ↓
8 데이터 데이터 데이터 데이터

검색 시 루트에서 시작해서 크고 작음을 비교하며 내려간다. 100만 건이 있어도 트리 깊이가 몇 단계에 불과해서 탐색이 빠르다.

B-Tree 인덱스가 빠른 이유는 정렬된 상태를 항상 유지하기 때문이다. 덕분에 =, >, <, BETWEEN, LIKE 'prefix%' 같은 조건에서 효과적이다.

[!warning] LIKE '%suffix'는 인덱스를 못 쓴다 B-Tree는 앞에서부터 비교한다. LIKE '%alice'처럼 앞이 불확정이면 어디서 시작해야 할지 모르니 전체 스캔을 한다. LIKE 'alice%'는 앞이 고정이라 인덱스를 탄다.

클러스터드 인덱스 — InnoDB PK가 특별한 이유

InnoDB에서 PRIMARY KEY는 단순한 인덱스가 아니다. 데이터 자체가 PK 순서로 정렬되어 저장된다. 이걸 클러스터드 인덱스라고 한다.

text
1PK(id) 기준으로 데이터가 물리적으로 정렬됨:
2 Page 1: id=1~100
3 Page 2: id=101~200
4 Page 3: id=201~300
5 ...

PK로 조회하면 해당 페이지를 바로 찾아갈 수 있어서 가장 빠르다. 다른 인덱스(세컨더리 인덱스)는 PK를 거쳐서 데이터를 찾아야 하는 2단계 구조다.

여기서 PK 설계에서 자주 하는 실수가 나온다. UUID를 PK로 쓰면 어떻게 될까요?

sql
1-- UUID를 PK로 쓸 경우
2CREATE TABLE users (
3 id VARCHAR(36) PRIMARY KEY DEFAULT (UUID()),
4 -- ...
5);

UUID는 랜덤 값이라 새 데이터가 트리의 어디에 삽입될지 예측 불가능하다. 그때마다 페이지 분할(Page Split) 이 발생해서 INSERT 성능이 저하되고 인덱스가 단편화된다.

AUTO_INCREMENT 정수 PK가 권장되는 이유가 바로 이거다. 항상 마지막에 추가되어 페이지 분할이 최소화된다.

세컨더리 인덱스 — 2단계 조회를 이해해야 한다

PK 외의 인덱스를 세컨더리 인덱스(Secondary Index) 라고 한다. 세컨더리 인덱스는 인덱스 값 + PK를 저장한다.

text
1email 세컨더리 인덱스:
2 "alice@.." → id=1
3 "bob@.." → id=2
4 "carol@.." → id=3
5
6SELECT * FROM users WHERE email = 'alice@...' 쿼리 실행 시:
7 1단계: email 인덱스에서 "alice@.." 찾음 → id=1
8 2단계: PK 인덱스에서 id=1 찾음 → 실제 데이터 반환

이 2단계 조회를 북마크 룩업(Bookmark Lookup) 이라고도 한다. 세컨더리 인덱스로 조회할 때마다 이 과정이 발생한다.

커버링 인덱스 — 2단계 조회를 없애는 방법

2단계 조회가 불필요한 상황이 있다. 인덱스 자체에 필요한 컬럼이 다 있으면 PK로 다시 찾아갈 필요가 없다.

sql
1-- idx_user_status: (user_id, status) 복합 인덱스가 있을 때
2
3-- 커버링 인덱스 가능: SELECT 컬럼이 인덱스 안에 있음
4SELECT user_id, status
5FROM orders
6WHERE user_id = 123;
7
8-- 커버링 인덱스 불가능: memo는 인덱스에 없음
9SELECT user_id, status, memo
10FROM orders
11WHERE user_id = 123;

EXPLAIN에서 Extra: Using index가 보이면 커버링 인덱스가 적용된 것이다. 데이터 페이지를 아예 읽지 않아서 성능이 확 올라간다.

인덱스가 무효화되는 패턴

인덱스를 걸어도 쿼리에 따라 못 쓰는 경우가 있다. 이걸 모르면 "인덱스 걸었는데 왜 느리지?" 하게 된다.

sql
1-- ① 컬럼에 함수 적용
2WHERE YEAR(created_at) = 2024 -- ✗ 인덱스 무효
3WHERE created_at >= '2024-01-01' -- ✓
4
5-- ② 묵시적 형변환
6WHERE user_id = '123' -- user_id가 INT인데 문자열로 비교 → ✗
7WHERE user_id = 123 -- ✓
8
9-- ③ 앞에 % 붙은 LIKE
10WHERE name LIKE '%alice%' -- ✗
11WHERE name LIKE 'alice%' -- ✓
12
13-- ④ NULL 비교 (인덱스는 쓰이지만 조심)
14WHERE column IS NULL -- 인덱스 타긴 함
15WHERE column IS NOT NULL -- 카디널리티 낮으면 무시될 수 있음
16
17-- ⑤ OR 조건 (인덱스 병합이 안 되는 경우)
18WHERE user_id = 123 OR status = 'paid' -- 인덱스 하나만 쓰거나 Full Scan

가장 자주 실수하는 게 함수 적용묵시적 형변환이에요. 특히 형변환은 에러도 안 나고 느리기만 해서 찾기가 어렵다.

복합 인덱스 — 순서가 전부다

복합 인덱스는 왼쪽 컬럼부터 순서대로 사용해야 한다. 전화번호부를 생각하면 이해하기 쉬워요. 성(姓)→이름 순으로 정렬되어 있으면 이름만으로는 찾을 수가 없다.

sql
1-- 자주 실행되는 쿼리
2SELECT * FROM orders
3WHERE user_id = 123 AND status = 'paid'
4ORDER BY created_at DESC;
5
6-- 최적 복합 인덱스
7-- 등호 조건 → 카디널리티 높은 것 → 범위/정렬 순서
8CREATE INDEX idx_user_status_date ON orders (user_id, status, created_at);
sql
1-- (user_id, status, created_at) 인덱스 기준
2
3-- 사용됨 ✓
4WHERE user_id = 123
5WHERE user_id = 123 AND status = 'paid'
6WHERE user_id = 123 AND status = 'paid' AND created_at > '2024-01-01'
7
8-- 사용 안 됨 ✗
9WHERE status = 'paid' -- 첫 컬럼 빠짐
10WHERE created_at > '2024-01-01' -- 첫 컬럼 빠짐

옵티마이저가 인덱스를 무시하는 경우 — "인덱스 걸었는데 왜 느리지?"

인덱스를 만들었는데도 EXPLAIN에서 type: ALL이 뜨는 경우가 있어요. 옵티마이저가 의도적으로 인덱스를 무시하는 거다.

이유가 뭘까요? 옵티마이저는 인덱스를 쓸지 말지 항상 비용(Cost)을 계산한다. 세컨더리 인덱스는 2단계 조회가 필요하다. 데이터가 많이 걸릴수록 이 2단계 비용이 누적된다.

text
1전체 100만 건 중 50만 건 조회 시:
2 인덱스 사용:
3 50만 번 인덱스 레코드 읽기 +
4 50만 번 PK로 랜덤 I/O → 매우 느림
5
6 Full Table Scan:
7 전체 페이지를 순서대로 읽기 → 오히려 더 빠름

선택도(Selectivity) 라는 개념이 여기서 나온다. 전체 대비 조회되는 비율이 낮을수록 인덱스가 효과적이다. 일반적으로 전체의 20~25% 이상을 읽어야 하면 옵티마이저는 Full Scan을 선택한다.

sql
1-- 테이블의 인덱스 통계 확인
2SHOW INDEX FROM orders;
3
4-- Cardinality 컬럼: 중복 없는 값의 추정 개수
5-- Cardinality가 낮을수록 선택도가 낮아서 인덱스 효과 없음

[!tip] 인덱스를 강제로 쓰게 하고 싶을 때

sql
1-- 옵티마이저 힌트로 인덱스 강제 지정
2SELECT * FROM orders USE INDEX (idx_status) WHERE status = 'paid';
3SELECT * FROM orders FORCE INDEX (idx_status) WHERE status = 'paid';

단, 이건 임시방편이에요. 근본적으로 쿼리나 인덱스 설계를 개선하는 게 맞다.

PK 없는 테이블 — InnoDB가 몰래 하는 일

"InnoDB 테이블에 PK를 안 만들면 어떻게 되나요?" — 이것도 궁금했던 포인트다.

InnoDB는 PK가 없으면 두 가지를 순서대로 시도한다.

text
11순위: PK가 있으면 그걸 클러스터드 인덱스로 사용
22순위: NOT NULL + UNIQUE 컬럼이 있으면 그걸 클러스터드 인덱스로 사용
33순위: 둘 다 없으면 InnoDB가 내부적으로 6바이트 숨겨진 rowid를 만들어 사용

3순위까지 가면 문제가 생긴다. 이 숨겨진 rowid는 개발자가 접근할 수 없고, 세컨더리 인덱스를 전혀 사용할 수 없다. 모든 조회가 Full Table Scan이 된다.

PK는 반드시 명시적으로 선언해야 한다.

세컨더리 인덱스에 왜 PK가 포함되나

세컨더리 인덱스는 인덱스 값 + PK를 저장한다고 했는데, 왜 PK까지 저장하는 걸까요?

다른 DB(예: Oracle)는 인덱스에 ROWID(디스크 물리 주소)를 저장한다. 빠르게 접근할 수 있지만 데이터가 이동하면(INSERT로 페이지 분할 등) ROWID가 바뀌어서 모든 인덱스를 다시 써야 한다.

InnoDB는 PK를 저장하는 방식을 택했다. 데이터가 이동해도 PK는 안 바뀌니까 세컨더리 인덱스를 재작성할 필요가 없다. 유지보수 비용을 줄이는 설계다.

반대급부로 세컨더리 인덱스 크기가 커지고(PK가 크면 더 크게), 2단계 조회가 필요하다는 단점이 있다. 이래서 PK는 가능한 작고 단순하게 INT AUTO_INCREMENT를 쓰는 게 좋다.

복합 인덱스에서 범위 조건 이후가 잘리는 이유

복합 인덱스에서 자주 오해하는 부분이 있다. 범위 조건(>, <, BETWEEN) 이후 컬럼은 인덱스를 타지 못한다.

sql
1-- (user_id, created_at, status) 복합 인덱스
2
3-- created_at이 범위 조건이면 그 이후 status는 인덱스 못 탐
4WHERE user_id = 123
5 AND created_at > '2024-01-01' -- 여기까지만 인덱스 사용
6 AND status = 'paid' -- 이건 인덱스 후 WHERE 필터링

왜 그럴까요? 복합 인덱스는 (user_id, created_at, status) 순서로 정렬된다. user_id가 같은 구간에서 created_at으로 정렬, created_at이 같으면 status로 정렬하는 구조다.

created_at > '2024-01-01'처럼 범위가 정해지면 그 안에서 status가 뒤섞여 있다. 정렬이 보장되지 않으니 인덱스를 탈 수 없다.

text
1user_id=123인 구간:
2 (123, 2024-01-01, 'paid')
3 (123, 2024-01-02, 'cancel') ← created_at 범위 안에서
4 (123, 2024-01-02, 'paid') status가 뒤섞여 있음
5 (123, 2024-01-03, 'refund')

이 때문에 범위 조건 컬럼은 복합 인덱스에서 최대한 뒤에 배치하는 게 원칙이다.

ORDER BY와 인덱스 — 정렬이 공짜가 되는 조건

ORDER BY가 인덱스 순서와 일치하면 별도 정렬 없이 인덱스 순서대로 읽으면 된다.

sql
1-- (user_id, created_at) 복합 인덱스가 있을 때
2
3-- 인덱스로 정렬 가능 → Using filesort 없음 ✓
4SELECT * FROM orders
5WHERE user_id = 123
6ORDER BY created_at DESC;
7
8-- 인덱스로 정렬 불가 → Using filesort 발생 ✗
9SELECT * FROM orders
10WHERE user_id = 123
11ORDER BY status DESC; -- status는 인덱스에 없음

EXPLAIN에서 Extra: Using filesort가 보이면 정렬을 메모리나 디스크에서 따로 하고 있다는 뜻이다. 데이터가 많으면 심각한 성능 저하가 된다.

[!warning] ASC/DESC 방향도 맞춰야 한다 MySQL 8.0부터 내림차순 인덱스를 공식 지원한다.

sql
1-- 두 컬럼 방향이 다를 때: 내림차순 인덱스 필요
2SELECT * FROM orders
3ORDER BY user_id ASC, created_at DESC;
4
5-- 이 경우 인덱스를 이렇게 만들어야 함
6CREATE INDEX idx ON orders (user_id ASC, created_at DESC);

방향이 다른 복합 정렬은 MySQL 5.7 이하에서는 항상 filesort가 발생했다.

인덱스 스킵 스캔 — MySQL 8.0 신기능

MySQL 8.0부터 인덱스 스킵 스캔이 추가됐다. 복합 인덱스의 첫 번째 컬럼이 쿼리에 없어도 인덱스를 활용할 수 있게 됐다.

sql
1-- (gender, age) 복합 인덱스가 있을 때
2-- 원래는 첫 컬럼(gender) 없으면 인덱스 못 씀
3SELECT * FROM users WHERE age > 25;
4
5-- MySQL 8.0에서는 옵티마이저가 이렇게 처리:
6-- WHERE gender = 'M' AND age > 25 (M 케이스)
7-- WHERE gender = 'F' AND age > 25 (F 케이스)
8-- 두 결과 합쳐서 반환

단, gender의 카디널리티가 낮을 때(값의 종류가 적을 때) 효과적이다. 카디널리티가 높으면 너무 많은 스킵이 발생해서 Full Scan보다 느릴 수 있다.

EXPLAIN으로 인덱스 확인하기

인덱스 설계 후 EXPLAIN으로 반드시 확인해야 한다.

sql
1EXPLAIN SELECT * FROM orders
2WHERE user_id = 123 AND status = 'paid'
3ORDER BY created_at DESC;
text
1+------+-------+---------------------+------+-------+-------------+
2| type | key | possible_keys | rows | Extra | filtered |
3+------+-------+---------------------+------+-------+-------------+
4| ref | idx_.. | idx_user_status_dt | 12 | | 100.00 |
5+------+-------+---------------------+------+-------+-------------+

type 컬럼이 핵심이다:

type의미상태
ALLFull Table Scan최악 — 고쳐야 함
index인덱스 전체 스캔나쁨
range인덱스 범위 스캔괜찮음
ref인덱스로 특정 값 검색좋음
constPK/UNIQUE로 1건 검색최고

type: ALLrows가 수십만이면 장애 예약이다. Extra: Using filesort도 위험 신호다 — ORDER BY를 인덱스가 처리하지 못해서 정렬을 따로 하고 있다는 뜻이다.

[!tip] Extra 컬럼 읽는 법

  • Using index: 커버링 인덱스 적용됨 (좋음)
  • Using where: 인덱스로 찾은 후 추가 필터링
  • Using filesort: 정렬을 별도로 수행 (개선 필요)
  • Using temporary: 임시 테이블 사용 (개선 필요)

인덱스가 많을수록 좋은 게 아닌 이유

처음엔 "인덱스 많이 걸수록 빠르지 않나?" 싶었어요. 근데 인덱스에는 확실한 비용이 있다.

text
1인덱스가 10개 걸린 테이블에 INSERT 하면:
2 1. 실제 데이터 페이지 쓰기
3 2. 인덱스 1 업데이트
4 3. 인덱스 2 업데이트
5 ...
6 11. 인덱스 10 업데이트

조회가 빨라지는 대신 쓰기가 느려진다. 인덱스마다 B-Tree를 재정렬해야 하고, 페이지 분할도 각각 발생할 수 있다. 인덱스가 많아질수록 INSERT/UPDATE/DELETE 성능이 떨어진다.

또한 인덱스는 디스크 공간도 차지한다. 데이터 크기보다 인덱스 크기가 더 큰 테이블도 있다.

인덱스는 읽기와 쓰기의 트레이드오프다. 쓰기가 많은 테이블에서는 인덱스를 최소화해야 한다.


인덱스 설계 체크리스트

공부하면서 정리한 실무 인덱스 설계 원칙이에요.

text
1✓ WHERE 절에 자주 등장하는 컬럼에 인덱스
2✓ JOIN 연결 컬럼에 인덱스 (FK에는 항상)
3✓ 카디널리티가 높은 컬럼부터 복합 인덱스 구성
4✓ ORDER BY 컬럼을 복합 인덱스 마지막에 배치
5✓ FOR UPDATE를 쓸 컬럼에는 반드시 인덱스
6✓ 인덱스 변경 후 EXPLAIN으로 확인
7
8✗ 카디널리티 낮은 컬럼 단독 인덱스 (gender, boolean 등)
9✗ 인덱스 너무 많이 걸기 (쓰기 성능 저하)
10✗ UUID를 PK로 사용 (페이지 분할 주의)
11✗ 컬럼에 함수 적용한 채로 인덱스 기대

마치며

챕터 8까지 읽으면서 가장 많이 든 생각은 "ORM이 이 복잡함을 다 숨겨줬구나" 였어요.

Prisma나 TypeORM으로 findMany({ where: { userId: 123 } }) 한 줄이면 끝나는 것처럼 보이지만, 내부에서는 인덱스 선택, 페이지 I/O, 버퍼 풀 히트, 락 획득, MVCC 스냅샷 읽기가 동시에 일어나고 있다.

세 가지만 기억하면 된다.

  • 인덱스는 B-Tree 정렬 구조다. 왼쪽 컬럼부터, 함수 없이, 타입 일치해야 효과가 있다.
  • 클러스터드 인덱스가 PK다. UUID PK는 페이지 분할을 유발한다. AUTO_INCREMENT를 써라.
  • EXPLAIN은 매번 확인해라. type: ALL 보이면 고쳐야 한다.

2권(쿼리 최적화, 파티셔닝, 레플리케이션)도 마저 읽고 정리해볼게요 ㅎㅎ.