9장 : 인덱스
인덱스 실전
이론을 알았으면 실전으로 넘어갑니다. 인덱스를 생성하고, 실행 계획으로 인덱스 사용 여부를 확인하며, 인덱스가 안 타는 함정을 피하고, 실무에서 인덱스를 효과적으로 설계하는 방법을 다룹니다.
인덱스 생성
-- 단일 컬럼 인덱스
CREATE INDEX idx_users_email ON users(email);
-- 복합 인덱스 (다중 컬럼)
CREATE INDEX idx_orders_user_date ON orders(user_id, order_date);
-- 유니크 인덱스
CREATE UNIQUE INDEX idx_users_email_uk ON users(email);
-- 인덱스 삭제
DROP INDEX idx_users_email;
-- 인덱스 리빌드 (단편화 해소)
ALTER INDEX idx_orders_user_date REBUILD;
-- 인덱스 리빌드 (온라인, 서비스 중단 없이)
ALTER INDEX idx_orders_user_date REBUILD ONLINE;
-- 인덱스 상태 확인 (단편화 분석)
ANALYZE INDEX idx_orders_user_date VALIDATE STRUCTURE;
SELECT name, height, lf_rows, del_lf_rows,
ROUND(del_lf_rows / DECODE(lf_rows, 0, 1, lf_rows) * 100) AS frag_pct
FROM index_stats;-- 단일 컬럼 인덱스
CREATE INDEX idx_users_email ON users(email);
-- 복합 인덱스
CREATE INDEX idx_orders_user_date ON orders(user_id, order_date);
-- 인덱스 삭제
DROP INDEX idx_users_email ON users;
-- 인덱스 재구성 (MySQL 5.6+ ONLINE DDL)
ALTER TABLE orders DROP INDEX idx_orders_user_date,
ADD INDEX idx_orders_user_date(user_id, order_date),
ALGORITHM=INPLACE, LOCK=NONE;
-- 인덱스 통계 갱신
ANALYZE TABLE users;-- 기본 인덱스
CREATE INDEX idx_users_email ON users(email);
-- 서비스 중단 없는 인덱스 생성 (CONCURRENTLY)
CREATE INDEX CONCURRENTLY idx_orders_date
ON orders(order_date);
-- 부분 인덱스 (조건부 인덱스)
CREATE INDEX idx_orders_active
ON orders(user_id, order_date)
WHERE status = 'ACTIVE';
-- 인덱스 재구성
REINDEX INDEX idx_users_email;EXPLAIN으로 인덱스 사용 확인
인덱스를 만들었다고 반드시 사용되는 것은 아닙니다. 실행 계획을 확인하여 인덱스가 실제로 사용되는지 검증해야 합니다.
-- 실행 계획 생성
EXPLAIN PLAN FOR
SELECT * FROM users WHERE email = 'test@example.com';
-- 실행 계획 조회
SELECT * FROM TABLE(DBMS_XPLAN.DISPLAY);
-- 실제 실행 통계 포함 (Oracle 10g+)
SELECT /*+ GATHER_PLAN_STATISTICS */ *
FROM users WHERE email = 'test@example.com';
SELECT * FROM TABLE(DBMS_XPLAN.DISPLAY_CURSOR(
NULL, NULL, 'ALLSTATS LAST'));-- 기본 실행 계획
EXPLAIN SELECT * FROM users WHERE email = 'test@example.com';
-- 상세 실행 계획 (MySQL 8.0+)
EXPLAIN ANALYZE SELECT * FROM users WHERE email = 'test@example.com';
-- FORMAT=JSON으로 비용 정보 확인
EXPLAIN FORMAT=JSON SELECT * FROM users WHERE email = 'test@example.com';인덱스가 안 타는 경우
인덱스를 만들어 놓고도 옵티마이저가 인덱스를 무시하는 경우가 있습니다. 이를 인지하고 피하는 것이 인덱스 실전의 핵심입니다.
| 원인 | 예시 | 해결 |
|---|---|---|
| 컬럼에 함수 적용 | WHERE UPPER(name) = 'KIM' | 함수 기반 인덱스 또는 값을 변환 |
| 묵시적 타입 변환 | WHERE phone = 01012345678 | 타입 일치시키기 |
NOT, <> 조건 | WHERE status <> 'DELETED' | 긍정 조건으로 변환 |
| OR 조건 | WHERE a = 1 OR b = 2 | UNION ALL로 분리 |
| LIKE 앞쪽 와일드카드 | WHERE name LIKE '%철수' | 전문 검색 인덱스 사용 |
| NULL 비교 | WHERE column IS NULL | NOT NULL 제약 + 기본값 |
| 낮은 선택도 | WHERE gender = 'M' (50%) | 인덱스 불필요 (Full Scan이 나음) |
| 산술 연산 | WHERE salary * 12 > 50000000 | WHERE salary > 50000000/12 |
각 원인 상세 분석
인덱스 비효율
SELECT * FROM users
WHERE department_id = 10 OR location_id = 20;
→ 두 조건이 서로 다른 인덱스 → Full Scan 가능
해결: UNION ALL로 분리
SELECT * FROM users WHERE department_id = 10
UNION ALL
SELECT * FROM users WHERE location_id = 20
AND department_id <> 10; -- 중복 방지
→ 각 쿼리가 각 인덱스를 사용 ✓선택도(Selectivity)와 인덱스 효과
인덱스가 있어도 선택도가 낮으면(대부분의 행이 조건을 만족하면) 옵티마이저가 의도적으로 Full Scan을 선택합니다.
커버링 인덱스
쿼리에 필요한 모든 컬럼이 인덱스에 포함되어 있으면, 테이블을 접근하지 않고 인덱스만으로 결과를 반환할 수 있습니다. 이를 커버링 인덱스(Covering Index)라고 합니다.
복합 인덱스 컬럼 순서 결정
복합 인덱스에서 컬럼 순서는 성능에 결정적 영향을 미칩니다.
인덱스 관리와 모니터링
인덱스를 만들고 방치하면 안 됩니다. 정기적인 관리가 필요합니다.
──── 사용되지 않는 인덱스 찾기 (MySQL 8.0+) ────
SELECT * FROM sys.schema_unused_indexes
WHERE object_schema = 'mydb';
──── 인덱스 중복 확인 ────
SELECT * FROM sys.schema_redundant_indexes
WHERE table_schema = 'mydb';
──── 인덱스 사용 통계 ────
SELECT index_name,
rows_selected, -- 인덱스로 조회한 행 수
rows_inserted,
rows_updated,
rows_deleted
FROM performance_schema.table_io_waits_summary_by_index_usage
WHERE object_schema = 'mydb'
AND object_name = 'orders';특수 인덱스
B-Tree 외에도 특수한 상황에 적합한 인덱스 유형이 존재합니다.
-- 카디널리티가 낮은 컬럼에 적합
CREATE BITMAP INDEX idx_orders_status ON orders(status);
-- 비트맵 인덱스 장점: 여러 조건의 AND/OR 연산이 빠름
SELECT COUNT(*) FROM orders
WHERE status = 'COMPLETED' AND payment_type = 'CARD';
-- → 두 비트맵 인덱스의 비트 AND 연산 → 매우 빠름
-- 주의: DML이 빈번한 OLTP에는 부적합
-- → 행 잠금이 아닌 비트맵 세그먼트 잠금 → 동시성 저하-- 활성 주문만 인덱싱 (전체의 10%가 ACTIVE라면)
CREATE INDEX idx_orders_active_user
ON orders(user_id, order_date)
WHERE status = 'ACTIVE';
-- 효과: 인덱스 크기 90% 감소, 더 빠른 검색
-- 조건: WHERE status = 'ACTIVE' 쿼리에서만 사용됨인덱스 설계 실습
쇼핑몰 DB를 기준으로 실무적인 인덱스를 설계합니다.
-- 1. 주문 조회: 특정 사용자의 최근 주문
-- WHERE user_id = ? ORDER BY order_date DESC
CREATE INDEX idx_orders_user_date
ON orders(user_id, order_date DESC);
-- 2. 상품 검색: 카테고리별 가격순
-- WHERE category_id = ? ORDER BY price
CREATE INDEX idx_products_cat_price
ON products(category_id, price);
-- 3. 주문 상세: 주문별 상품 목록
-- WHERE order_id = ?
CREATE INDEX idx_oitems_order ON order_items(order_id);
-- 4. 리뷰: 상품별 최신 리뷰
-- WHERE product_id = ? ORDER BY created_at DESC
CREATE INDEX idx_reviews_prod_date
ON reviews(product_id, created_at DESC);
-- 5. 사용자 로그인: 이메일로 조회
-- WHERE email = ?
CREATE UNIQUE INDEX idx_users_email ON users(email);
-- 6. 주문 상태 조회: 특정 상태의 주문 목록
-- WHERE status = ? AND order_date >= ?
CREATE INDEX idx_orders_status_date
ON orders(status, order_date);
-- 7. 커버링 인덱스: 주문 목록 API
-- SELECT order_id, order_date, total, status
-- WHERE user_id = ? ORDER BY order_date DESC LIMIT 20
CREATE INDEX idx_orders_cover
ON orders(user_id, order_date DESC, total, status);| 쿼리 패턴 | 인덱스 | 이유 |
|---|---|---|
| 사용자별 주문 목록 | (user_id, order_date DESC) | 복합 인덱스로 정렬까지 커버 |
| 카테고리별 상품 | (category_id, price) | 필터 + 정렬 한 번에 |
| 이메일 로그인 | (email) UNIQUE | 등호 검색, 유일성 보장 |
| 상품별 리뷰 | (product_id, created_at DESC) | 최신순 정렬 포함 |
| 주문 목록 API | (user_id, order_date DESC, total, status) | 커버링 인덱스 |
인덱스 설계 시 주의사항
DBMS별 인덱스 특성 비교
| 항목 | Oracle | MySQL (InnoDB) | PostgreSQL |
|---|---|---|---|
| 기본 인덱스 | B-Tree | B+Tree (클러스터드 PK) | B-Tree |
| PK 클러스터링 | IOT(선택) | 강제 클러스터드 | 없음 |
| 온라인 생성 | ONLINE | ALGORITHM=INPLACE | CONCURRENTLY |
| 함수 인덱스 | ✓ | ✓ (가상 컬럼) | ✓ |
| 부분 인덱스 | ✗ | ✗ | ✓ |
| 비트맵 인덱스 | ✓ | ✗ | 실행 시 생성 |
| GIN/GiST | ✗ | ✗ | ✓ |
| 역방향 키 | ✓ | ✗ | ✗ |
| 통계 갱신 | DBMS_STATS | ANALYZE TABLE | ANALYZE |
인덱스 실전 종합 정리
인덱스는 자주 쓰는 쿼리 패턴에 맞춰 설계해야 합니다. 다음 장에서는 데이터 일관성의 핵심인 트랜잭션을 다루겠습니다.