서비스 아키텍처에서 Application과 DB는 필수 구성 요소다. Application으로 요청이 들어오면, 대부분의 경우 SQL이 DB로 전달된다.
그렇다면 DB는 이 SQL을 어떻게 실행할지 어떻게 결정할까? 이 질문의 핵심은 "옵티마이저가 실행 계획을 어떻게 세우는가?" 에 있다.
MySQL 8 기준으로 쿼리가 DB에 도착하면 아래와 같은 순서로 처리된다.
이 중 가장 핵심적인 단계가 바로 "옵티마이징 단계" 다. 옵티마이저는 미리 정의된 비용 모델(Cost Model) 을 기반으로 여러 실행 계획 후보 중 가장 비용이 낮다고 판단되는 계획을 선택한다.
실행 계획(EXPLAIN)을 보면 트리(Tree) 구조를 가진다.
실제 실행은 Leaf → Root 방향으로 진행된다. 자식 노드에서 만들어진 결과가 부모 노드로 전달되는 상향식(Bottom-Up) 구조다.
연산 노드는 크게 세 가지 형태로 나뉜다:
| 실행계획 | leaf 부터 올라오는 데이터 |
|---|---|
![]() | ![]() |
예를 들어, 아래와 같이 4개의 테이블이 있다고 가정해보자.

모든 테이블을 색깔(color) 기준으로 JOIN 한다면, 수학적으로는 다양한 조합이 가능하다:
Bricks JOIN (Toys JOIN (Colors JOIN Pens))(Colors JOIN Bricks) JOIN (Toys JOIN Pens)4개의 테이블을 JOIN 하는 경우의 수는 매우 많아진다. 그렇다면 MySQL 옵티마이저는 어떻게 순서를 정할까?
대부분의 비용 기반 옵티마이저(MySQL, Oracle, PostgreSQL 등)는 Left-Deep Tree 형태를 선호한다. 이는 JOIN 결과를 점진적으로 만들어가는 구조로, 각 JOIN의 왼쪽(Outer) 입력은 이전 JOIN의 결과이고, 오른쪽(Inner) 입력은 단일 테이블이거나 단순 연산의 결과다.

위 그림처럼 (((Colors JOIN Bricks) JOIN Pens) JOIN Toys) 형태로 순차적으로 JOIN이 진행된다.
핵심 원칙은 간단하다:
조건을 적용한 후 결과 row 수가 가장 적을 것으로 "추정되는" 테이블부터 시작한다
이 "추정"을 가능하게 해주는 것이 바로 통계 정보(Statistics) 다.
위 예제에서 통계 정보를 기반으로 각 테이블의 예상 row 수를 계산한 결과, 최종 JOIN 순서는:
Colors → Bricks → Pens → Toys 로 결정된다.
실행 계획에서 가장 오른쪽(Leaf)에 있는 첫 테이블이 시작점이 된다. 옵티마이저는 다음과 같이 동작한다:

