데이터베이스 인덱스 설계 플레이북
이 글은 문법 소개보다 인덱스 설계를 검토하는 기준과 운영 플레이북에 초점을 둔다.
인덱스는 공짜가 아니다
인덱스는 조회를 빠르게 하지만 그 대가로 다음 비용을 만든다.
- INSERT/UPDATE/DELETE마다 인덱스도 함께 갱신된다.
- 디스크와 메모리 사용량이 늘어난다.
- 잘못된 인덱스는 옵티마이저를 오히려 혼란스럽게 만든다.
- 복합 인덱스가 많아질수록 중복되는 구조가 쌓인다.
즉 읽기 최적화는 항상 쓰기 비용과 교환된다. 읽기 트래픽이 높고 패턴이 안정적인 시스템에서는 인덱스가 강력하지만, 쓰기 중심 이벤트 적재 테이블에 습관적으로 여러 인덱스를 두면 병목이 바로 생긴다.
시작점은 쿼리가 아니라 접근 패턴이다
좋은 인덱스 설계는 개별 SQL 한 줄보다 화면과 API의 접근 패턴에서 나온다.
[Screen / API]
|
+--> filter by status
+--> filter by tenant_id
+--> order by created_at desc
+--> page size 20
|
v
[Representative Query]
|
v
[Index candidates]
검토 순서는 보통 다음이 좋다.
- 가장 많이 호출되는 화면과 API를 고른다.
- WHERE, JOIN, ORDER BY, LIMIT 패턴을 정리한다.
EXPLAIN또는EXPLAIN ANALYZE로 실제 실행 계획을 본다.- 인덱스 후보를 만들고 읽기 이득과 쓰기 비용을 함께 본다.
쿼리를 본 뒤 인덱스를 짜는 것보다, 사용 패턴을 먼저 보고 대표 쿼리를 정의하는 편이 중복 인덱스를 훨씬 덜 만든다.
복합 인덱스는 왼쪽부터 쓰인다
복합 인덱스의 가장 기본적인 원칙은 leftmost prefix다. 예를 들어 (tenant_id, status, created_at) 인덱스는 앞부분부터 조건이 맞아야 효과가 크다.
tenant_id = ?는 잘 탄다.tenant_id = ? AND status = ?도 잘 탄다.tenant_id = ? AND status = ? ORDER BY created_at DESC는 더 좋다.status = ?만으로는 기대만큼 못 탈 수 있다.
그래서 칼럼 순서는 중요하다. 일반적으로는 다음을 함께 본다.
- 항상 들어오는 동등 조건
- 선택도가 높은 조건
- 정렬 조건
하지만 “선택도가 높은 칼럼을 무조건 앞에” 같은 단순 규칙은 위험하다. 멀티테넌트 시스템에서는 tenant_id처럼 선택도보다 분리 경계가 중요한 칼럼을 앞에 두는 것이 실제 운영에서 더 안전할 수 있다.
커버링 인덱스는 강력하지만 남용하기 쉽다
커버링 인덱스는 조회에 필요한 칼럼이 인덱스 안에 모두 있어 테이블 본문 접근을 줄인다. 목록 조회, 짧은 응답 API, 핫한 리드 패턴에서 매우 효과적이다.
CREATE INDEX idx_orders_tenant_status_created_cover
ON orders (tenant_id, status, created_at DESC)
INCLUDE (id, total_amount, customer_name);
하지만 커버링 인덱스는 넓어질수록 유지 비용이 커진다.
- 인덱스 크기 증가
- 쓰기 비용 증가
- 메모리 캐시 효율 저하
자주 호출되고 응답 시간에 민감한 조회에만 써야 한다. “자주 보는 컬럼이니까 다 넣자”는 금방 실패한다.
카디널리티와 선택도는 왜 중요한가
카디널리티가 낮은 칼럼, 예를 들어 status처럼 값 종류가 몇 개 안 되는 칼럼은 단독 인덱스 효율이 낮을 수 있다. 반대로 사용자 ID, 주문 번호처럼 값 분포가 넓은 칼럼은 선택도가 높아 인덱스 가치가 크다.
하지만 이것도 문맥이 있다.
status단독 인덱스는 약할 수 있다.tenant_id + status + created_at조합은 강할 수 있다.- 낮은 카디널리티 칼럼도 정렬, 파티셔닝, 부분 인덱스 조건과 결합하면 유용할 수 있다.
결국 카디널리티는 단독 진실이 아니라 다른 조건과 함께 얼마나 행 수를 빨리 줄일 수 있는가의 일부다.
실행 계획을 읽을 때 봐야 할 것
인덱스 설계는 EXPLAIN을 보지 않으면 거의 감에 의존하게 된다. 최소한 다음을 확인해야 한다.
- 어떤 테이블부터 읽는가
- access type이 range인지 full scan인지
- estimated rows가 과도한가
- filter 후 남는 row 수가 충분히 줄어드는가
- sort, temporary, filesort 비용이 큰가
중요한 점은 “인덱스를 썼다”가 성공이 아니라는 것이다. 인덱스를 타더라도 너무 많은 row를 읽으면 느릴 수 있다. 실제 질문은 얼마나 적게 읽었는가다.
자주 보는 안티패턴
단일 칼럼 인덱스를 여러 개 두고 옵티마이저가 알아서 합칠 거라 기대함
대부분 기대만큼 안 된다. 대표 조회 패턴에 맞는 복합 인덱스가 더 낫다.
정렬을 무시하고 WHERE 조건만 보고 인덱스를 만듦
ORDER BY created_at DESC LIMIT 20 같은 쿼리는 정렬 회피가 핵심일 수 있다.
함수나 변환으로 인덱스를 무력화함
WHERE DATE(created_at) = '2026-04-18' 는 범위 조건으로 바꾸는 편이 낫다.
과도한 중복 인덱스
(tenant_id) 와 (tenant_id, status) 와 (tenant_id, status, created_at) 가 모두 필요하지 않을 수 있다. 일부는 상위 인덱스로 대체 가능하다.
읽기 성능만 보고 추가한 뒤 쓰기 비용을 측정하지 않음
대량 적재, 배치 업데이트, CDC 테이블에서는 치명적이다.
예시: 운영 화면 목록 쿼리
SELECT id, customer_name, total_amount, created_at
FROM orders
WHERE tenant_id = :tenantId
AND status = 'PAID'
ORDER BY created_at DESC
LIMIT 20;
이 쿼리의 목적은 명확하다.
- tenant 경계 필수
- 상태 필터 필수
- 최신순 정렬 필수
- 페이지당 적은 row
따라서 (tenant_id, status, created_at DESC) 가 강력한 기본 후보다. 그리고 자주 호출되는 목록이라면 필요한 반환 칼럼을 커버링할지 검토한다.
운영 리뷰 워크플로
인덱스는 코드 리뷰처럼 정기 리뷰가 필요하다.
slow query detected
-> collect representative SQL + bind values
-> inspect execution plan
-> estimate query frequency and p95 impact
-> propose index or query rewrite
-> estimate write amplification and storage cost
-> deploy
-> verify plan + latency + write metrics
이 절차 없이 추가된 인덱스는 시간이 지나면 거의 항상 정리 비용으로 돌아온다.
의사결정 체크리스트
- 이 인덱스는 어떤 화면/API를 위한 것인지 명확한가
- WHERE, JOIN, ORDER BY, LIMIT 패턴을 함께 만족하는가
- 대표 바인드 값 기준 실행 계획을 확인했는가
- 읽기 개선폭이 쓰기 비용 증가를 정당화하는가
- 기존 인덱스와 중복되지 않는가
- 낮은 카디널리티 칼럼을 단독 인덱스로 두는 이유가 명확한가
- 목록 API라면 커버링 인덱스 가치가 있는가
- 배포 후 plan regression과 write latency를 검증할 수 있는가
마무리
인덱스 설계의 핵심은 많이 추가하는 것이 아니라 대표 접근 패턴에 맞는 최소한의 인덱스로 읽기 비용을 크게 낮추고, 그 대가인 쓰기 비용을 통제하는 것이다. 좋은 인덱스는 쿼리 한 줄만 보고 나오지 않는다. 화면, API, 데이터 분포, 실행 계획, 운영 빈도를 함께 봐야 한다. 그 과정을 플레이북으로 만들면 감이 아니라 체계로 성능을 관리할 수 있다.
운영 환경에서 어려워지는 지점
- 인덱스 설계는 운영에서 느려진 뒤에 덧붙이는 사후 대응으로 보면 실패하기 쉽다.
- 어려운 지점은 문법이 아니라 workload 형태, predicate 순서, 유지비를 맞추는 일이다.
- 좋은 인덱스는 읽기를 빠르게 만들지만 나쁜 인덱스는 모든 쓰기 경로에 조용한 세금을 붙인다.
중요한 아키텍처 결정
- 가장 가치 높은 쿼리 패턴의 필터, 정렬, 선택도를 기준으로 시작한다.
- 복합 인덱스는 희망적인 범용 재사용이 아니라 실제 predicate 순서와 접근 경로에 맞춘다.
- 인덱스를 더 추가하기 전에 write amplification, 저장소 비용, vacuum/유지보수 영향을 검토한다.
실무 예시
복합 인덱스는 실제 workload 접근 경로를 따라야 한다.
query: WHERE tenant_id = ? AND status = ? ORDER BY created_at DESC
index: (tenant_id, status, created_at DESC)
피해야 할 안티패턴
- 단일 컬럼 인덱스를 여기저기 추가하고 옵티마이저가 알아서 해주길 기대하는 것.
- covering 필요를 무시해 불필요한 테이블 lookup을 강제하는 것.
- workload가 바뀌었는데도 오래된 인덱스를 영구 보존하는 것.
운영 체크리스트
- 체감이 아니라 실제 빈도를 가진 느린 쿼리를 수집한다.
- 인덱스 변경 전후 실행 계획을 비교한다.
- 쓰기 비용과 저장소 증가를 측정한다.
- 중복되거나 겹치는 인덱스를 주기적으로 정리한다.
최종 판단
인덱스 설계는 workload 설계다. 가장 좋은 인덱스는 가장 똑똑해 보이는 인덱스가 아니라, 지배적인 쿼리 패턴에 맞으면서 쓰기 비용을 과도하게 늘리지 않는 인덱스다.
Continue Reading
다음으로 읽기 좋은 글
쿼리 플랜 회귀를 막는 데이터베이스 가드
인덱스 변경, 통계 갱신, 배포 이후 쿼리 실행 계획이 나빠지는 문제를 사전에 감지하는 방법을 정리합니다.
🗄️ Database데이터베이스 학습 경로: 입문부터 고급까지
인덱스와 쿼리 기초부터 복제, 파티셔닝, 스키마 변경 운영까지 체계적으로 배우는 데이터베이스 로드맵입니다.
📈 최신 동향PostgreSQL 18 최신 동향: 실무에서 진짜 중요한 변화
PostgreSQL 18은 단순 업그레이드 뉴스가 아닙니다. AIO, skip scan, 업그레이드 후 성능 회복, OAuth, generated columns까지 운영팀과 개발팀 모두에게 영향이 큰 변화가 들어왔습니다.
🖥️ FrontendCore Web Vitals 최적화 — LCP, CLS, INP 실전 가이드
Core Web Vitals를 체크리스트 수준이 아니라 사용자 체감 성능과 렌더링 구조의 관점에서 정리합니다. LCP, CLS, INP가 왜 나빠지는지, 무엇부터 측정하고 어떤 순서로 최적화해야 하는지 실무 예제로 설명합니다.
다음 탐색