데이터베이스 용어 사전
인덱스선택도 · cardinality

카디널리티

칼럼이 가진 서로 다른 값의 개수. 낮으면 인덱스를 만들어도 옵티마이저가 쓰지 않는다.

한 칼럼이 가진 서로 다른 값의 개수. 인덱스가 실제로 쓰일지를 결정하는 핵심 지표다.

왜 낮으면 인덱스가 무의미한가

  • 회원 100만 명 테이블
gender값이 2개→ 인덱스를 타면 50만 행을 가리킨다
email값이 100만 개→ 인덱스를 타면 1행을 가리킨다

gender = 'M'에 인덱스를 쓰면 인덱스를 읽고 → 50만 번 테이블을 랜덤 접근한다. 그냥 테이블을 처음부터 순차로 읽는 편(풀 스캔)이 빠르다. 순차 I/O가 랜덤 I/O보다 훨씬 싸기 때문이다.

그래서 옵티마이저는 일부러 인덱스를 무시한다. "인덱스를 걸었는데 왜 안 타지?"의 가장 흔한 답이 이것이다.

선택도 — 비율로 보는 같은 이야기

  • 선택도 = 조건에 맞는 행 수 / 전체 행 수

  • 선택도 0.001 (0.1%) — → 인덱스가 압도적으로 유리

  • 선택도 0.3 — (30%) → 대개 풀 스캔이 낫다

경계는 엔진과 데이터 분포에 따라 다르지만, 수 %를 넘어가면 인덱스 이점이 급격히 준다는 감각이 중요하다.

낮은 카디널리티를 살리는 법

-- ① 복합 인덱스의 앞 칼럼으로 쓰고, 뒤에 선택도 높은 칼럼을 붙인다
CREATE INDEX idx ON orders (status, created_at);

-- ② 부분 인덱스 — 소수인 값만 골라 색인한다 (PostgreSQL)
CREATE INDEX idx_pending ON orders (created_at) WHERE status = 'PENDING';

②는 전체의 1%뿐인 PENDING만 담으므로 인덱스가 작고 빠르다. "값 종류는 적지만 분포가 치우친" 경우의 정석이다.

면접 함정

  • "인덱스를 걸었으니 무조건 빠르다" → 카디널리티가 낮으면 옵티마이저가 안 쓴다.
  • ⚠️ 통계가 낡으면 옵티마이저가 카디널리티를 잘못 추정해 엉뚱한 계획을 고른다. 대량 적재 뒤에는 통계를 갱신한다.

실제로 확인하는 쿼리

-- 직접 세어 본다
SELECT count(DISTINCT status) AS distinct_values, count(*) AS total,
       count(DISTINCT status)::float / count(*) AS selectivity
  FROM orders;

-- PostgreSQL: 옵티마이저가 보는 값 (n_distinct 가 음수면 '전체 대비 비율')
SELECT attname, n_distinct, most_common_vals, most_common_freqs
  FROM pg_stats WHERE tablename = 'orders' AND attname = 'status';

-- MySQL: 인덱스 통계상의 카디널리티
SHOW INDEX FROM orders;      -- Cardinality 칼럼

most_common_valsmost_common_freqs가 함께 보이면 분포가 얼마나 치우쳤는지까지 알 수 있다. 값 종류는 3개인데 하나가 99%라면, 그 흔한 값은 풀 스캔이 낫고 나머지 1%는 인덱스가 유리하다 — 옵티마이저는 이 히스토그램을 보고 같은 칼럼에서도 값에 따라 다른 계획을 고른다.

치우친 분포를 다루는 법

-- 부분 인덱스 — 소수인 값만 색인 (PostgreSQL)
CREATE INDEX idx_pending ON orders (created_at) WHERE status = 'PENDING';

-- 통계 정밀도를 올린다 — 히스토그램 구간 수를 늘려 추정을 정확하게
ALTER TABLE orders ALTER COLUMN status SET STATISTICS 1000;
ANALYZE orders;

두 번째는 분포가 복잡한 칼럼에서 옵티마이저의 오추정을 줄이는 표준 처방이다.

함께 보면 좋은 용어

노트에서 맥락과 함께 보기 — 인덱스 — B+Tree·클러스터형·커버링(PostgreSQL·MySQL)