MySQL Bug #120021 — IN-list 샘플링 파라미터 제안
2026년 3월 MySQL Optimizer Team 에 제출한 Bug #120021 (
eq_range_index_dive_sample_count파라미터) 제안한 배경, 구현, 리뷰 과정을 정리한 글입니다. 패치는 8.4.x trunk 기준이며, MySQL Optimizer Team Lead 의 정식 피드백을 받아 Verified 상태로 분류된 시점까지의 기록입니다.
파라미터 제안 배경
운영 환경에서 IN-list 가 길어진 쿼리가 갑자기 느려지는 케이스를 마주칠 때가 있습니다. EXPLAIN 으로 확인해 보면 의도했던 type: ref 가 아닌 type: range 로 떨어지고, rows 추정치가 실제 결과 행 수와 한 자릿수 이상 차이를 보이면서 옵티마이저가 의도하지 않은 실행 계획을 선택합니다.
이 현상의 뿌리에는 eq_range_index_dive_limit 파라미터(기본값 200) 가 있습니다. IN-list 의 값 개수가 이 limit 을 초과하는 순간, 옵티마이저는 개별 index dive 를 포기하고 index 통계정보 참조를 위한 fallback 을 시도합니다. 통계는 빠르지만 항상 동일하다는 균일 분포를 가정하기 때문에, 실제 데이터가 편향(skewed) 되어 있을수록 row estimate 가 나빠집니다. 그리고 IN-list 가 클수록 이 통계 참조 자체에도 오버헤드가 누적됩니다.
추정 정확도는 떨어지고 성능 오버헤드는 함께 늘어나는 이중 손해가 발생합니다. 이 구조적 문제를 분석하면서 제안하게 된 것이 Bug #120021 입니다.
기존 fallback 메커니즘을 한 흐름으로 정리하면 이렇습니다.
1
2
3
4
5
6
7
IN-list 수가 eq_range_index_dive_limit 파라미터의 값을 넘어서게 됨
↓
index 통계정보를 참조 (이 때, 균일 분포를 가정)
↓
row estimate 예측값 오류
↓
잘못된 실행 계획 선택
문제 제기의 근본 원인을 저는 “균일하게 분포되었다는 걸 가정” 하는 것이라고 생각했습니다. 인덱스 통계는 카디널리티(분포 통계의 값 다양성) 와 분포 요약을 미리 집계해 둡니다. 그래서 IN-list 의 어떤 값이 들어와도 동일한 평균 selectivity(필터링 후 남는 행 비율) 를 반환합니다. 그러나 운영 데이터의 실제 분포는 거의 항상 편향되어 있습니다.
이 가정이 깨지는 순간 옵티마이저는 잘못된 카디널리티 위에서 결정을 내리고, IN-list 가 길어질수록 fallback 호출 자체의 비용도 함께 늘어납니다. 그렇다면 fallback 을 통계 참조가 아닌 다른 무언가로 대체해야 한다는 것이 출발점이었습니다.
IN-list 일부만 측정한 뒤 그 결과를 전체에 곱해 추정하는 방식
1) 왜 K개만 샘플링하는가
전체 IN-list 를 모두 dive 하는 full index dive 는 정확하지만 IN-list 개수(N) 가 커질수록 비용 또한 늘어납니다. 반면, 통계 fallback 은 비용은 작지만 정확도가 좋지 않습니다. 그래서 저는 IN-list 개수 전체를 dive 하는 것이 아닌 몇 개만 Sampling 하는 것(K개) 이 효율적이지 않을까라고 생각했습니다.
K 가 너무 작으면(예: K=1) 한 번의 dive 결과가 곧 전체를 추정하게 되므로 결과가 매번 들쭉날쭉해집니다. K 가 N 에 가까워지면 full dive 와 동일해져 오버헤드만 늘어납니다. 실험적으로는 K=5 부근에서 정확도가 빠르게 수렴하고, 그 이상에서는 추가 K 가 의미 있는 정확도 개선을 만들지 못하는 정체 구간(더 늘려도 효과가 없는 지점) 이 관찰되었습니다.
이 sampling 아이디어는 Oracle 의 Dynamic Sampling 에서 착안했습니다. 동작 방식(Oracle DS 는 데이터 블록 sampling, 본 제안은 IN-list 값에 대한
records_in_range()호출) 은 서로 다르다는 점은 인지하고 있습니다.
2) K개 측정 결과를 전체를 추정하도록 유도
본 글에서 “비례 추정” 이라고 표현하였는데, 단어는 거창해 보이지만 의미는 단순합니다. K 개만 직접 측정해 보고, 그 평균을 IN-list 전체 N 개에 그대로 곱해서 전체 row 수를 짐작하는 것입니다. K 개의 IN-list 값에 대해 records_in_range() 를 직접 호출하면, 각 값이 실제로 몇 개의 행에 매칭되는지 알 수 있습니다. 그 평균에 N 을 곱하면 전체 IN-list 가 만들어낼 총 행 수를 가늠할 수 있다는 아이디어입니다. 공식 자체는 다음과 같이 단순합니다.
1
2
3
estimated_total_rows = (sum of K dive results) / K * N
↑
IN-list 전체 값 개수
기존 통계 fallback 과의 가장 큰 차이는 추정의 근거가 실측 이라는 점입니다. records_in_range() 는 인덱스의 B-tree(인덱스 자료구조) 를 직접 탐색해서, 해당 키 범위에 실제로 몇 개의 행이 있는지 세어 반환합니다. 미리 집계해 둔 통계의 균일 분포 가정에 기대지 않고, 그때그때 실제 값을 보고 오는 셈입니다. 그래서 K 개만 측정해도 데이터의 편향이 평균값에 자연스럽게 녹아들고, 전체 추정값이 실제와 가까워집니다.
3) RANDOM / UNIFORM / FIRST 세 가지 방식
K 개를 어떻게 고르는가에 따라 추정의 분산과 편향이 달라집니다. 초기 패치에는 세 가지 방식을 모두 구현했습니다.
| 방식 | 선택 알고리즘 | 특성 |
|---|---|---|
RANDOM | 무작위 추출 | 분포 가정 없이 랜덤. 일반 케이스에 무난 |
UNIFORM | 인덱스를 N/K 간격으로 균등 추출 | 정렬된 IN-list 에서 고빈도와 저빈도를 고르게 포함 |
FIRST | 앞에서부터 K개 순서대로 | 가장 빠르지만 IN-list 정렬 편향에 민감 |
추후 리뷰에서 세 방식 모두 통계 fallback 보다는 우수했고 차이는 크지 않다는 결과가 확인되어, MySQL 측에서 RANDOM 한 가지로 단순화하는 방향을 권유합니다 (뒤의 피드백 섹션 참조).
패치 구현 — 무엇을 어디에 어떻게 넣었는가
소스 트리에서 변경한 파일은 다음 6개입니다. 각각의 역할을 한 줄로 정리하면 이렇습니다.
| 파일 | 역할 |
|---|---|
sql/sys_vars.cc | 새 파라미터 2개를 MySQL 시스템 변수로 등록 |
sql/system_variables.h | 파라미터 값을 세션 메모리에 저장할 변수 선언 |
sql/range_optimizer/index_range_scan_plan.cc/h | 핵심 로직 — IN-list 샘플링 + 비례 추정 |
sql/range_optimizer/range_optimizer.cc/h | 샘플링 적용 조건 검사(안전 가드) 함수 |
sql/range_optimizer/range_opt_param.h | 샘플링 결과를 담는 상태 필드 |
index_range_scan_plan.cc 가 +209 lines 로 가장 변경량이 크고, 핵심 로직 모두가 이 파일에 담깁니다.
1) 신규 파라미터 2개 등록 (sys_vars.cc / system_variables.h)
MySQL 에서 새 시스템 변수를 추가하려면 두 파일에 동시에 선언이 필요합니다. 추가한 파라미터는 다음 두 개입니다.
eq_range_index_dive_sample_count
- 타입:
uint, 범위 0~200, 기본값 10 - 스코프: SESSION (특정 쿼리/워크로드에 선택 적용)
- 의미: IN-list 중 몇 개를 샘플링할지(K 값)
0으로 설정하면 기능 비활성화 → 기존 index statistics 경로로 fallback
eq_range_index_dive_sample_method
- 타입:
ENUM(RANDOM, UNIFORM, FIRST), 기본값RANDOM - 스코프: SESSION
- 의미: K 개를 고를 때의 추출 방식
세션 스코프로 둔 이유는 명확합니다. IN-list 가 매우 길어지는 일부 쿼리에만 선택적으로 적용하고, 다른 쿼리의 동작은 건드리지 않기 위해서입니다.
2) 핵심 로직 — heuristic_sample_records_in_range() 의 8단계
옵티마이저가 특정 인덱스로 스캔할 때의 예상 row 수를 계산하는 함수가 check_quick_select() 입니다. 이 함수 안에 “샘플링 분기점” 을 삽입하고, 샘플링 본체를 신규 함수 heuristic_sample_records_in_range() 로 분리했습니다. 동작 흐름은 다음과 같습니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
STEP 1. check_quick_select() 진입
→ 특정 인덱스 스캔의 예상 row 수 계산 시작
STEP 2. IN-list 수가 eq_range_index_dive_limit 초과 여부 판단
→ 기존이라면 여기서 index statistics 경로로 분기
STEP 3. [NEW] 샘플링 조건 충족 여부 확인
→ sample_count > 0 이고 안전 가드 통과 시 샘플링 경로 진입
STEP 4. [NEW] IN-list 전체 목록을 메모리에서 수집
→ optimizer 내부 트리 (SEL_ROOT) 에서 SEL_ARG leaf 추출
STEP 5. [NEW] K 개 인덱스 선택 (RANDOM / UNIFORM / FIRST)
→ 예: IN-list 100 개 중 K=10 개만 추출
STEP 6. [NEW] 선택한 K 개에 대해서만 records_in_range() 호출
→ 각 표본의 실제 row 수를 인덱스에서 직접 측정
STEP 7. [NEW] 비례 추정으로 전체 추정치 계산
→ (K 개 dive 합계) / K × N
STEP 8. 계산된 row 수를 optimizer 에 반환
→ optimizer 가 이 값으로 type, key, rows 결정
기존 통계 fallback 이 “전체 도메인 통계에서 균일 분포를 가정해 계산” 하는 반면, 이 경로는 “실제 인덱스 leaf 를 K 번 탐색해 측정” 합니다. 편향된 데이터에서 정확도가 올라가는 이유가 바로 여기에 있습니다.
3) 보조 함수 — collect_sel_arg_leaves / random_sample_indices / uniform_sample_indices
샘플링 본체를 받치는 세 보조 함수를 추가했습니다.
collect_sel_arg_leaves()—SEL_ROOT트리를 순회하며 IN-list 의 각 값 노드(SEL_ARG) 를 평탄화 리스트로 수집random_sample_indices()— 무작위로 K 개 인덱스 선택uniform_sample_indices()— N/K 간격으로 균등 K 개 인덱스 선택
FIRST 방식은 별도 함수 없이 단순 슬라이싱으로 처리합니다. 세 함수 모두 IN-list 길이 N 에 비례하는 메모리 할당 없이 동작하도록 설계했습니다.
4) 비활성화 되는 조건 4가지
qualifies_for_heuristic_sampling() 함수를 별도로 두어, 샘플링이 부작용을 일으킬 수 있는 케이스를 사전에 걸러냅니다. 다음 네 조건 중 하나라도 해당하면 샘플링이 활성화되지 않고 기존 경로로 떨어집니다.
| 가드 | 비활성화 조건 | 이유 |
|---|---|---|
| ① 다중 테이블 | 조인 쿼리 (테이블 2개 이상) | row estimate 가 조인 순서 결정에 영향을 주므로 샘플링 오차의 부작용이 더 큼 |
| ② FULLTEXT | FULLTEXT 인덱스 보유 테이블 | records_in_range() API 와 동작 방식이 달라 호환되지 않음 |
| ③ 서브쿼리 | WHERE 절에 서브쿼리 존재 | 서브쿼리는 옵티마이저가 별도 처리 — 샘플링 결과와 충돌 가능 |
| ④ 파티션 폭증 | 파티션 수 × K > 500 | 파티션마다 dive 가 발생해 오버헤드가 오히려 증가 |
이 4가지 가드의 공통 원칙은 “샘플링이 명확하게 도움이 되는 단일 테이블 단순 IN 쿼리에만 활성화” 입니다. 물론, 위 네 가지 케이스에서도 샘플링이 되면 좋겠지만, 제안하는 입장에서 최대한 보수적으로 진행하는 것이 맞겠다고 판단하여 위 케이스는 모두 제외시켰습니다.
샘플링 결과를 옵티마이저 내부에 전달하기 위해 RANGE_OPT_PARAM 구조체에는 세 필드를 추가했습니다.
1
2
3
used_heuristic_sampling : 이번 쿼리에 샘플링이 실제로 사용됐는지 (true/false)
heuristic_sample_rows : 샘플링으로 계산된 예상 row 수
heuristic_total_ranges : 실제 IN-list 값의 총 개수 (비례 추정 계산용)
테스트 결과 요약
천만 건의 테이블에 심하게 skewed 된 분포(val 1~10 각 약 80만 건 = 80%, val 11~500 각 약 4천 건 = 20%) 를 만들고, IN-list 에 고빈도 10 개를 넣어 N=10, limit=5 로 강제 fallback 시킨 환경에서 측정했습니다. 실제 매칭되는 행 수는 8M 건입니다.
1) 오차율 비교
| 방식 | row estimate | 오차율 | 비고 |
|---|---|---|---|
| Vanilla statistics (limit=5) | 193,970 | 97.5% | 극단적인 오차 발생 |
| Vanilla full dive (limit=200) | 9,718,200 | 21.4% | filtered=100% 가정으로 과대 추정 |
| Patch count=1 (RANDOM) | 8,027,233 | 0.3% | 운에 의한 값, 분산 극대 (비권장) |
| Patch count=3~7 (RANDOM) | ~7,200,000 | ~10% | 안정 구간 |
| Patch count=5, UNIFORM | 7,285,734 | 8.9% | 가장 베스트 |
| Patch count≥10 | 7,042,779 | 11.9% | K≥N 수렴, 추가 효과 없음 |
기존 통계 fallback 은 IN-list 의 모든 값을 동일한 평균 cardinality 로 추정합니다. 그래서 row estimate 가 19만 건이라는 극단적인 오차가 발생합니다. 반면 Patch 의 UNIFORM 샘플링은 인덱스 범위를 균등 간격으로 나누기 때문에 고빈도 영역과 저빈도 영역이 모두 표본에 포함됩니다. 그 결과 오차율이 97.5% 에서 8.9% 로 약 11 배 줄어들고, dive 호출 횟수는 N=10 에서 5 로 절반이 됐습니다.
2) 파라미터 권장 설정
| 항목 | 효과 |
|---|---|
| vs Vanilla statistics 오차율 | 97.5% → 8.9% (-88.6 pp) |
| vs Vanilla full dive 오차율 | 21.4% → 8.9% (-12.5 pp) |
| index dive 호출 횟수 | 10 → 5 (-50%) |
권장 설정은 다음과 같습니다.
1
2
SET SESSION eq_range_index_dive_sample_count = 5;
SET SESSION eq_range_index_dive_sample_method = 'UNIFORM';
이 두 파라미터를 세션 레벨에서 설정해서, 단순 IN-list 가 많은 업무에 선택적으로 적용하면 효과적이지 않을까 생각했습니다.
제안한 파라미터의 두 가지 한계점
1) “Inaccurate cost model” 역설
“An inaccurate cost model may give a more optimal plan than an accurate row estimate”
MySQL 의 cost model(옵티마이저가 플랜 비용을 계산하는 내부 모델) 자체가 이미 근사치 기반으로 동작합니다. 그래서 기존 index statistics 의 부정확한 row estimate 가 cost model 의 다른 오차와 우연히 상쇄되는 일이 생깁니다. 그 결과 오히려 좋은 플랜이 선택될 수 있다는 역설이 발생합니다. 반대로 샘플링으로 더 정확한 row estimate 를 주입하면 cost model 의 기존 오차가 그대로 노출되어, 오히려 더 나쁜 플랜을 선택할 가능성도 존재합니다. 키 값이 균일 분포라면 통계와 샘플링 모두 full dive 만큼 정확하고, 키 값이 편향되어 있어야 비로소 샘플링이 통계보다 우수해지므로 결국 데이터 분포에 따라 효과가 갈린다는 것입니다.
“더 정확한 통계 = 더 좋은 플랜” 이 항상 성립하지 않는다는 사실. 이것이 이번 제안에서 가장 중요한 Insight였습니다.
2) Manual parameter vs automatic decision framework
“future work should use some automatic decision as to whether to use full index dives, sampling, histogram statistics, or index statistics — rather than relying on manual system variables”
두 번째 한계는 인터페이스 차원입니다. 현재 제안은 사용자가 직접 K 값과 method 를 세션 변수로 설정해야 하는 구조입니다. MySQL 옵티마이저의 장기 발전 방향은 옵티마이저가 상황을 스스로 판단해 다음 네 경로 중 최적을 자동 선택하는 것입니다.
1
full index dive → sampling → histogram statistics → index statistics
새로운 수동 시스템 변수를 추가하는 방식 자체가 이 방향성과 맞지 않는다는 지적입니다. 같은 코멘트에서 담당자는 세 가지 sample method 를 모두 유지할 필요가 있는지에 대해서도 의문을 제기했습니다 (Maybe random is sufficient?). 다음 패치 리비전에서는 RANDOM 한 가지로 단순화하고, 테스트 케이스를 함께 묶어 제출하기로 답변했습니다.
Bug #117826 과의 접점 — 통합 프레임워크
같은 시점에 Bug #117826 또한 같은 내용에 대해 문제를 제기하고 있었습니다. 이 contribution 은 “key range 에 대한 histogram selectivity 활용” 으로, 기수집된 histogram 통계를 IN-list 의 row estimate 에 활용하는 방향입니다.
두 contribution 이 해결하려는 문제의 뿌리는 정확히 같습니다. eq_range_index_dive_limit 을 초과했을 때 옵티마이저가 균일 분포 가정의 index statistics 에 의존하게 되는 부정확성입니다. 하지만, 저와는 접근 방식이 달랐습니다.
| 구분 | Bug #120021 (sampling) | Bug #117826 (histogram) |
|---|---|---|
| 추정 방식 | IN-list 에서 K 개 샘플링 후 records_in_range() 호출 | 기수집된 histogram 을 key range 추정에 활용 |
| 통계 근거 | 실측 기반 (runtime 측정) | 사전 수집 통계 기반 |
| 실행 시 추가 비용 | 샘플링 비용 발생 | 추가 비용 없음 (통계는 이미 수집) |
| 약점 | 균일 분포에서는 통계와 동등 | histogram 미수집 시 동작 불가 |
담당자가 정리한 통합의 관점은, 두 contribution 을 별개의 수동 파라미터로 관리하지 말고 옵티마이저가 상황에 따라 다음 네 경로 중 적절한 것을 자동으로 선택하는 단일 프레임워크로 발전시키자는 것입니다.
1
full index dive → sampling (#120021) → histogram statistics (#117826) → index statistics
즉 두 버그 리포트는 향후 release 의 통합 프레임워크에서 서로 다른 두 축을 담당하는 관계로 정리됩니다. sampling 은 실측이 필요한 편향 데이터에서, histogram 은 통계가 이미 수집된 환경에서 각각 강점을 발휘하는 보완재입니다.
DBA 로서 배운 것
옵티마이저는 다양한 근사치를 모두 고려해서 동작하는데, 그중 한 축의 정확도만 개선했을 때 결과가 항상 더 좋아진다고 단정할 수 없다는 것이 이번 작업의 가장 큰 교훈이었습니다. 통계, 샘플링, histogram, full dive 모두가 각자 trade-off가 존재할 뿐이며, 어느 것을 선택할지는 데이터 분포와 cost model 의 상호작용에 따라 달라집니다.
또 하나, 파라미터를 제안하는 과정에서 reviewer 의 정식 피드백을 받는 경험은 패치 자체보다 더 큰 학습이었습니다. “내가 보지 못한 trade-off 가 reviewer 의 눈에는 먼저 보인다” 는 사실, 그리고 그 피드백이 거절이 아니라 함께 발전시키자는 제안이었다는 점이 인상 깊었습니다.
정확한 통계가 곧 좋은 플랜을 보장하지는 않습니다. 옵티마이저의 모든 추정이 cost model 의 다른 부정확성과 상호작용하기 때문입니다. Bug #120021 의 sampling 은 향후 release 에서 histogram 기반 추정과 함께, 옵티마이저가 스스로 골라주는 구조의 한 축으로 합쳐질 가능성이 있습니다.