← posts/b.log()

blog92@web:~$ cat posts/db-design-15-indexes.md

DATABASE4 min read

DB 설계 A to Z 15강 — 인덱스 설계

인덱스를 테이블이 아니라 쿼리에서 역산하는 절차, 복합 인덱스의 컬럼 순서 규칙, 커버링 인덱스, 인덱스를 타지 못하는 패턴, 그리고 인덱스가 청구하는 쓰기·공간·캐시 비용을 정리한다.

여기까지가 무엇을 저장할지에 대한 결정이었다면, 인덱스부터는 저장한 것을 어떻게 찾을지에 대한 결정입니다.

1. 인덱스는 쿼리에서 역산한다

인덱스 설계의 입력은 테이블 구조가 아니라 2강의 쿼리 프로파일입니다.

쿼리빈도조건정렬
Q1 주문의 주문항목 목록높음order_no = ?item_name
Q2 기간별 처리 실적높음work_date BETWEEN ? AND ?—
Q3 담당자별 최근 실적중간picker_id = ?work_date DESC LIMIT 20
Q4 처리단계·기간별 실적 합계중간stage_code = ? AND work_date BETWEEN ? AND ?—

2. 복합 인덱스 컬럼 순서

B-Tree 복합 인덱스는 앞 컬럼 순으로 정렬됩니다. 따라서 앞 컬럼 조건 없이 뒤 컬럼만으로는 범위를 좁히지 못합니다.

순서 원칙: 등치(Equality) → 정렬(Sort) → 범위(Range)

sql
-- Q4: 등치(stage_code) 먼저, 범위(work_date) 나중
CREATE INDEX ix_log_stage_date ON fulfillment_log (stage_code, work_date);
 
-- Q3: 등치(picker_id) + 정렬(work_date) → 정렬 작업 없이 앞에서 20건만 읽음
CREATE INDEX ix_log_picker_date ON fulfillment_log (picker_id, work_date DESC);

왜 범위를 뒤에 두는가 (work_date, stage_code) 순서라면 날짜 범위의 모든 항목을 스캔하면서 단계코드를 필터링합니다. (stage_code, work_date)는 해당 처리단계의 해당 기간만 정확히 읽습니다.

인덱스Q4 탐색 범위
(work_date, stage_code)기간 내 모든 처리단계 항목
(stage_code, work_date)기간 내 해당 처리단계 항목만

"선택도가 높은 컬럼을 앞에"라는 통념보다 등치/범위 구분이 우선입니다. 선택도는 등치 컬럼들 사이의 순서를 정할 때 참고합니다.

3. 커버링 인덱스

쿼리에 필요한 모든 컬럼이 인덱스에 있으면 테이블 접근을 생략합니다.

sql
-- Q4에서 man_hours 합계까지 인덱스로 해결
-- PostgreSQL 11+, SQL Server: INCLUDE 절 (키가 아닌 부가 컬럼)
CREATE INDEX ix_log_stage_date_cov
  ON fulfillment_log (stage_code, work_date) INCLUDE (man_hours);
 
-- MySQL, Oracle: 키 컬럼에 포함
CREATE INDEX ix_log_stage_date_cov ON fulfillment_log (stage_code, work_date, man_hours);
  • InnoDB 세컨더리 인덱스는 PK 컬럼을 자동 포함하므로, PK를 조회하는 쿼리는 이미 커버됩니다.
  • PostgreSQL의 Index Only Scan은 visibility map에 의존하므로, VACUUM이 밀린 테이블에서는 효과가 떨어집니다. (MVCC 학습 내용과 연결)

4. 인덱스가 사용되지 않는 패턴

패턴예시해결
컬럼 가공WHERE TRUNC(work_date) = ...범위 조건으로 변경 또는 함수 인덱스
암묵적 형변환문자열 컬럼에 숫자 비교 WHERE item_no = 101타입 일치
앞 와일드카드LIKE '%L101'역순 인덱스, trigram(pg_trgm), 전문 검색
선두 컬럼 누락(stage_code, work_date)에 work_date만 조건별도 인덱스 (일부 DBMS의 Skip Scan은 선두 카디널리티가 낮을 때만 유효)
OR 조건WHERE a = ? OR b = ?UNION ALL 분리 또는 비트맵 결합
부정 조건status <> 'DONE'긍정 조건으로 변환, 부분 인덱스

5. 부분 인덱스와 함수 인덱스

sql
-- 미완료 항목처리만 자주 조회 (전체의 5%)
CREATE INDEX ix_is_open ON item_stage (plan_date) WHERE actual_date IS NULL;   -- PG, SQL Server(필터)
 
-- 대소문자 무시 검색
CREATE INDEX ix_picker_email_lower ON picker (lower(email));    -- PG, Oracle, MySQL 8.0.13+

Oracle에는 부분 인덱스가 없지만, 모든 키가 NULL인 행은 인덱싱되지 않는 성질을 이용해 CASE WHEN actual_date IS NULL THEN plan_date END 함수 인덱스로 같은 효과를 냅니다.

6. 인덱스의 비용

비용설명
쓰기INSERT/DELETE마다 모든 인덱스 갱신, UPDATE는 인덱스 컬럼 변경 시
공간테이블보다 인덱스가 큰 경우도 흔함
캐시인덱스도 버퍼 풀을 차지
PG HOT 업데이트인덱스 컬럼을 갱신하면 HOT가 불가 → bloat 증가
옵티마이저인덱스가 많으면 계획 선택 비용 증가, 잘못된 선택 가능성

중복 인덱스 제거: (a)와 (a, b)가 있으면 (a)는 대부분 불필요합니다. (단, (a)가 UNIQUE 제약이면 유지)

7. 인덱스 종류

종류용도지원
B-Tree등치, 범위, 정렬전부
Hash등치만PG, MySQL(MEMORY)
GIN배열, JSONB, 전문 검색PG
GiST / SP-GiST범위, 공간, 근접PG
BRIN물리적으로 정렬된 대용량 (시계열)PG
Bitmap낮은 카디널리티, DWOracle
Columnstore분석 쿼리SQL Server
sql
-- 처리 실적이 날짜 순서대로 쌓인다면 BRIN은 B-Tree의 수백분의 1 크기
CREATE INDEX ix_log_date_brin ON fulfillment_log USING brin (work_date);

8. 인덱스 설계 절차

  1. 쿼리 프로파일에서 빈도 × 비용이 큰 쿼리를 고른다
  2. 각 쿼리에 대해 등치 → 정렬 → 범위 순 후보 인덱스를 만든다
  3. 후보들을 합쳐 공통 접두사를 공유하도록 통합한다
  4. FK 컬럼 인덱스를 추가한다 (13강)
  5. 실행 계획으로 검증한다 (EXPLAIN ANALYZE, EXPLAIN FORMAT=TREE, DBMS_XPLAN)
  6. 운영 중 사용되지 않는 인덱스를 주기적으로 찾는다 (pg_stat_user_indexes, sys.schema_unused_indexes)

15강 정리

  1. 인덱스는 테이블이 아니라 쿼리에서 역산한다.
  2. 복합 인덱스는 등치 → 정렬 → 범위 순서가 기본이다.
  3. 커버링 인덱스는 테이블 접근을 없애지만 PG에서는 VACUUM 상태에 의존한다.
  4. 인덱스는 쓰기·공간·캐시 비용이 있으므로 사용 여부를 계속 점검한다.

더 깊이

COMMENTS (…)

댓글을 불러오는 중이에요.

NEW COMMENT0 / 1000
⌘↵ 전송

blog92@web:~$ cd ..