티스토리 뷰

 

개요

주니어 백엔드 개발자가 반드시 알아야 할 실무 지식을 읽다가 selectivity 라는 표현을 처음보아서 관련한 내용을 GPT-5와 함께 정리해보았다.

왜 이 개념들이 중요한가?

실무에서 데이터베이스를 다루다 보면 이런 상황들을 자주 마주하게 된다:

  • 인덱스를 만들었는데도 쿼리가 여전히 느린 경우
  • 옵티마이저가 예상과 다른 실행 계획을 선택하는 경우
  • 사용되지 않는 인덱스가 스토리지만 차지하는 경우

이 모든 문제의 뿌리에는 카디널리티(Cardinality)와 선택도(Selectivity)에 대한 이해 부족이 있다. 이 두 개념을 제대로 알면 언제 인덱스를 만들고, 언제 만들지 말아야 하는지 정확히 판단할 수 있다.

핵심 개념 이해하기

카디널리티 (Cardinality)

카디널리티는 컬럼에 존재하는 서로 다른 값(고유값)의 개수를 의미한다.

-- 예시 테이블
CREATE TABLE users (
    id BIGINT PRIMARY KEY,
    email VARCHAR(255) UNIQUE,
    gender CHAR(1),
    age INT,
    city VARCHAR(100)
);
  • 높은 카디널리티: email (거의 모든 값이 고유) → 1,000,000개
  • 중간 카디널리티: city (50개 도시) → 50개
  • 낮은 카디널리티: gender (M, F) → 2개

선택도 (Selectivity)

선택도는 특정 조건이 전체 행 중 얼마나 많은 비율을 반환하는지를 나타낸다. 높고 낮다는 표현이 헷갈릴 수 있으니 주의하자.

선택도 = 조건에 맞는 행 수 ÷ 전체 행 수
  • 높은 선택도(High Selectivity): 매칭 비율이 낮음 (1%) → 인덱스 효율 높음
  • 낮은 선택도(Low Selectivity): 매칭 비율이 높음 (50%) → 풀 스캔이 더 효율적

실전 예시로 이해하기

전자상거래 주문 테이블(1,000,000건)을 예로 들어보자:

CREATE TABLE orders (
    id INT PRIMARY KEY,
    status VARCHAR(20),      -- 주문 상태
    user_type VARCHAR(20),   -- 사용자 타입
    created_date DATE,
    email VARCHAR(255)
);

예시 1: 낮은 카디널리티 + 낮은 선택도 (인덱스 비효율)

-- status 컬럼 분포
-- delivered: 600,000건 (60%)
-- shipped: 200,000건 (20%)
-- confirmed: 100,000건 (10%)
-- cancelled: 50,000건 (5%)
-- 기타: 50,000건 (5%)

SELECT * FROM orders WHERE status = 'delivered';
  • 카디널리티: 5개 (낮음)
  • 선택도: 600,000 ÷ 1,000,000 = 60% (매우 낮음!)
  • 결론: 이럴 때는 인덱스보다 풀 스캔이 더 효율적이다.

예시 2: 낮은 카디널리티 + 높은 선택도 (인덱스 효율)

-- user_type 컬럼 분포
-- standard: 950,000건 (95%)
-- premium: 40,000건 (4%)
-- guest: 10,000건 (1%)

SELECT * FROM orders WHERE user_type = 'guest';
  • 카디널리티: 3개 (매우 낮음)
  • 선택도: 10,000 ÷ 1,000,000 = 1% (낮은 선택도)
  • 결론: 이 도메인에서 user_type = 'guest’ 인 경우에 대한 쿼리를 많이 사용한다면, 카디널리티는 낮지만 선택도가 높아서 인덱스가 효율적이다.

예시 3: 높은 카디널리티 + 높은 선택도 (인덱스 최적)

SELECT * FROM orders WHERE email = 'kim@example.com';
  • 카디널리티: 거의 1,000,000개 (매우 높음)
  • 선택도: 1 ÷ 1,000,000 = 0.001% (매우 낮은 선택도)
  • 결론: 인덱스 강력 추천

인덱스 설정 판단 가이드

위 예제에서 감각을 익혔다면 다음 도표의 정리가 이해될 것이다.

카디널리티 선택도(매칭 비율) 권장사항

유니크/거의 유니크 매우 높음(≈0%) 단일 인덱스 적극 권장
중간 카디널리티, 분포 균등 높음(1~5%) 단일/복합 인덱스 고려
범위 조건 + 고유값 많음 분포에 따라 다름 좁으면 OK, 넓으면 비추
편향 심한 컬럼 값마다 다름 히스토그램 기반 판단
저카디널리티 (boolean, gender) 낮음(수십%) 단독 인덱스 비추천

데이터베이스별 도구 활용하기

MySQL - EXPLAIN 활용

-- 인덱스 효과 없는 경우
mysql> EXPLAIN SELECT * FROM orders WHERE status = 'delivered';
+------+------+-------+------+---------+------+--------+-------+
| type | key  | rows  | Extra                      |
+------+------+-------+------+---------+------+--------+-------+
| ALL  | NULL | 995k  | Using where                |
+------+------+-------+------+---------+------+--------+-------+
-- 풀 스캔 선택됨 (인덱스 있어도)

