인덱스 정리

반응형

2026년 4월3일 면접을 보게 되었습니다. 그때 나왔던 질문중 하나가 인덱스에 대한 질문이었습니다. 과거에 인덱스를 활용해서 성능을 개선해본적이 있었냐는 질문이었습니다. 했었던거 같아 일단 답변은 하긴했었는데 정재되지 않았습니다. 버벅된게 아니라 무슨 답변을 했는지도 기억이 안나는 정도였으니까요. 저는 인덱스를 통해 읽기 성능을 올렸던적이 있었다고 답변하였는데 지금 생각해보면 이것도 애매한 답변이었던거 같습니다. 왜냐하면 성능을 올렸던적이 있었다면 정확한 수치가 필요했을텐데 그렇지 못했으니까요.

아무튼 제 경험을 토대로 인덱스를 정리를 해보겠습니다.

인덱스는 어떤 성능을 올릴 수 있을까?

 

기존 테스트..

인덱스 적용브랜드 Id만 인덱스를 거는 경우는 어떨까? 생각보다 속도는 비슷하게 나오는듯하다.요렇게 나왔는데 이건 안 거는거랑 비슷한거 같다라는 생각이 들었다.그래서 좋아요를 기준으로

b-programmer.tistory.com

인덱스는 다들 알다시피 읽기 성능을 올릴 수 있습니다. 위 글을 확인해보면 2250 ms -> 590.58ms 약 73.75%가 감소했다는것을 확인할 수 있었습니다. 하지만 문제는 이것을 왜 했는지 그거에 대한 논리가 부족하다는점입니다. 아무래도 학습이 주가 되었기 때문에 읽기 속도가 늘었다는 점만 확인되었을 뿐이죠. 그렇다면, 어떻게 표현할 수 있을까요?

이렇게 표현한다면 어떨까요?

특정 조회 API에서 평균 2250ms의 지연이 발생하였고, 실행 계획 분석 결과 Full Table Scan이 발생하고 있었습니다.
필터링과 정렬이 동시에 수행되는 쿼리 특성을 고려하여 인덱스를 설계하였고, 이를 통해 Index Scan 기반으로 실행 계획을 개선했습니다.
그 결과 Latency를 2250ms → 590.58ms로 약 73.75% 감소시킬 수 있었습니다.

그렇다면, 인덱스를 사용하면 어째서 Latency가 감소하나요?

인덱스를 사용하지 않는 경우, 조건에 맞는 데이터를 찾기 위해 테이블의 모든 데이터를 확인하게 됩니다.
이는 테이블 전체를 순차적으로 읽는 Full Table Scan으로 이어집니다.
반면 인덱스를 사용하면, 정렬된 구조를 기반으로 필요한 데이터의 위치를 빠르게 탐색할 수 있습니다.
따라서 전체 데이터를 읽지 않고도 원하는 데이터에 접근할 수 있어, 결과적으로 읽기 성능이 향상되고 Latency가 감소합니다.

그렇다고 해서 인덱스를 건다고 해서 무조건 Latency가 감소하는 것은 아닙니다.

인덱스는 조건에 따라 선택적으로 사용되기 때문에, 선택도가 낮거나 쿼리 패턴과 맞지 않는 경우에는 Full Table Scan이 더 효율적일 수 있습니다. 또한 인덱스가 많아질수록 쓰기 성능 저하와 추가적인 관리 비용이 발생할 수 있습니다.

어떤경우에 성능이 떨어지는지 자세하게 말씀해주세요.

조회 결과가 너무 많은 경우

예를 들어 전체 100만 건 중 70만 건을 읽어야 한다고 해봅시다.
이때 인덱스를 타면 인덱스에서 조건에 맞는 위치를 찾고 다시 실제 데이터 페이지로 이동해서 읽어야 합니다

이 과정을
랜덤 I/O가 많이 발생하는 구조라고 볼 수 있습니다.
반면, 그냥 Full Table Scan은 테이블을 처음부터 끝까지 순서대로 읽습니다. 데이터를 거의 다 봐야 한다면, 오히려 이런 순차 읽기가 더 효율적일 수 있습니다.

예를 들면 이런 경우입니다.

SELECT *
FROM users
WHERE gender = 'M';
 

만약 남성 데이터가 전체의 50% 이상이라면, 인덱스를 타도 절반 이상을 다시 테이블에서 읽어야 하므로 큰 이점이 없습니다.
즉, 조건은 있지만, 실제로는 너무 많은 행을 가져오는 쿼리에서는 인덱스가 큰 효과를 내지 못할 수 있습니다.

선택도가 낮은 컬럼에 인덱스를 건 경우

