icon

안동민 개발노트

9장 : 인덱스

인덱스 실전

이론을 알았으면 실전으로 넘어갑니다. 인덱스를 생성하고, 실행 계획으로 인덱스 사용 여부를 확인하며, 인덱스가 안 타는 함정을 피하고, 실무에서 인덱스를 효과적으로 설계하는 방법을 다룹니다.


인덱스 생성

인덱스 생성 (Oracle)
-- 단일 컬럼 인덱스
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;
인덱스 생성 (MySQL)
-- 단일 컬럼 인덱스
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;
인덱스 생성 (PostgreSQL)
-- 기본 인덱스
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으로 인덱스 사용 확인

인덱스를 만들었다고 반드시 사용되는 것은 아닙니다. 실행 계획을 확인하여 인덱스가 실제로 사용되는지 검증해야 합니다.

실행 계획 확인 (Oracle)
-- 실행 계획 생성
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'));
실행 계획 확인 (MySQL)
-- 기본 실행 계획
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 = 2UNION ALL로 분리
LIKE 앞쪽 와일드카드WHERE name LIKE '%철수'전문 검색 인덱스 사용
NULL 비교WHERE column IS NULLNOT NULL 제약 + 기본값
낮은 선택도WHERE gender = 'M' (50%)인덱스 불필요 (Full Scan이 나음)
산술 연산WHERE salary * 12 > 50000000WHERE salary > 50000000/12

각 원인 상세 분석

4. OR 조건과 UNION ALL
인덱스 비효율
  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)
──── 사용되지 않는 인덱스 찾기 (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 외에도 특수한 상황에 적합한 인덱스 유형이 존재합니다.

비트맵 인덱스 (Oracle)
-- 카디널리티가 낮은 컬럼에 적합
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에는 부적합
-- → 행 잠금이 아닌 비트맵 세그먼트 잠금 → 동시성 저하
부분 인덱스 (PostgreSQL)
-- 활성 주문만 인덱싱 (전체의 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별 인덱스 특성 비교

항목OracleMySQL (InnoDB)PostgreSQL
기본 인덱스B-TreeB+Tree (클러스터드 PK)B-Tree
PK 클러스터링IOT(선택)강제 클러스터드없음
온라인 생성ONLINEALGORITHM=INPLACECONCURRENTLY
함수 인덱스✓ (가상 컬럼)
부분 인덱스
비트맵 인덱스실행 시 생성
GIN/GiST
역방향 키
통계 갱신DBMS_STATSANALYZE TABLEANALYZE

인덱스 실전 종합 정리

인덱스는 자주 쓰는 쿼리 패턴에 맞춰 설계해야 합니다. 다음 장에서는 데이터 일관성의 핵심인 트랜잭션을 다루겠습니다.