Skip to Content

19장. 나아졌다는 걸 어떻게 증명하나 — 전후 측정의 규율

{/* 삽화 자리 — 같은 저울에 같은 물건을 스무 번 올리는 사람. 바늘은 매번 다른 곳을 가리키는데, 옆에 놓인 추의 개수는 늘 똑같다 */}

같은 쿼리를 스무 번 돌렸습니다. 시간은 60.7ms에서 114ms까지 81% 흔들렸어요. 그런데 페이지 요청은 스무 번 전부 385로 한 자리도 안 변했습니다. 그리고 캐시가 차가운 첫 실행은 2.7배 느렸고, 논리 요청조차 1.7배 달랐습니다.

면접 실전 질문: ① 성능 개선을 어떻게 증명하나요? ② 몇 번 재야 하나요? ③ 시간 말고 뭘 봐야 하나요?


배경 — 넣었으니 빨라졌겠죠

18장까지 왔으면 인덱스는 이미 들어가 있습니다. 이제 누가 묻습니다 — 얼마나 좋아졌어요?

여기서 대부분 한 번 재서 답합니다. 그 한 번이 무엇인지가 이 장이에요.

한 번이 하필 운이 좋았다면요. 아니면 앞서 같은 쿼리를 돌려둬서 데워진 상태였다면요. 아니면 그 숫자가 애초에 다른 걸 재고 있었다면요.

이 책은 열여덟 장에 걸쳐 수치를 냈습니다. 그 과정에서 제가 저지른 측정 실패가 열세 건이에요. 이 장은 그걸 정리합니다.

스토리 — 세 가지 함정

첫째, 한 번만 잽니다. 개선 전에 한 번, 후에 한 번. 두 숫자를 나란히 놓고 배수를 냅니다. 그 두 번이 각각 얼마나 흔들리는 값인지는 안 봅니다.

둘째, 데워진 걸 잽니다. 개선 후 쿼리는 보통 두 번째 이후에 잽니다 — 한 번 돌려보고 “오, 빨라졌네” 한 다음 다시 재니까요. 그런데 두 번째 실행은 원래 빠릅니다.

셋째가 제일 무섭습니다. 다른 걸 잽니다. 숫자는 정확한데 그 숫자가 내가 생각한 것을 안 재고 있는 경우요.

여는 글이 그 사례였습니다. 조건을 뺀 쿼리가 3,524배 빨랐는데, 원인이 조건이 아니라 **클라이언트가 자동으로 붙인 LIMIT 500**이었어요. 조건이 없으면 아무 500행이나 담고 끝나고, 조건이 있으면 맞는 500행을 못 채워 끝까지 갑니다. 숫자는 맞았고 해석이 틀렸습니다.

셋 다 겪었습니다. 하나씩 재보죠.

핵심 — 시간은 81% 흔들리고 페이지는 0% 흔들립니다

같은 쿼리를 20회 돌리며 매번 소요 시간과 페이지 요청을 같이 받았습니다.

쿼리시간 최소중앙최대시간 진폭요청요청 진폭
인덱스 없는 조회387 ms41347421.1%44,5710.0%
인덱스 범위 조회60.7 ms65.711481.1%3850.0%
커버링 스캔381 ms41247923.8%5,6340.0%

✅ 실측 (MySQL 8.0.46 / Docker / Apple M1 / macOS, 2026-08. scan_demo 200만 행, 버퍼풀 1GiB, 워밍업 뒤 20회. 진폭 = (최대 − 최소) ÷ 중앙값입니다. 그리고 두 열은 같은 실행이 아니에요 — 요청은 평문 실행의 카운터 델타, 시간은 그 뒤 EXPLAIN ANALYZE의 값이라 계측 오버헤드가 붙습니다. 재현: ./ch19/variance.sh. 전체 소스: docs/book/code/db-index/ch19/variance.sh)

요청 진폭이 0.0%입니다. “거의 같다”가 아니라 스무 번 전부 같은 값이에요.