-- 인덱스 효과 있는 경우
mysql> EXPLAIN SELECT * FROM orders WHERE user_type = 'guest';
+------+--------------+-------+------+---------+------+--------+-------+
| type | key          | rows  | Extra                      |
+------+--------------+-------+------+---------+------+--------+-------+
| ref  | idx_user_type| 10k   | Using index condition      |
+------+--------------+-------+------+---------+------+--------+-------+
-- 인덱스 스캔 선택됨

PostgreSQL - EXPLAIN ANALYZE 활용

-- 비효율적인 인덱스 사용 케이스
postgres=# EXPLAIN ANALYZE SELECT * FROM orders WHERE status = 'delivered';

QUERY PLAN
--------------------------------------------------------------------
Seq Scan on orders (cost=0.00..18345.00 rows=600000 width=45)
  (actual time=0.156..891.234 rows=600000 loops=1)
  Filter: (status = 'delivered'::text)
Planning Time: 0.891 ms
Execution Time: 945.123 ms
-- 풀 스캔이 더 효율적이라고 판단

-- 효율적인 인덱스 사용 케이스
postgres=# EXPLAIN ANALYZE SELECT * FROM orders WHERE user_type = 'guest';

QUERY PLAN
--------------------------------------------------------------------
Index Scan using idx_user_type on orders (cost=0.43..456.78 rows=10000 width=45)
  (actual time=0.089..23.456 rows=10000 loops=1)
  Index Cond: (user_type = 'guest'::text)
Planning Time: 0.234 ms
Execution Time: 28.123 ms
-- 인덱스 스캔으로 빠른 성능

MySQL Workbench Performance 권장사항

📊 Index Recommendations

❌ DROP INDEX idx_status ON orders;
Reason: Index selectivity too low (선택도 60% - 매우 높음)
Usage: Scans 60% of table on average
Recommendation: Remove this index, use full table scan

✅ KEEP INDEX idx_user_type ON orders;
Reason: Good selectivity for 'guest' queries (선택도 1% - 낮음)
Usage: 매우 선별적이어서 소수 값 검색에 효율적
Recommendation: Keep and monitor usage

💡 CONSIDER: CREATE INDEX idx_user_type_status ON orders(user_type, status);
For queries filtering both columns

실무에서 자주 하는 실수들

1. 카디널리티만 보고 판단하기

복합 조건에서는 전체 선택도가 낮아질 수 있다.

-- 잘못된 생각: "성별은 카디널리티가 낮으니까 인덱스 안 만들어야지"
-- 올바른 접근: 실제 쿼리 패턴과 선택도를 확인해야 함

SELECT * FROM users WHERE gender = 'F' AND age BETWEEN 25 AND 30;

2. 데이터 분포 무시하기

'HIGH' 검색에는 인덱스 효과적, 'NORMAL' 검색에는 비효과적이다. 이럴때는 부분 인덱스, 복합 인덱스를 고민해볼 필요가 있다.

-- 같은 카디널리티라도 분포에 따라 다름
SELECT * FROM orders WHERE priority = 'HIGH';  -- 전체의 1%
SELECT * FROM orders WHERE priority = 'NORMAL'; -- 전체의 80%

3. 통계 정보 방치하기

데이터베이스는 주기적으로 테이블 정보를 분석, 집계하여 optimize, explain 시에 이를 기반으로 계획을 짜는데, 통계가 오래되면 옵티마이저가 잘못된 판단을 할 수 있다. 보통은 자동으로 실행되지만 특정 상황에서는 수동으로 갱신하자.

-- PostgreSQL 통계 갱신
ANALYZE orders;     -- 테이블 단위
ANALYZE;            -- 데이터베이스 전체

-- MySQL 통계 갱신
ANALYZE TABLE orders;
-- (MySQL 8.0: 값 편향이 크면 히스토그램도 고려)
-- CREATE HISTOGRAM ON orders.column_name WITH 100 BUCKETS;

결론

데이터베이스 인덱스 최적화의 핵심은 다음과 같다:

핵심 원칙

  • 카디널리티는 인덱스의 잠재력을 보여주는 지표
  • 선택도가 실제 인덱스 효율성을 결정하는 핵심 지표
  • 데이터 분포를 반드시 고려해야 한다.

실무 체크리스트

  • 선택도 5% 미만 → 인덱스 적극 권장
  • 선택도 5~20% → 복합 인덱스나 다른 조건과 함께 고려
  • 선택도 20% 이상 → 풀 스캔이 더 효율적

지속적인 최적화

  • 정기적인 통계 갱신 (ANALYZE)
  • 사용되지 않는 인덱스 정리
  • 쿼리 패턴 변화에 따른 인덱스 재검토
반응형
반응형
잡학툰 뱃지
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
«   2025/12   »
1 2 3 4 5 6
7 8 9 10 11 12 13
14 15 16 17 18 19 20
21 22 23 24 25 26 27
28 29 30 31
글 보관함