이건 방금 사례와 연결됩니다. 선택도는 간단히 말하면 이 컬럼이 데이터를 얼마나 잘 구분해 주는가입니다.

예를 들어 성별: 남/여, 상태값: Y/N, 삭제 여부: true/false이런 값은 종류가 적습니다.
즉, 한 값으로 조회했을 때 너무 많은 행이 걸립니다.

예를 들어 is_deleted = false 같은 조건은 대부분의 데이터가 false일 수 있습니다.
그럼 인덱스를 타더라도 거의 전체 데이터를 읽게 됩니다.
그래서 이런 컬럼 단독 인덱스는 기대만큼 성능이 안 나오는 경우가 많습니다.

SELECT *처럼 인덱스만으로 처리가 안 되는 경우

인덱스는 조건 검색에는 도움이 되지만, 실제 필요한 컬럼이 인덱스에 다 들어 있지 않으면 결국 테이블 본문까지 다시 읽어야 합니다.

예를 들어

SELECT *
FROM product
WHERE brand_id = 10;
 
brand_id에 인덱스가 있어도, *를 조회하면 나머지 컬럼들을 가져오기 위해 테이블 페이지를 다시 읽어야 합니다.
즉, 인덱스는 탐색은 빠르게 해주지만 최종적으로 읽어야 할 데이터가 많으면 비용이 커집니다.

반면 이런 경우는 좋습니다.

SELECT brand_id, likes
FROM product
WHERE brand_id = 10
ORDER BY likes DESC;

만약 인덱스가 (brand_id, likes)로 잡혀 있고
조회 컬럼도 그 안에 다 있다면, 테이블을 다시 안 보고 인덱스만 읽어도 됩니다.
이걸 보통 커버링 인덱스 관점에서 설명합니다.

정리하면 인덱스를 썼더라도, 결국 테이블을 다시 많이 읽어야 하면 느려질 수 있습니다.

복합 인덱스 순서가 쿼리와 맞지 않는 경우

복합 인덱스는 아무 순서로나 만들면 되는 게 아닙니다.

예를 들어 인덱스가 (brand_id, likes) 이렇게 잡혀 있다고 해봅시다.
그러면 보통은 brand_id를 먼저 활용한 뒤, 그 안에서 likes를 활용하는 구조가 됩니다.

이때 다음 쿼리는 잘 탈 가능성이 높습니다.

WHERE brand_id = ?
ORDER BY likes DESC
 

그런데 이런 쿼리는 애매할 수 있습니다.

WHERE likes > 100
 

왜냐하면 인덱스의 첫 컬럼인 brand_id를 안 쓰고 있기 때문입니다.
즉, 복합 인덱스는 앞쪽 컬럼부터 맞춰서 써야 효과가 큽니다.

함수나 연산 때문에 인덱스를 못 타는 경우

컬럼에 가공을 해버리면 DB가 기존 인덱스를 그대로 활용하기 어려워집니다.

예를 들어:

WHERE YEAR(created_at) = 2026

이렇게 쓰면 created_at 인덱스가 있어도 컬럼에 함수가 먼저 적용되기 때문에 인덱스를 제대로 못 탈 수 있습니다.

보통은 이렇게 바꾸는 편이 좋습니다.

WHERE created_at >= '2026-01-01'
AND created_at < '2027-01-01'
 
또 비슷하게
WHERE price + 10 > 100

이런 식으로 컬럼에 계산이 들어가도 인덱스 활용이 어렵습니다.
핵심은 인덱스는 원본 값 기준으로 정렬되어 있는데, 쿼리에서 값을 바꿔버리면 그 정렬 구조를 활용하기 어려워진다는 점입니다.

앞에 와일드카드가 붙는 LIKE 검색

이것도 매우 흔합니다.

WHERE name LIKE '%phone'

이 경우는 문자열 앞부분이 고정되어 있지 않기 때문에 B-Tree 인덱스로 어디서부터 찾아야 할지 애매합니다.
반면 이런 경우는 비교적 유리합니다.

WHERE name LIKE 'phone%'
 

앞부분이 정해져 있으면 인덱스 범위 탐색이 가능해집니다.

즉,

  • 'abc%' → 인덱스 활용 가능성 높음
  • '%abc' → 인덱스 활용 어려움
  • '%abc%' → 보통 더 어려움

그래서 contains 검색이 많으면 일반 인덱스보다 풀텍스트 검색이나 별도 검색 엔진을 고민하기도 합니다.

정렬과 인덱스 방향이 맞지 않는 경우