여는 글에서 “시간은 다르게 나올 겁니다 — 그게 정상이에요. 다만 페이지 수는 거의 그대로입니다”라고 썼습니다. 그 “거의”는 다른 기계·다른 패치 버전을 염두에 둔 유보였어요. 같은 기계에서 스무 번을 재면 ‘거의’조차 아닙니다 — 한 자리도 안 움직여요. (기계가 바뀌면 이 문장도 다시 재야 하고요.)

그리고 시간 진폭을 보세요. 범위 조회는 81% 흔들립니다. 60.7ms와 114ms 사이 어느 값이든 나올 수 있어요. 개선 전후를 각각 한 번씩만 재면, 1.9배 차이를 아무것도 안 바꾸고 만들어낼 수 있습니다.

이게 이 책이 페이지를 앞세우는 이유입니다. 시간은 결과고 페이지는 원인이에요. 원인은 안 흔들립니다. 단, 조건이 하나 붙습니다. 그건 다음 절에서요.

(표의 44,571은 여는 글에서 본 값이고 5,634는 11장에서 본 값입니다. 다른 장 다른 스크립트에서 같은 숫자가 다시 나왔어요.)

차가운 첫 실행은 2.7배 느립니다

컨테이너를 재기동해 버퍼풀을 비우고 같은 쿼리를 연달아 돌렸습니다.

소요논리 요청물리 readread-aheadInnodb_data_reads
1회차 (차가움)791.2 ms72,99614928,31528,464
2회차308.1 ms44,571000
3회차298.1 ms44,571000
워밍업 뒤304.1 / 295.8 ms44,571000

✅ 실측 (같은 환경. 여는 글의 그 풀스캔입니다. 카운터 읽기와 쿼리를 한 세션에서 처리했어요 — 따로 접속해 읽었더니 그 사이 백그라운드 활동이 섞여 값이 튀었습니다. 재현: ./ch19/coldwarm.sh)

첫 실행이 3회차(298.1ms) 대비 2.7배 느립니다. 아무것도 안 고쳤는데요.

그리고 논리 요청조차 다릅니다 — 72,996 대 44,571로 1.64배예요. 페이지를 버퍼풀에 처음 올리는 작업이 요청으로 잡히기 때문입니다.

그러니 앞에서 “요청 진폭 0.0%“라고 한 건 데워진 상태에서만 참입니다. 조건을 붙여야 하는 문장이었어요.

그리고 여기서 한 건 더 틀렸습니다

이 표의 물리 read 열을 처음엔 세 열 없이 149 하나만 놓고 이렇게 썼습니다.

“버퍼풀을 비웠는데 물리 read가 240밖에 안 됩니다. 8장에서 밝힌 대로 Docker Desktop for Mac은 호스트 페이지 캐시가 뒤에 있어서 상당수가 진짜 디스크가 아니에요.”

틀렸습니다. 카운터를 잘못 읽고 있었어요.

Innodb_buffer_pool_reads포그라운드 스레드가 직접 가져온 페이지만 셉니다. 백그라운드 read-ahead가 미리 올려둔 페이지는 거기 안 들어가고 Innodb_buffer_pool_read_ahead에 따로 잡혀요. 그리고 순차 풀스캔은 linear read-ahead의 교과서적 발동 조건입니다.

숫자가 그대로 말해줍니다.

물리 read 149 read-ahead 28,315 ─────── 합계 28,464 = Innodb_data_reads 28,464

한 자리도 안 틀리고 맞습니다. 3만 페이지 가까이 디스크에서 올라왔는데, 제가 본 카운터에는 149만 찍혀 있었어요.

그러니 2.7배는 “호스트 캐시 때문에 디스크를 거의 안 갔다”가 아닙니다. 디스크를 2만 8천 번 갔고, 제가 그걸 못 보고 있었을 뿐입니다.

이게 이 장이 말하려는 함정의 셋째 — 다른 걸 잰 것 — 의 열세 번째 사례고, 하필 이 장에서 나왔습니다.

이 책이 저지른 실패들

여는 글에서 “틀린 측정과 못 찾은 원인을 지우지 않았다”고 했습니다. 열여덟 장을 쓰는 동안 쌓인 게 이만큼입니다.

