LabHub

데이터베이스 개념 · 인덱스 · 이론

인덱스 — 만들었는데 안 타는 이유

LabHub 에서 이어서 보기

한 줄 요약

인덱스는 값으로 정렬된 별도의 자료구조이고, 옵티마이저가 그것을 쓰지 않는 것은 대개 옳은 판단이며, 옳지 않은 소수의 경우는 원인이 정해져 있다.

왜 이게 필요했나

조건에 맞는 행을 찾으려면 원칙적으로 표를 다 읽어야 한다. 100만 행에서 한 행을 찾으려고 100만 행을 읽는 것은 낭비다. 책의 색인처럼 값과 위치를 정렬해 둔 보조 구조가 있으면 몇 번의 비교로 위치를 알아낼 수 있다. 이것이 B 트리 인덱스다.

어떻게 동작하나

B 트리는 값으로 정렬된 균형 트리다. 정렬되어 있으므로 등호 탐색뿐 아니라 범위 탐색과 정렬 요구도 처리할 수 있다. 반대로 말하면 컬럼에 무언가를 씌우는 순간 그 정렬은 쓸모없어진다.

-- 안 탄다: 컬럼에 함수가 걸렸다SELECT * FROM orders WHERE date(created_at) = '2026-07-26';-- 탄다: 범위 조건으로 바꿔 컬럼을 그대로 둔다SELECT * FROM ordersWHERE created_at >= '2026-07-26' AND created_at < '2026-07-27';

같은 부류로 타입이 안 맞아 컬럼 쪽이 변환되는 암묵적 캐스팅, 그리고 시작점을 고정할 수 없는 LIKE '%kim%' 이 있다. 실행 계획에서 컬럼 이름에 캐스팅 표기가 붙어 있거나 Filter 줄에 함수 호출이 보이면 이 문제다.

복합 인덱스에는 선두 컬럼 규칙이 있다. (a, b, c) 인덱스는 a 로 정렬하고 a 가 같은 것끼리 b 로, b 도 같으면 c 로 정렬한 구조다. 전화번호부를 성으로 정렬하고 같은 성 안에서 이름으로 정렬한 것과 같아서, 성을 모르는 채로 이름만 갖고는 찾을 수 없다. 컬럼 순서의 원칙은 등호 조건 컬럼을 앞에, 범위 조건 컬럼을 뒤에 두는 것이다. 범위 조건이 걸린 컬럼 뒤의 컬럼들은 탐색에 쓰이지 못하고 필터로만 동작하기 때문이다.

여기서 가장 중요한 전제를 짚어야 한다. 옵티마이저가 인덱스를 쓰지 않는 것은 대개 옳다. 인덱스 스캔은 인덱스 페이지를 읽은 뒤 매칭된 행마다 힙 페이지를 랜덤하게 방문해야 한다. 반환할 행이 전체의 상당 비율이면 결국 표 전체를 랜덤 순서로 읽는 꼴이 되어 순차 스캔보다 느려진다. 실제로 enable_seqscan = off 로 강제해 보면 231ms 짜리 쿼리가 1094ms 로 네 배 느려지는 일이 흔하다.

경계를 정하는 변수는 세 가지다. 랜덤 접근의 상대 비용 설정(random_page_cost), 인덱스 순서와 힙 순서의 일치도(상관도), 그리고 반환 컬럼이 인덱스에 다 들어 있는지 여부다. 마지막 것을 만족시키려고 일부러 컬럼을 얹은 것이 커버링 인덱스이고, 이때 힙을 방문하지 않는 Index Only Scan 이 가능해진다.

현장에서 만나는 모습

부분 인덱스는 여러 문제를 한 번에 푸는 도구다. 전체 주문 중 몇 퍼센트뿐인 pending 상태만 조회하는 워크로드라면 WHERE status = 'pending' 을 붙인 인덱스가 42MB 에서 312KB 로 줄어든다. 크기만 주는 것이 아니라 조건에 맞지 않는 행의 INSERT 와 UPDATE 가 이 인덱스를 건드리지 않으므로 쓰기 비용도 줄어든다.

그리고 인덱스의 반대편 비용을 잊으면 안 된다. 모든 INSERT 와 DELETE 는 그 표의 모든 인덱스를 갱신한다. 인덱스가 늘면 계획 후보도 늘어 계획 수립 시간까지 늘어난다. 쓰지 않는 인덱스를 주기적으로 걷어내야 하는 이유다.

다음 실습에서 할 것

실제 표에 여러 종류의 인덱스를 만들어 보고, 실행 계획이 어떻게 달라지는지 파일로 남긴다. 특히 강제로 인덱스를 태웠을 때 오히려 느려지는 경우를 직접 측정한다.