웹 개발자를 위한 데이터베이스 인덱싱 설명
개발 중에는 웹 앱이 빠르게 느껴지지만, 데이터가 증가하면 밀리초 만에 반환되던 쿼리가 몇 초씩 걸리기 시작합니다. 사용자는 불만을 제기하고 데이터베이스 CPU는 치솟습니다. 원인은 대개 누락되거나 잘못 사용된 인덱스입니다. 인덱싱은 백엔드 개발자에게 가장 큰 레버리지를 제공하는 기술 중 하나이지만, 자주 오해를 받습니다. 이 가이드에서는 데이터베이스 인덱스가 작동하는 방식, 사용 시기, 흔한 함정을 피하는 방법을 설명합니다.
데이터베이스 인덱스란 무엇인가?
인덱스를 교과서 뒤쪽의 색인이라고 생각해 보세요. 주제를 찾기 위해 모든 페이지를 훑는 대신, 색인에서 찾아보면 해당 페이지를 가리켜 줍니다. 데이터베이스 인덱스도 비슷하게 작동합니다. 전체 테이블을 스캔하지 않고 데이터베이스 엔진이 행을 빠르게 찾을 수 있도록 해주는 데이터 구조입니다.
인덱스가 없으면 SELECT * FROM users WHERE email = 'alice@example.com' 같은 쿼리는 전체 테이블 스캔을 강제합니다. 데이터베이스는 일치하는 행을 찾을 때까지 모든 행을 읽습니다. email에 인덱스가 있으면 데이터베이스가 일치하는 행으로 바로 이동할 수 있습니다.
인덱스의 내부 작동 원리
대부분의 관계형 데이터베이스는 기본적으로 B-트리(균형 트리) 인덱스를 사용합니다. B-트리는 데이터를 정렬된 상태로 유지하며 검색, 순차 접근, 삽입, 삭제를 로그 시간에 수행할 수 있습니다. 그래서 수백만 행이 있어도 인덱스 조회가 빠릅니다.
다른 인덱스 유형은 다음과 같습니다:
- 해시 인덱스: 정확한 일치 조회에 적합하며 범위 쿼리에는 부적합합니다.
- 비트맵 인덱스: 카디널리티가 낮은 열(예: 상태 플래그)에 효율적이며 데이터 웨어하우징에서 흔히 사용됩니다.
- 전체 텍스트 인덱스: 텍스트 콘텐츠 검색에 특화되어 있습니다.
- GiST/GIN 인덱스: PostgreSQL에서 기하 및 JSON 데이터에 사용됩니다.
대부분의 웹 애플리케이션에서는 B-트리 인덱스가 주력입니다.
언제 인덱스를 생성해야 할까?
인덱스는 공짜가 아닙니다. 저장 공간을 차지하고 쓰기 속도를 늦춥니다. 전략적으로 생성하세요:
- WHERE 절의 열: 특정 열로 자주 필터링한다면 인덱스를 생성하세요.
- JOIN 조건의 열: 조인 속도를 높이려면 외래 키에 인덱스를 생성하세요.
- ORDER BY의 열: 인덱스는 정렬 작업을 없애줄 수 있습니다.
- GROUP BY의 열: 인덱스는 집계 쿼리에 도움이 될 수 있습니다.
하지만 거의 쿼리되지 않거나 카디널리티가 매우 낮은 열(예: 불리언 플래그)은 다른 열과 함께 사용되지 않는 한 인덱싱을 피하세요.
인덱스 유형과 사용 사례
| 인덱스 유형 | 적합한 경우 | 예시 |
|---|---|---|
| 단일 열 | 단순 필터 | CREATE INDEX idx_email ON users(email); |
| 복합 | 여러 열로 필터링하는 쿼리 | CREATE INDEX idx_name_age ON users(last_name, first_name); |
| 고유 | 고유성 강제 | CREATE UNIQUE INDEX idx_username ON users(username); |
| 부분 | 행의 하위 집합 인덱싱 | CREATE INDEX idx_active ON users(email) WHERE active = true; |
| 커버링 | 인덱스된 열만 필요한 쿼리 | CREATE INDEX idx_covering ON users(email, name); |
인덱스 생성 및 확인 방법
인덱스 생성은 간단합니다. 예를 들어 PostgreSQL에서는:
CREATE INDEX idx_users_email ON users(email);
인덱스를 생성한 후에는 실제로 사용되는지 확인하세요. EXPLAIN(또는 EXPLAIN ANALYZE)을 사용하여 쿼리 계획을 확인합니다:
EXPLAIN ANALYZE SELECT * FROM users WHERE email = 'alice@example.com';
"Seq Scan" 대신 "Index Scan" 또는 "Index Only Scan"을 찾으세요. 순차 스캔이 보인다면 유형 불일치, 열에 적용된 함수, 오래된 통계 때문에 인덱스가 사용되지 않을 수 있습니다.
흔한 인덱싱 실수
- 모든 것을 인덱싱: 인덱스가 너무 많으면 쓰기 속도가 느려지고 공간이 낭비됩니다.
- 복합 인덱스 순서 무시:
(a, b)에 대한 복합 인덱스의 경우,b만으로 필터링하는 쿼리는 인덱스를 효율적으로 사용할 수 없습니다. - 인덱스된 열에 함수 사용:
WHERE YEAR(created_at) = 2025는 인덱스 사용을 방해합니다. 대신 범위 조건을 사용하세요:WHERE created_at >= '2025-01-01' AND created_at < '2026-01-01'. - 통계 업데이트 안 함: 데이터베이스는 통계에 의존하여 인덱스를 선택합니다. 정기적으로
ANALYZE를 실행하세요. - 쓰기 오버헤드 간과: 모든 INSERT, UPDATE, DELETE는 인덱스를 업데이트해야 합니다. 쓰기가 많은 테이블에서는 선택적으로 인덱스를 생성하세요.
고급 기법
커버링 인덱스
커버링 인덱스는 쿼리에 필요한 모든 열을 포함하므로 데이터베이스가 테이블을 건드리지 않고 인덱스에서 직접 데이터를 검색할 수 있습니다. 이는 읽기 위주의 쿼리 속도를 크게 향상시킬 수 있습니다.
부분 인덱스
행의 하위 집합(예: 활성 사용자)을 자주 쿼리하는 경우, 부분 인덱스는 전체 인덱스보다 작고 빠릅니다.
인덱스 전용 스캔
일부 데이터베이스는 필요한 모든 데이터가 인덱스에 있는 인덱스 전용 스캔을 지원합니다. 이는 가장 빠른 인덱스 접근 유형입니다.
인덱스 모니터링 및 유지 관리
인덱스는 업데이트와 삭제로 인해 시간이 지나면 비대해질 수 있습니다. PostgreSQL에서는 VACUUM과 REINDEX가 성능 유지에 도움이 됩니다. MySQL에서는 OPTIMIZE TABLE이 인덱스를 재구축할 수 있습니다. 느린 쿼리 로그를 정기적으로 검토하여 누락된 인덱스를 식별하세요.
FAQ
내 쿼리가 인덱스를 사용하는지 어떻게 알 수 있나요?
쿼리 앞에 EXPLAIN 명령(또는 EXPLAIN ANALYZE)을 사용하세요. 출력은 데이터베이스가 인덱스 스캔을 사용하는지 순차 스캔을 사용하는지 보여줍니다.
인덱스가 너무 많아도 되나요?
네. 각 인덱스는 쓰기 작업에 오버헤드를 추가하고 저장 공간을 소비합니다. 읽기/쓰기 비율에 따라 균형을 맞추는 것이 좋습니다.
클러스터형 인덱스와 비클러스터형 인덱스의 차이는 무엇인가요?
클러스터형 인덱스는 테이블에서 행의 물리적 순서를 결정합니다(InnoDB의 기본 키처럼). 비클러스터형 인덱스는 행을 가리키는 별도의 구조입니다. 테이블은 클러스터형 인덱스를 하나만 가질 수 있지만 비클러스터형 인덱스는 여러 개 가질 수 있습니다.
데이터베이스를 최적화할 준비가 되셨나요? 느린 쿼리를 분석하고 필요한 곳에 인덱스를 추가하는 것부터 시작하세요. 빠른 JSON 포맷팅 및 유효성 검사를 원하시면 JSON Formatter를 사용해 보세요. 무료이며 전적으로 브라우저에서 실행됩니다.