무엇을 틀렸나종류그 장에 남아 있나
여는 글조건 때문에 느린 줄 알았는데 LIMIT 500 착시다른 걸 쟀다
1장AVG_ROW_LENGTH를 썼는데 그건 추정 행 수로 나눈 값다른 걸 쟀다
2장시드를 잘못 만들어 페이지당 엔트리를 4.8로 재놓고 헤맴데이터가 틀렸다
2장파일을 파싱하다 빌드 잔재를 루트로 세어 트리가 둘이 됨다른 걸 쟀다
4장구간 을 행 수로 착각해 비율 열을 잘못 적음다른 걸 쟀다
9장범위 리터럴이 17자인데 20자인 줄 알았음데이터가 틀렸다고친 값만
12장표마다 ”%“의 정의가 달라 두 표가 충돌정의가 흔들렸다고친 값만
13장누적 평균의 상승을 “인덱스끼리 방해한다”로 읽음없는 현상을 만들었다
14장스크립트가 6회 실행분을 누적하는데 1회로 읽음다른 걸 쟀다
15장”행 열 개에 아홉 번”이라 썼는데 열 행에 한 번산술이 틀렸다고친 값만
17장전 행이 같은 값인 컬럼에 인덱스를 만들어 재놓고 대표값처럼 씀다른 걸 쟀다고친 값만
17장대량 적재 직후를 기준선으로 삼음데워지지 않은 걸 쟀다
19장read-ahead 페이지가 다른 카운터에 잡히는 걸 모르고 “디스크를 거의 안 갔다”고 읽음다른 걸 쟀다✅ (바로 위)

열세 건입니다. 그리고 이 중 **절반이 “다른 걸 쟀다”**예요.

마지막 열은 정직하게 밝힙니다. 네 건은 출간 전에 잡혀서 본문에는 고친 값만 남았습니다. 여는 글에서 “지우지 않았다”고 했으니 그 넷은 여기서만 볼 수 있어요. 나머지는 해당 장을 펴면 그 자리에 그대로 있습니다.

숫자가 틀린 경우는 적었습니다. 대부분 숫자는 맞는데 그 숫자가 내가 생각한 것을 안 재고 있었어요. 13장이 제일 심했습니다 — 1,614 → 1,620 → 1,629라는 숫자는 정확했는데, 그게 누적 평균이라는 걸 몰라서 있지도 않은 현상을 만들어냈습니다.

그래서 “몇 번 재느냐”보다 “무엇을 재고 있느냐”가 먼저입니다. 반복 측정은 첫 번째 함정만 막아줍니다.

그래서 이렇게 잽니다

단계하는 일무엇을 막나
0무엇을 재는지 한 문장으로 쓴다. “이 쿼리가 요청하는 페이지 수” 같은 식으로다른 걸 재는 것
1워밍업을 먼저 돌리고 버린다차가운 첫 실행
25회 이상 재고 중앙값을 쓴다. 최소·최대도 같이 적는다한 번만 재는 것
3시간과 함께 페이지 요청을 기록한다시간의 흔들림
4전후를 같은 조건·같은 스키마로 잰다. 순서를 뒤집어 한 번 더 확인한다순서 효과
5환경을 적는다 — 버전·행 수·데이터 크기·버퍼풀나중에 비교 못 하는 것

3번이 이 책의 핵심 처방입니다. 페이지 요청이 안 줄었는데 시간만 줄었다면, 줄어든 건 측정 조건이지 쿼리가 아닙니다.

거꾸로 페이지 요청이 44,571에서 385로 줄었다면, 시간이 얼마로 나오든 읽는 양은 확실히 줄어든 겁니다.

운영 수치는 아직 비어 있습니다

정직하게 밝힐 게 하나 있습니다.

이 책의 모든 실측은 제 노트북의 Docker 컨테이너에서 나왔습니다. 여는 글의 45.36초와 41.17초만 운영 값이고, 그건 개선 전 수치예요.

인덱스를 넣은 뒤의 운영 수치는 아직 없습니다(2026-08 기준). 개발환경에서 1,881ms가 0.108ms가 됐다는 값은 있지만(출처), 그건 운영이 아닙니다.