각 단계에서 결과 row 수를 예측하기 위해, 테이블의 모든 row를 실제로 세는 것은 매우 비효율적이다. 따라서 옵티마이저는 통계 정보(Statistics)를 기반으로 row 수를 추정한다.
MySQL에서는 ANALYZE TABLE 명령으로 통계 정보를 수집한다:
ANALYZE TABLE Bricks;
이 명령은 다음 정보를 수집하고 갱신한다:
이 통계 정보가 정확할수록 옵티마이저가 최적의 실행 계획을 세울 가능성이 높아진다.
Cardinality (또는 NDV, Number of Distinct Values) 는 특정 컬럼의 고유값 개수를 의미한다.
| id | color | shape |
|---|---|---|
| 1 | green | square |
| 2 | blue | square |
| 3 | red | circle |
| 4 | red | triangle |
| ... | ... | ... |
위 테이블에서:
color 컬럼의 Cardinality: 3 (green, blue, red)shape 컬럼의 Cardinality: 3 (square, circle, triangle)다음 두 쿼리를 비교해보자:
-- 1번 쿼리
SELECT *
FROM Bricks;
-- 2번 쿼리
SELECT *
FROM Bricks
WHERE color = 'red';
MySQL 옵티마이저의 추정 방식:
MySQL은 기본적으로 균등 분포(Uniform Distribution) 를 가정한다.
Selectivity ≈ 1 / Cardinality = 1 / 3
Estimated Rows ≈ Total Rows × Selectivity = 30 × 1/3 = 10
따라서 WHERE color = 'red' 조건에서 약 10개의 row가 반환될 것으로 추정한다.
이 추정치를 바탕으로 인덱스 스캔 vs 테이블 풀 스캔의 비용을 비교하여 실행 계획을 선택한다.
MySQL 8의 Cost Model은 다음 요소들을 고려한다:
-- Cost Model 파라미터 확인
SELECT * FROM mysql.server_cost;
SELECT * FROM mysql.engine_cost;
예를 들어:
row_evaluate_cost: 행 하나를 처리하는 비용 (기본값: 0.1)key_compare_cost: 인덱스 키 비교 비용 (기본값: 0.05)disk_temptable_create_cost: 임시 테이블 생성 비용실제 비용 계산 예시:
Index Scan Cost = (Index Pages × IO Cost) + (Estimated Rows × Row Evaluate Cost)
Table Scan Cost = (Table Pages × IO Cost) + (Total Rows × Row Evaluate Cost)
Cardinality에만 의존하면 잘못된 추정으로 이어질 수 있다.
실제 데이터 분포:
MySQL 옵티마이저의 추정 (균등 분포 가정):
이로 인해 다음과 같은 문제가 발생할 수 있다:
인덱스를 타야 할 쿼리가 풀 스캔으로 실행
WHERE color = 'red' (실제 3개)를 10개로 추정복합 조건의 독립성 가정 문제
WHERE 조건이 여러 개인 경우, 옵티마이저는 각 컬럼이 서로 독립적이라고 가정하고 selectivity를 곱셈으로 계산한다:
SELECT *
FROM Bricks
WHERE color = 'red' AND shape = 'square';
Combined Selectivity = (1/3) × (1/3) = 1/9
Estimated Rows = 30 × 1/9 ≈ 3.3
하지만 실제로 red와 square의 조합이 전혀 없다면, 실제 결과는 0개다. 컬럼 간 상관관계를 반영하지 못하는 한계가 있다.
MySQL 8.0부터는 히스토그램(Histogram) 을 지원하여 균등 분포 가정의 한계를 보완할 수 있다.
ANALYZE TABLE Bricks
UPDATE HISTOGRAM ON color WITH 256 BUCKETS;
히스토그램이 있으면:
처럼 값별 분포를 정확하게 반영할 수 있다.
SELECT * FROM information_schema.COLUMN_STATISTICS
WHERE SCHEMA_NAME = 'your_database' AND TABLE_NAME = 'Bricks';
히스토그램은 완벽한 해결책이 아니며, 다음과 같은 한계가 있다:
color + shape)는 여전히 해결 불가INSERT, UPDATE, DELETE 시 자동으로 갱신되지 않음ANALYZE TABLE ... UPDATE HISTOGRAM을 실행해야 함다음 조건을 모두 만족하는 경우 히스토그램 도입을 고려할 만하다:
예시:
-- 상태 컬럼: active(95%), inactive(4%), deleted(1%)
ANALYZE TABLE users UPDATE HISTOGRAM ON status;
-- 지역 컬럼: 서울(60%), 기타(40%)
ANALYZE TABLE orders UPDATE HISTOGRAM ON region;
MySQL 8.0.18부터는 EXPLAIN ANALYZE를 통해 실제 실행 결과와 옵티마이저의 추정을 비교할 수 있다:
EXPLAIN ANALYZE
SELECT *
FROM Bricks
WHERE color = 'red';
결과 예시:
-> Filter: (bricks.color = 'red') (cost=3.25 rows=10) (actual time=0.123..0.456 rows=3 loops=1)
-> Table scan on Bricks (cost=3.25 rows=30) (actual time=0.089..0.432 rows=30 loops=1)
rows=10: 옵티마이저가 추정한 row 수actual rows=3: 실제 반환된 row 수옵티마이저는 통계 정보를 기반으로 한 비용 계산을 통해 실행 계획을 수립한다. 이 과정을 이해하면서, 인덱스와 복합 인덱스의 역할이 단순히 "검색을 빠르게 하는 방법"이 아니라 옵티마이저가 올바른 판단을 내리도록 돕는 핵심 요소임을 깨달았다.
핵심 요약:
ANALYZE TABLE로 통계를 주기적으로 갱신하고, EXPLAIN ANALYZE로 추정의 정확도를 검증하는 것이 중요하다처음에는 이 영상을 통해 옵티마이저의 동작 방식을 이해하기 시작했고, Oracle 기반 DB와 MySQL의 옵티마이저 및 통계 정보 관리 방식의 차이를 학습하면서 더 깊이 있게 정리할 수 있었다.
참고 자료: