--- id: db-index-strategy title: DB Index 전략 — 만들 것과 만들지 말 것 category: Coding status: draft source_trust_level: B verification_status: conceptual created_at: 2026-05-09 updated_at: 2026-05-09 tags: [database, index, query-optimization, postgres, vibe-coding] tech_stack: { language: "Postgres / MySQL", applicable_to: ["Backend"] } applied_in: [] aliases: [B-tree, composite index, partial index, covering index] --- # DB Index 전략 > 인덱스는 **읽기 빨라지지만 쓰기 느려짐**. 무지성으로 만들지 말고 **EXPLAIN ANALYZE 보고 결정**. composite index 는 **컬럼 순서가 핵심**. 6개 이상 인덱스 가진 테이블은 검토 대상. ## 📖 핵심 개념 - 인덱스는 별도 자료구조 (B-tree). 매 INSERT/UPDATE 마다 갱신. - WHERE / JOIN / ORDER BY 의 등호 / 범위 / 정렬 컬럼이 인덱스 후보. - composite (a, b) 는 (a) 만 검색해도 사용 가능, (b) 만은 X. ## 💻 코드 패턴 ### EXPLAIN ANALYZE 로 결정 ```sql EXPLAIN (ANALYZE, BUFFERS) SELECT * FROM orders WHERE user_id = 123 ORDER BY created_at DESC LIMIT 10; -- Seq Scan ← 느림. user_id 인덱스 추가 후 비교. ``` ### Composite index — 순서가 중요 ```sql -- 자주 함께 검색: (user_id, status, created_at) CREATE INDEX idx_orders_user_status_created ON orders (user_id, status, created_at DESC); -- 사용 가능: -- WHERE user_id = ? -- WHERE user_id = ? AND status = ? -- WHERE user_id = ? AND status = ? AND created_at > ? -- WHERE user_id = ? ORDER BY created_at DESC -- 사용 불가: -- WHERE status = ? (앞 컬럼 skip) ``` 규칙: **= 컬럼 → IN 컬럼 → 범위 컬럼 → 정렬 컬럼** 순서. ### Partial index — 작은 도메인만 ```sql -- 90%의 행이 status='completed' 인 경우, active 만 인덱스 CREATE INDEX idx_orders_active_user ON orders (user_id) WHERE status IN ('pending', 'processing'); -- 인덱스 크기 작음, 갱신 비용 낮음 ``` ### Covering / INCLUDE ```sql CREATE INDEX idx_users_email_inc ON users(email) INCLUDE (name, avatar_url); -- WHERE email = ? + SELECT name, avatar_url 만 → 테이블 안 읽고 인덱스로 종료 ``` ### Functional / Expression ```sql CREATE INDEX idx_users_lower_email ON users (LOWER(email)); -- WHERE LOWER(email) = LOWER(?) 사용 ``` ### Index 미사용 패턴 ```sql -- ❌ 함수 호출 → 인덱스 X WHERE LOWER(email) = 'foo' -- 위 functional 인덱스 없으면 WHERE created_at::date = '2025-01-01' -- → BETWEEN 로 -- ❌ leading wildcard WHERE email LIKE '%@example.com' -- B-tree X. trgm/GIN 필요 -- ❌ OR 가 다른 컬럼 WHERE user_id = 1 OR email = 'x' -- 둘 다 별도 인덱스 + Bitmap OR ``` ## 🤔 의사결정 기준 | 컬럼 | 인덱스? | |---|---| | Primary key | 자동 | | Foreign key | ✅ — JOIN 빈번 | | Unique 제약 | 자동 | | 자주 WHERE | ✅ | | 자주 ORDER BY + LIMIT | ✅ | | 카디널리티 낮음 (boolean) | ❌ — 보통 무용. partial 가능 | | Text 검색 (LIKE %x%) | trgm / GIN | | JSON 안 검색 | GIN on jsonb | | 시계열 최신만 | partial WHERE created_at > now() - interval '30d' | ## ❌ 안티패턴 - **모든 컬럼에 단일 인덱스**: write 폭증 + planner 가 못 고름. composite 가 보통 답. - **composite index 컬럼 순서 무관 가정**: (a, b) 와 (b, a) 다름. EXPLAIN 으로. - **거대 테이블 동기 CREATE INDEX**: write lock 길게. CONCURRENTLY 사용. - **사용 안 되는 인덱스 청소 안 함**: pg_stat_user_indexes idx_scan = 0 인 거 정기 청소. - **VACUUM / ANALYZE 안 함**: 통계 stale → planner 잘못된 선택. - **인덱스 = 만능 가정**: 작은 테이블은 Seq Scan 이 더 빠름. - **timestamp 그대로 인덱스 + 매일 새 값**: 인덱스 끝부분만 hot. BRIN 도 검토. ## 🤖 LLM 활용 힌트 - 새 쿼리 추가 시: "EXPLAIN ANALYZE 결과 + 인덱스 추천" 함께 요청. - composite index 컬럼 순서 = (=) (IN) (range) (order). - Postgres 면 partial / INCLUDE / GIN / BRIN 도 후보. ## 🔗 관련 문서 - [[DB_N_Plus_One]] - [[DB_Migration_Safety]]