그러니 이 책이 보여준 건 “이 구조에서 이렇게 된다”이지 “우리 서비스가 이만큼 좋아졌다”가 아닙니다. 후자를 쓰려면 위 여섯 단계를 운영에서 다시 밟아야 하고, 그게 아직 안 됐습니다.

이 장을 쓰면서 제일 부끄러운 게 이 문단입니다. 측정 규율을 여섯 단계로 정리해놓고 정작 가장 중요한 측정을 안 했으니까요.

정리

  • 같은 쿼리를 20회 돌리면 시간은 21~81% 흔들리고 페이지 요청은 0.0% 흔들립니다. 스무 번 전부 같은 값이에요.
  • 범위 조회는 60.7ms와 114ms 사이를 오갑니다. 전후를 한 번씩만 재면 아무것도 안 바꾸고 1.9배를 만들어낼 수 있어요.
  • 물리 read 카운터 하나만 보면 안 됩니다. 차가운 풀스캔에서 Innodb_buffer_pool_reads는 149인데 read-ahead28,315였어요. 합이 Innodb_data_reads 28,464와 정확히 맞습니다.
  • 차가운 첫 실행은 2.7배 느립니다. 논리 요청조차 72,996 대 44,571로 1.64배 다르고요 — “요청은 안 흔들린다”는 데워진 상태에서만 참입니다.
  • 이 책이 열여덟 장 동안 저지른 측정 실패가 열세 건이고, 그중 **절반이 “다른 걸 쟀다”**입니다. 숫자가 틀린 경우는 오히려 적었어요. 열세 번째는 이 장에서 나왔습니다read-ahead 페이지가 다른 카운터에 잡히는 걸 모르고 “디스크를 거의 안 갔다”고 읽었어요.
  • 그래서 “몇 번 재느냐”보다 “무엇을 재고 있느냐”가 먼저입니다. 반복 측정은 첫 번째 함정만 막습니다.
  • 측정 순서: ⓪무엇을 재는지 쓴다 → ①워밍업을 버린다 → ②5회 이상 중앙값 → ③시간과 페이지를 같이 → ④같은 조건으로, 순서를 뒤집어 확인 → ⑤환경을 적는다.
  • 페이지 요청이 안 줄었는데 시간만 줄었다면, 줄어든 건 측정 조건이지 쿼리가 아닙니다.
  • 📄 이 책의 운영 TO-BE 수치는 아직 비어 있습니다. 개선 전 45.36초만 운영 값이에요.

생각해볼 질문

  1. 페이지 요청이 그대로인데 시간이 절반이 됐습니다. 무엇이 달라진 걸까요? 후보를 셋 이상 대보세요.
  2. 반대로 페이지 요청이 절반인데 시간이 그대로입니다. 이건 개선일까요, 아닐까요? (8장을 떠올려보세요)
  3. 18장 질문 3이 “MDL 대기와 빌드 구간 지연을 운영에서 어떻게 구분해 관측하나”였습니다. 이 장의 여섯 단계 중 어느 것이 그 구분에 쓰일까요?
  4. 이 장의 여섯 단계를 운영에서 밟으려면 무엇이 더 필요할까요? 노트북에서는 쉬웠던 것 중 운영에서 안 되는 게 무엇일까요?

이제 인덱스에 관해 할 이야기는 거의 다 했습니다. 왜 빠른지(1부), 무엇을 읽는지(2부), 왜 안 타는지(3부), 무엇을 청구하는지(4부), 어떻게 넣고 증명하는지(5부)요.

그런데 이 책이 계속 피해온 질문이 하나 있습니다.

인덱스로 안 되는 건 뭔가요?

여는 글의 그 테이블은 계속 자라고 있었습니다. 인덱스를 걸어 조회를 45초에서 밀리초로 줄였다고 해서, 그 테이블이 100GB가 되는 걸 막지는 못합니다.

20장. 인덱스로 못 푸는 것 — 보관정책·파티셔닝·스키마

Last updated on