인덱스는 데이터를 빨리 찾기 위해, 미리 정렬해서 따로 저장해둔 자료구조다.
도서관에 비유하면 이렇다.
책을 찾으려면 두 가지 방법이 있다.
1. 1층부터 5층까지 모든 책장을 한 권씩 다 뒤진다 → 풀 테이블 스캔
2. 검색 컴퓨터로 위치를 먼저 찾고, 그곳으로 간다 → 인덱스 조회
당연히 2번이 빠르다. 인덱스는 "검색 컴퓨터를 만들어두는 작업"이라고 생각하면 된다.
DB가 SELECT 쿼리를 처리할 때 데이터를 읽어오는 방식은 크게 4가지다. 빠른 순서로 정리하면 이렇다.
테이블의 모든 행을 처음부터 끝까지 읽는 방식이다.
도서관 비유: 모든 책장의 모든 책을 한 권씩 다 꺼내본다. 1만 권을 다 보는 것.
인덱스가 없거나, 있어도 활용할 수 없을 때 이 방식이 쓰인다. 또는 옵티마이저가 "어차피 거의 모든 행을 가져와야 하니 그냥 다 읽는 게 빠르다"고 판단할 때도 발생한다.
인덱스의 처음부터 끝까지 다 훑는 방식이다.
도서관 비유: 검색 컴퓨터에 키워드를 입력하지 않고, 전체 도서 목록을 처음부터 끝까지 스크롤해서 본다.
테이블 자체보다 인덱스가 작기 때문에, 풀 테이블 스캔보다는 빠르다. 하지만 정상적인 인덱스 활용에 비하면 여전히 비효율적이다.
가장 일반적인 인덱스 활용 방식이다. 두 단계로 진행된다.
도서관 비유: 검색 컴퓨터로 "해리포터 → 3층 F구역"을 알아낸 뒤, 실제로 3층까지 가서 책을 꺼낸다.
여기서 2단계, 즉 "테이블로 다시 가는 행위"를 테이블 룩업 또는 북마크 룩업이라고 부른다. 룩업이 많아지면 디스크 I/O 비용이 커져서 느려진다.
쿼리에 필요한 모든 컬럼이 이미 인덱스 안에 들어있는 경우, 테이블 룩업 자체를 생략할 수 있다.
도서관 비유: 검색 컴퓨터에 "위치, 작가, 출판년도"가 다 표시돼 있어서 굳이 책장까지 안 가도 답이 나온다.
이런 인덱스를 커버링 인덱스라고 한다. 4가지 방식 중 가장 빠르다.
-- 인덱스: (user_id, name, email)
SELECT name, email FROM users WHERE user_id = 100;
-- → 인덱스에 name, email이 다 있으니 테이블 안 봐도 됨
복합 인덱스는 여러 컬럼을 묶어서 만든 인덱스다.
예를 들어, 아래와 같은 복합 인덱스를 생성한다고 하면
CREATE INDEX idx_user_created ON orders(user_id, created_at);
1순위로
user_id기준 정렬, 같은user_id안에서 2순위로created_at기준 정렬하여 인덱싱한다는 의미다.
비유하자면 전화번호부와 같다. 성으로 먼저 정렬되고, 같은 성을 가진 사람들끼리 다시 이름순으로 정렬된다.
전화번호부:
김민수
김지영
김철수
박서준
박혜수
이 구조에서 어떤 검색이 가능할까?
이걸 SQL로 옮기면 이렇다.
-- 인덱스: (user_id, created_at)
-- O 인덱스 잘 활용
SELECT * FROM orders WHERE user_id = 100;
-- O 정렬도 공짜 (같은 user_id 안에서 이미 시간순)
SELECT * FROM orders WHERE user_id = 100 ORDER BY created_at DESC LIMIT 10;
-- X 인덱스 활용 못 함
SELECT * FROM orders WHERE created_at > '2025-01-01';
위 패턴에서 발견할 수 있는 규칙이 "왼쪽 컬럼부터 순서대로 써야 인덱스가 효율적으로 동작한다"는 원칙이다. 흔히 Leftmost Prefix Rule이라고 부른다.
이 규칙 때문에 복합 인덱스를 설계할 때는 첫 번째 컬럼이 항상 조건으로 들어온다는 전제로 만들어야 한다. 첫 번째 컬럼 없이 두 번째 컬럼만으로 조회하면, 앞서 설명한 인덱스 풀 스캔으로 떨어지거나 풀 테이블 스캔으로 떨어진다.
카디널리티(Cardinality)는 컬럼 값의 다양성을 의미한다.
gender 컬럼: M / F 두 가지 → 카디널리티 낮음email 컬럼: 사용자마다 거의 다름 → 카디널리티 높음일반적으로 카디널리티가 높은 컬럼을 첫 번째에 두는 것이 유리하다. 검색 결과를 더 좁게 좁힐 수 있기 때문이다.
-- (board_id, user_id) vs (user_id, board_id)
-- 게시판이 10개, 유저가 10만 명일 때
-- → user_id를 앞에 두는 게 일반적으로 유리
다만 절대적인 규칙은 아니다. 실제 쿼리 패턴에 따라 달라진다. "항상 board_id로만 조회한다면" board_id를 앞에 둬야 한다.
인덱스는 공짜가 아니다.
그래서 인덱스는 실제로 자주 사용되는 쿼리 패턴을 분석한 뒤에 만들어야 한다. "혹시 모르니 일단 만들자"는 좋지 않은 접근이다.
| 방식 | 어디를 읽는가 | 속도 |
|---|---|---|
| 풀 테이블 스캔 | 테이블 전체 | 가장 느림 |
| 인덱스 풀 스캔 | 인덱스 전체 | 중간 |
| 인덱스 액세스 + 테이블 룩업 | 인덱스 일부 + 테이블 일부 | 빠름 |
| 커버링 인덱스 | 인덱스 일부만 | 매우 빠름 |
복합 인덱스의 핵심은 계층적 정렬과 Leftmost Prefix Rule이다. 인덱스를 설계할 때는 카디널리티와 실제 쿼리 패턴을 함께 고려해야 한다.