Ver2.5 데이터베이스 인덱싱: 속도, 그 하나를 위해 모든 걸 불태울 텐가? 🏎️

데이터베이스 인덱싱: 속도, 그 하나를 위해 모든 걸 불태울 텐가? 🏎️
DB 성능 튜닝의 알파이자 오메가, 인덱스. 하지만 그 강력함 뒤에 숨겨진 대가를 제대로 알고 있니?
오늘은 너의 DB를 F1 머신으로 만들 수도, 혹은 고철 덩어리로 만들 수도 있는 인덱스의 모든 것을 파헤쳐 보자.
안녕, 개발자 친구! 혹시 '느리다'는 말에 PTSD 있어?
우리 같은 개발자에게 '느려요'라는 피드백만큼 심장을 철렁하게 만드는 말이 또 있을까?
특히 내가 만든 서비스가 사용자의 클릭 한 번에 세월아 네월아 로딩만 돌고 있다면... 아, 상상만 해도 끔찍하다. 😱
이 느려터진 속도의 주범은 높은 확률로 **데이터베이스(DB)**에 있어. 사용자가 늘고 데이터가 쌓일수록, 우리가 원하는 정보를 찾아오는 여정은 점점 더 험난해지지. 수백만, 수천만 건의 데이터 속에서 단 하나의 레코드를 찾기 위해 DB가 땀 뻘뻘 흘리는 모습을 상상해 봐. 그게 바로 우리가 마주하는 '느림'의 실체야.
이때, 구원투수처럼 등판하는 개념이 바로 **'인덱스(Index)'**야.
마치 낡은 패밀리카에 니트로 부스터를 장착하는 것처럼, 인덱스는 DB 쿼리 속도를 극적으로 끌어올리는 마법 같은 존재로 알려져 있지. 많은 개발자들이 "느리면 인덱스 걸면 돼"라는 말을 주문처럼 외우고 다니는 이유이기도 해.
하지만 세상에 공짜 점심은 없다는 말, 들어봤지?
인덱스도 마찬가지야. F1 머신이 엄청난 속도를 내는 대신, 기름을 퍼마시고 타이어는 몇 바퀴 돌지도 못하고 닳아 없어지는 것처럼, 인덱스 역시 엄청난 속도를 제공하는 대가로 우리가 모르는 사이 많은 것을 희생시키고 있어.
그래서 오늘 이 글의 주제는 바로 이거야.
"과연 우리는 속도라는 달콤한 열매를 위해 모든 것을 희생해도 괜찮은 걸까?"
인덱스가 정확히 무엇인지, 어떤 원리로 작동하는지부터 시작해서, 우리가 무심코 남발한 인덱스가 어떻게 우리 뒤통수를 치는지, 그리고 어떻게 하면 똑똑하게 인덱스를 활용해서 속도와 안정성이라는 두 마리 토끼를 모두 잡는 '밸런스형 드라이버'가 될 수 있는지. 그 모든 여정을 지금부터 함께 떠나보자고. 준비됐어? Let's go! 🚀
인덱스가 뭔데? 도서관 사서에게 배우는 DB의 지혜
자, 인덱스를 본격적으로 파헤치기 전에 아주 간단한 비유를 하나 들어볼게.
네가 지금 수천 페이지짜리 엄청나게 두꺼운 기술 서적에서 'B-Tree'에 대한 내용을 찾아야 한다고 상상해 봐. 어떻게 할 거야?
설마 책 첫 페이지부터 마지막 페이지까지 한 장 한 장 넘겨가면서 'B-Tree'라는 단어를 찾진 않겠지? 만약 그런다면 아마 하루가 꼬박 걸릴지도 몰라. 이게 바로 인덱스가 없는 DB가 데이터를 찾는 방식, 즉 **'풀 테이블 스캔(Full Table Scan)'**이야. 말 그대로 테이블 전체를 샅샅이 훑는 무식한 방법이지.
대부분의 사람들은 책 맨 뒤에 있는 **'찾아보기(Index)'**를 펼칠 거야. 거기서 'B' 섹션으로 가서 'B-Tree' 항목을 찾으면 'p.345'라고 적혀있는 걸 발견할 수 있겠지. 그럼 우리는 곧장 345페이지로 점프해서 원하는 정보를 얻을 수 있어. 책 전체를 훑는 것에 비해 비교도 안 될 정도로 빠르지. 이게 바로 **'인덱스'**의 핵심 원리야.
데이터베이스의 인덱스는 이 책의 '찾아보기'처럼, 특정 컬럼(들)의 값과 그 값이 저장된 레코드의 물리적 주소를 키-값 쌍으로 저장해 둔 별도의 데이터 구조야. DB는 쿼리 요청이 들어오면, 무작정 데이터 파일을 뒤지는 게 아니라 이 '인덱스'라는 잘 정돈된 목차부터 먼저 찾아보는 거지.
인덱스의 심장, B-Tree (Balanced-Tree)
그렇다면 DB는 이 '찾아보기'를 어떤 형태로 만들고 관리할까? 여러 가지 방식이 있지만, 관계형 데이터베이스(RDBMS)에서 가장 보편적으로 사용하는 구조는 바로 **B-Tree(Balanced-Tree)**야. 이름 그대로 '균형 잡힌 트리' 구조지.
B-Tree는 데이터를 항상 정렬된 상태로 유지하면서, 어떤 값을 찾더라도 트리의 루트(뿌리)에서 리프(잎)까지 가는 경로의 길이가 거의 같도록 스스로 균형을 맞추는 아주 똑똑한 자료구조야. 덕분에 데이터가 얼마나 많든 간에 매우 빠른 속도(로그 시간 복잡도, O(log N))로 검색, 삽입, 삭제가 가능해.
말로만 하면 어려우니, 그림으로 한번 볼까?
위 그림을 봐. 우리가 숫자 '25'를 찾는다고 해보자.
- 루트 노드(Root Node)에서 시작해. '50'이 기준이야. 25는 50보다 작으니까 왼쪽으로 가.
- 브랜치 노드(Branch Node)에 도착했어. 여기엔 '20'과 '35'가 있네. 25는 20보다 크고 35보다 작으니까, 두 값 사이를 가리키는 포인터를 따라가.
- 리프 노드(Leaf Node)에 도착! 이 노드는 21부터 34까지의 값들을 담당하고 있어. 여기서 '25'를 찾으면, 그 값 옆에 실제 데이터가 저장된 주소(포인터)가 함께 들어있는 거지. DB는 이 주소를 가지고 디스크에서 원본 데이터를 한 번에 쏙 뽑아오는 거야.
만약 데이터가 100만 건이 있다고 해도, 이런 트리 구조라면 대략 3~4번의 탐색만으로 원하는 데이터에 도달할 수 있어. 100만 번을 다 뒤지는 풀 테이블 스캔과는 비교가 안 되지. 이게 바로 인덱스가 마법을 부리는 원리야.
"와우!" 소리 나오는 인덱스의 위력: 직접 느껴보기
백문이 불여일견. 이론은 이제 충분히 알았으니, 인덱스가 있고 없고의 차이가 실제 쿼리에서 얼마나 극적인지 직접 눈으로 확인해 보자. 개발자라면 `EXPLAIN`이라는 명령어를 통해 DB가 어떤 계획으로 쿼리를 실행하는지 엿볼 수 있는데, 이걸 활용해 볼 거야.
여기 1,000만 명의 회원을 가진 가상의 `users` 테이블이 있다고 가정해 보자.
CREATE TABLE users (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
username VARCHAR(50) NOT NULL,
email VARCHAR(100) UNIQUE NOT NULL,
created_at DATETIME
);
상황 1: 인덱스 없는 지옥의 풀 스캔
자, 이 상태에서 특정 `username`을 가진 유저를 찾아보자. 아직 `username` 컬럼에는 아무런 인덱스가 없어.
EXPLAIN SELECT * FROM users WHERE username = 'jaenung_master';
이 쿼리를 실행하면, MySQL 기준으로 대략 이런 결과를 보게 될 거야.
+----+-------------+-------+------------+------+---------------+------+---------+------+----------+----------+-------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------+------------+------+---------------+------+---------+------+----------+----------+-------------+
| 1 | SIMPLE | users | NULL | ALL | NULL | NULL | NULL | NULL | 10000000 | 10.00 | Using where |
+----+-------------+-------+------------+------+---------------+------+---------+------+----------+----------+-------------+
다른 건 다 몰라도 돼. 딱 두 가지만 봐.
- **type: ALL**: 이게 바로 '풀 테이블 스캔'을 의미해. DB가 "나 그냥 테이블 처음부터 끝까지 다 뒤져볼게"라고 선언하는 거야.
- **rows: 10000000**: DB가 이 쿼리를 실행하기 위해 몇 개의 행을 살펴봐야 할지 예측한 값이야. 테이블 전체 행의 수인 1,000만 개가 그대로 찍혔지.
결과는? 아마 서버는 한동안 멈춘 것처럼 보일 거고, 운이 나쁘면 타임아웃 에러를 뱉어낼 수도 있어. 사용자는 무한 로딩에 지쳐 떠나가겠지.
상황 2: 인덱스 장착! F1 머신으로 변신
이제 이 끔찍한 상황을 해결하기 위해 `username` 컬럼에 인덱스를 걸어보자. 마법의 주문은 간단해.
CREATE INDEX idx_users_username ON users (username);
이 명령어를 실행하면 DB는 백그라운드에서 `username` 컬럼의 값들을 기준으로 B-Tree 인덱스를 생성하기 시작해. 데이터 양에 따라 시간이 좀 걸릴 수 있어. 자, 이제 인덱스가 모두 만들어졌어. 아까와 똑같은 쿼리를 다시 실행해 보자.
EXPLAIN SELECT * FROM users WHERE username = 'jaenung_master';
결과는 어떻게 바뀌었을까?
+----+-------------+-------+------------+------+--------------------+--------------------+---------+-------+------+----------+-------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------+------------+------+--------------------+--------------------+---------+-------+------+----------+-------+
| 1 | SIMPLE | users | NULL | ref | idx_users_username | idx_users_username | 202 | const | 1 | 100.00 | NULL |
+----+-------------+-------+------------+------+--------------------+--------------------+---------+-------+------+----------+-------+
결과가 완전히 달라졌지? 다시 두 가지만 집중해서 보자.
- **type: ref**: `ALL`이 `ref`로 바뀌었어. 이건 인덱스를 사용해서 동등 조건(`=`)으로 검색했다는 의미야. 풀 스캔이 아니라 아주 효율적인 탐색을 했다는 증거지.
- **rows: 1**: DB가 단 *1개*의 행만 살펴보면 된다고 예측하고 있어. B-Tree를 통해 'jaenung_master'라는 값을 가진 행을 바로 찾아냈다는 뜻이야.
1,000만 번의 작업을 해야 했던 쿼리가 단 한 번의 작업으로 끝났어. 속도는 수백, 수천 배 빨라졌을 거야. 아마 0.001초 만에 결과가 나왔을걸? 이게 바로 인덱스의 위력이야. "와우!" 소리가 절로 나오지 않아?
잠깐! `PRIMARY KEY`와 `UNIQUE`도 인덱스야!
우리가 테이블을 만들 때 `PRIMARY KEY` (기본 키)나 `UNIQUE` 제약조건을 걸잖아? 이것들도 사실은 인덱스야. DB는 기본 키에 대해서는 자동으로 '클러스터형 인덱스'를, `UNIQUE` 컬럼에 대해서는 '유니크 인덱스'를 생성해. 그래서 `id` 값으로 조회하는 건 처음부터 빨랐던 거야!
인덱스의 어두운 그림자: 속도를 위해 네가 치르는 대가
자, 지금까지 인덱스의 눈부신 활약상을 봤어. 이쯤 되면 "와, 인덱스 완전 짱인데? 그냥 모든 컬럼에 다 걸어버리면 안 되나?" 하는 순수한 생각을 할 수도 있어. 하지만 이제부터가 진짜 이야기의 시작이야. 그 강력한 힘에는 반드시 대가가 따른다.
네가 무심코 추가한 인덱스 하나하나가 조용히, 그리고 꾸준히 너의 시스템을 좀먹고 있을지도 몰라. 속도라는 달콤함에 취해 우리가 무엇을 희생하고 있는지, 인덱스의 어두운 그림자를 제대로 직시해 보자.
1. 쓰기(Write) 성능의 희생양: INSERT, UPDATE, DELETE의 비명
가장 치명적이고 직접적인 대가야. **인덱스는 조회(SELECT) 속도를 빠르게 하는 대신, 데이터를 추가(INSERT), 수정(UPDATE), 삭제(DELETE)하는 속도를 느리게 만들어.**
왜 그럴까? 아까 도서관 비유를 다시 가져와보자. 네가 책에 새로운 내용을 추가하거나 기존 내용을 수정했어. 그럼 그걸로 끝일까? 아니지. 책 맨 뒤의 '찾아보기'도 전부 수정해야 해. 새로운 항목을 추가하고, 페이지 번호가 바뀐 항목들을 업데이트해야 하잖아. 이 작업, 은근히 귀찮고 시간이 걸리겠지?
DB도 똑같아. `users` 테이블에 새로운 회원이 가입(`INSERT`)했다고 생각해 봐. DB는:
- `users` 테이블에 새로운 데이터를 쓴다.
- `idx_users_username` 인덱스(B-Tree)에 가서 새로운 `username`을 정렬된 위치에 추가한다.
- 만약 `email` 컬럼에도 인덱스가 있다면, 그 인덱스에도 새로운 `email`을 추가한다.
- 테이블에 걸린 인덱스가 5개라면? 이 작업을 5번 반복해야 해.
데이터를 수정(`UPDATE`)하거나 삭제(`DELETE`)할 때도 마찬가지야. 원본 데이터뿐만 아니라, 변경된 컬럼과 관련된 모든 인덱스를 찾아가서 값을 수정하거나 삭제해야 해. 인덱스가 많으면 많을수록 이 추가 작업의 부담은 눈덩이처럼 불어나.
끔찍한 손님, 페이지 분할(Page Split)
특히 B-Tree 인덱스에 새로운 데이터를 추가할 때, 특정 노드(페이지)가 꽉 차버리면 **'페이지 분할'**이라는 비싼 연산이 발생해. 꽉 찬 페이지를 둘로 쪼개고, 기존 데이터들을 새 페이지로 옮긴 다음, 상위 노드의 포인터 정보까지 수정해야 하는 대작업이야. 이 과정에서 디스크 I/O가 많이 발생하고, 심할 경우 DB 전체에 순간적인 락(Lock)이 걸려 성능 저하를 유발하기도 해.
쓰기 작업이 많은 서비스라면?
실시간 로그 수집 시스템, IoT 기기의 센서 데이터 저장, 채팅 메시지 저장 등 `INSERT`가 `SELECT`보다 훨씬 빈번한 서비스에 인덱스를 남발하는 건 재앙이야. 조회는 거의 없는데, 매번 데이터를 쓸 때마다 수많은 인덱스를 갱신하느라 DB가 제 성능을 못 내게 되지. 이런 경우엔 인덱스를 최소화하거나, 쓰기에 특화된 다른 종류의 DB(NoSQL 등)를 고려해야 해.
2. 저장 공간의 도둑: 디스크는 공짜가 아니다
인덱스는 공짜가 아니야. 엄연히 디스크 공간을 차지하는 파일이라고.
인덱스는 원본 데이터의 복사본(일부)과 포인터 정보를 담고 있기 때문에, 필연적으로 추가적인 저장 공간을 요구해.
특히 여러 컬럼을 묶은 **복합 인덱스(Composite Index)**나, `VARCHAR(255)`처럼 긴 텍스트 컬럼에 인덱스를 걸면 그 크기는 상상을 초월할 수 있어. 어떤 경우에는 원본 데이터 테이블보다 인덱스의 크기가 몇 배나 더 커지는 배보다 배꼽이 더 큰 상황이 발생하기도 해.
이게 왜 문제냐고?
- 비용 증가: 디스크 용량은 곧 돈이야. 클라우드 환경에서는 사용한 만큼 비용을 내야 하니, 불필요한 인덱스는 계속해서 너의 지갑을 털어가는 셈이지.
- 백업/복구 시간 증가: 데이터베이스를 백업하고 복구할 때, 이 거대한 인덱스 파일들도 함께 처리해야 해. 당연히 시간도 더 오래 걸리고, 장애 발생 시 서비스 복구 시간(RTO)이 길어지는 원인이 될 수 있어.
- 메모리 비효율: DB는 자주 사용하는 인덱스를 메모리(버퍼 풀)에 올려두고 사용하는데, 쓸데없이 큰 인덱스들이 메모리를 차지하고 있으면 정작 중요한 데이터나 다른 인덱스가 메모리에 올라오지 못해 성능이 저하될 수 있어.
3. 쿼리 옵티마이저의 혼란: 똑똑한 줄 알았는데...?
데이터베이스에는 **'쿼리 옵티마이저(Query Optimizer)'**라는 아주 똑똑한 두뇌가 있어. 우리가 쿼리를 날리면, 옵티마이저는 해당 쿼리를 가장 효율적으로 실행할 수 있는 방법(실행 계획)을 순식간에 결정해. 이때 "이 쿼리에는 A 인덱스를 쓰는 게 좋을까, B 인덱스를 쓰는 게 좋을까? 아니면 그냥 풀 스캔이 빠를까?" 같은 고민을 하지.
그런데 만약 테이블에 너무 많은 인덱스가 걸려있다면?
옵티마이저에게 너무 많은 선택지를 주는 셈이야. 선택지가 많아지면 고민하는 시간도 길어지고, 심지어 최악의 선택을 하는 경우도 종종 발생해.
예를 들어, `WHERE a = ? AND b = ?` 라는 쿼리가 있는데, `a` 컬럼에 단일 인덱스가 있고, `b` 컬럼에도 단일 인덱스가 있고, `(a, b)` 복합 인덱스도 있다고 쳐보자. 옵티마이저는 이 셋 중 무엇을 써야 할지 통계 정보를 기반으로 판단하는데, 이 통계 정보가 최신이 아니거나 데이터 분포가 특이할 경우 잘못된 판단을 내릴 수 있어. 분명 더 좋은 인덱스가 있는데도 엉뚱한 인덱스를 타거나, 심지어 풀 스캔을 해버리는 바람에 쿼리가 오히려 느려지는 현상이 발생할 수 있다는 거야.
불필요한 인덱스는 똑똑한 옵티마이저를 '결정 장애'에 빠뜨리는 방해꾼일 뿐이야.
"인덱싱 밸런스 게임"에서 승리하는 법: 스마트 인덱싱 전략
자, 이제 인덱스의 명과 암을 모두 알았어. 인덱스는 필요악일까? 아니, 인덱스는 여전히 DB 성능 튜닝의 가장 강력한 무기야. 중요한 건 '남발'이 아니라 '전략'이지. 속도와 안정성 사이에서 절묘한 줄타기를 하는 '밸런스형 드라이버'가 되기 위한 스마트 인덱싱 전략을 알아보자.
전략 1: `WHERE` 절의 단골손님을 공략하라
가장 기본적이고 중요한 원칙이야. 인덱스는 '검색'을 위해 존재해. 따라서 **`WHERE` 절이나 `JOIN`의 `ON` 절에 자주 사용되는 컬럼**이 인덱싱의 제1순위 타겟이야.
애플리케이션의 슬로우 쿼리 로그를 분석하거나, `EXPLAIN`을 통해 풀 스캔이 발생하는 쿼리들을 찾아봐. 그리고 그 쿼리들의 `WHERE` 절에 어떤 컬럼이 반복적으로 등장하는지 확인하는 거야. 그 컬럼이 바로 너의 첫 번째 인덱스 후보다.
예를 들어, 사용자를 검색할 때 `email`로 자주 조회한다면 `email` 컬럼에 인덱스를 걸고, 상품을 검색할 때 `category_id`와 `status`로 필터링한다면 이 컬럼들을 인덱싱 대상으로 고려해야 해.
전략 2: 카디널리티(Cardinality)는 왕이다
조금 전문적인 용어가 나왔지만 전혀 어렵지 않아. **카디널리티(Cardinality)**는 '해당 컬럼에 얼마나 다양한 값이 분포되어 있는가'를 나타내는 지표야. 즉, 중복도가 얼마나 낮은지를 의미해.
- 카디널리티가 높은 컬럼 (High Cardinality): 값의 종류가 매우 다양하고 중복이 거의 없는 컬럼. 예를 들면 `user_id`, `email`, `jumin_no` 같은 것들. 이런 컬럼은 인덱스 효율이 *매우 좋다*. 인덱스를 통해 몇 개의 행으로 범위를 확 좁힐 수 있기 때문이야.
- 카디널리티가 낮은 컬럼 (Low Cardinality): '남/여' 같은 `gender` 컬럼, 'Y/N' 같은 `is_deleted` 컬럼처럼 몇 가지 값만 반복되는 컬럼. 이런 컬럼은 인덱스 효율이 *매우 나쁘다*. `gender = '남'`으로 검색해봤자 전체 데이터의 절반을 걸러낼 뿐이라, DB는 "에이, 이럴 바엔 그냥 풀 스캔하고 말지"라고 판단할 가능성이 높아.
따라서, 카디널리티가 높은 컬럼을 우선적으로 인덱싱하는 것이 현명한 선택이야. `gender` 같은 컬럼에 단독으로 인덱스를 거는 건 저장 공간만 낭비하는 어리석은 짓일 수 있어.
전략 3: 콤보의 미학, 복합 인덱스(Composite Index)
실제 서비스에서는 `WHERE` 절에 조건이 하나만 달리는 경우보다 여러 개가 `AND`로 묶이는 경우가 훨씬 많아. 예를 들면, "IT 카테고리에서, 평점 4.5 이상인 전문가를, 최근 활동 순으로 찾아줘" 같은 거지.
이럴 때 사용하는 것이 바로 **복합 인덱스**야. 여러 컬럼을 묶어서 하나의 인덱스로 만드는 거지.
-- 좋은 예: 카테고리 ID와 평점을 묶은 복합 인덱스
CREATE INDEX idx_experts_category_rating ON experts (category_id, rating);
이 인덱스는 아래와 같은 쿼리에서 아주 강력한 힘을 발휘해.
SELECT * FROM experts WHERE category_id = 5 AND rating >= 4.5;
🚨 여기서 가장 중요한 포인트! 복합 인덱스는 컬럼의 '순서'가 생명이다.
`INDEX(a, b)`와 `INDEX(b, a)`는 완전히 다른 인덱스야. 인덱스는 첫 번째 컬럼(`a`)으로 먼저 정렬하고, 그 안에서 두 번째 컬럼(`b`)으로 다시 정렬하는 방식으로 만들어져. 전화번호부를 생각하면 쉬워. '성(Last Name)'으로 먼저 정렬하고, 같은 성 안에서 '이름(First Name)'으로 정렬하잖아.
그래서 `INDEX(a, b)`는 아래 두 쿼리에 모두 효과적이야.
- `WHERE a = ? AND b = ?` (당연히 OK)
- `WHERE a = ?` (첫 번째 컬럼만 사용해도 OK)
하지만 아래 쿼리에는 효과가 *없어*.
- `WHERE b = ?` (두 번째 컬럼만 단독으로 사용 불가)
전화번호부에서 이름이 '민준'인 사람을 찾으려면 모든 성씨를 다 뒤져봐야 하는 것과 같은 이치지. 따라서 복합 인덱스를 만들 때는 쿼리에서 사용되는 순서, 그리고 카디널리티가 더 높은 컬럼을 앞쪽에 배치하는 것이 일반적인 전략이야.
전략 4: 궁극의 속도 핵, 커버링 인덱스(Covering Index)
이건 정말 아는 사람만 쓴다는 고급 기술이야. **커버링 인덱스**는 쿼리에 필요한 *모든* 데이터를 인덱스가 포함하고 있는 경우를 말해.
예를 들어보자. 아래 쿼리는 `users` 테이블에서 특정 `id`를 가진 유저의 `username`과 `email`을 조회하고 있어.
SELECT username, email FROM users WHERE id = 12345;
만약 `id`에만 인덱스가 걸려있다면, DB는:
- `id` 인덱스를 탐색해서 `id=12345`인 데이터의 물리적 주소를 찾는다.
- 찾아낸 주소를 가지고, `users` 테이블의 원본 데이터 파일에 접근해서 `username`과 `email` 값을 읽어온다.
그런데 만약 우리가 이렇게 생긴 복합 인덱스를 가지고 있다면 어떨까?
CREATE INDEX idx_users_id_username_email ON users (id, username, email);
이 인덱스 안에는 `id`뿐만 아니라 `username`과 `email` 값까지 모두 들어있어. 따라서 DB는:
- `idx_users_id_username_email` 인덱스를 탐색해서 `id=12345`인 데이터를 찾는다.
- 인덱스 안에 `username`과 `email`이 이미 있네? **테이블에 접근할 필요 없이 인덱스에서 모든 정보를 읽어서 바로 반환한다.** (끝)
원본 데이터가 저장된 디스크에 접근하는 2단계 과정이 생략된 거야. 디스크 I/O를 극적으로 줄여주기 때문에 속도가 엄청나게 빨라져. `EXPLAIN`을 찍어보면 `Extra` 컬럼에 **`Using index`** 라는 문구가 뜨는데, 이게 바로 커버링 인덱스가 적용되었다는 신호야.
자주 실행되면서 조회하는 컬럼 수가 적은 핵심 쿼리가 있다면, 커버링 인덱스를 만들어주는 것만으로도 서비스 전체의 성능을 한 단계 레벨업 시킬 수 있어.
전략 5: `LIKE` 검색의 함정을 피하라
문자열 검색에 자주 쓰는 `LIKE` 연산자는 인덱스 사용에 있어 아주 까다로운 녀석이야.
- `WHERE name LIKE '김%'` (앞부분 일치): **인덱스 사용 가능 (O)**. B-Tree는 정렬되어 있으므로 '김'으로 시작하는 부분을 빠르게 찾을 수 있어.
- `WHERE name LIKE '%민준'` (뒷부분 일치): **인덱스 사용 불가 (X)**.
- `WHERE name LIKE '%이%'` (중간 부분 일치): **인덱스 사용 불가 (X)**.
앞부분이 와일드카드(`%`)로 시작하는 검색은 B-Tree 인덱스를 무용지물로 만들어. 전화번호부에서 이름이 '준'으로 끝나는 사람을 찾을 수 없는 것과 같지. 이런 쿼리는 결국 풀 테이블 스캔으로 이어져 DB에 큰 부하를 주게 돼.
만약 전문적인 텍스트 검색 기능이 필요하다면, 관계형 DB의 B-Tree 인덱스에 의존할 게 아니라 **'풀텍스트 인덱스(Full-text Index)'**를 사용하거나, **Elasticsearch** 같은 전문 검색 엔진을 도입하는 것이 올바른 해결책이야.
실전! 이런 상황엔 이런 인덱스
이론을 알았으니, 실제 서비스 시나리오에 적용해 보자. 네가 다양한 서비스를 개발하는 개발자라고 상상해 봐.
🛒 시나리오 1: 이커머스 쇼핑몰
- 주요 쿼리: "여성 의류 카테고리에서, 가격이 5만 원 이하인 신상품 순으로 상품 목록 보여줘."
- 분석: `WHERE` 절에 `category_id`, `price`가 사용되고, `ORDER BY`에 `created_at`이 사용된다.
- 추천 인덱스: `INDEX(category_id, price, created_at)`.
카테고리로 1차 필터링, 가격으로 2차 필터링, 그리고 정렬까지 인덱스를 활용할 수 있도록 설계. `category_id`의 카디널리티가 `price`보다 높을 가능성이 크므로 순서도 적절해 보인다.
✍️ 시나리오 2: 소셜 미디어(SNS) 앱
- 주요 쿼리: "내가 팔로우하는 유저들의 최신 게시글 20개를 피드에 보여줘."
- 분석: `WHERE` 절에 `user_id`가 `IN (...)` 형태로 들어가고, `ORDER BY created_at DESC`로 최신순 정렬이 핵심이다.
- 추천 인덱스: `INDEX(user_id, created_at DESC)`.
특정 유저의 게시글을 찾고, 그것을 다시 최신순으로 정렬하는 작업을 매우 빠르게 처리해 준다. `DESC` 옵션을 주면 역순 정렬에 더 효율적이다.
🏢 시나리오 3: 재능 공유 플랫폼 '재능넷'
- 주요 쿼리: "디자인 > 로고 디자인 카테고리에서, 응답률이 95% 이상이고 평점이 높은 순으로 전문가 목록을 보여줘."
- 분석: 만약 당신이 **재능넷** 같은 플랫폼에서 특정 분야의 전문가를 빠르게 찾고 싶다면, 백엔드에서는 이런 쿼리가 실행될 가능성이 높아. `category_id`, `response_rate`가 `WHERE` 조건에, `rating`이 `ORDER BY` 조건에 들어간다.
- 추천 인덱스: `INDEX(category_id, response_rate, rating DESC)`.
카테고리로 대상을 크게 좁히고, 응답률로 한 번 더 거른 뒤, 평점순으로 정렬하는 과정을 인덱스만으로 효율적으로 처리할 수 있다. 덕분에 사용자는 쾌적한 전문가 탐색 경험을 할 수 있는 거지.
인덱싱은 정답이 없는 예술의 영역
기억해. 인덱싱에는 '절대적인 정답'이란 없어. 서비스의 특징, 데이터의 분포, 주요 쿼리 패턴에 따라 최적의 인덱스는 계속해서 달라져. 중요한 건 **지속적인 모니터링과 개선**이야. 슬로우 쿼리 로그를 주기적으로 검토하고, 사용하지 않는 인덱스는 과감히 삭제하고, 새로운 쿼리 패턴에 맞춰 인덱스를 수정하는 '인덱스 가드닝(Gardening)' 활동이 필요해.
결론: 맹목적인 드라이버가 아닌, 균형 잡힌 전략가가 되자
오늘 우리는 데이터베이스 인덱스라는 F1 머신의 엔진룸을 속속들이 들여다봤어. 인덱스가 어떻게 우리에게 짜릿한 속도를 선물하는지, 그리고 그 대가로 쓰기 성능과 저장 공간, 안정성을 어떻게 희생시키는지도 알게 됐지.
결론은 명확해. **"속도를 위해 모든 걸 희생할 순 없다."**
인덱스는 은탄환(Silver Bullet)이 아니야. 모든 문제를 해결해 주는 마법의 도구가 아니라, 양날의 검과 같은 섬세한 도구지. 잘 쓰면 세상을 구하는 영웅이 되지만, 잘못 휘두르면 나 자신을 베는 흉기가 될 수도 있어.
이제 "느리면 인덱스 걸면 돼"라는 막연한 주문 대신, 이렇게 질문을 던져보자.
- "이 쿼리는 얼마나 자주 실행되지?"
- "WHERE 절에 들어가는 컬럼의 카디널리티는 충분히 높은가?"
- "이 인덱스를 추가함으로써 느려지는 쓰기 성능을 감수할 만한 가치가 있나?"
- "더 효율적인 복합 인덱스나 커버링 인덱스를 만들 순 없을까?"
진정한 고수는 맹목적으로 액셀만 밟는 드라이버가 아니야. 트랙의 모든 코너와 차량의 한계를 정확히 이해하고, 가장 부드럽고 빠른 최적의 라인을 그리는 전략가이지. 데이터베이스 인덱싱도 마찬가지야. 너의 데이터와 쿼리를 이해하고, 성능과 비용, 안정성 사이에서 최적의 균형점을 찾아내는 것. 그게 바로 우리가 지향해야 할 '스마트한 개발자'의 모습일 거야.
오늘 배운 내용들을 바탕으로, 너의 데이터베이스를 다시 한번 점검해 봐. 잠자고 있던 괴물 쿼리를 찾아내고, 불필요한 인덱스를 정리하고, 최적의 인덱스를 설계하는 과정을 통해 너의 서비스는 더 빠르고 안정적으로 거듭날 거야. 그리고 너는, 한 단계 더 성장한 개발자가 되어 있겠지. 그 여정을 항상 응원할게!
댓글 작성
이 글에 대한 여러분의 생각을 들려주세요
로그인이 필요합니다
댓글을 작성하려면 먼저 로그인해주세요.
댓글 0
지식인의 숲 - 지적 재산권 보호 고지
지적 재산권 보호 고지
- 저작권 및 소유권: 본 컨텐츠는 재능넷의 독점 AI 기술로 생성되었으며, 대한민국 저작권법 및 국제 저작권 협약에 의해 보호됩니다.
- AI 생성 컨텐츠의 법적 지위: 본 AI 생성 컨텐츠는 재능넷의 지적 창작물로 인정되며, 관련 법규에 따라 저작권 보호를 받습니다.
- 사용 제한: 재능넷의 명시적 서면 동의 없이 본 컨텐츠를 복제, 수정, 배포, 또는 상업적으로 활용하는 행위는 엄격히 금지됩니다.
- 데이터 수집 금지: 본 컨텐츠에 대한 무단 스크래핑, 크롤링, 및 자동화된 데이터 수집은 법적 제재의 대상이 됩니다.
- AI 학습 제한: 재능넷의 AI 생성 컨텐츠를 타 AI 모델 학습에 무단 사용하는 행위는 금지되며, 이는 지적 재산권 침해로 간주됩니다.
아직 댓글이 없습니다.