한 줄 정의

쿼리 힌트는 옵티마이저의 자동 판단을 무시하고 개발자의 의도를 강제 하는 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_ORDERFROM 절 순서대로 조인
조인 순서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단계로 해결되면 장기적으로 안정 합니다
  • 힌트는 순간의 해결책 이지만 코드에 영원히 남습니다
  • 통계가 바뀌면 힌트가 오히려 발목을 잡을 수 있습니다
힌트를 쓸 때 반드시 할 일
  1. 주석으로 이유 기록 — “왜 이 힌트가 필요한지”를 코드 옆에 적기

    -- 2026-05-10: 통계 부정확으로 옵티마이저가 ix_status를 잘못 선택,
    -- ix_created_at 강제. 통계 안정되면 제거 검토.
    SELECT /*+ INDEX(orders ix_created_at) */ ...
  2. 재검토 시점 기록 — Jira/Issue로 후속 작업 등록

  3. 운영 모니터링 — 힌트가 여전히 적절한지 주기적으로 확인

정리

인덱스 힌트 vs 옵티마이저 힌트

기준인덱스 힌트옵티마이저 힌트
도입5.x5.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 한 세트로 반드시 검증해야 합니다. 힌트 이름 오타로 무력화된 채 운영되는 사례가 의외로 많습니다.

관련 개념

출처

  • Real MySQL 8.0 (1권), 9.4 쿼리 힌트