인덱스는 검색뿐 아니라 정렬에도 영향을 줍니다. 그런데 정렬 조건이 인덱스 구조와 안 맞으면 결국 별도 정렬 작업이 필요해집니다.
예를 들어 필터는 brand_id로 하고, 정렬은 created_at인데
인덱스가 (brand_id, likes)라면 created_at 정렬에는 직접 도움이 안 됩니다.
그러면 DB는 조건으로 일부 찾고 결과를 따로 정렬하게 됩니다.
이 정렬 비용이 큰 경우 Latency가 늘어납니다.

즉, 필터링과 정렬을 함께 고려하지 않은 인덱스는 기대보다 효과가 약할 수 있습니다.

쓰기 작업이 많은 테이블

인덱스는 조회를 빠르게 해주지만, INSERT, UPDATE, DELETE 시에는 오히려 부담이 됩니다.
왜냐하면 테이블 데이터만 바꾸는 게 아니라 관련 인덱스도 같이 수정해야 하기 때문입니다.
예를 들어 인덱스가 1개면 한 번 수정할 걸, 인덱스가 5개면 5군데를 더 관리해야 할 수 있습니다.

  • 주문
  • 결제
  • 로그성 이벤트
  • 실시간 트래픽 테이블

처럼 쓰기 비중이 높은 곳에서는 인덱스를 많이 걸수록 전체 처리량이 떨어질 수 있습니다.
즉, 조회 한 번 빠르게 하려다가, 전체 쓰기 성능이 계속 손해 볼 수 있습니다.

업데이트가 잦은 컬럼에 인덱스를 건 경우

예를 들어 view_count, likes, updated_at처럼 값이 자주 바뀌는 컬럼에 인덱스를 걸면 인덱스 재정렬/유지 비용이 자주 발생합니다.
이 경우는 단순 조회뿐 아니라 수정이 반복될 때마다 인덱스도 계속 손봐야 하므로 부담이 커질 수 있습니다.
그래서 자주 바뀌는 컬럼은 정말 필요한 경우가 아니면 신중하게 봐야 합니다.

 

조인에서 인덱스가 어긋나는 경우

조인도 인덱스 영향이 큽니다.
예를 들어 두 테이블을 조인할 때 조인 컬럼에 인덱스가 없으면, 한쪽 데이터를 반복적으로 훑으면서 비용이 커질 수 있습니다.
특히 큰 테이블끼리 조인할 때 조인 키 인덱스가 없으면 성능이 급격히 나빠질 수 있습니다.
반대로 인덱스를 걸었더라도 조인 순서나 필터 조건이 맞지 않으면 기대만큼 개선이 안 될 수도 있습니다.


결국, 인덱스는 항상 Latency를 줄이는 것이 아닙니다.
조회 대상이 너무 많거나, 선택도가 낮은 컬럼에 인덱스를 건 경우에는 효과가 작을 수 있습니다.
또한 함수 사용, 와일드카드 LIKE, 복합 인덱스 순서 불일치처럼 쿼리 패턴이 인덱스 구조와 맞지 않으면 인덱스를 제대로 활용하지 못합니다. 반대로 인덱스가 많아지면 INSERT/UPDATE/DELETE 시 유지 비용이 증가해 전체 성능이 저하될 수도 있습니다.

이제 정리할것은 2가지정도 남은것으로 확인되었습니다.

1. 커버링 인덱스 
2. 함수에는 인덱스가 먹히지 않는 이유 
이 두개는 간단하게 작성하고 넘어가겠습니다.~

커버링 인덱스는 쿼리에 필요한 모든 컬럼을 인덱스만으로 해결하는 것을 뜻합니다.
함수에 인덱스가 먹히지 않는 이유는 인덱스는 원본 값 기준 정렬인데, 함수는 값을 바꿔버리기 때문에 그 정렬을 못 쓴다

결론

인덱스를 사용하면 어떻게 성능이 오르는지 언제 사용하는지에 대해 알아봤습니다. 만약에, 인덱스 관련 질문이 들어온다면, 일단은 다음과 같이 답변을 할것 같습니다.

특정 조회 API에서 평균 2250ms의 지연이 발생하였고, 실행 계획 분석 결과 Full Table Scan이 발생하고 있었습니다.
필터링과 정렬이 동시에 수행되는 쿼리 특성을 고려하여 인덱스를 설계하였고, 이를 통해 Index Scan 기반으로 실행 계획을 개선했습니다.
그 결과 Latency를 2250ms → 590.58ms로 약 73.75% 감소시킬 수 있었습니다.

반응형

'개발' 카테고리의 다른 글

카프카의 설정은 진짜일까?? - offset(1)  (0) 2026.04.08
Batch- Reader  (0) 2026.04.07
배치는 언제 사용이 되어질까?  (0) 2026.04.04
LLM이란 무엇일까?  (1) 2026.04.01
로드벨런서 vs API 게이트웨이  (0) 2026.03.31

댓글

Designed by JB FACTORY