인덱스를 걸었는데도 느린 이유 7가지 | 실무에서 자주 틀리는 포인트
SQL 성능 문제를 처음 잡을 때 가장 많이 듣는 말이 있습니다.
"인덱스 걸었는데 왜 아직도 느리지?"
실제로 실무에서는 인덱스를 추가했는데도 쿼리가 기대만큼 빨라지지 않는 경우가 정말 많습니다. 심지어 어떤 경우에는 인덱스를 걸고도 풀스캔(Full Scan) 이 나거나, 오히려 성능이 더 나빠지기도 합니다.
즉, 중요한 건 단순히 인덱스를 걸었느냐가 아니라, DB가 그 인덱스를 제대로 탈 수 있는 조건인지 입니다.
이번 글에서는 인덱스를 걸었는데도 느린 대표적인 이유 7가지를 실무 관점에서 쉽게 정리해보겠습니다.
데이터베이스 옵티마이저는 여러 실행 방법 중에서 비용(cost) 이 더 낮다고 판단하는 방식을 선택합니다.
즉,
- 인덱스가 있어도
- 인덱스를 타는 비용이 더 크다고 판단되면
- DB는 그냥 테이블 전체를 읽는 방식을 선택할 수 있습니다.
그래서 성능 문제를 볼 때는 항상 아래 순서로 생각하는 게 좋습니다.
- 인덱스가 있는가?
- 인덱스를 탈 수 있는 조건인가?
- 탈 수 있어도 실제로 타는가?
- 실행계획상 왜 그 경로를 골랐는가?
가장 흔한 실수 중 하나입니다.
예를 들어 created_at 컬럼에 인덱스가 있어도 아래처럼 쓰면 문제가 됩니다.
SELECT *
FROM orders
WHERE DATE(created_at) = '2026-03-25';
이 경우 DB는 인덱스 컬럼 원본값이 아니라 함수가 적용된 결과를 비교해야 해서, 인덱스를 비효율적으로 사용하거나 아예 못 탈 수 있습니다.
더 좋은 방식은 이렇게 범위 조건으로 바꾸는 것입니다.
SELECT *
FROM orders
WHERE created_at >= '2026-03-25 00:00:00'
AND created_at < '2026-03-26 00:00:00';
예를 들어 (status, created_at) 순서로 복합 인덱스가 있다고 해보겠습니다.
그런데 쿼리가 이렇게 생기면:
SELECT *
FROM orders
WHERE created_at >= '2026-03-01';
앞쪽 컬럼인 status 조건이 없기 때문에 인덱스를 기대만큼 못 쓸 수 있습니다.
복합 인덱스는 보통 왼쪽부터(left-most) 활용된다고 이해하면 쉽습니다.
즉,
(status, created_at)인덱스면status조건이 중요하고- 그 다음에
created_at이 따라오는 구조입니다.
인덱스는 보통 일부만 빨리 찾을 때 강합니다.
그런데 조건을 걸어도 전체 데이터의 30%, 50%, 80%를 읽어야 한다면 어떨까요? 이 경우 DB는 인덱스를 타고 다시 테이블을 많이 읽는 것보다, 그냥 한 번에 전체를 읽는 게 더 싸다고 판단할 수 있습니다.
예를 들어 gender, is_deleted, status 같은 선택도가 낮은 컬럼은 인덱스를 걸어도 기대보다 효과가 약할 수 있습니다.
예를 들어 아래 쿼리는 많이 느려집니다.
SELECT *
FROM member
WHERE name LIKE '%park';
앞에 %가 붙으면 문자열 시작점을 알 수 없기 때문에 일반적인 B-Tree 인덱스를 제대로 활용하기 어렵습니다.
반면 아래처럼 접두사 검색은 인덱스를 탈 가능성이 큽니다.
WHERE name LIKE 'park%'
즉,
park%→ 인덱스 활용 가능성 높음%park→ 인덱스 활용 어려움
예를 들어 WHERE 절은 인덱스를 탔는데, 정렬 기준이 인덱스 순서와 맞지 않으면 DB는 별도로 정렬(Sort) 작업을 해야 할 수 있습니다.
즉,
- WHERE는 빠른데
- ORDER BY에서 느리고
- 실행계획상 filesort / sort cost가 커지는 상황이 생길 수 있습니다.
복합 인덱스를 설계할 때는 조회 조건 + 정렬 조건을 같이 봐야 하는 이유가 여기에 있습니다.
옵티마이저는 데이터 분포를 추정해서 실행계획을 정합니다. 그런데 통계 정보가 오래됐거나 데이터 편차가 크면, 실제보다 잘못 추정해서 비효율적인 실행계획을 고를 수 있습니다.
즉,
- 인덱스는 맞게 있는데
- 옵티마이저가 잘못 판단해서
- 엉뚱한 인덱스를 타거나 풀스캔을 선택할 수 있습니다.
실무에서는 이런 경우
- 통계 갱신
- 실행계획 비교
- 힌트 사용 여부 검토 를 같이 보게 됩니다.
인덱스는 조건을 찾는 데는 유리하지만, 실제 조회 컬럼이 너무 많으면 결국 테이블 본문(데이터 페이지)까지 많이 접근해야 합니다.
특히 SELECT * 는
- 필요 없는 컬럼까지 다 가져오고
- 인덱스만으로 해결되지 않아서
- 추가 I/O 비용을 키우는 경우가 많습니다.
그래서 실무에서는 가능하면
- 필요한 컬럼만 조회하고
- 경우에 따라 커버링 인덱스(covering index) 를 고려합니다.
| 번호 | 느린 이유 | 핵심 포인트 |
|---|---|---|
| 1 | 컬럼 가공 | 함수 사용으로 인덱스 효율 저하 |
| 2 | 복합 인덱스 순서 | 왼쪽 컬럼부터 맞아야 함 |
| 3 | 조회 건수 과다 | 풀스캔이 더 유리할 수 있음 |
| 4 | LIKE 사용 방식 | 앞부분 % 는 인덱스 활용 어려움 |
| 5 | 정렬/집계 비용 | ORDER BY, GROUP BY 에서 느려질 수 있음 |
| 6 | 통계 정보 문제 | 옵티마이저가 잘못 판단할 수 있음 |
| 7 | SELECT * | 추가 I/O 비용 증가 |
실무에서는 보통 이렇게 확인합니다.
- 실행계획에서 실제 어떤 경로를 탔는지 확인
- 조건절에 함수/형변환이 있는지 확인
- 복합 인덱스 순서가 맞는지 확인
- 정렬/집계 단계에서 추가 비용이 큰지 확인
- 조회 건수가 너무 많은지 확인
- 통계 정보가 오래되지 않았는지 점검
즉, 인덱스 성능은 인덱스 생성 자체보다 쿼리 구조와 실행계획 해석 능력이 더 중요합니다.
인덱스 튜닝은 인덱스를 더 만드는 작업이 아니라, DB가 인덱스를 잘 쓸 수 있는 구조로 쿼리를 바꾸는 작업에 가깝습니다.