인덱스 하나 만들기 전에 뭘 먼저 봐야 하나?
결론부터: 인덱스 종류를 고르는 문제가 아니라 쿼리 패턴과 읽기·쓰기 비율을 먼저 재는 문제다. status 컬럼 하나에 전체 인덱스를 걸었더니 대시보드 쿼리가 2.3초에서 1.1초로 줄었지만, 조건에 맞춘 부분 복합 인덱스는 380ms까지 내려갔다는 사례가 이걸 그대로 보여준다.[관련 사례, 2026년 5월 공개]
- 인덱스는 공짜가 아니다.
INSERT/UPDATE/DELETE마다 갱신 비용이 붙는다. - 값 종류가 적은 컬럼(status, boolean)은 전체 인덱스보다 부분 인덱스가 유리한 경우가 많다.
- 복합 인덱스는 "필터+정렬"이 반복되는 쿼리에서 순서가 성능을 가른다.
- 트랜잭션 격리 수준을 올리면 일관성은 얻지만 처리량은 소폭 내려간다.
- 이 모든 판단은 테스트 데이터가 아니라 실제 운영 데이터 분포에서 확인해야 한다.
실무에서 첫 판단 축은 "이 테이블이 읽기 중심이냐 쓰기 중심이냐"다. 조인이 많은 조회 중심 워크로드라면 조회 패턴에 맞춘 인덱스 설계가 이득이고, 쓰기 비율이 높은 테이블이라면 인덱스 개수 자체를 줄이는 방향이 맞다. 여기서부터 정규화 정도, 부분/복합 인덱스, 격리 수준이 갈린다.
조인 많은 읽기 워크로드는 인덱스를 어떻게 늘려야 하나?
조회 패턴에 맞춰 좁혀야 한다. 정규화를 깨지 않고도 조인 컬럼과 필터 컬럼을 조합한 인덱스를 추가하면 스캔 범위가 줄어든다. 다만 읽기 중심이라고 인덱스를 무한정 늘려도 되는 건 아니다. 인덱스가 많아질수록 쓰기 시점의 갱신 비용이 누적되기 때문에, 실제로 조회에 쓰이는지 pg_stat_statements로 확인한 뒤 남기는 편이 안전하다.
low-cardinality 컬럼은 왜 전체 인덱스로 만들면 손해인가?
값 종류가 몇 개 안 되는 컬럼 전체를 인덱싱하면 인덱스 크기만 커지고 선택도는 낮아진다. payments 테이블 사례에서 status가 94%가 completed인 상황에 전체 인덱스를 걸자 인덱스가 4,700만 행을 유지했는데, 자주 조회되는 pending/processing/failed만 잡은 부분 인덱스는 약 300만 행, 테이블의 6%만 포함했다. 크기가 작아지니 메모리에 올라가기 쉬워지고 스캔 비용도 줄었다. PostgreSQL 공식 문서도 부분 인덱스를 "공통값을 인덱싱하지 않기 위한" 수단으로 설명한다.PostgreSQL 공식 문서 – Partial Indexes
단, 부분 인덱스는 쿼리의 WHERE 절이 인덱스 predicate와 정확히 맞거나 그 하위집합일 때만 잘 쓰인다. 조건이 조금이라도 어긋나면 옵티마이저가 인덱스를 포기할 수 있다는 점은 감안해야 한다.
필터와 정렬이 같이 오는 쿼리엔 복합 인덱스 순서를 어떻게 잡나?
필터 컬럼을 앞에, 정렬/범위 컬럼을 뒤에 두는 게 기본이다. status, created_at 복합 인덱스는 상태로 좁힌 뒤 최신순 범위를 읽는 패턴에 맞아떨어진다. 반대로 순서를 바꿔 created_at, status로 만들면 같은 쿼리에서도 인덱스 활용도가 떨어질 수 있다. 이건 이론이 아니라 매번 EXPLAIN ANALYZE로 실행계획을 찍어 확인해야 하는 부분이다.
격리 수준을 올리면 얼마나 손해를 보나?
일관성을 얻는 대가로 처리량이 소폭 줄어든다. 2025년 실험 자료에 따르면 Repeatable Read 계열(SI)은 Read Committed 대비 평균 3% 성능 감소, Serializable(SSI)은 약 5% 감소가 보고됐다. 다른 실험에서는 SSI가 스냅샷 격리 대비 7% 미만의 비용으로 동작했고, 메모리 구성에 따라 CPU 사용 증가로 약 5% 느려진 경우도 있었다.PostgreSQL 공식 문서 – Transaction Isolation 예약·정산·재고처럼 강한 일관성이 필요한 데이터는 SSI를 검토할 만하지만, 고처리량 읽기 서비스라면 이 몇 퍼센트가 누적되어 부담이 될 수 있다.
이 수치는 어떤 조건에서 나온 값인가?
여기 인용한 수치는 특정 테이블 크기와 쿼리 조건에서 측정된 값이지, 모든 환경에 그대로 적용되는 보장치가 아니다. payments 5,000만 행 사례의 380ms, 격리 수준 실험의 3~7% 수치 모두 데이터 분포와 하드웨어, 버전에 따라 달라진다. 공식 문서도 실제 데이터로 ANALYZE를 돌린 뒤 EXPLAIN ANALYZE로 전후를 비교하라고 못 박는다. 테스트 데이터로 만든 인덱스는 테스트 데이터에만 맞을 뿐 운영 데이터 분포를 반영하지 못할 수 있다는 게 핵심이다.PostgreSQL 공식 문서 – Using EXPLAIN
부분 인덱스가 항상 정답일까? 안 타는 경우는 뭔가?
아니다. 부분 인덱스는 predicate와 쿼리 조건이 정확히 일치하거나 하위집합일 때만 확실히 쓰인다. 애플리케이션 코드에서 조건을 살짝 다르게 짜면(status = 'pending' OR status = 'failed'처럼 predicate 밖 조건이 섞이면) 옵티마이저가 인덱스를 포기하고 풀스캔으로 돌아갈 수 있다. 이건 배포 후에야 발견되는 경우가 많아서, 부분 인덱스를 만들면 그 조건을 쓰는 쿼리 전체를 목록화해 두는 게 안전하다. 2026년 기준으로도 이 정합성 문제는 여전히 수동 점검 영역이다.
그래서 뭘 고르면 되나?
읽기·쓰기 비율과 일관성 요구를 먼저 재고, 그 다음에 인덱스 종류를 정하면 된다.
- 조인 많은 읽기 중심: 조회 경로에 맞춘 복합 인덱스 추가를 우선 검토
- 값 종류 적은 컬럼: 전체 인덱스 대신 부분 인덱스로 핫셋만 좁히기
- 쓰기 비율 높은 테이블: 실제 쓰이는 인덱스만 남기고 정리
- 예약·정산·재고처럼 강한 일관성 필요: SSI 비용(약 5~7%)을 감수할지 검토
- 고처리량 읽기 서비스: RC/SI 유지가 무난한 선택지
핵심 정리
- 인덱스 선택의 출발점은 종류가 아니라 쿼리 패턴과 읽기·쓰기 비율이다.
- 부분 인덱스는 low-cardinality 컬럼에서 인덱스 크기를 줄이는 효과가 있지만, predicate와 쿼리 조건이 어긋나면 무용지물이 될 수 있다.
- 복합 인덱스는 필터 컬럼을 앞, 정렬/범위 컬럼을 뒤에 두는 순서가 조회 패턴과 맞아야 효과가 난다.
- 격리 수준을 SI·SSI로 올리면 3~7% 수준의 처리량 감소를 감수해야 하며, 이는 강한 일관성이 필요한 데이터에 한해 검토할 사안이다.
- 모든 판단은 실데이터 기반
ANALYZE와EXPLAIN ANALYZE로 전후 비교한 뒤 확정해야 한다.