Skip to Content

10장. filtered=10이라는 상수 — 통계가 없을 때

{/* 삽화 자리 — 저울에 아무것도 안 올려놓고 눈금만 읽는 사람. 무엇을 달든 같은 값이 나온다 */}

실제 비율이 90%, 9%, 1%인 세 값을 물었더니 옵티마이저가 셋 다 똑같이 49,735행이라고 답했습니다. 실제로는 90배 차이가 나는데요.

면접 실전 질문: ① filtered가 10.00이면 무슨 뜻인가요? ② 인덱스를 만들면 통계도 같이 정확해지나요? ③ 인덱스 없이 분포를 알려줄 방법은 없나요?


배경 — 9장이 남긴 소수점

9장 끝에서 이상한 걸 봤습니다.

EXPLAIN SELECT * FROM scan_demo WHERE payload = 'nope'; -- filtered=10.00 EXPLAIN SELECT * FROM scan_demo WHERE status = 'DONE'; -- filtered=10.00

payload는 200만 행이 전부 같은 값이고 status도 값이 몇 종류뿐입니다. 분포가 완전히 다른데 소수점까지 같은 값이 나올 리는 없죠. 저건 계산한 값이 아니라 꽂아 넣은 값입니다.

4장에서 본 사슬의 맨 앞이 여기입니다.

통계 → 선택도 추정 → 결과 건수 추정 → 비용 계산 → 경로 선택

이 장은 그 맨 앞이 비어 있을 때 무슨 일이 벌어지는지를 다룹니다.

스토리 — 옵티마이저가 분포를 아는 방법

InnoDB는 인덱스를 훑어 “이 컬럼에 서로 다른 값이 몇 개인가”를 셉니다. 전부 세지는 않아요. 페이지 몇 장을 뽑아 거기서 추정합니다.

뽑는 장수는 innodb_stats_persistent_sample_pages이고 기본값이 20입니다. 값 종류가 적으면 그보다 일찍 멈추고요.

✅ 실측 — 이 장 실험 테이블의 mysql.innodb_index_stats를 열어보면 PK는 sample_size 20, grade 인덱스는 4입니다. 뒤에서 이 4가 문제를 일으킵니다.

그런데 인덱스가 없으면 뽑을 대상 자체가 없습니다.

통계는 인덱스를 훑어 만드는 것이니, 인덱스 없는 컬럼에 대해서는 옵티마이저가 아무것도 모릅니다. 값이 몇 종류인지도, 어느 값이 흔한지도요.

그럴 때 답을 안 할 수는 없으니 상수를 꽂습니다. 등호일 때 그 값이 10입니다.

상수는 하나가 아니라 표입니다

연산자마다 꽂는 값이 다릅니다. 인덱스가 없는 컬럼으로 확인한 값입니다.

조건filtered
col = ?10.00
col > ?33.33
col BETWEEN ? AND ?11.11
col LIKE '?%'11.11
col <> ?90.00

✅ 실측 (MySQL 8.0.46 / Docker, 2026-08. 인덱스 없는 CHAR(50) 컬럼)

WHERE created_at > ?EXPLAIN했는데 33.33이 나온다고 당황할 필요 없습니다. 10과 같은 자리에서 온 값이에요.

그럼 분포를 알고 만든 테이블에 물어봅시다. 90%짜리와 1%짜리가 같은 대답을 받는지요.

핵심 — 90%도 1%도 답은 49,735입니다

값이 셋이고 비율이 90 / 9 / 1로 치우친 컬럼을 만들었습니다.

CREATE TABLE skew ( id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY, grade CHAR(8) NOT NULL, -- A 90% / B 9% / C 1% body CHAR(50) NOT NULL ) ENGINE=InnoDB;
실제 행실제 비율인덱스 없음 (예상 결과)어긋난 정도인덱스 있음 (예상 결과)어긋난 정도
A450,02990.0%49,7359.05배 과소248,6751.81배 과소
B44,9759.0%49,7351.11배 과대90,6462.02배 과대
C4,9961.0%49,7359.95배 과대4,996정확

✅ 실측 (MySQL 8.0.46 / Docker / Apple M1 / macOS, 2026-08. 50만 행. 예상 결과 = rows × filtered. 인덱스 없을 땐 rows가 497,350에 filtered가 10.00으로 고정이고, 인덱스를 걸면 typeALL에서 ref로 바뀌며 filtered가 100이 됩니다. 오른쪽 열은 ANALYZE TABLE을 돌리기 값이고, 돌린 뒤에도 한 자리도 안 바뀌었습니다. 재현: ./ch10/statsblind.sh. 전체 소스: docs/book/code/db-index/ch10/statsblind.sh)

