티스토리 뷰

개요
주니어 백엔드 개발자가 반드시 알아야 할 실무 지식을 읽다가 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)
- 사용되지 않는 인덱스 정리
- 쿼리 패턴 변화에 따른 인덱스 재검토
반응형
'development' 카테고리의 다른 글
| 용어: 낙관적 동시성(Optimistic Concurrency) (0) | 2025.10.01 |
|---|---|
| TIL: Headless Mode (0) | 2025.08.22 |
| Next.js 프로젝트 생성하기: 처음부터 완벽하게 시작하는 방법 (0) | 2025.05.12 |
| TIL: 본질적인 복잡성과 우발적인 복잡성 (0) | 2025.04.27 |
| TIL: 순환복잡도 cyclomatic complexity (0) | 2025.04.27 |
반응형
최근에 올라온 글
최근에 달린 댓글
- Total
- Today
- Yesterday
TAG
- clean agile
- 클린 애자일
- postgres
- backend
- 체호프
- 인텔리제이
- go
- Echo
- 티스토리챌린지
- middleware
- 독서후기
- golang
- Gin
- bun
- MCP
- intellij
- strange
- API
- notion
- github
- agile
- 독서
- solid
- 오블완
- OpenAI
- 영화
- ChatGPT
- gocore
- websocket
- 잡학툰
| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 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 |
글 보관함