두 가지를 구분해야 한다

인덱스를 두고 풀 스캔이 나오는 경우는 성격이 다른 두 부류다.

  1. 쓸 수 있는데 안 쓰는 게 낫다고 판단한 경우
  2. 조건 때문에 쓸 수 없는 경우

안 쓰는 게 나은 경우

옵티마이저는 비용을 계산해서 고른다. 인덱스 조회는 인덱스를 타고 내려가 위치를 찾은 뒤, 실제 행을 읽으러 테이블로 다시 간다. 이 왕복이 행마다 발생한다.

매칭되는 행이 전체의 상당 부분이라면 — 흔히 20~30%를 넘으면 — 랜덤 접근을 수만 번 하느니 테이블을 순차로 훑는 게 싸다. 디스크는 순차 읽기가 훨씬 빠르기 때문이다.

WHERE status = 'ACTIVE' 같은 조건에서, 대부분의 행이 ACTIVE라면 인덱스는 도움이 되지 않는다. 선택도가 낮은 컬럼의 인덱스는 만들어도 쓰이지 않는다.

통계 정보가 낡아 실제 분포와 어긋나도 잘못된 선택이 나온다. 대량 변경 뒤에는 통계를 갱신한다.

쓸 수 없는 경우

이쪽은 대개 실수다.

컬럼에 연산이나 함수를 씌운 경우

WHERE DATE(created_at) = '2025-05-19'   -- 못 씀
WHERE created_at >= '2025-05-19'
  AND created_at <  '2025-05-20'        -- 씀

인덱스는 created_at의 값으로 정렬돼 있지 DATE(created_at)의 값으로 정렬돼 있지 않다.

타입이 다른 경우

문자열 컬럼에 숫자를 비교하면 암묵적 형변환이 일어나고, 컬럼 쪽에 변환이 걸리면 위와 같은 이유로 인덱스를 못 쓴다.

복합 인덱스의 선행 컬럼을 건너뛴 경우

(a, b, c) 인덱스는 a부터 시작해야 탄다. b만 조건에 있으면 쓸 수 없다. 전화번호부를 성 없이 이름만으로 찾는 것과 같다.

앞이 열린 LIKE

LIKE 'abc%'는 쓰지만 LIKE '%abc'는 못 쓴다. 시작점을 정할 수 없기 때문이다.

확인하는 법

EXPLAIN으로 실행 계획을 본다. 그리고 예상이 아니라 실제를 보려면 EXPLAIN ANALYZE를 쓴다. 예상 행 수와 실제 행 수가 크게 다르면 통계를 의심한다.

정리

풀 스캔이 항상 나쁜 건 아니다. “왜 인덱스를 안 쓰지”가 아니라 “지금 인덱스를 쓰는 게 이득인가”를 먼저 묻는 편이 빠르다.