한 줄 정의
쿼리 힌트는 옵티마이저의 자동 판단을 무시하고 개발자의 의도를 강제 하는 SQL 주석이며, MySQL 8.0에서는 레거시 인덱스 힌트와 정식 옵티마이저 힌트(
/*+ ... */) 두 종류가 공존합니다.
쉽게 말하면
힌트는 운전자의 직접 지시 입니다.
- 평소엔 내비게이션(옵티마이저)을 믿고 따라갑니다
- 그러나 도로 사정을 더 잘 알면 “여기로 가지 마” / “이 길로 가” 라고 지시할 수 있습니다
- 단, 잘못된 지시는 더 큰 사고 를 부릅니다. 통계가 바뀌어도 힌트는 그대로 남기 때문입니다
힌트는 “마지막 수단” 으로만 써야 합니다.
왜 중요한가?
옵티마이저가 항상 옳은 건 아닙니다.
- 통계가 부정확할 때
- 데이터 분포가 극단적일 때
- 짧은 시간 안에 즉시 해결해야 할 때
이런 상황에서 힌트는 즉효약 이 됩니다. 하지만 힌트의 종류와 한계를 모르면 오히려 발목을 잡습니다. 특히 레거시 인덱스 힌트와 옵티마이저 힌트의 차이 를 구분 못 하면 8.0 환경에서 손해 봅니다.
핵심 내용
두 종류의 힌트
flowchart LR H["쿼리 힌트"] H --> IDX["인덱스 힌트<br/>(레거시, MySQL 5.x부터)"] H --> OPT["옵티마이저 힌트<br/>(MySQL 5.6+, 8.0 권장)"] IDX --> IDX_DESC["USE/FORCE/IGNORE INDEX<br/>인덱스 이름에 종속"] OPT --> OPT_DESC["/*+ ... */ 주석<br/>선언적, 더 다양한 제어"]
인덱스 힌트 (Index Hints) — 레거시
세 가지 종류
| 힌트 | 의미 |
|---|---|
USE INDEX (idx) | ”이 인덱스를 고려해라” (강제 아님) |
FORCE INDEX (idx) | ”이 인덱스를 무조건 써라” (풀 스캔보다 비싸도 사용) |
IGNORE INDEX (idx) | ”이 인덱스는 쓰지 마라” |
적용 위치별 변형
| 변형 | 적용 작업 |
|---|---|
USE INDEX FOR JOIN (idx) | 조인·WHERE의 행 탐색에만 |
USE INDEX FOR ORDER BY (idx) | ORDER BY 처리에만 |
USE INDEX FOR GROUP BY (idx) | GROUP BY 처리에만 |
사용 예
SELECT * FROM employees USE INDEX (ix_firstname)
WHERE first_name = 'Matt';
SELECT * FROM employees IGNORE INDEX (ix_firstname)
WHERE first_name = 'Matt';
SELECT * FROM employees FORCE INDEX (ix_hiredate)
WHERE hire_date > '2000-01-01';인덱스 힌트의 한계
- 인덱스 이름에 종속 — 이름이 바뀌면 힌트가 무력화됨 (오류 안 남, 조용히 무시)
- 테이블당 하나만 지정 가능
- JOIN 순서나 조인 알고리즘은 제어 불가
- MySQL 8.0+ 환경에서는 옵티마이저 힌트로 대체 권장
옵티마이저 힌트 (Optimizer Hints) — 권장
SELECT /*+ INDEX(employees ix_firstname) */ *
FROM employees WHERE first_name = 'Matt';/*+ ... */형태의 특수 주석- SELECT 직후에 위치
- 여러 힌트 동시 지정 가능
- 8.0에서 공식 권장 방식
힌트 분류
flowchart TD OPT["옵티마이저 힌트"] OPT --> SCOPE1["글로벌"] OPT --> SCOPE2["테이블 단위"] OPT --> SCOPE3["인덱스 단위"] OPT --> SCOPE4["조인 순서"] OPT --> SCOPE5["서브쿼리"]
주요 힌트 카탈로그
| 분류 | 힌트 | 의미 |
|---|---|---|
| 인덱스 | INDEX(t idx) | 인덱스 사용 권장 |
| 인덱스 | NO_INDEX(t idx) | 인덱스 사용 금지 |
| 인덱스 | GROUP_INDEX(t idx) | GROUP BY에 인덱스 사용 |
| 인덱스 | ORDER_INDEX(t idx) | ORDER BY에 인덱스 사용 |
| 인덱스 | JOIN_INDEX(t idx) | 조인에 인덱스 사용 |
| 조인 순서 | JOIN_ORDER(t1, t2, t3) | 지정한 순서로 조인 |
| 조인 순서 | JOIN_FIXED_ORDER | FROM 절 순서대로 조인 |
| 조인 순서 | JOIN_PREFIX(t1, t2) | 이 테이블들을 먼저 |
| 조인 순서 | JOIN_SUFFIX(t3) | 이 테이블들을 마지막에 |
| 조인 알고리즘 | BKA(t) / NO_BKA(t) | BKA 사용/금지 |
| 조인 알고리즘 | BNL(t) / NO_BNL(t) | BNL 사용/금지 |
| 조인 알고리즘 | HASH_JOIN(t1, t2) | 해시 조인 강제 (8.0.18-19에서만, 이후는 자동) |
| 푸시다운 | ICP(t idx) / NO_ICP(t idx) | ICP 사용/금지 |
| 멀티 레인지 | MRR(t) / NO_MRR(t) | MRR 사용/금지 |
| 머지 | INDEX_MERGE(t) / NO_INDEX_MERGE(t) | 인덱스 머지 사용/금지 |
| 서브쿼리 | SEMIJOIN(strategy) | 세미 조인 전략 강제 |
| 서브쿼리 | SUBQUERY(strategy) | 서브쿼리 전략 강제 |
| 시스템 | SET_VAR(name=value) | 쿼리 단위 시스템 변수 변경 |
| 시스템 | MAX_EXECUTION_TIME(n) | 쿼리 최대 실행 시간(ms) |
| 시스템 | RESOURCE_GROUP(name) | 리소스 그룹 지정 |
힌트 적용 예시
-- 1. 조인 순서 강제
SELECT /*+ JOIN_ORDER(e, de, d) */ *
FROM employees e
JOIN dept_emp de ON e.emp_no = de.emp_no
JOIN departments d ON de.dept_no = d.dept_no;
-- 2. 특정 테이블에 BNL 금지 (해시 조인 유도)
SELECT /*+ NO_BNL(t1, t2) */ *
FROM big_t1 t1 JOIN big_t2 t2 ON t1.id = t2.id;
-- 3. 인덱스 강제 사용
SELECT /*+ INDEX(employees ix_hiredate) */ *
FROM employees WHERE hire_date > '2000-01-01';
-- 4. 쿼리 단위 변수 변경
SELECT /*+ SET_VAR(sort_buffer_size=16M) */ *
FROM big_table ORDER BY col1;
-- 5. 최대 실행 시간 제한 (3초)
SELECT /*+ MAX_EXECUTION_TIME(3000) */ COUNT(*) FROM huge_table;
-- 6. 여러 힌트 결합
SELECT /*+ JOIN_ORDER(e, d) NO_BNL(d) INDEX(e ix_hiredate) */ *
FROM employees e
JOIN departments d ON e.dept_no = d.dept_no
WHERE e.hire_date > '2000-01-01';힌트 우선순위
flowchart LR SQL["SQL 실행"] --> H1{옵티마이저 힌트?} H1 -->|Yes| APPLY1["옵티마이저 힌트 적용"] H1 -->|No| H2{인덱스 힌트?} H2 -->|Yes| APPLY2["인덱스 힌트 적용"] H2 -->|No| AUTO["옵티마이저 자동 결정"] APPLY1 --> OPT_SW["optimizer_switch와 결합"]
- 옵티마이저 힌트가 인덱스 힌트보다 우선 합니다
- 두 힌트가 동시에 있으면 옵티마이저 힌트가 이깁니다
- 옵티마이저 힌트는
optimizer_switch설정을 쿼리 단위로 오버라이드 합니다
힌트가 무시되는 경우
힌트는 권고 일 뿐, 옵티마이저가 무시할 수 있습니다.
| 상황 | 동작 |
|---|---|
| 인덱스 이름이 틀림 | 조용히 무시 (경고만) |
| 힌트 문법 오류 | 조용히 무시 |
| 힌트 적용이 불가능한 케이스 | 조용히 무시 |
FORCE INDEX + 풀 스캔이 명백히 빠름 | FORCE는 강제, USE는 무시 가능 |
힌트 적용 여부 확인
EXPLAIN FORMAT=TREE SELECT /*+ INDEX(t ix_a) */ * FROM t WHERE a = 1;
SHOW WARNINGS;
-- 무시된 힌트가 있으면 경고 메시지로 표시됨SHOW WARNINGS로 힌트가 적용됐는지 반드시 확인 해야 합니다.
힌트 사용 원칙
flowchart TD PROBLEM["쿼리 성능 문제"] PROBLEM --> S1["Step 1: EXPLAIN으로 실행 계획 확인"] S1 --> S2["Step 2: 통계 갱신 ANALYZE TABLE"] S2 --> S3["Step 3: 인덱스 추가/재설계"] S3 --> S4["Step 4: 쿼리 재작성"] S4 --> S5["Step 5 마지막: 힌트 추가"] style S5 fill:#fdd
힌트는 마지막 수단
- 1~4단계로 해결되면 장기적으로 안정 합니다
- 힌트는 순간의 해결책 이지만 코드에 영원히 남습니다
- 통계가 바뀌면 힌트가 오히려 발목을 잡을 수 있습니다
힌트를 쓸 때 반드시 할 일
-
주석으로 이유 기록 — “왜 이 힌트가 필요한지”를 코드 옆에 적기
-- 2026-05-10: 통계 부정확으로 옵티마이저가 ix_status를 잘못 선택, -- ix_created_at 강제. 통계 안정되면 제거 검토. SELECT /*+ INDEX(orders ix_created_at) */ ... -
재검토 시점 기록 — Jira/Issue로 후속 작업 등록
-
운영 모니터링 — 힌트가 여전히 적절한지 주기적으로 확인
정리
인덱스 힌트 vs 옵티마이저 힌트
| 기준 | 인덱스 힌트 | 옵티마이저 힌트 |
|---|---|---|
| 도입 | 5.x | 5.6 (8.0+ 정식 권장) |
| 형식 | USE/FORCE/IGNORE INDEX | /*+ ... */ 주석 |
| 인덱스 종속 | 이름 의존 (이름 바뀌면 무효) | 이름 의존 (동일) |
| 제어 범위 | 인덱스 사용 여부 | 인덱스, 조인, 변수까지 |
| 우선순위 | 낮음 | 높음 |
| 다중 적용 | 어려움 | 쉬움 |
| 권장 (8.0+) | 비권장 | 권장 |
힌트별 사용 시나리오
| 시나리오 | 힌트 |
|---|---|
| 옵티마이저가 잘못된 인덱스 선택 | INDEX(t idx) 또는 NO_INDEX(t idx) |
| 조인 순서가 명백히 비효율 | JOIN_ORDER(...) |
| 대규모 조인에서 BNL 회피 | NO_BNL(t) (해시 조인 유도) |
| 특정 쿼리에서 정렬 버퍼 키우기 | SET_VAR(sort_buffer_size=16M) |
| 폭주 쿼리 차단 | MAX_EXECUTION_TIME(n) |
| 인덱스 머지 강제/금지 | INDEX_MERGE(t) / NO_INDEX_MERGE(t) |
| 일시적으로 인비저블 인덱스 사용 | SET_VAR(optimizer_switch='use_invisible_indexes=on') |
내 생각
-
MAX_EXECUTION_TIME힌트는 운영 안전망 입니다. 의도치 않은 폭주 쿼리(예: 잘못 들어온 분석 쿼리)가 DB를 마비시키는 사고를 막을 수 있습니다. 모든 분석/리포트 쿼리에는 기본으로 추가하는 게 좋습니다. -
SET_VAR힌트는 글로벌 설정 변경 없이도 쿼리 단위 튜닝 을 가능하게 합니다. 특정 배치 작업에서만sort_buffer_size를 키우거나, 특정 쿼리에서만optimizer_switch를 조정할 수 있어, 운영 영향을 최소화한 튜닝이 가능합니다. -
힌트를 추가했으면 반드시 주석으로 추가 이유와 재검토 일자 를 기록해야 합니다. 6개월 후 코드를 보는 사람(혹은 미래의 나)이 “이 힌트 왜 있지?”라고 묻지 않게 만들어야 합니다. 이유 없는 힌트는 코드 부패의 시작입니다.
-
인덱스 힌트(
USE INDEX)는 존재만 알고 쓰지 말자 가 원칙입니다. 8.0 환경에서는/*+ INDEX(t idx) */형태의 옵티마이저 힌트만 쓰면 됩니다. 둘이 섞이면 우선순위 헷갈림 + 유지보수 부담만 늘어납니다. -
힌트가 적용됐는지 확인 안 하고 “잘 됐겠지” 하는 게 가장 위험합니다.
EXPLAIN+SHOW WARNINGS한 세트로 반드시 검증해야 합니다. 힌트 이름 오타로 무력화된 채 운영되는 사례가 의외로 많습니다.
관련 개념
- Ch09-1 옵티마이저의 역할 — 힌트가 옵티마이저 결정을 어떻게 덮어쓰는지
- Ch09-3 고급 최적화 — 옵티마이저 스위치를 쿼리 단위로 적용하는 게 옵티마이저 힌트
- Ch08-3 B-Tree 인덱스 — 어떤 인덱스를 강제할지 판단하기 위한 기반 지식
- Ch10 실행 계획 —
EXPLAIN으로 힌트 적용 검증
출처
- Real MySQL 8.0 (1권), 9.4 쿼리 힌트