인덱스가 없을 때 — 셋이 같은 답을 받습니다

497,350 × 10% = 49,735. 그게 전부입니다. 옵티마이저는 grade라는 컬럼을 들여다본 적이 없어요. 볼 수 있는 게 없었으니까요.

여기서 B를 보세요. 실제 9%인데 상수 10%와 1.11배밖에 안 어긋납니다. 상수가 우연히 맞을 때도 있습니다.

그래서 더 까다롭습니다. 다만 이 실험에서는 그 위험이 드러나지 않아요 — 셋 다 type=ALL이고 계획이 같습니다. 단일 테이블 조회에서 filtered는 계획을 안 바꿉니다. 이 값이 실제로 무는 자리는 조인 순서를 정할 때, 그리고 인덱스가 있는데도 쓸지 말지 고를 때입니다.

인덱스를 걸면 — 셋이 서로 다르게 틀립니다

C는 4,996행을 4,996으로 맞혔습니다. 인덱스 다이브(index dive) 덕분이에요 — 옵티마이저가 인덱스를 실제로 내려가 양 끝을 짚어보고 그 사이를 셉니다.

B는 2.02배 과대입니다. 4장의 그 두 배가 다른 테이블에서 또 나왔어요. 왜 하필 두 배인지는 4장에서도 못 밝혔고 여기서도 못 밝힙니다. 다만 두 테이블에서 같은 값이 나온 건 기록해둡니다.

A의 248,675는 다른 데서 왔습니다.

497,350 ÷ 2 = 248,675 ↑ ↑ 통계가 본 행 수 통계가 본 grade 값 종류

mysql.innodb_index_stats를 열면 grade 인덱스의 서로 다른 값이 2로 적혀 있습니다. 실제로는 셋인데요. 표본 4장만 보고 내린 결론입니다.

값이 둘이라고 믿으면 등호 하나가 절반을 가져간다고 추정하는 게 자연스럽습니다. 그래서 90%짜리 값을 50%로 답한 거예요.

이게 다이브가 아니라 통계에서 나온 값이라는 건 다이브를 꺼보면 확인됩니다.

다이브 사용 다이브 끔 A 248,675 248,675 ← 안 바뀐다 B 90,646 248,675 C 4,996 248,675

✅ 실측 (SET SESSION eq_range_index_dive_limit=1로 다이브를 끕니다)

B와 C는 다이브 덕에 정확해졌고, A만 다이브를 켜든 끄든 통계값 그대로입니다. 왜 A에서만 다이브가 통계와 같은 답을 냈는지는 확인하지 못했습니다.

⚠️ 4장의 그 천장과 헷갈리기 쉽습니다. 4장에서는 추정이 통계 행 수의 절반(958,819)에서 포화했지만, 여기 248,675는 행 수 ÷ 값 종류입니다. 실제로 이 테이블에서 99%·100%를 조회하면 248,675가 아니라 497,350이 나와요. 절반에서 막히는 게 아닙니다. 두 현상이 우연히 같은 모양의 숫자를 냈을 뿐입니다.

인덱스를 만들면 통계도 생기지만, 정확해지진 않습니다

위 오른쪽 열은 ANALYZE TABLE 에 잰 값이고, 돌린 뒤에도 숫자가 안 바뀌었습니다.

이유는 두 가지가 겹쳐서예요. B와 C는 애초에 통계를 안 보고 다이브로 답했고, A는 통계를 봤는데 그 통계가 ANALYZE를 돌려도 여전히 “값 두 종류”였습니다.

“인덱스를 만들면 통계가 정확해진다”는 말은 틀립니다. 통계는 생기지만, 표본이 4장이면 4장만큼만 압니다.

인덱스 없이 분포를 알려줄 방법

MySQL 8.0에는 히스토그램이 있습니다. 인덱스 없는 컬럼의 분포를 직접 저장합니다.

ANALYZE TABLE skew2 UPDATE HISTOGRAM ON grade WITH 16 BUCKETS;

인덱스를 하나도 안 건 같은 데이터에 이것만 돌려봤습니다.

히스토그램 없음히스토그램 있음실제오차
A49,735446,968450,0290.7%
B49,73545,35844,9750.9%
C49,7355,0234,9960.5%

✅ 실측 (같은 환경. filtered가 10.00에서 89.87 / 9.12 / 1.01로 바뀝니다. 히스토그램을 지우면 다시 10.00으로 돌아갑니다)

오차 1% 이내입니다. 상수를 꽂던 자리에 실제 분포가 들어온 거예요.

다만 type은 셋 다 여전히 ALL입니다. 히스토그램은 추정만 고치고 계획은 안 바꿉니다. 인덱스가 없으면 여전히 풀스캔이에요. 그리고 자동으로 갱신되지 않아서, 데이터가 바뀌면 다시 돌려야 합니다.

그럼 왜 쓰냐고요. 조인 순서나 인덱스 선택처럼 후보가 둘 이상일 때 그 추정이 판단을 바꾸니까요.

운영에서는 37만 배 어긋났습니다

4장 끝에서 미리 던져둔 숫자가 이겁니다.

370만 행짜리 로그 테이블에 인덱스가 PK뿐이었습니다. 거래번호로 조회한 실행계획입니다.

-> Filter: (request_log.transaction_no = ...) (cost=86661 rows=372663) (actual time=45358..45361 rows=1 loops=1) -> Table scan on request_log (cost=86661 rows=3.73e+6) (actual time=1.21..41174 rows=3.82e+6 loops=1)

(✅ 운영 실측 인용 — 조건을 안 걸면 빠릅니다)

9장에서 배운 대로 읽으면, 추정 rows=372663과 실제 rows=1이 한 줄 안에 나란히 있습니다. 37만 배입니다.

여기서 중요한 건 옵티마이저가 멍청했다는 게 아닙니다. 줄 수 있는 정보가 없었다는 겁니다.

그리고 이 사례에서는 후보가 풀스캔 하나뿐이라 추정이 틀려도 고를 게 없었습니다. 45초를 만든 건 잘못된 추정이 아니라 없는 인덱스예요. 상수가 진짜로 계획을 바꾸는 건 후보가 둘 이상일 때고, 그 장면은 12장입니다.

정리

  • filtered=10.00은 계산한 값이 아니라 꽂아 넣은 상수입니다. 그리고 상수는 하나가 아니라 연산자별 표예요(= 10, > 33.33, BETWEEN 11.11, <> 90).
  • 실측: 실제 비율이 90 / 9 / 1%인 세 값의 예상 결과가 셋 다 49,735였습니다. A는 9.05배 과소, C는 9.95배 과대입니다.
  • 상수가 우연히 맞을 때도 있습니다(B는 1.11배). 다만 단일 테이블 조회에서 filtered는 계획을 안 바꿔요. 무는 자리는 조인 순서와 인덱스 선택입니다.
  • 인덱스를 걸면 셋이 서로 다르게 틀립니다. C는 인덱스 다이브로 정확, B는 2.02배 과대, A는 행 수 ÷ 값 종류로 절반. 통계가 표본 4장만 보고 값이 두 종류라고 결론 낸 탓입니다.
  • “인덱스를 만들면 통계가 정확해진다”는 틀립니다. 통계는 생기지만 표본만큼만 압니다.
  • 히스토그램은 인덱스 없이도 분포를 알려줍니다. 오차 1% 이내로 떨어졌어요. 다만 추정만 고치고 계획은 안 바꿉니다.
  • 운영에서는 추정이 실제와 37만 배 어긋났습니다. 다만 인덱스가 없어 고를 경로 자체가 없었으니, 그 45초의 원인은 추정이 아니라 인덱스입니다.

생각해볼 질문

  1. B는 상수 10%와 실제 9%가 가까워 우연히 맞았습니다. 데이터가 바뀌어 B가 40%가 되면 무슨 일이 생길까요? 그리고 그게 조인에 끼어 있다면요?
  2. C는 다이브로 정확했고 A는 통계값을 받았습니다. 같은 인덱스, 같은 등호 조회인데 왜 갈렸을까요?
  3. 히스토그램은 type을 안 바꿨습니다. 그럼 추정만 정확해져서 얻는 게 무엇일까요?

이 장은 옵티마이저가 볼 게 없어서 틀리는 경우였습니다. 다음 장은 볼 게 있는데도 우리가 못 보게 만드는 경우입니다.

11장. 조용히 느려지는 것들 — 따옴표·함수·앞 와일드카드

Last updated on