닫는 글 — 다시, 읽는 양 앞에서
{/* 삽화 자리 — 첫 장의 그 창구 앞에 다시 선 사람. 이번엔 손에 페이지 몇 장을 세어 들고 있다 */}
시작은 말이 안 되는 장면이었습니다. 조건을 붙이면 45초, 빼면 즉시.
여는 글에서 이 모순을 놓고 시작했습니다.
SELECT * FROM request_log; -- 즉시 나온다
SELECT * FROM request_log WHERE transaction_no = ?; -- 45초그리고 답은 허무할 만큼 단순했어요. 클라이언트가 붙인 LIMIT 500 때문에 한쪽은 500행만 읽고 끝났고, 다른 쪽은 조건에 맞는 500행을 못 채워 200만 행을 끝까지 읽었습니다.
조건이 있냐 없냐로 보면 말이 안 되고, 몇 행을 읽느냐로 보면 당연합니다. 렌즈가 틀렸던 거죠.
스무 장 동안 그 렌즈를 바꿔 끼웠습니다. 행에서 페이지로요.
이 책이 센 것
관통한 문장은 하나였습니다.
인덱스는 빠르게 읽는 기술이 아니라 덜 읽는 기술입니다. 그리고 그 “덜”의 단위는 행이 아니라 16KB 페이지예요.
그리고 계속 같은 걸 셌어요. 몇 페이지를 요청하는가.
| 부 | 무엇을 셌나 |
|---|---|
| 1부 | 디스크 한 번 읽는 값이 비싸서 트리 모양이 정해졌다. 팬아웃 907 대 63(3장) |
| 2부 | 16KB 안에 무엇이 들어 있고, 왜 왕복이 생기는가. 행당 3.02회(7장) |
| 3부 | 옵티마이저가 무엇을 보고 인덱스를 버리는가. 4.4% 비용 차이가 실행계획을 뒤집는다(12장) |
| 4부 | 그 읽기를 위해 쓰기가 무는 값. 난수 키가 남긴 여유 공간 5,603바이트(14장) |
| 5부 | 처리량 31% 감소를 감수하고 넣고(17장), 시간 대신 페이지를 세고(19장), 인덱스 밖을 본다 |
같은 숫자가 다른 장에서 다시 나왔습니다
이 책을 쓰면서 제일 좋았던 순간들이 이겁니다. 다른 장에서 다른 스크립트로 쟀는데 같은 값이 나올 때요.
| 숫자 | 어디서 |
|---|---|
| 44,571 페이지 요청 | 여는 글의 풀스캔, 1장, 19장의 분산 측정 |
| 3.02 행당 왕복 | 7장(왕복 몫만) 3.02, 16장(총 요청) 3.02 — 16장 값을 7장 정의로 환산하면 3.01 |
| 1,063 리프 페이지 | 13장의 단조 증가 인덱스, 17장에서 난수 인덱스를 재구성한 뒤(1,062) |
| 65.8% 채움률 | 7장(엔트리 수로 어림 64%), 14장(바이트로 실측), 16장, 17장(66%로 반올림) |
| 5,634 요청 | 11장의 조건 붙은 인덱스 스캔, 19장 |
숫자가 맞아떨어질 때보다 틀렸을 때 배운 게 많았습니다. 그건 19장에 열두 건으로 정리해뒀어요.
아직 못 밝힌 것들
정직하게 남겨둡니다. 이 책이 재고도 설명하지 못한 것들입니다.
| 장 | 못 밝힌 것 |
|---|---|
| 1장 | JSON 행의 시간이 106ms·118ms·973ms로 8배까지 흔들린 이유 |
| 12장 | 중복 없는 컬럼인데 인덱스 다이브 추정이 2.31배 부푼 이유 |
| 13장 | 공간은 그대로 더해지는데 시간만 3.00 대 3.29로 갈린 이유 |
| 14장 | 페이지당 요청 계수가 단조 1.34, 난수 1.79로 다른 이유 |
| 15장 | 버퍼풀 1,024MiB에서 삽입이 20~50분 걸린 현상(물리 read는 오히려 적음) |
| 17장 | 처리량 31% 감소가 I/O 경합·redo 압박·MDL 중 어느 몫인지 |
| 20장 | 15.2%를 지운 자리에서 OPTIMIZE가 2.8%만 돌려준 이유 (purge·반복 실행은 배제했고, 같은 행 수를 처음부터 넣으면 484.0MiB인데 재구성하면 552.0MiB) |
(대표적인 것만 추렸습니다. 1장에는 100만 행 구간만 페이지당 시간이 20% 높게 나온 것도, 2장에는 루트가 두 개로 보인 것도 원인 미상으로 남아 있어요.)
그리고 제일 큰 빈칸이 하나 있습니다.
운영 수치가 여전히 비어 있습니다. 이 책의 실측은 전부 노트북의 Docker 컨테이너에서 나왔어요. 운영 값은 여는 글의 45.36초와 41.17초뿐이고, 둘 다 개선 전 수치입니다.
19장의 여섯 단계를 운영에서 밟는 것 — 이 책의 다음 판은 거기서 시작합니다.
가져갈 것 하나만 고른다면
스무 장에서 딱 하나를 남긴다면 이겁니다.
느리다고 느껴질 때, 시간을 재기 전에 읽는 양을 세보세요.
시간은 21~81% 흔들립니다(19장). 캐시가 데워졌는지, 옆에서 뭐가 도는지, 기계가 무엇인지에 따라 달라져요. 그래서 시간만 보면 아무것도 안 바꾸고 1.9배를 만들어낼 수 있습니다.
페이지 요청은 데워진 상태라면 안 흔들립니다. 스무 번을 재도 같은 값이에요. (차가운 첫 실행은 예외고, 그것도 19장에 적어뒀습니다.)
그리고 그 숫자가 44,571에서 385로 줄었다면, 시간이 얼마로 나오든 읽는 양은 확실히 줄어든 겁니다.
여는 글에서 열어둔 숫자가 하나 있었습니다.
노트북에서 200만 행을 읽는 데 504밀리초, 운영에서 같은 성격의 스캔이 41.17초. 82배였죠. 그중 18배가 초당 읽기 속도 차이(988MB/s 대 54MB/s)였고, 순차 읽기치고 54MB/s는 낮다고 했습니다.
8장이 후보를 줬습니다 — 논리적으로 순차인 스캔이 물리적으로는 랜덤에 가까울 수 있다고요. 확정은 아닙니다. 운영 DB를 계측할 수 없어 무엇이 지배적이었는지는 여전히 못 가렸어요. 다만 15장에서 랜덤 읽기 한 번을 40µs로 쟀고, 그 대역이면 54MB/s가 설명됩니다.
같은 “페이지 하나”가 어떤 때는 공짜에 가깝고 어떤 때는 밀리초를 잡아먹습니다.
그러니 마지막으로 남길 문장은 이겁니다. 인덱스는 그 페이지를 빨리 읽게 해주지 않습니다. 안 읽어도 되게 만들 뿐이에요.
읽지 않은 페이지는 언제나 공짜니까요.
여기까지 읽어주셔서 고맙습니다.
여기서 MVCC나 락이 궁금해지셨다면 『약속된 데이터』가, @Async와 스레드풀이 궁금해지셨다면 『잠드는 스레드』가 이어집니다.