12장. 일부러 안 타는 경우 — 선택도와 랜덤 접근 비용
{/* 삽화 자리 — 지름길 표지판 앞에서 걸음을 멈추고 큰길로 도로 나서는 사람. 지름길은 멀쩡히 열려 있다 */}
옵티마이저가 인덱스를 버리는 지점을 재고, 그 판단을 FORCE INDEX로 강제로 뒤집어 실제로 어느 쪽이 빨랐는지 확인했습니다. 13.1%에서 버렸는데 실제로 풀스캔이 이기기 시작한 건 26~34% 구간이었어요. 그 사이에서 옵티마이저는 2.3배 손해를 보고 있었습니다.
면접 실전 질문: ① 인덱스가 있는데 왜 풀스캔을 고르나요? ② 선택도가 몇 %면 인덱스가 손해인가요? ③
FORCE INDEX를 써도 되나요?
배경 — 이번엔 아무도 잘못하지 않았습니다
3부를 여기까지 오면서 인덱스가 안 타는 이유를 둘 봤습니다.
10장은 옵티마이저가 볼 게 없어서 틀렸습니다. 통계가 없으니 filtered=10 같은 상수를 꽂았죠. 11장은 우리가 못 보게 만들어서 안 탔습니다. 컬럼에 함수를 씌우면 정렬을 쓸 수 없으니까요.
이 장은 둘 중 어느 쪽도 아닙니다. 인덱스도 멀쩡하고, 통계도 있고, 쿼리도 정상이에요. 그런데도 옵티마이저가 안 쓰기로 합니다. 그리고 그건 대체로 옳은 판단입니다.
4장에서 이유는 이미 봤습니다. 세컨더리 인덱스로 찾은 행마다 클러스터드 인덱스로 왕복해야 하고(7장), 그 왕복이 많아지면 차라리 테이블을 처음부터 끝까지 훑는 게 쌉니다.
남은 질문은 하나입니다. 그 판단이 정말 옳았나요?
4장은 옵티마이저가 마음을 바꾸는 지점을 쟀을 뿐, 그 지점이 맞는 지점인지는 안 쟀습니다. 이 장이 그걸 잽니다.
먼저 말이 헷갈리지 않게 둘로 갈라두겠습니다.
- 버리는 지점 — 옵티마이저가
range에서ALL로 마음을 바꾸는 곳. 4장이 잰 게 이겁니다. - 실제 교차점 — 풀스캔이 실제로 더 빨라지기 시작하는 곳. 이 장이 재려는 게 이겁니다.
둘이 같으리라는 보장은 어디에도 없습니다.
(4장은 scan_demo 200만 행에 SELECT *였고, 이 장은 7·8장에서 만든 rt_random 50만 행에 COUNT(body)입니다. 테이블도 쿼리도 다르니 4장의 13.7%와 이 장의 13.1%가 비슷한 건 우연이에요. 같은 측정이 아닙니다.)
스토리 — 판단을 강제로 뒤집어보기
방법은 단순합니다. 같은 쿼리를 세 가지로 재는 거예요.
-- 자율: 옵티마이저가 알아서 고른다
SELECT COUNT(body) FROM rt_random
WHERE txno BETWEEN LPAD(1,20,'0') AND LPAD(100000,20,'0');
-- 인덱스 강제 / 풀스캔 강제
SELECT COUNT(body) FROM rt_random FORCE INDEX (idx_txno) WHERE ...;
SELECT COUNT(body) FROM rt_random IGNORE INDEX (idx_txno) WHERE ...;옵티마이저가 고른 쪽과 버린 쪽을 나란히 재면 됩니다. 버린 쪽이 더 빨랐다면 그 판단은 틀린 겁니다.
COUNT(body)로 잰 건 의도적입니다 — body는 인덱스에 없으니 행마다 클러스터드 인덱스로 왕복해야 합니다. 11장처럼 커버링으로 빠져나가면 이 장이 재려는 왕복 비용이 안 잡히니까요.
그리고 이 장의 모든 비율은 행 비율입니다. 조건에 맞는 행 ÷ 전체 행이요. txno 값 범위의 비율이 아닙니다 — 두 값은 크게 달라요. (txno는 id에서 왔는데 그 id에 auto_increment 구멍이 있습니다. 그래서 값 범위를 두 배로 넓혀도 행은 두 배가 안 돼요. 표의 행 수가 16,384·32,768·65,536처럼 2의 거듭제곱으로 떨어지는 것도 그 때문입니다.)
먼저 옵티마이저가 무엇을 보고 결정하는지 optimizer_trace로 꺼내봤습니다.
| 실제 행 | 행 비율 | 추정 행 | 배율 | 인덱스 범위 비용 | 풀스캔 비용 | 트레이스 판정 |
|---|---|---|---|---|---|---|
| 42,248 | 8.4% | 98,400 | 2.33배 | 34,440.3 | 50,764.2 | chosen: true |
| 65,536 | 13.1% | 151,402 | 2.31배 | 52,991.0 | 50,764.2 | chosen: false, cause: "cost" |
✅ 실측 (MySQL 8.0.46 / Docker / Apple M1 / macOS, 2026-08. 재현: ./ch12/verdict.sh)
풀스캔 비용은 고정입니다. 테이블 전체를 훑는 값이니 범위가 얼마든 50,764.2예요. 인덱스 범위 비용만 범위가 넓어질수록 올라갑니다. 그러다 어느 순간 넘어서고, 그때 옵티마이저는 인덱스를 놓습니다.
두 값의 차이가 **4.4%**입니다. 52,991.0 대 50,764.2요. 이 얇은 차이가 실행계획을 통째로 갈아치웁니다.
그럼 그 4.4%가 현실에서도 4.4%였을까요.
핵심 — 13.1%에서 버렸는데 실제 교차점은 26~34%였습니다
범위를 넓혀가며 셋을 다 재봤습니다.
| 실제 행 | 행 비율 | 추정 행 | 배율 | 범위 비용 | 풀스캔 비용 | 자율 | 인덱스 강제 | 풀스캔 강제 | 실제 승자 | 옳았나 |
|---|---|---|---|---|---|---|---|---|---|---|
| 16,384 | 3.3% | 35,142 | 2.14배 | 12,300.0 | 50,764.2 | range | 20.6 ms | 177 ms | 인덱스 | 예 |
| 32,768 | 6.6% | 71,944 | 2.20배 | 25,180.7 | 50,764.2 | range | 44.2 ms | 183 ms | 인덱스 | 예 |
| 42,248 | 8.4% | 98,400 | 2.33배 | 34,440.3 | 50,764.2 | range | 55.4 ms | 180 ms | 인덱스 | 예 |
| 65,536 | 13.1% | 151,402 | 2.31배 | 52,991.0 | 50,764.2 | ALL | 79.8 ms | 180 ms | 인덱스 | 아니오 |
| 65,536 | 13.1% | 151,402 | 2.31배 | 52,991.0 | 50,764.2 | ALL | 83.8 ms | 179 ms | 인덱스 | 아니오 |
| 84,481 | 16.9% | 199,660 | 2.36배 | 69,881.3 | 50,764.2 | ALL | 107 ms | 183 ms | 인덱스 | 아니오 |
| 131,072 | 26.2% | 247,601 | 1.89배 | 86,660.6 | 50,764.2 | ALL | 165 ms | 181 ms | 인덱스 | 아니오 |
| 168,947 | 33.8% | 247,601 | 1.47배 | 86,660.6 | 50,764.2 | ALL | 219 ms | 197 ms | 풀스캔 | 예 |
✅ 실측 (같은 환경. rt_random 50만 행, 108MB, 버퍼풀 1GB로 전부 상주. 시간은 워밍업 뒤 5회 중앙값. 재현: ./ch12/verdict.sh. 전체 소스: docs/book/code/db-index/ch12/verdict.sh)
비용은 4.4% 차이였는데 실제 시간은 126% 차이였습니다. 79.8ms 대 180ms요. 4.4%를 보고 갈아탄 결과가 2.3배입니다.
(13.1% 줄이 둘인 건 오타가 아닙니다. 스크립트가 도는 여덟 구간 중 두 개가 같은 65,536행으로 떨어져요 — 위에 적은 auto_increment 구멍 때문입니다. 지우지 않고 둔 건 같은 조건을 두 번 잰 값이 79.8과 83.8로 얼마나 흔들리는지 보이려고요.)
옵티마이저는 13.1%에서 인덱스를 버렸는데, 풀스캔이 실제로 이긴 건 33.8%였습니다. 26.2%에서는 아직 인덱스가 이겼고요(165 대 181). 그러니 실제 교차점은 26%와 34% 사이 어딘가이고, 이 실행에서는 33.8%였습니다. 소수점까지 단정할 값이 아니에요 — 풀스캔 시간이 177~197ms로 흔들리는데 판정 폭도 그 정도라서요.
그래도 결론은 흔들리지 않습니다. 13.1%부터 26.2%까지는 어느 쪽으로 재도 인덱스가 이겼고, 옵티마이저는 그 구간 내내 풀스캔을 골랐습니다.
왜 일찍 포기했나 — 범인은 비용 모델이 아니라 추정입니다
표의 배율 열을 보세요. 옵티마이저가 인덱스를 잘 쓰고 있던 구간에서도 추정은 계속 어긋나 있습니다. 2.14배, 2.20배, 2.33배 — 일관되게 2배 넘게 많이 봅니다. 그리고 마음을 바꾸는 그 지점에서도 2.31배였어요. 65,536행짜리 범위를 151,402행으로 본 겁니다.
여기서 계산을 해보려면 “비용이 행 수에 비례하는가”부터 확인해야 합니다. 표에 답이 있어요.
12,300.0 ÷ 35,142 = 0.3500 34,440.3 ÷ 98,400 = 0.3500
25,180.7 ÷ 71,944 = 0.3500 52,991.0 ÷ 151,402 = 0.3500
69,881.3 ÷ 199,660 = 0.3500 86,660.6 ÷ 247,601 = 0.3500여섯 지점 전부 행당 0.3500입니다. 가정이 아니라 관측이에요. 그러면 정확한 행 수였을 때의 비용이 바로 나옵니다.
65,536 × 0.35 = 22,938풀스캔의 50,764.2보다 한참 쌉니다. 옵티마이저는 인덱스를 놓지 않았을 거예요.
즉 비용 모델이 틀린 게 아니라 입력이 틀렸습니다. 모델은 받은 숫자로 옳게 계산했는데, 받은 숫자가 2.3배 부풀어 있었어요. 4장이 “추정이 몇 배 틀려야 옵티마이저가 잘못된 경로를 고를까요?”라고 물었는데, 이 테이블에서의 답은 2.3배입니다.
페이지 값을 내려봤지만 소수점까지 그대로였습니다
처음에 제가 확인하려던 건 다른 쪽이었습니다. 4장에서 풀스캔 비용을 row_evaluate_cost 0.1과 memory_block_read_cost 0.25로 분해해 맞췄죠. 그렇다면 디스크 상수인 io_block_read_cost(기본 1.0)는 데이터가 메모리에 있을 때 거의 청구되지 않아야 합니다. 인덱스 경로에서도 그런지 보려고 값을 4분의 1로 내려봤습니다.
| 실제 행 | 행 비율 | io=1.0 | 범위 비용 | io=0.25 | 범위 비용 |
|---|---|---|---|---|---|
| 32,768 | 6.6% | range | 25,180.7 | range | 25,180.7 |
| 65,536 | 13.1% | ALL | 52,991.0 | ALL | 52,991.0 |
| 84,481 | 16.9% | ALL | 69,881.3 | ALL | 69,881.3 |
| 131,072 | 26.2% | ALL | 86,660.6 | ALL | 86,660.6 |
| 168,947 | 33.8% | ALL | 86,660.6 | ALL | 86,660.6 |
✅ 실측 (같은 환경. 재현: ./ch12/costconst.sh)
소수점 한 자리까지 똑같습니다. 디스크 상수를 4분의 1로 깎았는데 비용이 1원도 안 움직였어요.
📄 문서 기반 (미검증) — MySQL 8.0은 페이지 읽기 비용을 낼 때 InnoDB가 알려주는 “버퍼풀에 있을 확률”로 두 상수를 가중 평균합니다. 이 테이블은 전부 메모리에 있다고 추정되니 디스크 상수의 가중치가 0에 가깝고, 그래서 아무 일도 안 일어난 겁니다.
그러니까 이건 4장과 같은 결과입니다. 인덱스 경로도 메모리 상수로 청구되고 있었어요. 그리고 4장에서 비용의 96%가 페이지 값이 아니라 행 값이었으니, 페이지 상수를 만져봐야 나머지 4%를 만지는 셈입니다.
여는 글에서 미리 밝혀둔 게 이거였습니다. 페이지는 읽을 양의 단위지 시간의 단위가 아니라고요.
10장의 처방도 안 들었습니다
입력이 틀린 게 원인이라면, 10장에서 배운 도구로 고칠 수 있어야 합니다. 셋 다 써봤어요.
| 시도 | type | 추정 행 | 배율 |
|---|---|---|---|
| 손대기 전 | ALL | 151,402 | 2.31배 |
ANALYZE TABLE | ALL | 151,402 | 2.31배 |
STATS_SAMPLE_PAGES=200 (기본 20) | ALL | 151,402 | 2.31배 |
ANALYZE TABLE ... UPDATE HISTOGRAM ON txno WITH 128 BUCKETS | ALL | 151,402 | 2.31배 |
✅ 실측 (같은 환경. 조건에 맞는 실제 행 65,536. 재현: ./ch12/stats.sh)
전부 안 통했습니다. 통계를 새로 뜨든, 샘플을 10배로 늘리든, 히스토그램을 128버킷으로 만들든 추정은 151,402에서 1행도 안 움직여요.
📄 문서 기반 (미검증) — 히스토그램이 안 듣는 건 그럴 만합니다. MySQL은 인덱스가 있는 컬럼의 범위 추정에 히스토그램 대신 인덱스 다이브를 씁니다. 실제 인덱스를 찔러 세는 방식이라 원래 더 정확해야 하죠.
그런데 여기선 그게 2.31배 부풀었습니다. 왜 부풀었는지는 못 밝혔습니다. txno는 50만 행에 고유값이 50만 개로 중복이 하나도 없는데도 그래요.
10장에서 “히스토그램은 추정을 고치지 실행계획을 고치지 않는다”고 했는데, 여기선 추정조차 못 고쳤습니다.
같은 크기인데 판단이 갈립니다
같은 실험을 rt_ordered로도 돌렸습니다. 행 수도 컬럼도 같고, 차이는 하나예요 — 세컨더리 키 순서가 PK 순서와 맞물리는가.
rt_ordered : txno 순서로 읽으면 id 가 1,2,3,4,5,6,7,8,9,10,11,12
rt_random : txno 순서로 읽으면 id 가 168115,39796,193981,350739,174567,...| 테이블 | 행 비율 | 추정 배율 | 자율 | 인덱스 강제 | 풀스캔 강제 | 옳았나 |
|---|---|---|---|---|---|---|
rt_ordered | 13.1% | 2.01배 | range | 56.3 ms | 186 ms | 예 |
rt_ordered | 16.9% | 2.06배 | ALL | 73.8 ms | 187 ms | 아니오 |
rt_ordered | 33.8% | 1.47배 | ALL | 146 ms | 191 ms | 아니오 |
rt_random | 13.1% | 2.31배 | ALL | 84 ms | 188 ms | 아니오 |
rt_random | 16.9% | 2.36배 | ALL | 106 ms | 187 ms | 아니오 |
rt_random | 33.8% | 1.47배 | ALL | 211 ms | 193 ms | 예 |
✅ 실측 (같은 환경. 재현: ./ch12/correlation.sh)
흩어진 쪽이 더 일찍 놓습니다. rt_ordered는 13.1%까지 붙들고 rt_random은 거기서 이미 놓았어요. 그리고 rt_ordered는 33.8%에서도 인덱스가 이깁니다(146 대 191) — 실제 교차점이 더 뒤에 있다는 뜻이죠.
방향은 맞습니다. 왕복이 여기저기로 튀면 실제로 더 비싸니까요(7·8장). 그런데 옵티마이저가 “배치”를 알아서 그런 건 아닙니다. InnoDB의 통계를 꺼내보면 이렇습니다.
| 테이블 | n_rows | idx_txno 리프 페이지 | idx_txno 전체 페이지 |
|---|---|---|---|
rt_ordered | 495,203 | 1,063 | 1,125 |
rt_random | 495,203 | 1,654 | 1,959 |
✅ 실측 (mysql.innodb_index_stats / innodb_table_stats)
같은 50만 행인데 인덱스가 1.74배 큽니다. 난수 순서로 넣은 쪽이 페이지 분할을 겪어 더 부풀었어요(그게 왜 그런지는 13·14장입니다).
그러니까 옵티마이저가 본 건 순서가 아니라 페이지 수입니다. 이 책이 계속 세온 그 단위요. 추정 배율도 2.01배 대 2.31배로 달랐고요. 둘 다 rt_random을 더 비싸 보이게 만듭니다.
다만 두 테이블 다, 놓는 시점에서는 여전히 인덱스가 2배 이상 빨랐습니다.
반대로 틀리기도 합니다
지금까지는 “너무 일찍 포기한다”였습니다. 10장의 skew 테이블에서는 정반대가 나왔어요. grade 값이 심하게 치우친 50만 행 테이블입니다.
| 조건 | 맞는 행 | 비율 | 자율 | 인덱스 강제 | 풀스캔 강제 | 옳았나 |
|---|---|---|---|---|---|---|
grade='C' | 4,996 | 1.0% | ref | 1.15 ms | 77.5 ms | 예 |
grade='B' | 44,975 | 9.0% | ref | 10.5 ms | 73.7 ms | 예 |
grade='A' | 450,029 | 90.0% | ref | 103 ms | 94.1 ms | 아니오 |
grade<>'C' | 495,004 | 99.0% | range | 119 ms | 103 ms | 아니오 |
✅ 실측 (같은 환경. 재현: ./ch12/selectivity.sh)
먼저 밝힐 게 있습니다. 이건 SELECT COUNT(*)라 grade 하나만 있으면 답이 나오는 커버링 조회입니다. 왕복이 없으니 인덱스 경로가 앞의 실험보다 훨씬 싸요. 그래서 90%를 긁어와도 손해가 10~16%에 그칩니다.
그래도 방향은 분명히 반대입니다. 90%와 99%에서도 인덱스를 놓지 않아요.
왜 반대인지도 10장에 답이 있습니다. 거기서 grade='A'의 추정은 248,675였는데 실제는 450,029였어요. 0.55배 — 과소 추정입니다.
세 사례를 나란히 놓으면 그림이 하나로 모입니다.
| 사례 | 실제 행 | 추정 행 | 배율 | 결과 |
|---|---|---|---|---|
rt_random 13.1% | 65,536 | 151,402 | 2.31배 과대 | 너무 일찍 놓는다 |
skew grade='A' | 450,029 | 248,675 | 0.55배 과소 | 너무 늦게까지 붙든다 |
scan_demo <> | 1,999,999 | 1,575,841 | 0.79배 과소 | 안 좁혀지는데 붙든다 |
옵티마이저가 한 방향으로 보수적인 게 아닙니다. 틀린 추정의 부호가 반대일 뿐이에요. 과대 추정하면 일찍 놓고, 과소 추정하면 늦게까지 붙듭니다. 판단은 두 경우 다 받은 숫자에 충실했고요.
그러니 “몇 % 넘으면 인덱스가 손해”라는 고정 숫자는 없습니다. 이 장에서만 실제 교차점이 26~34%(rt_random)와 34% 초과(rt_ordered)와 90%(skew)로 갈렸어요. 선택도가 무의미하다는 건 아닙니다 — 1%에서 인덱스가 67배 빠른 건 확실하니까요. 다만 경계는 데이터마다 다릅니다.
11장이 남긴 질문 — 못 타는 것과 안 좁혀지는 것
11장 마지막 질문이 transaction_no <> '...'였습니다. 200만 행 테이블에서 재보면 이렇습니다.
type=range key=idx_txno rows=1,575,841 Extra=Using where; Using index인덱스를 탑니다. type이 range이고 key도 찍혀 있어요. 11장의 따옴표 누락과는 다릅니다 — 그건 index(전체 훑기)였죠. FORCE INDEX를 붙여도 같은 계획입니다.
다른 건 좁혀지는 정도입니다. <>는 “한 값을 뺀 나머지 전부”라서, 인덱스를 정상적으로 타고도 데이터의 거의 전부가 범위 안에 들어옵니다. 실제로는 1,999,999행 — 200만 중 딱 한 행만 빠졌어요.
그리고 여기서도 추정이 어긋납니다. 1,575,841은 EXPLAIN의 rows, 즉 추정치예요. 실제보다 21% 모자랍니다. 방금 “추정이 2.3배 부풀었다”고 쓴 장에서 이번엔 모자란 겁니다 — 위 표의 셋째 줄이 그거고요.
11장은 인덱스를 쓸 수 없게 만드는 문제였고, 이 장은 인덱스를 써도 안 좁혀지는 문제입니다. 고치는 곳도 다릅니다. 앞은 쿼리를 고치면 되고, 뒤는 조건 자체가 데이터의 전부를 원하니 고칠 게 없어요.
그래서 FORCE INDEX를 쓰라는 건가요
아닙니다. 이 장은 FORCE INDEX를 측정 도구로 썼습니다.
운영 쿼리에 박아두는 건 다른 얘기예요. 강제는 오늘의 데이터에 대한 판단을 코드에 고정하는 일입니다. 데이터가 커지고 분포가 변하면 그 판단은 언젠가 틀리는데, 옵티마이저와 달리 힌트는 스스로 마음을 바꾸지 않습니다. 지금 2.3배 이득인 힌트가 1년 뒤 2.3배 손해로 남을 수 있어요.
이 장에서 가져갈 건 힌트가 아니라 절차입니다.
| 하는 일 | 어떻게 |
|---|---|
| 이게 일어나는지 찾는다 | 슬로우 쿼리 로그의 Rows_examined ÷ Rows_sent, 또는 performance_schema.events_statements_summary_by_digest의 SUM_ROWS_EXAMINED. 반환 행에 비해 훑은 행이 크면 후보다 |
| 추정이 얼마나 어긋났는지 본다 | EXPLAIN의 rows ÷ 실제 COUNT(*). 2배 넘게 벌어지면 의심한다. ALL이면 optimizer_trace의 range_scan_alternatives에서 버려진 후보의 rows를 본다 |
| 버린 쪽을 실제로 재본다 | FORCE INDEX / IGNORE INDEX + EXPLAIN ANALYZE로 양쪽. 한 번 말고 여러 번 |
| 통계로 고쳐본다 | ANALYZE TABLE → STATS_SAMPLE_PAGES → 히스토그램 순. 안 들을 수도 있다 |
| 그래도 차이가 크면 | 힌트 대신 인덱스 설계를 본다. 커버링으로 왕복을 없애면(16장) 판이 바뀐다 |
정말 박아야 한다면 FORCE INDEX보다 옵티마이저 힌트(/*+ INDEX(t idx) */)를 쓰고, 재검토 날짜를 주석에 남기세요. 힌트는 스스로 마음을 안 바꾸니 사람이 대신 바꿔줘야 합니다.
마지막 줄이 4부로 넘어가는 다리입니다. 이 장에서 인덱스 경로가 비쌌던 이유는 행마다 클러스터드 인덱스로 왕복해야 해서였습니다. 왕복이 없으면 이 비교 자체가 달라져요 — skew 표가 이미 그 맛보기였고요.
정리
- 옵티마이저가 인덱스를 안 쓰는 건 고장이 아니라 비용 비교의 결과입니다. 풀스캔 비용은 고정이고 인덱스 범위 비용만 자라서, 어느 순간 넘어섭니다.
- 실측: 두 비용이 50,764.2 대 52,991.0 — 4.4% 차이로 실행계획이 통째로 바뀝니다. 그런데 실제 시간은 126% 차이였어요(79.8ms 대 180ms).
- 그 판단이 항상 옳지는 않습니다.
rt_random에서 13.1%에 인덱스를 버렸는데 실제 교차점은 26~34% 구간이었고, 그 사이에서 2.3배 손해였습니다. 서로 다른 일곱 지점 중 셋이 틀렸어요. “대체로 옳다”는 맞지만 그 “대체로”가 생각보다 좁습니다. - 원인은 비용 모델이 아니라 입력이었습니다. 범위 행 수를 2.1~2.4배 많게 봤고, 비용이 행당 정확히 0.3500으로 선형이니 정확한 값이었다면 22,938 — 풀스캔의 50,764.2보다 한참 쌌습니다. 4장이 물은 “몇 배 틀려야 경로가 뒤집히나”의 답이 2.3배입니다.
io_block_read_cost를 1.0에서 0.25로 내려도 비용이 소수점까지 그대로였습니다. 인덱스 경로도 4장처럼 메모리 상수로 청구되고 있었어요.ANALYZE TABLE·STATS_SAMPLE_PAGES=200·히스토그램 128버킷 — 셋 다 추정을 1행도 못 움직였습니다. 왜 2.31배 부풀었는지는 못 밝혔습니다.- 틀릴 때는 양쪽으로 틀립니다. 과대 추정(
rt_random2.31배)이면 일찍 놓고, 과소 추정(skew0.55배,<>0.79배)이면 늦게까지 붙듭니다. 한 방향으로 보수적인 게 아니라 틀린 추정의 부호가 반대입니다. - 같은 50만 행인데
rt_random이 더 일찍 놓은 건 옵티마이저가 배치를 알아서가 아니라 인덱스가 1.74배 커서입니다(1,959 대 1,125페이지). - “몇 % 넘으면 인덱스가 손해”라는 고정 숫자는 없습니다. 이 장에서만 26~34%, 34% 초과, 90%로 갈렸어요.
<>는 인덱스를 못 타는 게 아니라 안 좁혀지는 것입니다.type=range인데 실제 1,999,999행이 범위에 들어옵니다.FORCE INDEX는 측정 도구로 쓰세요. 운영에 박으면 오늘의 판단이 코드에 고정됩니다.
생각해볼 질문
- 인덱스 경로가 비쌌던 건 행마다 클러스터드 인덱스로 왕복했기 때문입니다.
skew표는 커버링이라 90%까지도 인덱스가 버텼고요. 그럼 왕복이 아예 없다면 이 장의 교차점은 어디로 갈까요? (16장) - 이 장의 측정은 전부 메모리에 올라온 108MB 테이블입니다. 데이터가 메모리보다 훨씬 커서 풀스캔이 디스크를 실제로 긁어야 한다면, 26~34%라는 교차점은 어느 쪽으로 움직일까요? (8장의 물리 read를 떠올려보세요)
rt_random의 인덱스는 같은 행 수에 1,959페이지,rt_ordered는 1,125페이지였습니다. 넣은 데이터가 같은데 왜 한쪽만 부풀었을까요? (13·14장)
3부는 여기까지입니다. “왜 안 타는가”를 네 장에 걸쳐 봤어요 — 읽는 법(9장), 통계가 없을 때(10장), 우리가 못 쓰게 만들 때(11장), 그리고 쓸 수 있는데 안 쓰기로 할 때(12장).
네 장이 공통으로 가리킨 게 하나 있습니다. 추정과 실제의 간극입니다. 10장은 filtered=10이라는 상수가 실제와 얼마나 벌어지는지였고, 11장은 key에 인덱스가 찍혀 있는데 5,608회를 요청하더라는 이야기였고, 12장은 2.3배 부푼 추정이 실행계획을 뒤집는 장면이었어요.
그 간극이 가장 비싸게 청구되는 자리가 왕복이었습니다. 세컨더리 인덱스로 찾은 행마다 클러스터드 인덱스로 다시 내려가는 그 비용이 교차점을 정했으니까요.
그런데 지금까지는 전부 읽는 쪽 이야기였습니다. 인덱스는 읽기를 위해 만드는 물건이니 당연하죠.
4부는 청구서입니다. 그 읽기를 위해 쓰기가 무는 값이요. 방금 본 1,959페이지 대 1,125페이지가 그 첫 장면이고, 그 값은 인덱스를 몇 개 걸었느냐로 정해지지 않습니다.