1. 복합 인덱스
- 지금까지 우리는 하나의 컬럼으로 구성된 **단일 인덱스(Single-column Index)**에 대해 주로 알아보았습니다.
- Index 문서를 참고해주세요.
- Index Optimizer 문서를 참고해주세요.
- 하지만 실제 쇼핑몰 운영 환경에서는 여러 조건을 조합해서 데이터를 검색하는 경우가 훨씬 더 많습니다.
- 예를 들어, "카테고리가 '전자기기'인 상품들 중에 가격이 100,000원 이상인 상품을 보여줘"와 같은 요구사항은 매우 흔합니다.
SELECT * FROM items
WHERE category = '전자기기' AND price >= 100000;
- 이런 다중 조건 쿼리의 성능을 최적화하기 위해 사용하는 것이 바로 복합 인덱스(Composite Index) 또는 **다중 컬럼 인덱스(Multi-column Index)**입니다.
- 복합 인덱스는 이름 그대로 두 개 이상의 컬럼을 묶어서 하나의 인덱스로 만드는 것입니다.
- Index Optimizer 문서에서
(price, item_name)인덱스를 만들었던 것을 기억하시나요? 그것이 바로 복합 인덱스입니다.
1.1 왜 컬럼 순서가 중요할까?
- 하지만 복합 인덱스를 제대로 사용하려면 한 가지 매우 중요한 규칙을 이해해야 합니다.
- 바로 **'컬럼의 순서'**입니다.
- 인덱스를 어떤 컬럼 순서로 만드느냐에 따라 쿼리 성능이 하늘과 땅 차이로 달라질 수 있습니다.
복합 인덱스의 동작 원리
- 복합 인덱스의 동작 원리는 우리가 실생활에서 사용하는 '전화번호부'나 '국어사전'과 똑같습니다.
- 전화번호부: '성(Last Name)'으로 먼저 정렬된 후, 같은 성 안에서 '이름(First Name)'으로 다시 정렬됩니다.
- 국어사전: '첫 번째 글자'로 먼저 정렬된 후, 같은 첫 글자로 시작하는 단어들끼리 '두 번째 글자'로 다시 정렬됩니다.
items테이블에(category, price)순서로 복합 인덱스를 만들었다고 상상해 보겠습니다.- 이 인덱스는 내부적으로 다음과 같이 정렬됩니다.
category를 기준으로 먼저 정렬합니다. ('도서', '생활용품', '전자기기', '패션', '헬스/뷰티' 순서)- 같은
category내에서는price를 기준으로 다시 정렬합니다.
idx_items_category_price 인덱스 예시
| category | price | item_id |
|---|---|---|
| 도서 | 18000 | 21 |
| 도서 | 22000 | 25 |
| 도서 | 28000 | 15 |
| 도서 | 30000 | 4 |
| 도서 | 35000 | 11 |
| 생활용품 | 5000 | 16 |
| 생활용품 | 15000 | 5 |
| 생활용품 | 40000 | 9 |
| 생활용품 | 60000 | 19 |
| 전자기기 | 80000 | 3 |
| 전자기기 | 90000 | 14 |
| 전자기기 | 120000 | 1 |
| 전자기기 | 200000 | 24 |
| 전자기기 | 250000 | 7 |
| 전자기기 | 300000 | 13 |
| 전자기기 | 350000 | 23 |
| 전자기기 | 450000 | 2 |
| 전자기기 | 800000 | 20 |
| 전자기기 | 1500000 | 10 |
| 패션 | 45000 | 18 |
| 패션 | 70000 | 6 |
| 패션 | 95000 | 8 |
| 패션 | 180000 | 17 |
| 헬스/뷰티 | 20000 | 12 |
| 헬스/뷰티 | 25000 | 22 |
- 이 구조를 보면 왜 컬럼 순서가 중요한지 감이 올 것입니다.
검색 효율성 비교
category로 검색: 매우 효율적입니다. 인덱스의 앞부분만 보고 빠르게 찾을 수 있습니다. (예: '전자기기' 섹션으로 바로 점프)category와price로 검색: 역시 매우 효율적입니다. '전자기기' 섹션을 찾은 뒤, 그 안에서price순으로 정렬된 데이터를 탐색하면 됩니다.price만으로 검색: 매우 비효율적입니다. 전화번호부에서 성은 모르고 이름만으로 찾는 것과 같습니다.price값은 각category섹션마다 흩어져 있기 때문에, 인덱스 전체를 다 훑어봐야 합니다. 이런 경우 옵티마이저는 차라리 풀 테이블 스캔을 선택할 수도 있습니다.
price만으로 검색할 수 없는 이유
각각의 카테고리 안에서는 price가 항상 정렬된 상태를 유지합니다. 1차 정렬의 기준 안에서 2차 정렬은 항상 정렬된 상태를 유지합니다.
하지만 price만으로 검색하면 왜 인덱스를 사용하기 어려운지 자세히 알아보겠습니다.
idx_items_category_price 인덱스 예시에서 price 컬럼만 딱 분류해서 보면, 가격순으로 정렬이 되어 있는 것 같아 보이지만 중간에 정렬이 다시 틀어집니다. 결과적으로 price 컬럼만 보면 정렬이 되어 있지 않습니다. 결과적으로 1차 정렬의 기준 없이 2차 정렬의 값만 가지고는 정렬된 상태를 유지할 수 없습니다.
1.2 인덱스 왼쪽 접두어 규칙
- 이처럼 복합 인덱스는 첫 번째 컬럼을 기준으로 정렬된 상태에서만 제 역할을 할 수 있습니다.
- 이를 **인덱스 왼쪽 접두어 규칙(Index Left-Prefix Rule)**이라고 합니다.
- 인덱스를
(A, B, C)순서로 생성했다면,WHERE조건에 다음과 같이 사용될 때 효율적입니다.(A)(A, B)(A, B, C)- 하지만
(B),(C),(B, C)와 같이 첫 번째 기준인A가 빠진 조건으로는 인덱스를 제대로 활용할 수 없습니다.
2. 복합 인덱스 대원칙
- 복합 인덱스를 설계하고 사용할 때는 다음 세 가지 대원칙을 반드시 기억해야 합니다.
- 인덱스는 순서대로 사용하라! (왼쪽 접두어 규칙)
- 등호(=) 조건은 앞으로, 범위 조건(
<,>)은 뒤로! - 정렬(ORDER BY)도 인덱스 순서를 따르라!
- 복합 인덱스의 대원칙을 순서대로 하나씩 배워보겠습니다.
3. 복합 인덱스 준비 과정
SHOW INDEX FROM items;
- 실행 결과
idx로 시작하는 모든 인덱스를 제거하겠습니다. - 인덱스는 아쉽게도
IF EXISTS구문이 없습니다. 하나하나를 직접 제거해야 합니다.
DROP INDEX idx_items_item_name ON items;
DROP INDEX idx_items_price_name ON items;
DROP INDEX idx_items_price ON items;
DROP INDEX idx_items_price_category_temp ON items;
DROP INDEX idx_items_category_price ON items;
- 이전에 만들어진
idx로 시작하는 인덱스는 모두 제거해야 합니다. - 실습을 위해
(category, price)순서로 복합 인덱스를 생성하겠습니다.
CREATE INDEX idx_items_category_price ON items (category, price);
- 만들어진 인덱스를 최종 확인하겠습니다.
SHOW INDEX FROM items;
- 결과는 반드시 다음과 같이
idx로 시작하는 인덱스가idx_items_category_price하나만 추가되어야 합니다.
| Table | Non_unique | Key_name | Seq_in_index | Column_name | Cardinality |
|---|---|---|---|---|---|
| items | 0 | PRIMARY | 1 | item_id | 25 |
| items | 1 | fk_items_sellers | 1 | seller_id | 10 |
| items | 1 | idx_items_category_price | 1 | category | 5 |
| items | 1 | idx_items_category_price | 2 | price | 25 |
idx_items_category_price는 하나의 복합 인덱스입니다.Seq_in_index는 복합 인덱스의 컬럼 순서를 나타냅니다.idx_items_category_price의 경우 1번은category, 2번은price순서로 만들어진 것을 확인할 수 있습니다.
4. 복합 인덱스 성공 예제1: category 사용
- 복합 인덱스의 첫 번째 컬럼만
WHERE절에 사용하는 경우입니다. - 이는 인덱스 왼쪽 접두어 규칙을 가장 잘 활용하는 기본적인 예시입니다.
- "카테고리가 '전자기기'인 모든 상품을 찾아보자."
EXPLAIN SELECT * FROM items WHERE category = '전자기기';
실행 결과
| id | type | key | rows | filtered | Extra |
|---|---|---|---|---|---|
| 1 | ref | idx_items_category_price | 10 | 100.00 | NULL |
- 실행 계획을 분석해 보겠습니다.
type: ref
category가 '전자기기'인 조건을 만족하는 데이터를 찾기 위해 동등 비교(=)나JOIN을 사용하고, 인덱스를 효율적으로 사용했음을 의미합니다.- 쉽게 이야기해서 Key 값 하나를 딱 집어서 찾은 것입니다.
key: idx_items_category_price
- 우리가 만든 복합 인덱스가 사용되었습니다.
rows: 10
- 옵티마이저는 '전자기기' 카테고리에 해당하는 상품이 약 10건 있을 것으로 정확히 예측하고, 그 부분만 탐색합니다.
- 이는
(category, price)로 정렬된 인덱스에서category가 '전자기기'인 첫 번째 위치를 빠르게 찾아낸 뒤, '전자기기'가 끝날 때까지 인덱스를 순차적으로 읽기만 하면 되므로 매우 효율적입니다. - 전화번호부에서 '김'씨를 찾는 것과 같이, 'ㄱ' 섹션으로 바로 이동해서 '김'씨가 끝날 때까지 읽는 것과 같습니다.
5. 복합 인덱스 성공 예제2: category, price 함께 사용
- 이번에는 복합 인덱스를 구성하는 모든 컬럼을
WHERE절에 사용하는 경우입니다. - 인덱스의 필터링 능력을 최대로 활용하는 상황입니다.
- "카테고리가 '전자기기'이면서, 가격이 정확히 120,000원인 상품을 찾아보자."
EXPLAIN SELECT * FROM items WHERE category = '전자기기' AND price = 120000;
실행 결과
| id | type | key | rows | filtered | Extra |
|---|---|---|---|---|---|
| 1 | ref | idx_items_category_price | 1 | 100.00 | NULL |
- 실행 계획은 훨씬 더 효율적으로 보입니다.
type: ref
- 두 개의 컬럼 조건(
category,price)을 모두 만족하는 데이터를 찾기 위해 인덱스를 사용했습니다.
rows: 1
-
탐색할 행의 수가 단
1개로 예측됩니다. -
데이터베이스는 먼저
category가 '전자기기'인 섹션을 찾고, 그 안에서price가120000인 지점을 탐색합니다. -
'전자기기' 섹션 내부는 이미
price순으로 정렬되어 있으므로, 원하는 데이터를 매우 빠르게 특정할 수 있습니다. -
이는 전화번호부에서 '김수로'라는 사람을 찾는 것과 같습니다. '김'씨 섹션을 찾고, 그 안에서 '수로'라는 이름을 찾아내는 과정은 매우 빠릅니다.
-
이처럼 인덱스를 구성하는 모든 컬럼을 조건으로 사용하면 가장 효과적으로 데이터를 필터링할 수 있습니다.