복합 인덱스, 컬럼 순서가 성능을 가른다
전화번호부처럼 생각하면 된다
(last_name, first_name) 복합 인덱스는 성으로 먼저 정렬하고, 같은 성 안에서 이름으로 정렬한 전화번호부와 똑같은 구조입니다. "김"씨를 찾는 건 쉽지만, 이름이 "민준"인 사람을 성 상관없이 찾으려면 전화번호부를 처음부터 끝까지 넘겨야 합니다. B-Tree 복합 인덱스도 마찬가지입니다. 첫 번째 컬럼(leftmost column) 조건이 없으면 인덱스 안에서 어디부터 봐야 할지 알 수 없어서, 인덱스가 있어도 못 씁니다.
CREATE INDEX idx_orders_customer_status ON orders (customer_id, status);
-- 인덱스를 그대로 탄다: customer_id 가 leftmost
EXPLAIN SELECT * FROM orders WHERE customer_id = 42 AND status = 'shipped';
-- 인덱스를 못 탄다: status 만으로는 시작점을 못 찾는다 (PG18 이전 기준, 뒤에서 다룸)
EXPLAIN SELECT * FROM orders WHERE status = 'shipped';컬럼 순서를 정하는 두 가지 기준
실무에서 가장 자주 나오는 질문이 "어떤 컬럼을 앞에 둬야 하나요"입니다. 저는 두 가지 기준을 순서대로 적용합니다.
첫째, 등호 조건으로 자주 쓰이는 컬럼을 앞에 둔다. 범위 조건(>, <, BETWEEN)이 걸린 컬럼 뒤에 오는 컬럼은 인덱스 활용이 끊깁니다. (created_at, customer_id) 순서로 인덱스를 만들면 created_at > '2026-01-01' AND customer_id = 42 쿼리에서 customer_id 조건은 인덱스 탐색 범위를 못 줄이고, 스캔 범위 안에서 하나하나 걸러내는 필터로만 쓰입니다. 반대로 (customer_id, created_at) 순서면 customer_id = 42 로 정확히 시작점을 찾고, 그 안에서 created_at 범위로 리프를 훑으면 됩니다.
-- 나쁜 순서: 범위 조건이 앞
CREATE INDEX idx_a ON orders (created_at, customer_id);
-- 좋은 순서: 등호 조건이 앞
CREATE INDEX idx_b ON orders (customer_id, created_at);둘째, 카디널리티(고유값 개수)가 높은 컬럼을 앞에 두는 것이 유리한 경우가 많습니다. status 컬럼이 pending/shipped/cancelled 세 가지뿐이라면 이 컬럼만으로는 행을 거의 못 좁힙니다. customer_id 처럼 고유값이 수만 개인 컬럼을 앞에 둬야 인덱스 탐색 범위가 확 줄어듭니다. 다만 이 규칙은 "대부분의 쿼리가 두 컬럼을 함께 조건절에 쓴다"는 전제에서만 유효합니다. 실제로는 쿼리 패턴이 우선입니다 — status 단독으로 걸리는 쿼리가 많다면 카디널리티가 낮아도 앞에 둬야 할 수 있습니다.
인덱스 하나로 여러 쿼리를 커버하기
(customer_id, status, created_at) 처럼 3개 이상 컬럼을 넣을 때는, 이 인덱스가 어떤 접두사(prefix) 조합까지 커버하는지 생각해야 합니다.
| 조건절 | 이 인덱스를 탈 수 있는가 |
|---|---|
customer_id = ? |
예 (첫 컬럼만) |
customer_id = ? AND status = ? |
예 (접두사 2개) |
customer_id = ? AND status = ? AND created_at > ? |
예 (전체) |
customer_id = ? AND created_at > ? |
status 부분은 필터로만 쓰임, 범위 탐색은 됨 |
status = ? |
아니오 (leftmost 없음) |
인덱스를 쿼리마다 따로 만들기 전에, 기존 복합 인덱스의 접두사로 커버되는지부터 확인하세요. (customer_id, status, created_at) 인덱스가 있는데 (customer_id, status) 인덱스를 따로 만드는 건 대부분 낭비입니다. 접두사가 이미 그 역할을 합니다. 반대로 순서만 다른 인덱스, 예를 들어 (status, customer_id) 는 완전히 다른 인덱스이므로 필요하다면 따로 만들어야 합니다.
인덱스는 쓰기(INSERT/UPDATE/DELETE) 때마다 갱신 비용이 붙는다는 것도 잊지 마세요. 컬럼 조합을 늘릴수록 조회는 빨라지지만 쓰기는 느려집니다. "혹시 몰라서" 만드는 인덱스가 가장 흔한 실수입니다.
다음 장에서 다룰 PostgreSQL 18의 스킵 스캔은 방금 설명한 "leftmost 컬럼이 없으면 못 쓴다"는 규칙에 예외를 만듭니다. 다만 만능은 아니라서, 이 장에서 정리한 원칙이 여전히 인덱스 설계의 기본입니다.