**현재 분류 구조(##6)**를 보면, “Data and Database Systems > Data Fundamentals > Data Modeling > Physical Design” 하위에 위치하는 것이 논리적으로 맞으며, 중복이나 경계 모호성도 적군요.[2][5]
“Indexing"은 실무자의 검색, 재사용 관점에서 탐색성이 높고 유사 주제들과의 일관성도 확보됩니다.
다만, “Indexing"은 Data Fundamentals의 Data Modeling보다 “Physical Design” 카테고리에서 좀 더 특화로 분류하는 것이 바람직합니다.
개선 제안: “Data Modeling > Physical Design > Indexing"으로 소분류 도입하여, 기본/특화/심화 항목 분리 및 실제적 크로스 도메인 연결(성능, 운영 등) 반영.
색인(Indexing)은 데이터베이스 파일의 물리적 구조에 맞춰 빠른 검색, 효율적 저장, 데이터 무결성 보장 등 데이터 활용 최적화를 목표로 설계되는 원리와 구현 방식이다. 색인은 분류, 구조화, 액세스 패스 선택, 쿼리 최적화 등 물리적 디자인의 핵심 요소이다.[5][1][2]
색인(Indexing)은 DBMS(데이터베이스 관리 시스템)에서 대량의 데이터 중 특정 정보를 신속하게 검색하고 효율적으로 관리하기 위해 사용하는 자료 구조 및 알고리즘의 집합을 의미합니다. 색인은 물리적 설계(Physical Design) 단계에서 테이블의 중요한 컬럼에 추가적으로 생성되며, 주로 B+ 트리, 해시(해싱) 등 다양한 종류의 인덱스 구조가 존재합니다. 데이터의 추출, 정렬, 집계, 조인 등의 쿼리 작업이 반복적으로 발생하는 실무 환경에서, 색인의 적용은 성능 개선과 시스템 자원 절감의 가장 효과적인 방법입니다. 하지만 색인은 저장 공간 증가, 쓰기 작업 지연 등 부작용도 발생하므로, 현업에서는 쿼리 패턴 분석, 데이터 볼륨, 인덱스 유지 관리 등 최적화 전략이 필수적입니다. 최근에는 AI(인공지능) 기반 인덱스 추천, 자동 튜닝 등 최신 기술이 등장하며 색인의 현대적 활용과 운영 효율이 한층 강조되고 있습니다. 색인 설계는 데이터 모델링, 물리적 설계, 운영 및 최적화, 성능 개선까지 전 과정과 연계되어 있으며, 실무 현장에서 데이터 품질과 비용 효율화의 핵심 수단으로 자리합니다.[6][4][1][2][3]
색인(Index, 인덱스): 데이터베이스(DBMS)에서 테이블의 특정 컬럼(열) 혹은 컬럼 조합에 대해 데이터 검색 속도를 획기적으로 높이기 위해 사용하는 자료구조입니다. 인덱스는 (키, 물리적 주소) 쌍으로 정렬된 별도의 저장 구조를 생성하여, 테이블 전체를 순차적으로 검색하지 않고도 데이터 위치를 빠르게 찾을 수 있게 해줍니다.[1][2][3][4]
구성 요소: 주로 검색 키(search key, 인덱싱 할 컬럼 값)와 포인터(pointer, 그 값이 실제 저장된 데이터의 위치)로 구성됩니다.[3][5]
주요 구조: 주로 B-트리(B-Tree), B+트리(B+Tree), 해시(Hash Table) 등으로 구현되며, 정렬된 구조를 통해 이진 탐색이나 트리 검색을 활용합니다. 삼차로 bitmap, inverted 등 특수 인덱스도 존재합니다.[5][3]
유형 및 동작: 프라이머리 인덱스(Primary Index), 세컨더리 인덱스(Secondary Index), 유니크 인덱스(Unique Index), 복합 인덱스(Composite Index)로 분류합니다. 각각 목적, 제약 조건, 성능에 따라 쓰임이 다릅니다.[6][1]
DBMS 연계: 인덱스는 테이블, 스토리지(디스크/메모리), 쿼리 옵티마이저(Query Optimizer)와 밀접하게 연관되어 있으며, SELECT(조회), UPDATE(수정), DELETE(삭제) 동작의 성능에 직접 영향을 줍니다.[7][8][5]
인덱스와 테이블: 테이블의 데이터 행(row)은 별도의 데이터 파일에 저장되고, 인덱스는 이 행들에 대한 빠른 접근을 보장하는 “주소록” 역할을 합니다. 실제 쿼리에서는 WHERE 조건에 맞는 키를 인덱스에서 탐색, 해당 키의 레코드 위치(포인터)로 곧장 점프합니다.[2][4]
검색/삽입/삭제 시 데이터 흐름
검색: 키를 이용해 인덱스를 빠르게 탐색, 포인터를 통해 원하는 데이터에 접근[4][3]
삽입/삭제/수정: 데이터가 바뀌면 인덱스 구조도 실시간 혹은 배치로 동기화됨. 빈번한 쓰기 작업에는 성능 부하가 커질 수 있음
쿼리 옵티마이저와의 연계: DBMS는 쿼리 실행 시 인덱스를 사용할지(=“인덱스를 탄다”) 아니면 풀 스캔(Full Table Scan)을 할지 자동으로 결정합니다. 이때 인덱스 설계가 실질적인 쿼리 성능에 결정적입니다.[8][7][5]
물리적 스토리지 구조: 인덱스는 기본적으로 메모리에 상주하거나, 일부는 보조 저장장치(디스크, SSD 등)에 저장됩니다. 인덱스 파일 크기가 실제 데이터에 비해 작아 메모리 캐시에 효율적으로 로딩될 수 있습니다.[9][5]
**인덱스(Index)**는 데이터베이스 테이블에서 데이터 검색 속도를 높이는 자료 구조이며, 주로 특정 컬럼(열) 또는 열의 조합에 대해 데이터를 빠르게 찾아낼 수 있도록 설계됩니다.[6][2][4][1]
구성 개념: 인덱스는 주로 검색 키(search key, 인덱싱되는 컬럼 값)와 포인터(pointer, 데이터의 실제 위치)로 구성되어 있습니다.[5][3]
주요 구조는 B-트리(B-Tree), B+트리(B+Tree), 해시(Hash Table) 등이며, 상황에 따라 bitmap 인덱스, 복합 인덱스 등도 사용됩니다.[5][3]
인덱스 유형에는 프라이머리 인덱스(Primary Index), 세컨더리 인덱스(Secondary Index), 유니크 인덱스(Unique Index), 복합 인덱스(Composite Index) 등이 포함되어, 적용 목적 및 제약에 따라 설정됩니다.[1][6]
**DBMS(데이터베이스 관리 시스템)**의 쿼리 옵티마이저(Query Optimizer)는 실행 계획 수립 시 인덱스를 사용할지, 또는 전체 테이블 스캔(Full Table Scan)을 수행할지 자동으로 판단하고 인덱스 선택이 성능에 직접적인 영향을 줍니다.[7][8][5]
등장 배경: 1970년대 E.F. Codd의 관계형 데이터 모델 제안 이후, 데이터량 증가·실시간 질의(검색) 요구가 늘면서 효율적 데이터 접근 방식이 필수로 대두되었습니다. 초기엔 테이블 전체 순회 방식이 표준이었으나, 곧 대용량과 복잡한 질의 대응이 불가능해졌습니다.[5][6][7][1]
발전 과정: 단일 컬럼 인덱스에서 시작해 여러 컬럼 복합 인덱스, B-트리(B-Tree), B+트리(B+Tree), 해시 인덱스(Hash Index), 비트맵 인덱스(Bitmap Index), 부분 인덱스 및 다차원(Spatial) 인덱스 등으로 진화했습니다. 최근은 AI·ML 기반 자동 인덱스 추천과 NoSQL 등 특화 환경에 맞는 신종 인덱스도 많이 활용되고 있습니다.[8][9][10][11]
대형 금융, 포털, 이커머스 등 데이터/트래픽 중심 기업에서는 정교한 인덱스 설계가 서비스 성능 및 품질의 핵심 경쟁력. 클라우드 PaaS(Platform as a Service), DBaaS(Database as a Service)에서도 자동 인덱싱 기능이 기본 장착되는 추세.[11][16][9][10]
| 장점 | 상세 설명 | 기술 근거 | 적용 상황 | 실무적 가치 |
|------------------------|---------------------------------------------------------------------------------------------------------------------------|------------------------------------------------------------------|----------------------------------|----------------------------------|
| 검색 속도/성능 향상 | WHERE, JOIN, ORDER BY, GROUP BY 등에서 전체 테이블 순회 대신 키-포인터 구조로 대폭 빠른 데이터 조회 구현 | B트리, B+트리, 해시 등 자료구조 기반 \(O(\log N)\) 탐색 | 대용량 DB, 빈번한 검색 질의 | 실시간 서비스·대량 트래픽 대응 |
| 연산 부하 감소 | 빠른 검색으로 인해 전체 시스템 CPU/메모리/디스크 부하와 응답 시간이 줄어듦 | DBMS의 내부 옵티마이저 최적화 로직 영향 | 웹/금융/이커머스 등 실시간 환경 | 사용자 경험·운영비 절감 |
| 정렬·집계 성능 개선 | 데이터 정렬된 상태 유지, ORDER BY·MIN/MAX·집계 쿼리에서 별도 정렬 과정 없이 즉시 추출 가능 | 인덱스 내부 정렬 자료구조 활용 | 주기적 리포팅·데이터 대시보드 | 분석·리포팅 자동화 |
| 무결성·유일성 보장 | PRIMARY KEY, UNIQUE 제약조건 인덱스를 통해 중복 방지, 데이터 정합성 유지 | 인덱스 기반 유니크 체크 로직 | ID·계좌번호 등 고유키 관리 | 데이터 품질 보증 |
| JOIN·복합 연산 성능 | 테이블간 JOIN, 다양한 복합 쿼리에서 조건 컬럼별 인덱스 활용해 연산 처리 속도 비약적 향상 | 옵티마이저의 인덱스 기반 JOIN 경로 선택 | 데이터 마트/분석 환경 | 쿼리 비용/자원 소모 최소화 |
| 단점 | 상세 설명 | 원인 | 실무에서 발생되는 문제 | 완화/해결 방안 | 대안 기술 |
|-------------------|---------------------------------------------------------------------|-------------------------------|----------------------------------------|------------------------------|--------------------|
| 저장 공간 증가 | 인덱스 자체가 별도 자료구조로 저장, DB 크기의 5~20% 추가 소모 | 별도 트리/해시 구조 유지 | 디스크 용량 부족, 비용 증가 | 필요 컬럼 최소 설계, 주기 삭제| 인메모리 캐시 등 |
| 쓰기 성능 저하 | INSERT/UPDATE/DELETE 시 인덱스도 함께 갱신하므로 성능 저하 | 인덱스 실시간 동기화 | 배치 작업·실시간 변동시 처리 지연 | 배치 인덱싱, 쓰기컬럼 제외 | 로그기반 분석 등 |
| 관리 복잡성 | 인덱스 과다·비효율적 설계시 오히려 성능 저하, 튜닝 업무 증가 | 인덱스 관리·옵티마이저 부담 | 쿼리/운영 장애, DBA 업무과중 | 쿼리패턴 분석, 자동추천 활용 | AI기반 튜닝 |
| 병렬성 감소 | 인덱스 컬럼에 동시 쓰기 동작시 잠금 경합, 병행성이 저하될 수 있음 | Row-level Lock 등 동시성 한계 | 다중 사용자 삽입·수정시 성능 저하 | 파티셔닝, 분산 인덱스 적용 | NoSQL 구조 |
| 제약사항 | 상세 설명 | 원인 | 영향 | 완화/해결 방안 | 대안 기술 |
|------------------|--------------------------------------------------------|--------------------|------------------|------------------------------------|------------------------|
| 값 다양성 요구 | 중복도 높은(카디널리티 낮음) 컬럼은 인덱스 효율 급감 | 데이터 분포 특성 | 성능 효과 미미 | 값 분포 높은 컬럼 중심 설계 | 비트맵 인덱스, 통계기법|
| 업무부하 적합성 | 잦은 데이터 쓰기·수정 환경에서는 인덱스 효과 감소 | 동적 DB 트랜잭션 | 트랜잭션 지연 | 읽기/쓰기 분리, 인덱스 최소화 설계 | 파티션·쓰기가중 분리 |
| 물리적 DBMS별 차이| 각 DBMS마다 인덱스 구조·옵션 다름(이식성/호환성 이슈) | 구현/아키텍처 상이 | 이식성·튜닝 한계 | DBMS별 최적화 가이드 활용 | 추상화 계층 도입 |
| 분류 기준 | 유형 | 주요 특징 및 목적 | 적용 예시 |
|------------------|------------------|-------------------------------------------------------|--------------------------|
| 구조적 분류 | 클러스터형(Clustered) | 데이터 행이 인덱스 순서에 맞춰 물리적으로 저장됨. 테이블당 1개 한정 | 보통 PK, MySQL InnoDB |
| | 비클러스터형(Non-Clustered) | 인덱스/데이터가 별개로 저장. 여러 개 생성 가능 | 추가 색인, MSSQL 등 |
| 목적별 분류 | 프라이머리 인덱스(Primary) | PK 기반 인덱스, 데이터 정렬과 직접 연결됨 | 기본키 |
| | 세컨더리 인덱스(Secondary) | PK 이외 컬럼에 대한 색인, 데이터의 위치만 참조 | 서브쿼리, 다양한 필터 |
| 특수 인덱스 | 유니크 인덱스(Unique) | 중복 불가 조건, 데이터 무결성 확보 | 주민번호, ID |
| | 복합 인덱스(Composite) | 여러 컬럼을 동시에 묶어서 인덱싱 | (성별, 나이) 복합조건 |
| 자료구조 분류 | B트리/B+트리 인덱스 | 최다 사용, 범위검색/정렬 지원, RDBMS 기본 구조 | 대부분 DBMS, MySQL |
| | 해시 인덱스(Hash) | 등식 비교 최적화, 범위불가, 빠른 단일 값 탐색 | NoSQL, 분산 DB |
| | 비트맵 인덱스(Bitmap) | 값 종류 적을 때(카디널리티↓), 대용량 집계에 유리 | 집계/분석 특화 DB |
| | 공간 인덱스(Spatial) | 좌표, 다차원 데이터 검색 최적화 | GIS/IoT 등 |
-- 회원 테이블 생성
CREATETABLEmember(idINTPRIMARYKEYAUTO_INCREMENT,nameVARCHAR(40),ageINT,emailVARCHAR(100));-- 이름 컬럼 인덱스 생성
CREATEINDEXidx_member_nameONmember(name);
## 용어 정리
| 카테고리 | 용어 | 정의 | 관련 개념 | 실무 활용 |
|----------|-------------|------------------------------------------------------------------|-------------------------|----------------------|
| 핵심 | 인덱스(Index)| DB 내 컬럼 값 기반 빠른 검색 지원 위한 자료구조 | 테이블, 키-포인터 구조 | 쿼리 속도 개선 |
| 구현 | B+트리(B+Tree)| 균형 잡힌 트리 기반 인덱스 자료구조 | 정렬, 빠른 탐색 | 범위검색/정렬 최적화 |
| 구현 | 해시 인덱스(Hash Index)| 해시 함수로 값 버킷 매핑해 검색 최적화 | 등식 비교, 해시 테이블 | 단일 값 빠른 접근 |
| 운영 | 리빌드(Rebuild)| 인덱스 구조 재생성 통한 성능 및 조각화 해소 | 인덱스 튜닝, 조각화 | 정기 유지보수 |
| 보안 | TLS 암호화 | 데이터 전송 시 암호화 기법 | 보안, 네트워크 | 개인정보·민감정보 보호|
Data & Database Systems > Data Fundamentals > Data Modeling > Physical Design > Indexing
└─ 크로스 레퍼런스: Data Operations > Database Optimization
└─ 크로스 레퍼런스: Data Structures and Algorithms > Core Concepts > Tree Structures & Algorithms
인덱싱(Indexing)은 데이터베이스에서 데이터 검색 성능을 획기적으로 향상시키기 위한 핵심 자료구조 및 기법입니다. 책의 색인처럼 데이터의 위치 정보를 별도로 관리하여, 전체 데이터를 순차 탐색하지 않고도 원하는 레코드를 빠르게 찾을 수 있게 합니다. B-Tree, Hash, Bitmap 등 다양한 인덱스 구조가 존재하며, 각각 특정 쿼리 패턴과 데이터 특성에 최적화되어 있습니다. 조회 성능 향상과 쓰기 오버헤드 간의 트레이드오프를 이해하고 적절히 설계하는 것이 데이터베이스 성능 최적화의 핵심입니다.
인덱싱은 대용량 데이터베이스 환경에서 효율적인 데이터 접근을 가능하게 하는 필수적인 기술입니다. 1970년대 관계형 데이터베이스의 등장과 함께 발전해온 인덱싱 기술은, 데이터의 물리적 저장 구조와는 독립적으로 논리적 접근 경로를 제공하여 쿼리 성능을 수십 배에서 수천 배까지 향상시킬 수 있습니다.
인덱스는 본질적으로 <키, 포인터> 쌍으로 구성된 정렬된 자료구조로, 특정 값을 빠르게 찾고 해당 데이터의 물리적 위치로 직접 접근할 수 있게 합니다. 이는 O(n)의 선형 탐색을 O(log n) 또는 O(1)로 개선하는 알고리즘적 최적화입니다.
학습 방향성은 다음과 같습니다: (1) 기본 개념과 동작 원리 이해, (2) B-Tree, Hash 등 주요 인덱스 구조의 알고리즘적 특성 파악, (3) 인덱스 설계 시 고려사항과 트레이드오프 분석, (4) 복합 인덱스, 커버링 인덱스 등 고급 기법 습득, (5) 실제 데이터베이스 시스템에서의 구현과 최적화 전략 적용. 이론적 기반과 실무 적용 능력을 균형있게 발전시키는 것이 중요합니다.
**인덱스(Index)**는 데이터베이스 테이블의 검색 속도를 향상시키기 위해 특정 컬럼의 값과 해당 레코드의 물리적 위치를 매핑한 별도의 자료구조입니다.
graph LR
A[쿼리 요청] --> B{인덱스 존재?}
B -->|Yes| C[인덱스 탐색<br/>O log n]
B -->|No| D[전체 테이블 스캔<br/>O n]
C --> E[데이터 직접 접근]
D --> F[순차 스캔]
E --> G[결과 반환]
F --> G
style C fill:#90EE90
style D fill:#FFB6C6
style E fill:#90EE90
style F fill:#FFB6C6
핵심 구성 요소:
검색 키(Search Key): 인덱스를 구성하는 컬럼(들)
인덱스 엔트리(Index Entry): <키 값, 레코드 포인터> 쌍
인덱스 구조(Index Structure): 데이터를 조직화하는 방식 (B-Tree, Hash 등)
graph TB
A[Indexing 개념 체계] --> B[기본 구조]
A --> C[알고리즘]
A --> D[성능 특성]
B --> B1[클러스터드<br/>Clustered]
B --> B2[논클러스터드<br/>Non-Clustered]
B --> B3[단일/복합<br/>Single/Composite]
C --> C1[B-Tree 계열<br/>균형 트리]
C --> C2[Hash 계열<br/>해시 함수]
C --> C3[Bitmap<br/>비트 배열]
C --> C4[특수 인덱스<br/>Full-text 등]
D --> D1[조회 성능 ↑]
D --> D2[쓰기 오버헤드 ↑]
D --> D3[저장 공간 ↑]
D --> D4[메모리 사용 ↑]
C1 --> E1[범위 검색 최적]
C2 --> E2[등가 검색 최적]
C3 --> E3[카디널리티 낮을 때]
C4 --> E4[특수 검색 요구]
style A fill:#87CEEB
style D1 fill:#90EE90
style D2 fill:#FFB6C6
style D3 fill:#FFB6C6
style D4 fill:#FFB6C6
인덱싱은 데이터베이스 관리 시스템(DBMS, Database Management System)에서 테이블의 데이터를 빠르게 검색하기 위해 특정 컬럼(또는 컬럼들의 조합)의 값을 키로 사용하여 해당 레코드의 물리적 위치 정보를 저장한 별도의 자료구조를 생성하고 관리하는 기법입니다.
도서관 장서 검색 시스템
├─ 책 자체 (실제 데이터)
│ └─ 서가에 물리적으로 배치
│
├─ 청구기호 카드 (인덱스)
│ ├─ 저자명 색인 → 책 위치
│ ├─ 제목 색인 → 책 위치
│ └─ 주제 색인 → 책 위치
│
└─ 검색 방법
├─ 색인 없이: 모든 서가 순회 (전체 테이블 스캔)
└─ 색인 활용: 카드로 빠른 검색 → 해당 위치로 직행 (인덱스 스캔)
수학적 정의
테이블 T를 n개의 레코드 집합이라 할 때:
T = {r₁, r₂, …, rₙ}
인덱스 I는 매핑 함수: I: K → P
K: 검색 키 공간 (Search Key Space)
P: 레코드 포인터 공간 (Pointer Space)
I(k) = p, 여기서 k는 검색 키, p는 레코드 위치
핵심 구성 요소 상세
graph TB
subgraph "인덱스 구조"
A[검색 키<br/>Search Key] --> B[인덱스 엔트리<br/>Index Entry]
C[레코드 포인터<br/>Record Pointer] --> B
B --> D[인덱스 페이지<br/>Index Page]
end
subgraph "실제 데이터"
E[데이터 레코드<br/>Data Record] --> F[데이터 페이지<br/>Data Page]
end
D -.포인터 참조.-> F
style B fill:#87CEEB
style D fill:#87CEEB
style E fill:#90EE90
style F fill:#90EE90
문제 상황
└─ 대용량 데이터에서 특정 레코드 검색
├─ 순차 탐색: O(n) 시간 복잡도
├─ 1억 건 테이블: 평균 5천만 번 비교
└─ 허용 불가능한 응답 시간
해결 필요성
└─ 검색 시간을 로그 또는 상수 시간으로 단축
├─ O(log n): B-Tree 계열
├─ O(1): Hash Index
└─ 디스크 I/O 최소화
-- 두 테이블 조인
SELECTo.order_id,c.customer_nameFROMordersoJOINcustomerscONo.customer_id=c.customer_id;-- 인덱스 없이 Nested Loop Join
-- 외부 테이블 각 행마다 내부 테이블 전체 스캔
-- 복잡도: O(n × m)
자원 사용 비교
전체 테이블 스캔:
├─ CPU: 모든 레코드 비교 연산
├─ 메모리: 대량 데이터 버퍼링
├─ 디스크 I/O: 모든 페이지 읽기
└─ 네트워크: 대량 데이터 전송 (분산 환경)
인덱스 스캔:
├─ CPU: 인덱스 탐색 연산 (최소)
├─ 메모리: 인덱스 페이지만 로드
├─ 디스크 I/O: 필요한 페이지만 읽기
└─ 네트워크: 최소 데이터 전송
목적 3: 동시성(Concurrency) 향상
쿼리 실행 시간 단축 → 락(Lock) 유지 시간 감소
대기 시간 감소 → 처리량(Throughput) 증가
더 많은 동시 사용자 지원 가능
목적 4: 확장성(Scalability) 확보
graph LR
A[데이터 증가] --> B{인덱스 전략}
B -->|적절한 인덱스| C[로그 증가<br/>O log n]
B -->|인덱스 없음| D[선형 증가<br/>O n]
C --> E[확장 가능]
D --> F[확장 불가]
style C fill:#90EE90
style D fill:#FFB6C6
style E fill:#90EE90
style F fill:#FFB6C6
데이터가 10배 증가할 때:
인덱스 없음: 검색 시간 약 10배 증가
B-Tree 인덱스: 검색 시간 약 1.5배 증가 (log₁₀(10n) ≈ log₁₀(n) + 1)
-- 분석 대상
1.WHERE절에자주사용되는컬럼2.JOIN조건에사용되는컬럼3.ORDERBY,GROUPBY에사용되는컬럼4.쿼리실행빈도및중요도-- 예시 분석
SELECTcolumn_name,COUNT(*)asusage_countFROMquery_logWHEREclause_type='WHERE'GROUPBYcolumn_nameORDERBYusage_countDESC;
워크로드 유형별 인덱스 전략
OLTP (Online Transaction Processing)
├─ 특징: 쓰기 빈번, 짧은 트랜잭션
├─ 전략: 필수 인덱스만 최소한으로
└─ 이유: 쓰기 오버헤드 최소화
OLAP (Online Analytical Processing)
├─ 특징: 읽기 중심, 대량 데이터 집계
├─ 전략: 다양한 인덱스 적극 활용
└─ 이유: 쿼리 성능 최대화
Hybrid (HTAP)
├─ 특징: 혼합 워크로드
├─ 전략: 시간대별 인덱스 전환 또는 분리
└─ 이유: 양쪽 요구사항 균형
3. 트레이드오프 인식
graph TB
A[인덱스 추가] --> B[장점]
A --> C[비용]
B --> B1[조회 성능 향상]
B --> B2[정렬 작업 감소]
B --> B3[조인 성능 개선]
C --> C1[쓰기 성능 저하]
C --> C2[저장 공간 증가]
C --> C3[메모리 사용 증가]
C --> C4[유지보수 복잡도 증가]
style B fill:#90EE90
style C fill:#FFB6C6
전통적 접근 (물리적 포인터)
├─ 계층형 DBMS: 레코드 간 직접 포인터
├─ 문제: 데이터 재배치 시 모든 포인터 갱신 필요
└─ 결과: 논리적 데이터 독립성 부재
인덱스 접근 (논리적 키)
├─ 관계형 DBMS: 키 값으로 간접 참조
├─ 장점: 물리적 위치 변경 시 인덱스만 갱신
└─ 결과: 논리적-물리적 독립성 달성
차별점:
배열/연결리스트: 위치 기반 접근, 재배치 불가
인덱스: 키 기반 접근, 물리적 재구성 유연
특징 2: 자기 균형 (Self-Balancing) 구조
B-Tree 계열의 자동 균형 유지:
graph TB
subgraph "삽입 시 자동 재조정"
A[새 키 삽입] --> B{노드 포화?}
B -->|Yes| C[노드 분할<br/>Split]
B -->|No| D[단순 삽입]
C --> E[부모로 중간값 승격]
E --> F{부모 포화?}
F -->|Yes| C
F -->|No| G[완료]
D --> G
end
style C fill:#FFE4B5
style E fill:#FFE4B5
-- 개발자는 논리적 쿼리만 작성
SELECT*FROMordersWHEREorder_dateBETWEEN'2024-01-01'AND'2024-12-31'ANDstatus='completed';-- 옵티마이저가 자동으로 결정
-- 1. 어떤 인덱스를 사용할지
-- 2. 인덱스를 사용할지 전체 스캔할지
-- 3. 여러 인덱스를 결합할지 (Index Merge)
비용 기반 최적화 (CBO)
├─ 통계 정보 수집
│ ├─ 테이블 크기 (레코드 수)
│ ├─ 인덱스 선택도
│ └─ 데이터 분포 (히스토그램)
│
├─ 실행 계획 후보 생성
│ ├─ Index Scan
│ ├─ Index Seek
│ ├─ Full Table Scan
│ └─ Index Merge
│
└─ 비용 계산 및 최적 계획 선택
├─ I/O 비용
├─ CPU 비용
└─ 네트워크 비용 (분산 환경)
동시성 제어 메커니즘
├─ 인덱스 래칭 (Latching)
│ └─ 매우 짧은 시간 동안 인덱스 구조 보호
│
├─ 인덱스 락킹 (Locking)
│ ├─ 키 범위 락 (Key-Range Lock)
│ └─ 팬텀 리드 방지
│
└─ 로깅 (Logging)
├─ 인덱스 변경 로그 기록
└─ 복구 시 인덱스 재구성
기술적 근거:
sequenceDiagram
participant T1 as Transaction 1
participant IX as Index
participant T2 as Transaction 2
T1->>IX: 키 K1 검색 및 락 획득
T2->>IX: 키 K1 검색 시도
IX-->>T2: 대기 (락 획득 실패)
T1->>IX: 변경 완료 및 락 해제
IX-->>T2: 락 획득 성공
T2->>IX: 키 K1 검색 완료
1. 최소 노드 수를 가진 B-Tree 구성:
- 루트: 1개 키
- 레벨 1: 2개 노드, 각 t-1개 키 → 2(t-1)개 키
- 레벨 2: 2t개 노드, 각 t-1개 키 → 2t(t-1)개 키
- 레벨 h: 2t^(h-1)개 노드, 각 t-1개 키 → 2t^(h-1)(t-1)개 키
2. 총 키 개수:
n ≥ 1 + 2(t-1) + 2t(t-1) + ... + 2t^(h-1)(t-1)
n ≥ 1 + 2(t-1)(1 + t + t² + ... + t^(h-1))
n ≥ 1 + 2(t-1) · (t^h - 1)/(t - 1)
n ≥ 1 + 2(t^h - 1)
n ≥ 2t^h - 1
3. 따라서:
n + 1 ≥ 2t^h
(n + 1)/2 ≥ t^h
log_t((n + 1)/2) ≥ h
∴ h ≤ log_t((n + 1)/2) ∎
정리 2: 탐색 시간 복잡도
정리: n개의 키를 가진 B-Tree에서 특정 키 탐색은 O(log n) 시간에 수행된다.
1. 하한 (Lower Bound):
- 최소 n개의 키 저장 필요
- Ω(n)
2. 상한 (Upper Bound):
- 각 노드 최대 2t-1개 키
- 노드 수 ≤ n/(t-1)
- 각 노드 포인터 수 ≤ 2t
- 총 공간 ≤ n + n/(t-1) · 2t = O(n)
∴ 공간 복잡도 = Θ(n) ∎
정리: 비교 기반 탐색의 정보 이론적 하한은 Ω(log n)이다.
증명 (결정 트리 모델):
1. n개 원소 중 1개를 찾는 결정 트리
2. 리프 노드 수 ≥ n (각 원소마다 하나)
3. 이진 트리 높이 h ≥ log₂ n
4. 비교 횟수 ≥ h
∴ Ω(log n) ∎
결론: B-Tree의 O(log n)은 점근적 최적
전통 인덱스: 트리/해시 자료구조
Learned Index: 머신러닝 모델
위치 예측 모델:
f(key) → position
where position ≈ actual_position ± ε
예시:
키 범위 [1, 1000000]가 균등 분포
→ 선형 모델: f(k) = k (매우 정확)
구조:
graph TB
A[Root Model<br/>Neural Network] --> B[Model Layer 1<br/>여러 전문 모델]
A --> C[Model Layer 1]
A --> D[Model Layer 1]
B --> E[Data Layer<br/>실제 데이터 배열]
C --> F[Data Layer]
D --> G[Data Layer]
style A fill:#87CEEB
style E fill:#90EE90
style F fill:#90EE90
style G fill:#90EE90
거래 시스템 (OLTP)
├─ 초당 수만 건의 거래 처리
├─ 밀리초 단위 응답 시간 요구
└─ 99.999% 가용성 필요
핵심 인덱스 전략:
├─ 계좌번호, 거래ID: Clustered B-Tree
├─ 고객 조회: Composite Index (이름+생년월일)
└─ 실시간 분석: In-Memory Columnstore
검색 최적화
├─ 상품 검색: Full-Text Index
├─ 카테고리 필터: Bitmap Index
├─ 가격 범위: B-Tree Index
└─ 재고 관리: 복합 인덱스
주문 처리
├─ 주문번호: Primary Key (Clustered)
├─ 사용자ID: Non-Clustered Index
└─ 주문일시: Partition + Index
타임라인 생성
├─ 팔로우 관계: 그래프 인덱스
├─ 게시물 시간순: Time-Series Index
└─ 해시태그: Inverted Index
검색 기능
├─ 사용자명: Prefix Index
├─ 위치 기반: Geospatial Index (R-Tree)
└─ 콘텐츠: Full-Text Index
전자 건강 기록 (EHR)
├─ 환자 ID: Clustered Index
├─ 진단명: Multi-Value Index (ICD 코드)
├─ 투약 이력: Temporal Index
└─ 의료 영상: BLOB + 메타데이터 인덱스
규정 준수
├─ 감사 로그: 시계열 인덱스
├─ 개인정보: 암호화 + 인덱스
└─ 접근 제어: 복합 인덱스
통화 기록 (CDR)
├─ 통화자 번호: Hash Index
├─ 통화 시각: Partition + Time Index
├─ 통화 유형: Bitmap Index
└─ 통화 지역: Geospatial Index
네트워크 모니터링
├─ 장비 ID: Clustered Index
├─ 이벤트 타임스탬프: B-Tree
└─ 알람 유형: Covering Index
플레이어 데이터
├─ 플레이어 ID: Primary Key
├─ 레벨/랭킹: 정렬 인덱스
├─ 길드/친구: 그래프 인덱스
└─ 인벤토리: JSON 인덱스
리더보드
├─ 점수 기반: Sorted Set (Redis)
├─ 실시간 갱신: In-Memory Index
└─ 시즌별: Partition Index
위치 추적
├─ 배송 ID: Primary Key
├─ 현재 위치: Geospatial Index
├─ 배송 상태: Partial Index
└─ 예상 도착: Time-Window Index
경로 최적화
├─ 출발지/도착지: Composite Spatial Index
├─ 교통 상황: Time-Series Index
└─ 차량 정보: Clustered Index
1억 건 데이터 검색
이진 탐색 트리 (fanout=2):
├─ 높이: log₂(100,000,000) ≈ 27
└─ 디스크 I/O: 27회
B-Tree (fanout=100):
├─ 높이: log₁₀₀(100,000,000) ≈ 4
└─ 디스크 I/O: 4회
개선: 85% 감소
defbtree_search(node,key):"""B-Tree 검색 알고리즘"""i=0# 현재 노드에서 키 위치 찾기whilei<node.num_keysandkey>node.keys[i]:i+=1# 키를 찾은 경우ifi<node.num_keysandkey==node.keys[i]:returnnode.pointers[i]# 레코드 포인터 반환# 리프 노드인데 못 찾은 경우ifnode.is_leaf:returnNone# 내부 노드: 적절한 자식으로 재귀returnbtree_search(node.children[i],key)
defbtree_insert(tree,key):"""B-Tree 삽입 알고리즘"""root=tree.root# 루트가 꽉 찬 경우ifroot.is_full():new_root=Node()new_root.children[0]=rootsplit_child(new_root,0)# 루트 분할tree.root=new_rootinsert_non_full(tree.root,key)definsert_non_full(node,key):"""여유 있는 노드에 삽입"""i=node.num_keys-1ifnode.is_leaf:# 리프 노드: 적절한 위치에 삽입whilei>=0andkey<node.keys[i]:node.keys[i+1]=node.keys[i]i-=1node.keys[i+1]=keynode.num_keys+=1else:# 내부 노드: 적절한 자식 찾기whilei>=0andkey<node.keys[i]:i-=1i+=1# 자식이 꽉 찬 경우 분할ifnode.children[i].is_full():split_child(node,i)ifkey>node.keys[i]:i+=1insert_non_full(node.children[i],key)
Case 1: 리프 노드에서 삭제
├─ 여유 있음 (>t-1 키): 단순 제거
└─ 부족함 (≤t-1 키): 재조정 필요
Case 2: 내부 노드에서 삭제
├─ 왼쪽 자식 여유: 선행자(predecessor)로 대체
├─ 오른쪽 자식 여유: 후행자(successor)로 대체
└─ 둘 다 부족: 병합 후 재귀 삭제
Case 3: 재조정
├─ 형제에서 차용 (rotation)
└─ 형제와 병합 (merge)
sequenceDiagram
participant App as 애플리케이션
participant Parser as 파서
participant Optimizer as 옵티마이저
participant Executor as 실행 엔진
participant Buffer as 버퍼 풀
participant Index as 인덱스
participant Disk as 디스크
App->>Parser: SQL 쿼리
Parser->>Optimizer: 파싱된 쿼리 트리
Optimizer->>Optimizer: 통계 정보 조회
Optimizer->>Optimizer: 실행 계획 생성
Optimizer->>Executor: 최적 실행 계획
Executor->>Buffer: 인덱스 페이지 요청
alt 버퍼에 있음
Buffer-->>Executor: 캐시된 페이지 반환
else 버퍼에 없음
Buffer->>Disk: 디스크 I/O 요청
Disk-->>Buffer: 페이지 로드
Buffer-->>Executor: 페이지 반환
end
Executor->>Index: 인덱스 탐색
Index-->>Executor: 레코드 포인터
Executor->>Buffer: 데이터 페이지 요청
Buffer-->>Executor: 데이터 반환
Executor->>App: 결과 집합
-- 입력 쿼리
SELECTcustomer_name,order_dateFROMordersWHEREorder_id=12345;-- 파싱 결과 (추상 구문 트리)
Query:SELECT:-customer_name-order_dateFROM:-ordersWHERE:-order_id=12345
옵티마이저 의사결정 과정:
1. 가능한 실행 계획 열거
Plan A: Full Table Scan
Plan B: Index Seek on order_id
Plan C: Index Scan + Filter
2. 통계 정보 수집
- 테이블 크기: 1,000,000 rows
- order_id 인덱스 선택도: 0.999999 (거의 고유)
- 페이지 수: 10,000 pages
3. 비용 계산
Plan A:
└─ I/O: 10,000 pages × 10ms = 100,000ms
Plan B:
└─ I/O: log₁₀₀(1,000,000) ≈ 3 pages × 10ms = 30ms
+ 1 data page = 40ms
Plan C:
└─ I/O: 5,000 pages × 10ms = 50,000ms
4. 최적 계획 선택: Plan B
defexecute_index_seek(index,key,table):"""인덱스 Seek 실행"""# 1. 인덱스 탐색pointer=index.search(key)ifpointerisNone:return[]# 2. 데이터 페이지 접근page_id,slot_id=pointerpage=buffer_pool.get_page(page_id)# 3. 레코드 추출record=page.get_slot(slot_id)# 4. 투영(Projection)result={'customer_name':record['customer_name'],'order_date':record['order_date']}return[result]
버퍼 풀 (Buffer Pool)
├─ 크기: 예) 1GB (262,144 페이지 @4KB)
├─ 구조: 해시 테이블 + LRU 리스트
└─ 정책: Least Recently Used
동작:
1. 페이지 요청
2. 해시 테이블에서 검색
├─ Hit: LRU 리스트 앞으로 이동
└─ Miss: 디스크 로드 + 가장 오래된 페이지 교체
Transaction T1: UPDATE orders SET status = 'shipped' WHERE order_id = 100
Transaction T2: SELECT * FROM orders WHERE order_id = 100
타임라인:
T1: BEGIN
T1: 인덱스에서 order_id=100 검색
T1: 인덱스 엔트리에 X-Lock 획득
T1: 데이터 페이지에 X-Lock 획득
T1: status 업데이트
T2: BEGIN
T2: 인덱스에서 order_id=100 검색
T2: 인덱스 엔트리에 S-Lock 요청
T2: WAIT (T1의 X-Lock과 충돌)
T1: COMMIT
T1: 모든 락 해제
T2: S-Lock 획득 성공
T2: 데이터 읽기
T2: COMMIT
버전 관리:
- 각 행은 여러 버전 보유
- 인덱스는 모든 버전 가리킴
- Visibility Check로 올바른 버전 선택
검색 흐름:
1. 인덱스에서 키 검색
2. 모든 매칭 버전의 포인터 획득
3. 각 버전의 트랜잭션 ID 확인
4. 현재 트랜잭션에 보이는 버전 필터링
5. 최종 결과 반환
// B-Tree 노드의 물리적 구조
structBTreeNode{// 헤더 정보
uint16_tnode_type;// 내부/리프 구분
uint16_tnum_keys;// 현재 키 개수
uint32_tparent_pointer;// 부모 노드 포인터
uint32_tright_sibling;// 오른쪽 형제 (리프만)
// 키 배열 (정렬됨)
KeyTypekeys[MAX_KEYS];// 최대 2t-1개
// 포인터 배열
union{// 내부 노드: 자식 노드 포인터
uint32_tchildren[MAX_CHILDREN];// 최대 2t개
// 리프 노드: 레코드 포인터
RecordPointerrecords[MAX_KEYS];};// 체크섬 (무결성 검증)
uint32_tchecksum;};// 레코드 포인터 구조
structRecordPointer{uint32_tpage_id;// 데이터 페이지 ID
uint16_tslot_id;// 페이지 내 슬롯 번호
};
B+ Tree 완전한 구조 (차수 t=3)
레벨 0 (루트):
[50]
/ \
레벨 1 (내부): [20,35] [65,80]
/ | \ / | \
레벨 2 (리프): [10] [25] [40] [55] [70] [90]
↔ ↔ ↔ ↔ ↔ ↔
(양방향 연결 리스트)
각 노드 저장:
- 내부: 키 + 자식 포인터
- 리프: 키 + 레코드 포인터 + 다음 리프 포인터
Hash Index 구성
Directory (메타데이터):
+------------------+
| Hash Function | → hash(key) % bucket_count
| Bucket Count | → 1024
| Global Depth | → log₂(buckets)
| Load Factor | → 0.75
+------------------+
Bucket Array:
+--------+--------+--------+--------+
| Ptr 0 | Ptr 1 | Ptr 2 | ... 1023|
+--------+--------+--------+--------+
| | |
v v v
Bucket 0 Bucket 1 Bucket 2
[k1,p1] [k5,p5] [k3,p3]
[k2,p2] [k8,p8] [k7,p7]
... ... ...
확장 가능 해싱 (Extendible Hashing):
- 디렉토리 배가: 버킷 포화 시
- 버킷 분할: 충돌 많을 때
동적 확장 과정:
graph TB
A[버킷 포화 감지] --> B{Global Depth<br/>증가 필요?}
B -->|Yes| C[디렉토리 배가]
B -->|No| D[버킷만 분할]
C --> E[새 버킷 할당]
D --> E
E --> F[엔트리 재분배]
F --> G[Local Depth 갱신]
style A fill:#FFE4B5
style G fill:#90EE90
테이블 데이터:
Row_ID | Gender | Country
-------|--------|----------
1 | M | USA
2 | F | UK
3 | M | USA
4 | F | USA
5 | M | UK
Bitmap Index on Gender:
M: 10101
F: 01010
Bitmap Index on Country:
USA: 10110
UK: 01001
쿼리: WHERE Gender='M' AND Country='USA'
결과: 10101 AND 10110 = 10100
→ Row 1, 3
Run-Length Encoding (RLE):
원본: 00000001110000011
압축: (7,0)(3,1)(6,0)(2,1)
= 7개 0, 3개 1, 6개 0, 2개 1
Word-Aligned Hybrid (WAH):
31-bit 워드 단위로 압축
- Literal word: 실제 비트 31개
- Fill word: 같은 비트 반복 횟수
장점:
- 압축 상태에서 비트 연산 가능
- 메모리 효율 10-100배 향상
행 저장 (Row-Store):
페이지 1: [ID=1,Name='Alice',Age=30,City='NYC']
페이지 1: [ID=2,Name='Bob',Age=25,City='LA']
페이지 2: [ID=3,Name='Charlie',Age=35,City='NYC']
...
컬럼 저장 (Column-Store):
ID 세그먼트: [1,2,3,4,5,...]
Name 세그먼트: ['Alice','Bob','Charlie',...]
Age 세그먼트: [30,25,35,...]
City 세그먼트: ['NYC','LA','NYC',...]
쿼리: SELECT AVG(Age) FROM users;
행 저장: 모든 페이지 읽기
컬럼 저장: Age 세그먼트만 읽기
1. 비압축 비트맵:
- 각 비트맵: n비트
- k개 비트맵 AND: k-1회 연산
- 각 AND: n번 비트 연산
- 총: O(n·k)
2. 워드 단위 압축 (WAH):
- n비트를 n/w개 워드로 압축
- 워드 단위 AND: O(1)
- k개 비트맵: O(k) 워드당
- 총 워드 수: O(n/w)
T(n,k,w) = O((n/w)·k) = O(n·k/w)
3. 최적화:
w = 32 또는 64 (CPU 워드 크기)
→ 32-64배 가속
∴ 워드 단위 비트맵 AND = O(n·k/w) ∎
정리 9: 커버링 인덱스 I/O 절감
정리: 커버링 인덱스는 쿼리당 평균 I/O를 O(log n + k)에서 O(log n)으로 감소시킨다. (k는 결과 행 수)
문제: B-Tree의 동시성 제어
- 기존 방법: 루트부터 리프까지 락 보유
- 문제점: 동시성 저하, 데드락 위험
해결: B-link Tree
- 각 노드에 right-link 추가
- 래치를 짧게 보유 (crabbing)
- 구조 변경 중에도 읽기 가능
알고리즘:
1. 탐색 중 노드 N 접근
2. 목표 키 k > N.max_key?
→ right-link 따라 이동
3. 래치 해제하고 다음 노드로
defbtree_search_concurrent(root,key):"""동시성을 고려한 B-Tree 검색"""current=rootcurrent.latch_shared()# S-latchwhilenotcurrent.is_leaf():# 목표 키 위치 찾기i=find_child_index(current,key)next_node=current.children[i]# 다음 노드 래치 획득next_node.latch_shared()# 현재 노드 래치 해제 (커플링)current.unlatch()current=next_node# 리프 노드에서 검색result=current.search_key(key)current.unlatch()returnresult
PostgreSQL 방식:
- 인덱스는 모든 버전 가리킴
- Heap Tuple에 버전 정보 저장
- Visibility Check로 올바른 버전 선택
문제: 죽은 튜플 누적
해결: Vacuum 프로세스
Oracle 방식:
- Undo 세그먼트로 이전 버전 관리
- 인덱스는 최신 버전만 가리킴
- 읽기 일관성을 위해 Undo 조회
장단점:
PostgreSQL: 읽기 빠름, Vacuum 오버헤드
Oracle: 쓰기 빠름, Undo 공간 필요
CAP Theorem (Brewer, 2000):
- Consistency: 일관성
- Availability: 가용성
- Partition Tolerance: 분할 내성
분산 인덱스 선택:
├─ CP 시스템 (일관성 + 분할 내성)
│ └─ 예: HBase, MongoDB
│ └─ 강한 일관성, 일시적 불가용
│
└─ AP 시스템 (가용성 + 분할 내성)
└─ 예: Cassandra, Riak
└─ 최종 일관성, 항상 가용
인덱스 영향:
- CP: 글로벌 인덱스, 분산 트랜잭션
- AP: 로컬 인덱스, 최종 일관성
의사결정 트리:
1. 인덱스 후보 열거
└─ WHERE, JOIN, ORDER BY 컬럼의 인덱스
2. 각 후보 비용 계산
├─ Index Scan Cost
├─ Full Table Scan Cost
└─ Index Merge Cost (여러 인덱스 조합)
3. 최소 비용 계획 선택
if min_cost(index_scan) < full_scan_cost:
use index_scan
else:
use full_table_scan
4. 힌트 고려 (있는 경우)
override optimizer decision
ARC (Adaptive Replacement Cache):
- LRU와 LFU의 하이브리드
- 인덱스 페이지와 데이터 페이지 균형
구조:
T1: 최근 한 번 접근 (LRU)
T2: 최근 여러 번 접근 (LFU)
B1: T1에서 방출된 페이지 목록
B2: T2에서 방출된 페이지 목록
적응:
- B1 히트 → T1 크기 증가
- B2 히트 → T2 크기 증가
인덱스 이점:
- 내부 노드: T2 (자주 접근)
- 리프 노드: T1 (한 번 접근)
defadaptive_prefetch(index,query):"""적응적 프리페칭"""ifquery.type=='RANGE':# 범위 검색: 시퀀셜 프리페치prefetch_next_leaves(index,count=4)elifquery.type=='POINT':# 포인트 검색: 경로 프리페치prefetch_index_path(index,query.key)elifindex.correlation>0.8:# 높은 상관관계: 데이터도 프리페치prefetch_data_pages(index,query)
계층적 락킹:
테이블 락
├─ Intent-Shared (IS)
├─ Intent-Exclusive (IX)
└─ Shared-Intent-Exclusive (SIX)
|
└─ 인덱스 락
├─ Key-Range Lock
│ └─ 팬텀 방지
└─ Individual Key Lock
└─ 일반 충돌 방지
예시:
Transaction T1: UPDATE ... WHERE id=100
1. IX lock on table
2. X lock on index entry (key=100)
3. X lock on data row
Transaction T2: SELECT ... WHERE id=100
1. IS lock on table
2. S lock on index entry (key=100) → WAIT
Hot/Warm/Cold 분류:
Hot Data (SSD/NVMe):
├─ 자주 접근하는 인덱스 리프
├─ 인덱스 내부 노드
└─ 최근 데이터
Warm Data (SSD):
├─ 보통 접근 인덱스
└─ 중간 시기 데이터
Cold Data (HDD/Object Storage):
├─ 거의 접근 안 함
└─ 아카이브 데이터
마이그레이션:
access_count > threshold_hot → move to SSD
access_count < threshold_cold → move to HDD
압축 수준 결정:
Level 0: 압축 없음
- CPU: 최소
- 저장 공간: 최대
- 사용: Hot 인덱스
Level 1: 경량 압축 (Prefix)
- CPU: 낮음
- 저장 공간: 30% 절감
- 사용: Warm 인덱스
Level 2: 중간 압축 (Dictionary)
- CPU: 중간
- 저장 공간: 50% 절감
- 사용: Cold 인덱스
Level 3: 고압축 (Zstd)
- CPU: 높음
- 저장 공간: 70% 절감
- 사용: Archive 인덱스
사례: 대량 INSERT 성능 저하
상황:
- 일일 1억 건 로그 데이터 적재
- 테이블에 5개 인덱스 존재
- 배치 작업 시간: 8시간 → 18시간 초과
원인 분석:
1. 각 INSERT마다 5개 인덱스 갱신
2. 랜덤 키로 인한 페이지 분할 빈발
3. 인덱스 단편화로 I/O 증가
해결 방안:
1. 적재 전 인덱스 DROP
2. BULK INSERT (인덱스 없음): 1.5시간
3. 정렬 후 인덱스 REBUILD: 2시간
4. 총 시간: 3.5시간 (5배 개선)
교훈:
- 대량 작업 시 인덱스 재구성 전략 필수
- 쓰기 중심 워크로드는 인덱스 최소화
워크로드 비율 기반 결정:
읽기 90% : 쓰기 10% (OLAP, 보고서)
└─> 공격적 인덱싱 전략
├─ 다양한 복합 인덱스
├─ 커버링 인덱스
└─ 부분 인덱스
읽기 70% : 쓰기 30% (OLTP, 일반 웹)
└─> 균형 인덱싱 전략
├─ 핵심 쿼리 인덱스
├─ 복합 인덱스 선택적
└─ 정기 검토
읽기 50% : 쓰기 50% (실시간 스트리밍)
└─> 보수적 인덱싱 전략
├─ Primary/Foreign Key만
├─ 필수 검색 인덱스 최소
└─ Write-Optimized 고려
트레이드오프 2: 공간 효율 vs 성능
선택지 분석:
graph LR
A[저장 공간] -->|트레이드오프| B[쿼리 성능]
C[압축 인덱스] -->|공간 ↓ 30%| A
C -->|성능 ↓ 10%| B
D[비압축 인덱스] -->|공간 ↑ 100%| A
D -->|성능 ↑ 최고| B
E[부분 인덱스] -->|공간 ↓ 50%| A
E -->|성능 ↑ 특정 쿼리| B
style A fill:#FFB6C6
style B fill:#90EE90
1. 저장 비용이 주요 관심사인가?
├─ Yes → 압축 또는 부분 인덱스
└─ No → 2번으로
2. 쿼리 패턴이 명확한가?
├─ Yes → 부분 인덱스
└─ No → 3번으로
3. 성능이 최우선인가?
├─ Yes → Full Index (비압축)
└─ No → 압축 인덱스
4. 모니터링 후 지속적 조정
시나리오: WHERE a=1 AND b=2 쿼리
옵션 A: 복합 인덱스 (a, b)
장점:
+ 해당 쿼리 최적 (한 번에 해결)
+ 저장 공간 효율
+ 정렬 지원 (ORDER BY a, b)
단점:
- 'b'만 조건일 때 사용 불가
- 'a' 순서 의존성
- 유연성 부족
옵션 B: 단일 인덱스 두 개 (a), (b)
장점:
+ 'a'만 또는 'b'만 쿼리 지원
+ 유연성 높음
+ 순서 무관
단점:
- Index Merge 오버헤드
- 저장 공간 2배
- AND 조건 시 느릴 수 있음
-- 패턴 1: 항상 함께 조회 → 복합 인덱스
-- 쿼리: WHERE user_id=? AND created_date BETWEEN ? AND ?
CREATEINDEXidx_user_dateONorders(user_id,created_date);-- 패턴 2: 독립적 조회 빈번 → 단일 인덱스
-- 쿼리 A: WHERE status=?
-- 쿼리 B: WHERE category=?
CREATEINDEXidx_statusONproducts(status);CREATEINDEXidx_categoryONproducts(category);-- 패턴 3: 하이브리드 → 복합 + 단일
-- 쿼리 A: WHERE country=? AND city=?
-- 쿼리 B: WHERE city=?
CREATEINDEXidx_country_cityONusers(country,city);CREATEINDEXidx_cityONusers(city);-- city만 조회 지원
클러스터드 인덱스 선택 시:
✓ 범위 검색이 매우 빈번한 컬럼
✓ GROUP BY, ORDER BY 자주 사용
✓ 순차 증가 키 (성능 좋음)
✓ 주로 읽기 중심 워크로드
예: PRIMARY KEY on (order_date)
→ 날짜별 조회, 집계 쿼리 최적
논클러스터드 인덱스 선택 시:
✓ 포인트 검색(등가 조건)
✓ 여러 검색 경로 필요
✓ 쓰기 빈번한 환경
✓ 저선택도 컬럼도 인덱싱 필요
예: INDEX on (user_email)
→ 로그인, 이메일 검색
실시간 인덱스 갱신:
├─ 장점:
│ ├─ 즉시 검색 가능
│ ├─ 데이터 일관성 보장
│ └─ 구현 단순
│
└─ 단점:
├─ 각 쓰기마다 오버헤드
├─ 동시성 경합
└─ 예측 불가능한 지연
배치 인덱스 갱신:
├─ 장점:
│ ├─ 쓰기 처리량 높음
│ ├─ 효율적 재구성
│ └─ 예측 가능한 성능
│
└─ 단점:
├─ 검색 지연 (staleness)
├─ 복잡한 동기화
└─ 추가 인프라 필요
classHybridIndexStrategy:"""
Delta + Base 인덱스 전략
"""def__init__(self):self.base_index=BTreeIndex()# 대용량, 주기적 재구성self.delta_index=LSMTreeIndex()# 최근 변경, 실시간 갱신defsearch(self,key):# 1. Delta 인덱스 먼저 검색 (최신 데이터)result=self.delta_index.search(key)ifresult:returnresult# 2. Base 인덱스 검색returnself.base_index.search(key)defmerge_periodically(self):# 주기적으로 Delta → Base 병합ifself.delta_index.size()>THRESHOLD:self.base_index.merge(self.delta_index)self.delta_index.clear()
특성:
├─ OLTP + OLAP 혼합
├─ 실시간 대시보드
├─ 트랜잭션 + 분석
└─ 높은 성능 요구
하이브리드 전략:
✓ Row-Store: B-Tree (트랜잭션)
✓ Column-Store: Columnstore (분석)
✓ 메모리 인덱스 (속도)
✓ 분리된 워크로드 처리
기술 선택:
- MemSQL, SAP HANA (통합)
- PostgreSQL + Citus (분산)
- MySQL + TiDB (HTAP)
적합도: ⭐⭐⭐⭐ (기술 성숙도 따라)
-- 패턴 1: 등가 검색
-- WHERE column = value
-- 최적: Hash Index (O(1))
-- 차선: B-Tree Index (O(log n))
CREATEINDEXidx_hashONtableUSINGHASH(column);-- 패턴 2: 범위 검색
-- WHERE column BETWEEN v1 AND v2
-- 최적: B-Tree Index
-- 부적합: Hash Index (범위 불가)
CREATEINDEXidx_btreeONtable(column);-- 패턴 3: 패턴 매칭
-- WHERE column LIKE 'prefix%'
-- 최적: B-Tree (prefix 검색 가능)
-- 부적합: Hash (LIKE 불가)
-- WHERE column LIKE '%suffix'
-- 최적: Full-Text Index, Trigram
CREATEINDEXidx_trigramONtableUSINGGIN(columngin_trgm_ops);-- 패턴 4: 다중 조건 AND
-- WHERE a=? AND b=? AND c=?
-- 최적: 복합 인덱스 (a, b, c)
-- 차선: Index Merge (개별 인덱스)
CREATEINDEXidx_compositeONtable(a,b,c);-- 패턴 5: 다중 조건 OR
-- WHERE a=? OR b=?
-- 최적: Bitmap Index Scan
-- 차선: 두 개별 인덱스 UNION
-- Bitmap 지원 시:
CREATEBITMAPINDEXidx_aONtable(a);CREATEBITMAPINDEXidx_bONtable(b);-- 패턴 6: 정렬
-- ORDER BY column
-- 최적: 해당 컬럼 B-Tree Index
-- 효과: 정렬 단계 생략
CREATEINDEXidx_orderONtable(column);-- 패턴 7: 집계
-- SELECT COUNT(*), SUM(value) WHERE condition
-- 최적: 커버링 인덱스
CREATEINDEXidx_coveringONtable(condition_col)INCLUDE(value);-- 패턴 8: 조인
-- FROM t1 JOIN t2 ON t1.id = t2.fk
-- 최적: t2.fk에 인덱스
CREATEINDEXidx_fkONt2(fk);
유연성:
├─ 하드웨어 직접 제어
├─ 고급 옵션 활용
├─ 세밀한 튜닝
└─ 비용 예측 가능
권장 전략:
✓ NVMe SSD 활용 (저지연)
✓ NUMA 고려 인덱스 배치
✓ RAID 구성 최적화
✓ 메모리 극대화 (인덱스 상주)
하드웨어 최적화:
- CPU: 높은 코어 클럭 (단일 쿼리)
- Memory: 큰 버퍼 풀 (인덱스 캐싱)
- Storage: NVMe RAID 0 (읽기 중심)
제약사항:
├─ 제한된 메모리 (<1GB)
├─ 느린 스토리지
├─ 낮은 CPU 성능
└─ 배터리 제약 (모바일)
적합 전략:
✓ 최소 인덱스 (필수만)
✓ 압축 인덱스 (메모리 절약)
✓ In-Memory DB (SQLite)
✓ 단순 구조 (B-Tree 기본)
기술 선택:
- SQLite: 임베디드 최적
- RocksDB: 모바일 고성능
- LevelDB: 경량 key-value
정리: 비교 기반 자료구조는 Ω(log n) 검색 시간이 하한이다.
증명 스케치:
1. n개 원소를 구별하려면 log₂ n 비트 정보 필요
2. 각 비교는 1비트 정보 제공 (>, =, <)
3. 따라서 최소 log₂ n 비교 필요
결론: B-Tree의 O(log n)은 점근적 최적
정리: 검색 시간 t와 공간 s는 다음을 만족:
t × s = Ω(n)
즉, 시간을 O(1)로 하려면 공간 Ω(n) 필요
(예: Hash Index)
증명:
1. 각 쿼리는 최대 s개 위치 접근
2. n개 원소 구별하려면 ≥ n/s 쿼리 필요
3. t ≥ n/s → t × s ≥ n ∎
다차원 인덱스 (R-Tree) 한계:
차원 d에서:
- 공간 분할 효율: O(2^d) 영역 필요
- 범위 검색: O(n^(1-1/d) + k)
- 고차원(d > 20): 거의 O(n) 성능
실무 영향:
- 저차원 (d ≤ 3): R-Tree 효과적 (GIS)
- 중차원 (3 < d ≤ 10): 특수 기법 필요
- 고차원 (d > 10): 차원 축소 또는 근사 검색
External Memory Model (Aggarwal & Vitter):
파라미터:
- M: 메모리 크기
- B: 블록 크기
- N: 데이터 크기
B-Tree 분석:
- 최적 차수: B (블록당 B개 키)
- 높이: log_B(N/B)
- I/O 복잡도: O(log_B(N/B))
Cache-Oblivious B-Tree:
- 파라미터 없이 캐시 최적
- 모든 메모리 계층에서 효율적
classBTreeNode:"""B-Tree 노드 구현"""def__init__(self,t,is_leaf=False):self.t=t# 최소 차수self.keys=[]# 키 배열 (최대 2t-1개)self.children=[]# 자식 포인터 (최대 2t개)self.is_leaf=is_leafself.n=0# 현재 키 개수defsearch(self,key):"""키 검색 (재귀적)"""i=0# 현재 노드에서 키 위치 찾기whilei<self.nandkey>self.keys[i]:i+=1# 키를 찾은 경우ifi<self.nandkey==self.keys[i]:returnself# 리프 노드에서 못 찾음ifself.is_leaf:returnNone# 적절한 자식으로 재귀returnself.children[i].search(key)definsert_non_full(self,key):"""여유 있는 노드에 키 삽입"""i=self.n-1ifself.is_leaf:# 리프: 정렬 유지하며 삽입self.keys.append(None)whilei>=0andkey<self.keys[i]:self.keys[i+1]=self.keys[i]i-=1self.keys[i+1]=keyself.n+=1else:# 내부 노드: 자식 찾기whilei>=0andkey<self.keys[i]:i-=1i+=1# 자식이 꽉 찬 경우 분할ifself.children[i].n==2*self.t-1:self.split_child(i)ifkey>self.keys[i]:i+=1self.children[i].insert_non_full(key)defsplit_child(self,i):"""자식 노드 분할"""t=self.tfull_child=self.children[i]new_child=BTreeNode(t,full_child.is_leaf)# 중간 키 찾기mid_key=full_child.keys[t-1]# 오른쪽 절반을 새 노드로new_child.keys=full_child.keys[t:]new_child.n=t-1# 자식이 내부 노드면 포인터도 분할ifnotfull_child.is_leaf:new_child.children=full_child.children[t:]# 왼쪽 절반만 남기기full_child.keys=full_child.keys[:t-1]full_child.n=t-1ifnotfull_child.is_leaf:full_child.children=full_child.children[:t]# 부모에 중간 키 삽입self.keys.insert(i,mid_key)self.children.insert(i+1,new_child)self.n+=1classBTree:"""B-Tree 구현"""def__init__(self,t):self.root=BTreeNode(t,True)self.t=tdefsearch(self,key):returnself.root.search(key)definsert(self,key):root=self.root# 루트가 꽉 찬 경우ifroot.n==2*self.t-1:new_root=BTreeNode(self.t,False)new_root.children.append(self.root)new_root.split_child(0)self.root=new_rootself.root.insert_non_full(key)defrange_search(self,start_key,end_key):"""범위 검색 구현"""results=[]# 시작 키 위치 찾기node=self._find_leaf(start_key)# 리프 노드 순회하며 범위 내 키 수집whilenode:forkeyinnode.keys:ifstart_key<=key<=end_key:results.append(key)elifkey>end_key:returnresults# 다음 리프로 (B+ Tree의 경우)node=node.next_leafifhasattr(node,'next_leaf')elseNonereturnresultsdef_find_leaf(self,key):"""키가 속할 리프 노드 찾기"""node=self.rootwhilenotnode.is_leaf:i=0whilei<node.nandkey>node.keys[i]:i+=1node=node.children[i]returnnode
classHashIndex:"""확장 가능 해시 인덱스 (Extendible Hashing)"""def__init__(self,initial_depth=2):self.global_depth=initial_depthself.directory_size=2**initial_depthself.directory=[Bucket()for_inrange(self.directory_size)]defhash_function(self,key):"""해시 함수 (상위 비트 사용)"""returnhash(key)&((1<<self.global_depth)-1)definsert(self,key,value):"""키-값 쌍 삽입"""bucket_index=self.hash_function(key)bucket=self.directory[bucket_index]# 버킷에 공간 있으면 삽입ifnotbucket.is_full():bucket.insert(key,value)return# 버킷 포화: 분할 필요ifbucket.local_depth==self.global_depth:# 디렉토리 배가self._double_directory()# 버킷 분할self._split_bucket(bucket_index)# 재시도self.insert(key,value)defsearch(self,key):"""키로 값 검색"""bucket_index=self.hash_function(key)bucket=self.directory[bucket_index]returnbucket.search(key)def_double_directory(self):"""디렉토리 크기 2배 증가"""self.global_depth+=1self.directory_size*=2# 기존 디렉토리 복제new_directory=self.directory+self.directoryself.directory=new_directorydef_split_bucket(self,index):"""버킷 분할"""old_bucket=self.directory[index]old_bucket.local_depth+=1# 새 버킷 생성new_bucket=Bucket(local_depth=old_bucket.local_depth)# 엔트리 재분배forkey,valueinold_bucket.entries[:]:new_index=self.hash_function(key)ifnew_index!=index:old_bucket.entries.remove((key,value))new_bucket.entries.append((key,value))# 디렉토리 업데이트step=2**(self.global_depth-new_bucket.local_depth)foriinrange(index+step,self.directory_size,step*2):self.directory[i]=new_bucketclassBucket:"""해시 버킷"""def__init__(self,capacity=4,local_depth=0):self.capacity=capacityself.local_depth=local_depthself.entries=[]# [(key, value), ...]defis_full(self):returnlen(self.entries)>=self.capacitydefinsert(self,key,value):self.entries.append((key,value))defsearch(self,key):fork,vinself.entries:ifk==key:returnvreturnNone
fromcollectionsimportdefaultdictimportreimportmathclassFullTextIndex:"""TF-IDF 기반 전문 검색 인덱스"""def__init__(self):self.inverted_index=defaultdict(list)# {term: [(doc_id, positions)]}self.doc_count=0self.doc_lengths={}# {doc_id: length}self.idf_scores={}# {term: idf}deftokenize(self,text):"""텍스트 토큰화 (소문자 변환, 특수문자 제거)"""text=text.lower()tokens=re.findall(r'\b\w+\b',text)returntokensdefbuild(self,documents):"""문서 집합으로부터 인덱스 구축"""self.doc_count=len(documents)# 역색인 구축fordoc_id,textinenumerate(documents):tokens=self.tokenize(text)self.doc_lengths[doc_id]=len(tokens)# 각 토큰의 위치 기록forpos,terminenumerate(tokens):self.inverted_index[term].append((doc_id,pos))# IDF 점수 계산forterm,postingsinself.inverted_index.items():doc_freq=len(set(doc_idfordoc_id,_inpostings))self.idf_scores[term]=math.log(self.doc_count/doc_freq)defsearch(self,query,top_k=10):"""쿼리에 대한 문서 검색 (TF-IDF 랭킹)"""query_terms=self.tokenize(query)# 문서별 점수 계산scores=defaultdict(float)forterminquery_terms:iftermnotinself.inverted_index:continueidf=self.idf_scores[term]postings=self.inverted_index[term]# TF-IDF 계산fordoc_id,_inpostings:tf=sum(1ford,_inpostingsifd==doc_id)tf=tf/self.doc_lengths[doc_id]# 정규화scores[doc_id]+=tf*idf# 상위 k개 문서 반환ranked=sorted(scores.items(),key=lambdax:x[1],reverse=True)returnranked[:top_k]defphrase_search(self,phrase):"""구문 검색 (연속된 단어)"""terms=self.tokenize(phrase)ifnotterms:return[]# 첫 번째 단어의 포스팅 리스트ifterms[0]notinself.inverted_index:return[]candidates=self.inverted_index[terms[0]]# 나머지 단어들이 연속되는지 확인fori,terminenumerate(terms[1:],1):iftermnotinself.inverted_index:return[]next_postings=self.inverted_index[term]new_candidates=[]fordoc_id,posincandidates:# 다음 위치에 다음 단어가 있는지 확인if(doc_id,pos+i)innext_postings:new_candidates.append((doc_id,pos))candidates=new_candidatesreturnlist(set(doc_idfordoc_id,_incandidates))# 사용 예시docs=["database indexing performance optimization","full text search engine implementation","database performance tuning techniques"]index=FullTextIndex()index.build(docs)# 검색results=index.search("database performance")print(f"검색 결과: {results}")# [(0, score1), (2, score2)]# 구문 검색phrase_results=index.phrase_search("database performance")print(f"구문 검색: {phrase_results}")# [2]
실시간 갱신 인덱스:
├─ B-Tree: 즉시 갱신, 트랜잭션 일관성
├─ Hash: 즉시 갱신, 간단한 구조
└─ 장점: 항상 최신 상태
단점: 쓰기 오버헤드 높음
쓰기 최적화 인덱스:
├─ LSM-Tree: 메모리 버퍼 → 디스크 병합
├─ Append-Only: 추가만, 주기적 압축
└─ 장점: 높은 쓰기 처리량
단점: 읽기 증폭, 지연된 반영
하이브리드:
├─ Delta + Base: 최근 변경 + 주 인덱스
├─ Hot + Cold: 활성 + 아카이브
└─ 장점: 읽기/쓰기 균형
단점: 복잡한 관리
분류 기준 6: 분산 환경 인덱스
유형
범위
특징
장점
단점
사용 시나리오
로컬 인덱스 (Local Index)
샤드/파티션 내부
각 노드 독립 인덱스
쓰기 빠름, 관리 단순
크로스 샤드 느림
샤딩 키 포함 쿼리
글로벌 인덱스 (Global Index)
전체 클러스터
통합 인덱스
모든 쿼리 빠름
분산 트랜잭션 필요
샤딩 키 없는 쿼리
파티션 인덱스 (Partitioned)
파티션별
파티션 키 기반 분산
병렬 처리, 독립 관리
파티션 키 의존
시계열, 지역별 데이터
실무 선택 가이드
graph TD
A[인덱스 선택 시작] --> B{쿼리 패턴?}
B -->|등가 검색만| C[Hash Index]
B -->|범위 검색| D[B-Tree/B+ Tree]
B -->|텍스트 검색| E[Full-Text Index]
B -->|공간 검색| F[R-Tree/Spatial]
D --> G{카디널리티?}
G -->|높음| H[B-Tree]
G -->|낮음| I[Bitmap Index]
H --> J{쓰기 빈도?}
J -->|높음| K[LSM-Tree]
J -->|낮음| H
I --> L{읽기 패턴?}
L -->|복잡 조건| I
L -->|단순| H
style C fill:#90EE90
style H fill:#90EE90
style E fill:#87CEEB
style F fill:#87CEEB
style K fill:#FFE4B5
Elasticsearch/OpenSearch:
├─ 역색인 (Inverted Index)
│ ├─ Lucene 기반
│ ├─ 분산 샤딩
│ └─ 실시간 인덱싱
│
├─ 인덱스 타입
│ ├─ text: 전문 검색
│ ├─ keyword: 정확한 매칭
│ ├─ numeric: 숫자 범위
│ ├─ geo_point: 위치 검색
│ └─ nested: 중첩 객체
│
└─ 최적화 기법
├─ 샤드 분산
├─ 레플리카 복제
└─ 세그먼트 병합
Apache Solr:
├─ Lucene 코어
├─ 패싯 검색
├─ 하이라이팅
└─ 제안 (Suggestion)
Sphinx:
├─ Full-Text 특화
├─ 실시간 인덱스
└─ 분산 검색
분산 인덱스 패턴:
1. Sharding + Local Index
예: MySQL Sharding
├─ 각 샤드에 독립 인덱스
├─ 샤딩 키 포함 쿼리 최적
└─ 크로스 샤드 쿼리 느림
2. Global Secondary Index
예: DynamoDB GSI
├─ 클러스터 전체 인덱스
├─ 모든 쿼리 지원
└─ 최종 일관성 (eventually consistent)
3. Distributed Hash Table
예: Cassandra
├─ 해시 링 기반
├─ 파티션 키 필수
└─ 수평 확장 용이
4. Consistent Hashing + Range Index
예: CockroachDB
├─ 자동 리밸런싱
├─ B-Tree 인덱스
└─ 분산 트랜잭션
# Python - SQLAlchemyfromsqlalchemyimportIndex,Column,Integer,String# 단일 인덱스idx=Index('ix_user_email',User.email)# 복합 인덱스idx=Index('ix_user_name_email',User.name,User.email)# 함수 인덱스fromsqlalchemyimportfuncidx=Index('ix_lower_email',func.lower(User.email))# 부분 인덱스idx=Index('ix_active_users',User.email,postgresql_where=(User.active==True))
-- ANSI SQL 표준 인덱스 생성 (기본)
CREATEINDEXindex_nameONtable_name(column_name);-- 고유 인덱스 (SQL-92)
CREATEUNIQUEINDEXindex_nameONtable_name(column_name);-- 복합 인덱스
CREATEINDEXindex_nameONtable_name(col1,col2,col3);-- 인덱스 삭제
DROPINDEXindex_name;-- 표준에서 명시하지 않은 부분 (DBMS 확장):
-- - 인덱스 유형 지정 (USING)
-- - 부분 인덱스 (WHERE)
-- - 함수 인덱스
-- - 인덱스 힌트
-- ANSI INFORMATION_SCHEMA (표준)
SELECTtable_name,index_name,column_name,ordinal_positionFROMinformation_schema.statisticsWHEREtable_schema='mydb'ORDERBYtable_name,index_name,ordinal_position;-- PostgreSQL 시스템 카탈로그
SELECTschemaname,tablename,indexname,indexdefFROMpg_indexesWHEREschemaname='public';-- MySQL SHOW 구문
SHOWINDEXFROMtable_name;-- Oracle 데이터 딕셔너리
SELECTtable_name,index_name,column_name,column_positionFROMuser_ind_columnsORDERBYtable_name,index_name,column_position;
ACID 속성과 인덱스:
1. Atomicity (원자성):
- 인덱스 변경도 트랜잭션 일부
- COMMIT/ROLLBACK 시 함께 처리
2. Consistency (일관성):
- 인덱스는 항상 데이터와 동기화
- UNIQUE 제약 위반 방지
3. Isolation (격리성):
- 격리 수준에 따른 인덱스 락
- Serializable: 범위 락 (key-range lock)
- Read Committed: 키 락만
4. Durability (영속성):
- WAL(Write-Ahead Logging)로 보장
- 인덱스 변경도 로그 기록
-- SQL 표준 권한 관리
-- 인덱스 생성 권한
GRANTINDEXONtable_nameTOuser_name;-- 인덱스 삭제 권한
GRANTALTERONtable_nameTOuser_name;-- PostgreSQL 역할 기반
CREATEROLEindex_manager;GRANTCREATEONSCHEMApublicTOindex_manager;GRANTUSAGEONSCHEMApublicTOindex_manager;-- 시스템 카탈로그 접근 제한
REVOKESELECTONpg_catalog.pg_indexFROMpublic;
AWS RDS:
├─ 인덱스 생성: 제한 없음
├─ 파라미터 제약: 일부 고급 옵션 불가
├─ Performance Insights: 인덱스 권장
└─ 자동 백업: 인덱스 포함
Azure SQL:
├─ Intelligent Insights: 자동 인덱스 제안
├─ 자동 튜닝: 인덱스 자동 생성/삭제
├─ Hyperscale: 대용량 인덱스 지원
└─ 탄력적 풀: 리소스 공유
Google Cloud SQL:
├─ Query Insights: 성능 분석
├─ 인덱스 권장: 자동 제안
├─ 고가용성: 인덱스 동기화
└─ 읽기 복제본: 인덱스 복제
개념: 공통 접두사를 제거하여 노드당 더 많은 키 저장
일반 B-Tree 노드:
[database, dataflow, datasheet, dataviz]
→ 4개 키, 각 8-9 bytes
Prefix B-Tree 노드:
prefix: "data"
[base, flow, sheet, viz]
→ 4개 키, 각 4-5 bytes + prefix 4 bytes
→ 약 50% 공간 절약
구현:
class PrefixBTreeNode:
def __init__(self):
self.prefix = "" # 공통 접두사
self.suffixes = [] # 접미사 배열
def compress(self, keys):
# 최장 공통 접두사 찾기
self.prefix = os.path.commonprefix(keys)
self.suffixes = [k[len(self.prefix):] for k in keys]
def search(self, key):
if not key.startswith(self.prefix):
return None
suffix = key[len(self.prefix):]
return binary_search(self.suffixes, suffix)
장점:
- 메모리 사용량 30-70% 감소
- 노드당 더 많은 키 → 높이 감소
- 캐시 효율성 향상
적용: 문자열 키, 공통 접두사 많은 데이터
개념: 노드 크기를 자식 수에 맞춰 동적 조정
노드 타입:
- Node4: 최대 4개 자식 (작은 배열)
- Node16: 최대 16개 자식 (배열)
- Node48: 최대 48개 자식 (인덱스 배열)
- Node256: 최대 256개 자식 (직접 배열)
메모리 사용:
일반 Trie: 256 포인터 × 8 bytes = 2KB/노드
ART Node4: 4 포인터 × 8 + 4 키 = 36 bytes
성능:
- 검색: O(k), k = 키 길이
- 메모리: 일반 Trie의 1/10
- 캐시: 우수 (작은 노드)
구현 개념:
class ARTNode:
def grow(self):
if isinstance(self, Node4) and len(self.children) > 4:
return self.to_node16()
elif isinstance(self, Node16) and len(self.children) > 16:
return self.to_node48()
# ...
def shrink(self):
if isinstance(self, Node16) and len(self.children) <= 4:
return self.to_node4()
# ...
사용:
- 인메모리 DB 인덱스
- 라우팅 테이블
- IP 주소 검색
-- PostgreSQL 예시
CREATETABLEorders(order_idSERIALPRIMARYKEY,customer_idINTEGERNOTNULL,order_dateDATENOTNULL,total_amountDECIMAL(10,2)NOTNULL,statusVARCHAR(20)NOTNULL,created_atTIMESTAMPDEFAULTCURRENT_TIMESTAMP);-- 100만 건 테스트 데이터 생성
INSERTINTOorders(customer_id,order_date,total_amount,status)SELECT(random()*10000)::integer,-- 10,000명의 고객
CURRENT_DATE-(random()*365)::integer,-- 최근 1년
(random()*1000)::numeric(10,2),-- 0-1000 금액
CASEWHENrandom()<0.7THEN'completed'WHENrandom()<0.9THEN'pending'ELSE'cancelled'ENDFROMgenerate_series(1,1000000);-- 통계 정보 갱신
ANALYZEorders;
-- 쿼리 실행 시간 측정 활성화
\timingon-- 실행 계획 확인
EXPLAIN(ANALYZE,BUFFERS)SELECT*FROMordersWHEREcustomer_id=5000;/*
예상 결과:
Seq Scan on orders (cost=0.00..20834.00 rows=100 width=xx) (actual time=50.123..250.456 rows=100 loops=1)
Filter: (customer_id = 5000)
Rows Removed by Filter: 999900
Buffers: shared hit=8334
Planning Time: 0.123 ms
Execution Time: 250.789 ms
해석:
- Seq Scan: 전체 테이블 스캔
- 999,900행 필터링으로 제거
- 실행 시간: 약 250ms
*/
-- B-Tree 인덱스 생성
CREATEINDEXidx_orders_customerONorders(customer_id);-- 인덱스 정보 확인
\di+idx_orders_customer-- 동일 쿼리 재실행
EXPLAIN(ANALYZE,BUFFERS)SELECT*FROMordersWHEREcustomer_id=5000;/*
예상 결과:
Bitmap Heap Scan on orders (cost=5.12..412.34 rows=100 width=xx) (actual time=0.345..1.234 rows=100 loops=1)
Recheck Cond: (customer_id = 5000)
Heap Blocks: exact=95
Buffers: shared hit=98
-> Bitmap Index Scan on idx_orders_customer (cost=0.00..5.10 rows=100 width=0) (actual time=0.234..0.234 rows=100 loops=1)
Index Cond: (customer_id = 5000)
Buffers: shared hit=3
Planning Time: 0.234 ms
Execution Time: 1.456 ms
해석:
- Index Scan 사용
- 3개 인덱스 페이지 + 95개 데이터 페이지
- 실행 시간: 약 1.5ms (170배 개선!)
*/
-- 복합 인덱스 효과 비교
CREATEINDEXidx_orders_cust_dateONorders(customer_id,order_date);-- 쿼리 1: 두 컬럼 모두 조건
EXPLAINANALYZESELECT*FROMordersWHEREcustomer_id=5000ANDorder_dateBETWEEN'2024-01-01'AND'2024-12-31';-- 결과: 복합 인덱스 사용, 매우 빠름
-- 쿼리 2: 두 번째 컬럼만 조건
EXPLAINANALYZESELECT*FROMordersWHEREorder_dateBETWEEN'2024-01-01'AND'2024-12-31';-- 결과: 복합 인덱스 미사용, Full Scan
-- 교훈: 복합 인덱스는 첫 컬럼부터 사용해야 효과
-- VACUUM으로 가시성 맵 갱신
VACUUMANALYZEorders;-- Index Only Scan 효과 극대화
EXPLAIN(ANALYZE,BUFFERS)SELECTcustomer_id,COUNT(*),SUM(total_amount)FROMordersWHEREcustomer_idBETWEEN5000AND5010GROUPBYcustomer_id;/*
Index Only Scan 조건:
1. 모든 컬럼이 인덱스에 포함
2. 가시성 맵 갱신됨 (VACUUM)
3. Heap Fetches: 0 -- 완벽한 커버링!
*/
-- PostgreSQL: pgstattuple 확장 설치
CREATEEXTENSIONpgstattuple;-- 인덱스 단편화 확인
SELECTschemaname,tablename,indexname,pg_size_pretty(pg_relation_size(indexrelid))asindex_size,idx_scanasscans,idx_tup_readastuples_read,idx_tup_fetchastuples_fetchedFROMpg_stat_user_indexesWHEREschemaname='public'ORDERBYpg_relation_size(indexrelid)DESC;-- 상세 단편화 정보
SELECT*FROMpgstatindex('idx_orders_customer');/*
주요 지표:
- leaf_fragmentation: 리프 단편화율 (목표 < 30%)
- avg_leaf_density: 평균 밀도 (목표 > 70%)
- leaf_pages: 리프 페이지 수
*/
-- 방법 1: REINDEX (테이블 잠금)
REINDEXINDEXidx_orders_customer;-- 방법 2: REINDEX CONCURRENTLY (PostgreSQL 12+, 온라인)
REINDEXINDEXCONCURRENTLYidx_orders_customer;-- 방법 3: DROP and CREATE
DROPINDEXidx_orders_customer;CREATEINDEXidx_orders_customerONorders(customer_id);-- 재구성 후 확인
SELECTleaf_fragmentation,avg_leaf_densityFROMpgstatindex('idx_orders_customer');/*
재구성 후:
- leaf_fragmentation: 0-5%
- avg_leaf_density: 90%+
→ 최적 상태 복구
*/
graph TB
subgraph "Uber 위치 인덱싱 아키텍처"
A[모바일 앱] -->|위치 스트림| B[Kafka]
B --> C[Flink 스트림 처리]
C --> D[Redis Geo Index]
C --> E[Cassandra<br/>R-Tree Index]
F[승객 검색 요청] --> G[검색 서비스]
G --> D
G --> E
D -->|Hot Data<br/>최근 5분| H[결과 집계]
E -->|Warm Data<br/>5분 이상| H
H --> I[가까운 차량 목록]
end
style D fill:#90EE90
style E fill:#87CEEB
style H fill:#FFE4B5
# Uber의 Geohash 기반 공간 인덱싱 (단순화)importgeohash2fromcassandra.clusterimportClusterclassUberLocationIndex:def__init__(self,precision=6):self.precision=precision# Geohash 정밀도self.cluster=Cluster(['cassandra-node'])self.session=self.cluster.connect('uber_locations')defupdate_location(self,driver_id,lng,lat,timestamp):"""드라이버 위치 업데이트"""# Geohash 계산 (파티션 키)gh=geohash2.encode(lat,lng,precision=self.precision)# Cassandra 삽입 (R-Tree 자동 갱신)query="""
INSERT INTO driver_locations
(geohash_prefix, driver_id, location, timestamp)
VALUES (%s, %s, (%s, %s), %s)
"""self.session.execute(query,(gh,driver_id,lng,lat,timestamp))deffind_nearby_drivers(self,lng,lat,radius_km):"""주변 드라이버 검색"""# 1. 중심 Geohash와 이웃 계산center_gh=geohash2.encode(lat,lng,self.precision)neighbors=geohash2.neighbors(center_gh)search_areas=[center_gh]+neighbors# 2. 각 파티션에서 병렬 검색results=[]forgh_prefixinsearch_areas:# R-Tree 공간 쿼리query="""
SELECT driver_id, location, timestamp
FROM driver_locations
WHERE geohash_prefix = %s AND within(location, point(%s, %s), %s)
AND timestamp > now() - interval '5 minutes'
"""rows=self.session.execute(query,(gh_prefix,lng,lat,radius_km*1000)# 미터 단위)results.extend(rows)# 3. 정확한 거리 계산 및 정렬fromgeopy.distanceimportgeodesicdrivers=[]forrowinresults:driver_lng,driver_lat=row.locationdistance=geodesic((lat,lng),(driver_lat,driver_lng)).kmifdistance<=radius_km:drivers.append({'driver_id':row.driver_id,'distance':distance,'location':(driver_lng,driver_lat),'timestamp':row.timestamp})# 거리순 정렬drivers.sort(key=lambdax:x['distance'])returndrivers[:10]# 상위 10개# 사용 예시index=UberLocationIndex()# 위치 업데이트 (드라이버 앱에서)index.update_location(driver_id='550e8400-e29b-41d4-a716-446655440000',lng=-122.4194,lat=37.7749,timestamp='2024-09-26 10:30:00')# 주변 검색 (승객 앱에서)nearby=index.find_nearby_drivers(lng=-122.4194,lat=37.7749,radius_km=2.0)print(f"반경 2km 내 드라이버: {len(nearby)}명")fordriverinnearby:print(f" - {driver['driver_id']}: {driver['distance']:.2f}km")
1. Geohash 정밀도 선택:
- precision=5 (5km × 5km): 도시 전체
- precision=6 (1.2km × 0.6km): 권장 (Uber)
- precision=7 (150m × 150m): 정밀하지만 파티션 많음
2. 경계 처리:
- Geohash 경계에 걸친 검색 누락 방지
- 8개 이웃 영역 모두 검색 필요
3. 시간 윈도우:
- 오래된 위치 데이터 TTL 설정
- Cassandra: TTL 300 (5분)
- Redis: EXPIRE 300
4. 확장 전략:
- Geohash prefix로 자동 샤딩
- 핫스팟 모니터링 및 재분산
graph TB
subgraph "Netflix 검색 파이프라인"
A[콘텐츠 DB<br/>MySQL] -->|CDC| B[Kafka]
B --> C[ETL Pipeline]
C --> D[Elasticsearch<br/>클러스터]
E[사용자 검색] --> F[Search API]
F --> G{캐시 체크}
G -->|Hit| H[Redis]
G -->|Miss| D
D --> I[랭킹 모델<br/>ML 기반]
I --> J[개인화 필터]
J --> K[검색 결과]
K --> H
K --> E
end
style D fill:#87CEEB
style I fill:#FFE4B5
style H fill:#90EE90
1. 인덱스 설계:
- 필드별 가중치 튜닝 (A/B 테스트)
- keyword vs text 적절히 선택
- nested 타입은 쿼리 복잡도 증가
2. 성능 최적화:
- Routing으로 쿼리 분산
- Filter context 활용 (캐싱됨)
- 필요한 필드만 _source에서 반환
3. 운영 고려사항:
- 재인덱싱 전략 (zero-downtime)
- 클러스터 모니터링 (heap 사용률)
- 샤드 리밸런싱 자동화
4. 비용 최적화:
- Hot-Warm 아키텍처
- 오래된 데이터는 압축 저장
- Snapshot으로 백업 (S3)
classBlackbirdIndex:"""GitHub의 Suffix Array 기반 코드 인덱스 (단순화)"""def__init__(self):self.suffix_array=[]# 접미사 배열self.lcp_array=[]# Longest Common Prefixself.text=""# 전체 코드 텍스트self.doc_boundaries=[]# 문서 경계defbuild_index(self,documents):"""문서 집합으로부터 인덱스 구축"""# 1. 모든 문서 연결 (구분자로)separator='\x00'# NULL 문자self.text=separator.join(doc['content']fordocindocuments)# 2. 문서 경계 기록pos=0fordocindocuments:self.doc_boundaries.append({'start':pos,'end':pos+len(doc['content']),'repo':doc['repo'],'path':doc['path']})pos+=len(doc['content'])+1# 3. Suffix Array 구축n=len(self.text)self.suffix_array=sorted(range(n),key=lambdai:self.text[i:])# 4. LCP Array 구축 (쿼리 최적화용)self._build_lcp_array()def_build_lcp_array(self):"""LCP 배열 구축 (Kasai 알고리즘)"""n=len(self.text)rank=[0]*nfori,suffix_idxinenumerate(self.suffix_array):rank[suffix_idx]=iself.lcp_array=[0]*nh=0foriinrange(n):ifrank[i]>0:j=self.suffix_array[rank[i]-1]whilei+h<nandj+h<nandself.text[i+h]==self.text[j+h]:h+=1self.lcp_array[rank[i]]=hifh>0:h-=1defsearch(self,pattern):"""패턴 검색 (이진 탐색)"""# 패턴으로 시작하는 첫 번째/마지막 위치 찾기left=self._lower_bound(pattern)right=self._upper_bound(pattern)ifleft>=right:return[]# 매칭 위치들matches=[]foriinrange(left,right):pos=self.suffix_array[i]doc=self._find_document(pos)ifdoc:matches.append({'repo':doc['repo'],'path':doc['path'],'line':self._get_line_number(pos),'snippet':self._extract_snippet(pos,len(pattern))})returnmatchesdef_lower_bound(self,pattern):"""패턴 이상인 첫 위치 (이진 탐색)"""left,right=0,len(self.suffix_array)whileleft<right:mid=(left+right)//2suffix=self.text[self.suffix_array[mid]:]ifsuffix<pattern:left=mid+1else:right=midreturnleftdef_upper_bound(self,pattern):"""패턴 초과인 첫 위치"""left,right=0,len(self.suffix_array)whileleft<right:mid=(left+right)//2suffix=self.text[self.suffix_array[mid]:]ifsuffix[:len(pattern)]<=pattern:left=mid+1else:right=midreturnleftdef_find_document(self,pos):"""위치로부터 문서 찾기"""fordocinself.doc_boundaries:ifdoc['start']<=pos<doc['end']:returndocreturnNonedef_get_line_number(self,pos):"""위치의 라인 번호"""returnself.text[:pos].count('\n')+1def_extract_snippet(self,pos,length,context=50):"""코드 스니펫 추출"""start=max(0,pos-context)end=min(len(self.text),pos+length+context)returnself.text[start:end]# 사용 예시index=BlackbirdIndex()# 인덱스 구축documents=[{'repo':'torvalds/linux','path':'kernel/sched/core.c','content':'''
void schedule(void) {
struct task_struct *prev, *next;
prev = current;
next = pick_next_task(rq);
context_switch(prev, next);
}
'''},{'repo':'torvalds/linux','path':'kernel/fork.c','content':'''
long do_fork(unsigned long clone_flags) {
struct task_struct *p;
p = copy_process(clone_flags);
wake_up_new_task(p);
return p->pid;
}
'''}]index.build_index(documents)# 함수명 검색results=index.search("schedule")print(f"'{pattern}' 검색 결과: {len(results)}개")formatchinresults:print(f"\n{match['repo']}/{match['path']} (line {match['line']})")print(f" {match['snippet']}")
인덱스 통계:
├─ 인덱싱 속도: 초당 10,000 파일
├─ 인덱스 크기: 원본의 30% (압축)
├─ 검색 지연: P50 80ms, P99 300ms
└─ 처리량: 초당 50,000 쿼리
확장성:
├─ 리포지토리: 2억 개
├─ 파일: 100억 개
├─ 코드 라인: 1조 라인
└─ 인덱스 클러스터: 1000+ 노드
1. Suffix Array vs Inverted Index:
장점:
- 정규식 지원
- 부분 문자열 검색
- 메모리 효율
단점:
- 구축 시간 길음 O(n log n)
- 순위화 어려움
선택 기준:
- 정규식 필요 → Suffix Array
- 일반 텍스트 → Inverted Index
2. 코드 특화 최적화:
- 식별자 추출 (함수명, 변수명)
- 주석 제거 옵션
- 공백 정규화
3. 운영 전략:
- 증분 업데이트 (merge 시)
- 샤딩: 리포지토리 단위
- 캐싱: 인기 리포지토리
importredisimportpickleclassCachedIndex:"""인덱스 결과 캐싱"""def__init__(self,db_conn):self.db=db_connself.redis=redis.Redis(host='localhost',port=6379,db=0)self.ttl=300# 5분defget_or_fetch(self,query_key,query_func):"""캐시 확인 후 DB 조회"""# 1. 캐시 확인cached=self.redis.get(query_key)ifcached:returnpickle.loads(cached)# 2. DB 조회 (인덱스 사용)result=query_func()# 3. 캐시 저장self.redis.setex(query_key,self.ttl,pickle.dumps(result))returnresultdefinvalidate_on_write(self,affected_keys):"""쓰기 발생 시 관련 캐시 무효화"""ifaffected_keys:self.redis.delete(*affected_keys)# 사용 예시cached_idx=CachedIndex(db_conn)defquery_orders():returndb_conn.execute("SELECT * FROM orders WHERE customer_id = 5000").fetchall()# 캐시된 조회results=cached_idx.get_or_fetch(query_key="orders:customer:5000",query_func=query_orders)
-- 1. 인덱스 사용 통계
SELECTschemaname,tablename,indexname,idx_scanasscans,idx_tup_readastuples_read,idx_tup_fetchastuples_fetched,pg_size_pretty(pg_relation_size(indexrelid))assize,CASEWHENidx_scan=0THEN'미사용'WHENidx_scan<100THEN'저사용'ELSE'정상'ENDasusage_statusFROMpg_stat_user_indexesORDERBYidx_scanASC,pg_relation_size(indexrelid)DESC;-- 2. 중복 인덱스 탐지
SELECTpg_size_pretty(SUM(pg_relation_size(idx))::BIGINT)assize,(array_agg(idx))[1]asidx1,(array_agg(idx))[2]asidx2,(array_agg(idx))[3]asidx3,(array_agg(idx))[4]asidx4FROM(SELECTindexrelid::regclassasidx,(indrelid::text||E'\n'||indclass::text||E'\n'||indkey::text||E'\n'||COALESCE(indexprs::text,'')||E'\n'||COALESCE(indpred::text,''))askeyFROMpg_index)subGROUPBYkeyHAVINGCOUNT(*)>1ORDERBYSUM(pg_relation_size(idx))DESC;-- 3. 인덱스 블로트(Bloat) 측정
SELECTschemaname,tablename,indexname,pg_size_pretty(pg_relation_size(indexrelid))assize,ROUND(100*(pg_relation_size(indexrelid)-pg_relation_size(indexrelid,'main'))/NULLIF(pg_relation_size(indexrelid),0)::numeric,2)asbloat_pctFROMpg_stat_user_indexesWHEREpg_relation_size(indexrelid)>1000000-- 1MB 이상
ORDERBYbloat_pctDESC;-- 4. 인덱스 vs 테이블 스캔 비교
SELECTschemaname,tablename,seq_scanastable_scans,idx_scanasindex_scans,CASEWHENseq_scan=0THEN100.0ELSEROUND(100.0*idx_scan/(seq_scan+idx_scan),2)ENDasindex_usage_pctFROMpg_stat_user_tablesWHERE(seq_scan+idx_scan)>0ORDERBYindex_usage_pctASC;-- 5. 락 대기 시간 모니터링
SELECTpg_stat_get_backend_pid(s.backend_id)aspid,pg_stat_get_backend_activity(s.backend_id)asquery,l.mode,l.granted,age(clock_timestamp(),pg_stat_get_backend_activity_start(s.backend_id))asdurationFROMpg_stat_get_backend_idset()ass(backend_id),pg_lockslWHEREpg_stat_get_backend_pid(s.backend_id)=l.pidANDNOTl.grantedORDERBYdurationDESC;
# alerts.ymlgroups:- name:index_alertsinterval:60srules:# 미사용 인덱스 경고- alert:UnusedIndexDetectedexpr:postgres_index_scan_count == 0 and postgres_index_size_bytes > 10485760for:7dlabels:severity:warningannotations:summary:"Unused index detected: {{ $labels.index }}"description:"Index {{ $labels.index }} on {{ $labels.table }} has not been used for 7 days and is {{ $value | humanize }}B"# 높은 단편화율 경고- alert:HighIndexFragmentationexpr:postgres_index_bloat_ratio > 30for:1hlabels:severity:warningannotations:summary:"High index fragmentation: {{ $labels.index }}"description:"Index {{ $labels.index }} fragmentation is {{ $value }}%"# 인덱스 크기 급증- alert:IndexSizeSpikeexpr:rate(postgres_index_size_bytes[5m]) > 1048576 # 1MB/5minfor:15mlabels:severity:criticalannotations:summary:"Index size growing rapidly: {{ $labels.index }}"description:"Index {{ $labels.index }} growing at {{ $value | humanize }}B/s"# 낮은 인덱스 히트율- alert:LowIndexHitRateexpr:| (sum by (index) (rate(postgres_index_scan_count[5m])) /
sum by (index) (rate(postgres_table_seq_scan[5m]) + rate(postgres_index_scan_count[5m]))) < 0.5for:30mlabels:severity:warningannotations:summary:"Low index hit rate: {{ $labels.index }}"description:"Index {{ $labels.index }} hit rate is {{ $value | humanizePercentage }}"
-- PostgreSQL 권한 관리
-- 1. 인덱스 생성/삭제 권한
-- 테이블 소유자만 인덱스 생성 가능 (기본)
GRANTALLONTABLEsensitive_dataTOdata_admin;-- 2. 읽기 전용 사용자 (인덱스 자동 활용)
CREATEROLEreadonly_user;GRANTCONNECTONDATABASEmydbTOreadonly_user;GRANTUSAGEONSCHEMApublicTOreadonly_user;GRANTSELECTONALLTABLESINSCHEMApublicTOreadonly_user;-- 인덱스는 자동으로 사용됨 (별도 권한 불필요)
-- 3. 시스템 카탈로그 접근 제한
REVOKESELECTONpg_catalog.pg_indexFROMpublic;GRANTSELECTONpg_catalog.pg_indexTOdba_role;-- 4. 감사 로그 활성화
-- postgresql.conf 설정
-- log_statement = 'ddl' # DDL 문 기록
-- log_line_prefix = '%t [%p]: [%l-1] user=%u,db=%d,app=%a,client=%h '
-- 5. Row-Level Security와 인덱스
CREATETABLEdocuments(idSERIALPRIMARYKEY,user_idINTEGERNOTNULL,contentTEXT,classificationVARCHAR(20));-- RLS 정책
ALTERTABLEdocumentsENABLEROWLEVELSECURITY;CREATEPOLICYuser_documentsONdocumentsFORSELECTUSING(user_id=current_user_id()ORclassification='public');-- 인덱스 (정책 고려)
CREATEINDEXidx_user_docsONdocuments(user_id)WHEREclassification!='public';CREATEINDEXidx_public_docsONdocuments(classification)WHEREclassification='public';
importhashlibfromcryptography.fernetimportFernetclassEncryptedIndex:"""민감 데이터 인덱스 암호화"""def__init__(self,encryption_key):self.cipher=Fernet(encryption_key)defcreate_hashed_index(self,conn,table,column):"""해시 기반 인덱스 생성 (검색 가능, 복호화 불가)"""# 1. 해시 컬럼 추가conn.execute(f"""
ALTER TABLE {table} ADD COLUMN {column}_hash VARCHAR(64)
""")# 2. 해시 값 계산 및 저장conn.execute(f"""
UPDATE {table} SET {column}_hash = encode(
digest({column}::text || 'salt_value', 'sha256'),
'hex'
)
""")# 3. 해시 컬럼에 인덱스 생성conn.execute(f"""
CREATE INDEX idx_{table}_{column}_hash
ON {table}({column}_hash)
""")print(f"Hashed index created on {table}.{column}")defsearch_by_hash(self,conn,table,column,search_value):"""해시로 검색"""# 검색 값 해시 계산hash_value=hashlib.sha256((search_value+'salt_value').encode()).hexdigest()# 해시로 조회query=f"""
SELECT * FROM {table} WHERE {column}_hash = %s
"""returnconn.execute(query,(hash_value,)).fetchall()defcreate_encrypted_index(self,conn,table,column):"""암호화 인덱스 (검색 가능, 복호화 가능)"""# 1. 암호화 컬럼 추가conn.execute(f"""
ALTER TABLE {table} ADD COLUMN {column}_encrypted BYTEA
""")# 2. 데이터 암호화rows=conn.execute(f"SELECT id, {column} FROM {table}").fetchall()forrow_id,valueinrows:ifvalue:encrypted=self.cipher.encrypt(value.encode())conn.execute(f"UPDATE {table} SET {column}_encrypted = %s WHERE id = %s",(encrypted,row_id))# 3. 암호화 컬럼 인덱스conn.execute(f"""
CREATE INDEX idx_{table}_{column}_enc
ON {table}({column}_encrypted)
""")print(f"Encrypted index created on {table}.{column}")defdecrypt_result(self,encrypted_value):"""결과 복호화"""ifencrypted_value:returnself.cipher.decrypt(encrypted_value).decode()returnNone# 사용 예시# 암호화 키 생성 (안전하게 관리 필요)encryption_key=Fernet.generate_key()enc_index=EncryptedIndex(encryption_key)# 해시 인덱스 (SSN, 신용카드 등)enc_index.create_hashed_index(conn,'users','ssn')# 검색results=enc_index.search_by_hash(conn,'users','ssn','123-45-6789')
GDPR (개인정보 보호):
✓ 개인정보 컬럼 인덱스 암호화
✓ 삭제 요청 시 인덱스도 삭제
✓ 인덱스 접근 로그 기록
✓ 데이터 최소화 (필요한 컬럼만 인덱싱)
HIPAA (의료정보):
✓ PHI(개인건강정보) 인덱스 암호화
✓ 접근 제어 및 감사
✓ 백업 시 인덱스 포함
✓ 전송 시 TLS 사용
PCI-DSS (결제정보):
✓ 카드번호 해시 인덱스만 사용
✓ 실제 카드번호 인덱싱 금지
✓ 정기적 취약점 스캔
✓ 강력한 접근 제어
SOX (재무보고):
✓ 인덱스 변경 이력 보관
✓ 승인된 변경만 허용
✓ 정기적 감사
✓ 무결성 검증
-- 1. 비효율적인 인덱스 사용 패턴 탐지
-- 함수 사용으로 인한 인덱스 미사용
-- Bad
SELECT*FROMusersWHEREUPPER(email)='USER@EXAMPLE.COM';-- 인덱스 미사용 (함수 적용)
-- Good: 함수 인덱스 생성
CREATEINDEXidx_users_email_upperONusers(UPPER(email));-- 또는 대소문자 구분 없는 인덱스
CREATEINDEXidx_users_email_lowerONusers(LOWER(email));SELECT*FROMusersWHERELOWER(email)='user@example.com';-- 2. 암묵적 형변환 방지
-- Bad
CREATETABLEorders(order_idVARCHAR(50));CREATEINDEXidx_order_idONorders(order_id);SELECT*FROMordersWHEREorder_id=12345;-- 숫자로 비교
-- 인덱스 미사용 (형변환 발생)
-- Good
SELECT*FROMordersWHEREorder_id='12345';-- 문자열로 비교
-- 3. OR 조건 최적화
-- Bad
SELECT*FROMproductsWHEREcategory='electronics'ORcategory='computers';-- 인덱스 비효율
-- Good: IN 사용
SELECT*FROMproductsWHEREcategoryIN('electronics','computers');-- 또는 UNION
SELECT*FROMproductsWHEREcategory='electronics'UNIONALLSELECT*FROMproductsWHEREcategory='computers';-- 4. 부정 조건 최적화
-- Bad
SELECT*FROMordersWHEREstatus!='cancelled';-- 인덱스 비효율 (대부분 반환)
-- Good: 긍정 조건으로 변환
SELECT*FROMordersWHEREstatusIN('pending','processing','completed','shipped');-- 또는 부분 인덱스
CREATEINDEXidx_active_ordersONorders(order_id)WHEREstatus!='cancelled';
-- PostgreSQL: 통계 조정으로 힌트 효과
-- 옵티마이저가 잘못된 인덱스 선택 시
-- 방법 1: 통계 타겟 증가
ALTERTABLElarge_tableALTERCOLUMNsearch_colSETSTATISTICS1000;ANALYZElarge_table;-- 방법 2: 플랜 비용 조정
SETrandom_page_cost=1.1;-- SSD 환경
SETeffective_cache_size='8GB';-- MySQL: 인덱스 힌트 직접 지정
SELECT*FROMordersUSEINDEX(idx_customer_date)WHEREcustomer_id=5000ANDorder_date>'2024-01-01';-- 또는 강제 인덱스
SELECT*FROMordersFORCEINDEX(idx_customer_date)WHEREcustomer_id=5000;-- Oracle: 힌트 주석
SELECT/*+ INDEX(orders idx_customer_date) */*FROMordersWHEREcustomer_id=5000;
-- PostgreSQL 병렬 쿼리
-- 1. 병렬 실행 설정
SETmax_parallel_workers_per_gather=4;SETparallel_setup_cost=100;SETparallel_tuple_cost=0.01;-- 2. 병렬 인덱스 스캔 실행
EXPLAIN(ANALYZE,BUFFERS)SELECTCOUNT(*)FROMlarge_tableWHEREindexed_column>1000000;/*
결과:
Finalize Aggregate (cost=... rows=1)
-> Gather (workers=4)
-> Partial Aggregate
-> Parallel Index Scan on idx_column
병렬도: 4 workers
처리 시간: 단일 대비 1/4
*/-- 3. 파티션 + 병렬
CREATETABLEsales_data(sale_idBIGSERIAL,sale_dateDATE,amountDECIMAL)PARTITIONBYRANGE(sale_date);-- 파티션별 인덱스
CREATEINDEXidx_sales_2024_01ONsales_2024_01(amount);CREATEINDEXidx_sales_2024_02ONsales_2024_02(amount);-- 병렬 파티션 스캔
SELECTSUM(amount)FROMsales_dataWHEREsale_dateBETWEEN'2024-01-01'AND'2024-12-31'ANDamount>1000;-- 각 파티션 병렬 스캔 → 결과 병합
-- 1. Prefix 압축 (B-Tree)
-- PostgreSQL에서 자동 적용 (내부 노드)
-- 2. Page 압축 (InnoDB, MySQL 5.7+)
CREATETABLEcompressed_data(idINTPRIMARYKEY,dataTEXT)COMPRESSION='zlib';CREATEINDEXidx_dataONcompressed_data(data(100))WITH(COMPRESSION='zlib');-- 3. Columnstore 압축 (SQL Server)
CREATECOLUMNSTOREINDEXidx_analyticsONsales_data(product_id,sale_date,amount)WITH(DATA_COMPRESSION=COLUMNSTORE);-- 압축률: 70-90%
-- 분석 쿼리: 5-10배 빠름
-- 4. 커스텀 압축 (애플리케이션 레벨)
-- 큰 텍스트 컬럼의 해시 인덱스
CREATETABLEdocuments(idSERIALPRIMARYKEY,contentTEXT,content_hashVARCHAR(64));-- 해시 인덱스 (압축 효과)
CREATEINDEXidx_content_hashONdocuments(content_hash);-- 삽입 시 해시 계산
INSERTINTOdocuments(content,content_hash)VALUES('long text...',MD5('long text...'));
classShardedIndexOptimizer:"""샤딩 환경 인덱스 최적화"""def__init__(self,shards):self.shards=shardsdefoptimize_global_query(self,query,index_key):"""글로벌 쿼리 최적화"""# 1. 샤드 프루닝 (불필요한 샤드 제외)relevant_shards=self._prune_shards(query,index_key)# 2. 병렬 실행withconcurrent.futures.ThreadPoolExecutor()asexecutor:futures={executor.submit(self._query_shard,shard,query):shardforshardinrelevant_shards}results=[]forfutureinconcurrent.futures.as_completed(futures):results.extend(future.result())# 3. 결과 병합 및 정렬returnself._merge_results(results)def_prune_shards(self,query,index_key):"""샤드 프루닝"""# 쿼리 조건으로 필요한 샤드만 선택# 예: date range로 샤드 필터링if'date_range'inquery:start,end=query['date_range']return[shardforshardinself.shardsifshard['date_range'][0]<=endandshard['date_range'][1]>=start]returnself.shardsdefcreate_routing_index(self):"""라우팅 인덱스 생성 (메타데이터)"""routing_index={}forshardinself.shards:# 각 샤드의 키 범위 저장routing_index[shard['id']]={'key_range':shard['range'],'host':shard['host'],'indexes':self._get_shard_indexes(shard)}returnrouting_indexdef_get_shard_indexes(self,shard):"""샤드의 인덱스 목록"""conn=psycopg2.connect(**shard['db_config'])cur=conn.cursor()cur.execute("""
SELECT indexname, indexdef
FROM pg_indexes
WHERE schemaname = 'public'
""")indexes=cur.fetchall()conn.close()returnindexes# 사용optimizer=ShardedIndexOptimizer(shard_config)# 최적화된 글로벌 쿼리results=optimizer.optimize_global_query(query={'date_range':('2024-01-01','2024-12-31')},index_key='order_date')
-- 증상: EXPLAIN에서 Seq Scan 발생
-- 원인 1: 통계 정보 부족
ANALYZEtable_name;-- 통계 갱신
-- 원인 2: 선택도 낮음 (결과가 많음)
-- 해결: 부분 인덱스 또는 쿼리 재작성
-- 원인 3: 데이터 타입 불일치
-- Bad
SELECT*FROMordersWHEREorder_id='12345';-- VARCHAR
-- order_id가 INTEGER인 경우 형변환 발생
-- Good
SELECT*FROMordersWHEREorder_id=12345;-- 원인 4: 함수 적용
-- Bad
SELECT*FROMusersWHEREYEAR(created_at)=2024;-- Good: 함수 인덱스
CREATEINDEXidx_created_yearONusers(YEAR(created_at));-- 또는 범위 조건
SELECT*FROMusersWHEREcreated_at>='2024-01-01'ANDcreated_at<'2025-01-01';
-- 증상: 쿼리 대기, 타임아웃
-- 진단: 락 확인
SELECTblocked_locks.pidASblocked_pid,blocked_activity.usenameASblocked_user,blocking_locks.pidASblocking_pid,blocking_activity.usenameASblocking_user,blocked_activity.queryASblocked_statement,blocking_activity.queryASblocking_statement,age(clock_timestamp(),blocked_activity.query_start)ASblocked_durationFROMpg_catalog.pg_locksblocked_locksJOINpg_catalog.pg_stat_activityblocked_activityONblocked_activity.pid=blocked_locks.pidJOINpg_catalog.pg_locksblocking_locksONblocking_locks.locktype=blocked_locks.locktypeANDblocking_locks.databaseISNOTDISTINCTFROMblocked_locks.databaseANDblocking_locks.relationISNOTDISTINCTFROMblocked_locks.relationANDblocking_locks.pageISNOTDISTINCTFROMblocked_locks.pageANDblocking_locks.tupleISNOTDISTINCTFROMblocked_locks.tupleANDblocking_locks.pid!=blocked_locks.pidJOINpg_catalog.pg_stat_activityblocking_activityONblocking_activity.pid=blocking_locks.pidWHERENOTblocked_locks.granted;-- 원인: 인덱스 생성/재구성 중 락
-- 해결 1: CONCURRENTLY 옵션
CREATEINDEXCONCURRENTLYidx_nameONtable_name(column);REINDEXINDEXCONCURRENTLYidx_name;-- 해결 2: 락 타임아웃 설정
SETlock_timeout='30s';-- 해결 3: 작업 시간대 조정
-- 사용량 적은 시간에 인덱스 작업 수행
-- 증상: 쿼리 오류, 일관성 없는 결과
-- 진단: 인덱스 검증
-- PostgreSQL
SELECT*FROMbt_index_check('index_name');-- MySQL
CHECKTABLEtable_nameEXTENDED;-- 원인: 하드웨어 장애, 버그, 부적절한 종료
-- 해결: 재구성
DROPINDEXindex_name;CREATEINDEXindex_nameONtable_name(column);-- 또는
REINDEXINDEXindex_name;
1단계: 증상 확인
□ 느린 쿼리 식별 (Slow Query Log)
□ 실행 계획 분석 (EXPLAIN)
□ 리소스 사용률 확인 (CPU, I/O, 메모리)
2단계: 원인 분석
□ 인덱스 사용 여부 확인
□ 인덱스 통계 정보 확인
□ 데이터 분포 확인
□ 락 상태 확인
3단계: 해결 방안 적용
□ 통계 갱신 (ANALYZE)
□ 인덱스 추가/수정
□ 쿼리 재작성
□ 인덱스 재구성
4단계: 검증 및 모니터링
□ 성능 개선 확인
□ 부작용 모니터링
□ 장기 추적
문제: 차원이 증가할수록 인덱스 효율 급감
차원별 성능:
2D (위도/경도): R-Tree 효과적 (O(log n))
5D (시공간): 성능 저하 시작
10D: 거의 Full Scan 수준
50D+ (ML 특징 벡터): 인덱스 무용
원인:
- 고차원에서 모든 점이 '멀리' 있음
- 공간 분할 비효율 (2^d개 영역)
- 범위 쿼리가 대부분의 공간 포함
현재 접근법:
1. 차원 축소 (PCA, t-SNE)
- 50D → 10D로 압축
- 정보 손실 불가피
2. 근사 검색 (ANN - Approximate Nearest Neighbor)
- HNSW (Hierarchical NSW)
- IVF (Inverted File)
- 정확도 trade-off
3. 학습 기반 인덱스
- 데이터 분포 학습
- 모델로 위치 예측
문제: 초당 수백만 건 INSERT와 동시 검색
도전 과제:
- 쓰기 처리량 vs 읽기 지연 딜레마
- 인덱스 갱신 비용
- 시간 윈도우 관리
현재 한계:
B-Tree: INSERT 시 재조정 오버헤드
LSM-Tree: 읽기 증폭 (여러 레벨 검색)
부분적 해결:
1. 시계열 DB (InfluxDB, TimescaleDB)
- 시간 기반 파티셔닝
- 압축 및 다운샘플링
2. 스트림 처리 (Kafka Streams)
- 윈도우별 집계
- 머티리얼라이즈드 뷰
3. 하이브리드 저장
- Hot: 메모리 (최근 데이터)
- Warm: SSD (시간 인덱스)
- Cold: S3 (아카이브)
미해결:
- 정확한 순서 보장과 성능 양립
- 분산 환경 인덱스 일관성
문제: CAP 정리의 한계
시나리오:
- 글로벌 인덱스: 모든 노드 동기화 필요
- 일관성 보장 → 가용성/성능 희생
- 가용성 우선 → 일관성 희생
영향:
✗ 분산 트랜잭션 비용 (2PC, Paxos)
✗ 네트워크 파티션 시 불일치
✗ 크로스 리전 지연
현재 타협안:
1. 로컬 인덱스 + 쿼리 라우팅
- 샤딩 키 필수
- 크로스 샤드 느림
2. 최종 일관성 (Eventual Consistency)
- Dynamo, Cassandra 방식
- 읽기 지연 허용
3. CRDT 기반 인덱스
- 충돌 없는 복제
- 제한적 쿼리 지원
연구 중:
- 인과 일관성 (Causal Consistency)
- 하이브리드 일관성 모델
문제: 관계 탐색의 복잡도
예시: 소셜 네트워크
- 10억 사용자
- 평균 500명 친구
- "친구의 친구" 쿼리
도전:
- 멀티홉 탐색 (2-hop, 3-hop)
- 동적 그래프 (계속 변화)
- 중심성(Centrality) 계산 비용
현재 접근:
1. 인접 리스트 인덱스
- 각 노드의 이웃 저장
- 1-hop: 빠름
- 2-hop+: 조합 폭발
2. 경로 인덱스 (Path Index)
- 자주 사용하는 경로 미리 계산
- 저장 공간 膨대
3. 랜드마크 기반 (Landmark-based)
- 중요 노드 식별
- 거리 추정
한계:
- 동적 변화 반영 어려움
- 저장 공간 vs 쿼리 시간 트레이드오프
문제: 검색 가능성 vs 프라이버시
상충:
- 효율적 검색: 평문 인덱스 필요
- 프라이버시: 암호화 필요
현재 한계:
1. 완전 동형 암호 (FHE)
- 암호문 상태로 연산
- 100-1000배 느림 (실용 불가)
2. 검색 가능 암호 (Searchable Encryption)
- 특정 패턴만 검색
- 범위 쿼리 어려움
3. 차등 프라이버시 (Differential Privacy)
- 노이즈 추가
- 정확도 저하
연구 방향:
- 준동형 암호 (Somewhat HE)
- 보안 멀티파티 계산 (MPC)
- TEE (Trusted Execution Environment)
개념: 기계학습 모델을 인덱스로 사용
전통 B-Tree:
Key → Tree Traversal → Position
Learned Index:
Key → ML Model → Predicted Position
장점:
- 메모리: 1/10 크기
- 속도: 특정 분포에서 더 빠름
- 적응성: 데이터 패턴 학습
단점:
- 쓰기 비용: 모델 재학습
- 범용성 부족: 특정 분포 의존
현재 연구 (Google, MIT):
1. Recursive Model Index (RMI)
모델 계층 구조
Level 0: 전체 범위 예측
Level 1: 세부 범위
Level 2: 정확한 위치
2. Hybrid Index
전통 인덱스 + 학습 모델
모델 실패 시 폴백
구현 예시 (개념):
class LearnedIndex:
def __init__(self, data):
# 모델 학습 (선형 회귀 단순화)
self.model = self._train_model(data)
self.data = sorted(data)
def _train_model(self, data):
"""위치 예측 모델 학습"""
X = np.array([item[0] for item in data]).reshape(-1, 1)
y = np.arange(len(data))
from sklearn.linear_model import LinearRegression
model = LinearRegression()
model.fit(X, y)
return model
def search(self, key):
# 모델로 위치 예측
predicted_pos = int(self.model.predict([[key]])[0])
# 오차 범위 내 이진 탐색
error_bound = 10
start = max(0, predicted_pos - error_bound)
end = min(len(self.data), predicted_pos + error_bound)
# 좁은 범위 탐색
for i in range(start, end):
if self.data[i][0] == key:
return self.data[i]
return None
상용화 장벽:
- 쓰기 워크로드 처리
- 모델 갱신 비용
- 다양한 데이터 분포 대응
배경: AI/ML 시대의 유사도 검색 폭증
사용 사례:
- 이미지 검색 (512D 벡터)
- 문서 유사도 (1536D OpenAI)
- 추천 시스템
- RAG (Retrieval-Augmented Generation)
주요 알고리즘:
1. HNSW (Hierarchical Navigable Small World)
- 그래프 기반
- 빠른 근사 검색
- 메모리 효율
2. IVF (Inverted File Index)
- 클러스터링 기반
- 양자화(Quantization)
- 대규모 데이터
3. PQ (Product Quantization)
- 벡터 압축
- 메모리 1/8 절감
- 정확도 trade-off
벡터 DB 제품:
- Pinecone: 완전 관리형
- Weaviate: 오픈소스
- Milvus: 대규모
- Qdrant: Rust 기반
PostgreSQL 확장:
-- pgvector 사용
CREATE EXTENSION vector;
CREATE TABLE embeddings (
id SERIAL PRIMARY KEY,
content TEXT,
embedding vector(1536) -- OpenAI 차원
);
-- HNSW 인덱스
CREATE INDEX ON embeddings
USING hnsw (embedding vector_cosine_ops);
-- 유사도 검색
SELECT content
FROM embeddings
ORDER BY embedding <=> '[0.1, 0.2, ...]'::vector
LIMIT 10;
특징:
- 컴퓨팅과 스토리지 분리
- 탄력적 확장
- 서버리스 운영
아키텍처:
전통 방식:
[컴퓨팅 + 스토리지] → Scale Up
클라우드 네이티브:
[컴퓨팅 노드] ←→ [분리된 스토리지]
↕ ↕
독립 확장 독립 확장
장점:
- 컴퓨팅/스토리지 독립 확장
- 스냅샷 빠름 (CoW)
- 비용 효율
예시: Amazon Aurora
- 스토리지: 최대 128TB 자동 확장
- 읽기 복제본: 15개
- 인덱스: 스토리지 레이어에서 관리
구현 패턴:
1. 원격 인덱스 (Remote Index)
- S3/EBS에 인덱스 저장
- 로컬 캐시 활용
2. Serverless Index
- 사용량 기반 과금
- 자동 스케일
3. Multi-Tenant Index
- 인덱스 공유
- 격리 보장
HTAP (Hybrid Transactional/Analytical Processing)
요구사항:
- OLTP: 빠른 쓰기, 포인트 쿼리
- OLAP: 빠른 분석, 범위 쿼리
- 실시간: 최신 데이터 즉시 반영
하이브리드 전략:
1. Delta + Main 구조
[Delta Store] → 최근 데이터, Row-based, B-Tree
[Main Store] → 과거 데이터, Column-based, Compressed
쿼리 시 병합
2. 이중 인덱스
- Row Index: OLTP
- Column Index: OLAP
동기화 관리
3. 적응형 인덱스
- 워크로드 패턴 학습
- 자동 인덱스 전환
제품:
- SAP HANA
- MemSQL (SingleStore)
- TiDB
- CockroachDB
성능:
- OLTP: 밀리초
- OLAP: 초 단위
- 데이터 신선도: <1초
MIT의 Learned Index 연구 결과:
성능 개선:
- 메모리: 99% 절감 (vs B-Tree)
- 속도: 특정 분포에서 3배 빠름
- 예측 오차: ±100 위치 이내
한계:
- 쓰기 시 모델 재학습 필요
- 데이터 분포 변화 대응 어려움
- 범용성 부족
적용 사례:
- Google: BigTable의 일부 워크로드
- 연구 단계: 상용화 제한적
미래 전망:
- 하이브리드: 전통 인덱스 + 학습 모델
- 적응형: 워크로드별 자동 전환
- 연합 학습: 분산 환경 모델 학습
인덱싱은 데이터베이스 성능 최적화의 핵심 기술로, 검색 시간을 선형 O(n)에서 로그 O(log n) 또는 상수 O(1)로 개선합니다. B-Tree 계열이 가장 보편적이며, 용도에 따라 Hash, Bitmap, Full-Text, Spatial 등 다양한 인덱스 구조가 존재합니다.
핵심 원리는 디스크 I/O 최소화, 균형 유지, 공간 지역성 활용입니다. 인덱스는 추가 저장 공간과 쓰기 오버헤드를 대가로 읽기 성능을 극적으로 향상시키는 공간-시간 트레이드오프의 전형입니다.
실무 적용에서는 쿼리 패턴 분석, 선택도 평가, 복합 인덱스 설계, 모니터링 및 유지보수가 중요합니다. 단편화 관리, 미사용 인덱스 제거, 통계 갱신 등 지속적인 관리가 필요합니다.
최신 트렌드로는 학습 기반 인덱스, 벡터 검색, 클라우드 네이티브 아키텍처, HTAP를 위한 하이브리드 인덱스가 있으며, 고차원 데이터와 분산 환경의 도전 과제가 여전히 존재합니다.
인덱스는 검색(=선택/조인/정렬) 비용을 낮추는 보조 구조다. 대표 구조는 B-트리(B-Tree), 해시(Hash), R-트리(R-Tree), GiST/GIN, BRIN, LSM-트리(Log-Structured Merge-Tree) 등이며, 엔진(예: PostgreSQL, MySQL/InnoDB, SQLite, RocksDB, Elasticsearch)에 따라 내부 동작과 동시성 제어, 유지비용이 다르다.
설계는 질의 패턴(필터/조인/정렬/그룹), 데이터 분포/카디널리티, 갱신 빈도, 스토리지/메모리, 옵티마이저 통계를 종합해 결정한다. 실무에서는 과도한 인덱스가 쓰기 성능과 공간을 잠식하므로, 커버링 인덱스, 부분/표현식 인덱스, 다중 컬럼 정렬순서, 파티셔닝/샤딩과의 상호작용, 모니터링과 회귀 방지가 필수다.
flowchart LR
Q[Query] --> P{Predicate?}
P -->|Yes| IDX[Index Search]
IDX --> C{Covering?}
C -->|Yes| R[Return rows]
C -->|No| T[Table Lookup]
P -->|No| FS[Full Scan]
CREATETABLEorders(idBIGSERIALPRIMARYKEY,user_idBIGINTNOTNULL,statusTEXTNOTNULL,created_atTIMESTAMPTZNOTNULLDEFAULTnow(),amountNUMERIC(12,2)NOTNULL);-- 샘플 데이터(임의 분포 가정)
INSERTINTOorders(user_id,status,created_at,amount)SELECT(random()*1e6)::bigint,(ARRAY['NEW','PAID','SHIP','CANCEL'])[1+(random()*3)::int],now()-(random()*interval'90 days'),(random()*1000)::numeric(12,2)FROMgenerate_series(1,2e6::int);ANALYZEorders;-- 통계 수집
importsqlite3,time,randomconn=sqlite3.connect(':memory:')c=conn.cursor()# 테이블 생성c.execute('CREATE TABLE t(id INTEGER PRIMARY KEY, a INT, b INT)')# 데이터 삽입rows=[(i,random.randint(1,1000000),random.randint(1,10))foriinrange(300000)]c.executemany('INSERT INTO t VALUES (?,?,?)',rows)conn.commit()# 인덱스 없는 질의q='SELECT id FROM t WHERE a BETWEEN 100 AND 200 ORDER BY a LIMIT 50'start=time.time();list(c.execute(q));t1=time.time()-start# 인덱스 생성(키순서: a)c.execute('CREATE INDEX idx_t_a ON t(a)')conn.commit()# 인덱스 있는 질의start=time.time();list(c.execute(q));t2=time.time()-startprint({'no_index':round(t1,4),'with_index':round(t2,4)})print(list(c.execute('EXPLAIN QUERY PLAN '+q)))
클러스터형 인덱스 (Clustered Index) 클러스터형 인덱스는 테이블의 데이터가 인덱스의 순서에 따라 물리적으로 정렬되어 저장되는 방식. 즉, 데이터 자체가 인덱스를 구성하며, 인덱스의 키 값 순서에 따라 데이터가 정렬된다. 특징: 1. 데이터 정렬: 테이블의 데이터가 자동으로 정렬되며, 인덱스 키 값이 데이터의 저장 순서를 결정한다. 2. 테이블당 하나만 생성 가능: 클러스터형 인덱스는 데이터의 물리적 저장 방식을 변경하기 때문에 하나의 테이블에 하나만 생성할 수 있다. 3. 빠른 검색: 범위 검색이나 정렬된 결과를 반환하는 쿼리에 매우 효율적이다. 장점:
빠른 범위 검색: 데이터를 물리적으로 정렬하므로 범위 기반 검색이 빠르다.
효율적인 정렬 작업: ORDER BY와 같은 정렬 작업에서 추가적인 비용이 거의 들지 않는다. 단점:
데이터 수정 비용 증가: 데이터를 삽입, 삭제, 수정할 때마다 물리적 정렬을 유지해야 하므로 오버헤드가 발생한다.
-- employees 테이블 생성
CREATETABLEemployees(idINTPRIMARYKEY,last_nameVARCHAR(50),first_nameVARCHAR(50),ageINT,departmentVARCHAR(50));-- id 컬럼을 기준으로 클러스터형 인덱스 생성
CREATECLUSTEREDINDEXidx_idONemployees(id);
비클러스터형 인덱스 (Non-Clustered Index) 비클러스터형 인덱스는 테이블의 데이터와 별도로 저장되며, 인덱스는 데이터의 위치를 가리키는 포인터를 포함한다. 데이터 자체는 물리적으로 정렬되지 않고, 별도의 구조로 관리된다. 특징: 1. 독립적인 데이터 구조: 비클러스터형 인덱스는 테이블 데이터와 별도로 저장된다. 2. 여러 개 생성 가능: 하나의 테이블에 여러 개의 비클러스터형 인덱스를 생성할 수 있다. 3. 포인터 사용: 인덱스를 통해 데이터를 찾을 때 포인터를 사용하여 실제 데이터를 참조한다. 장점: 1. 유연성: 여러 열이나 열 조합에 대해 다양한 비클러스터형 인덱스를 생성할 수 있다. 2. 데이터 변경 시 영향 적음: 클러스터형 인덱스처럼 물리적 정렬을 유지할 필요가 없어 삽입/삭제 시 부담이 적다. 단점: 1. 속도 저하 가능성: 데이터를 검색할 때 한 번 더 포인터를 통해 실제 데이터를 참조해야 하므로 클러스터형보다 느릴 수 있다. 2. 추가 저장 공간 필요: 별도의 구조로 저장되기 때문에 추가적인 저장 공간이 요구됩니다.
-- employees 테이블 생성
CREATETABLEemployees(idINTPRIMARYKEY,last_nameVARCHAR(50),first_nameVARCHAR(50),ageINT,departmentVARCHAR(50));-- last_name 컬럼에 대한 비클러스터형 인덱스 생성
CREATENONCLUSTEREDINDEXidx_last_nameONemployees(last_name);
기본 인덱스 (Primary Index) 기본 인덱스는 테이블의 **기본 키(Primary Key)**에 대해 자동으로 생성되는 인덱스이다. 기본 키는 테이블의 각 행을 고유하게 식별하며, 데이터 무결성을 보장한다. 일반적으로 클러스터형 인덱스로 구현되며, 데이터가 물리적으로 정렬된다. 특징:
고유성 보장: 기본 키 값은 중복될 수 없으며, NULL 값을 허용하지 않는다.
데이터 정렬: 기본 키를 기준으로 데이터가 물리적으로 정렬된다.
테이블당 하나만 생성 가능: 한 테이블에 하나의 기본 인덱스만 존재할 수 있다. 장점:
데이터 검색 속도 향상: 기본 키를 이용한 검색이 매우 빠르다.
데이터 무결성 보장: 고유성과 NULL 금지를 통해 데이터의 일관성을 유지한다. 단점:
삽입/삭제/수정 시 오버헤드: 데이터 정렬을 유지해야 하므로 성능 저하가 발생할 수 있다.
테이블당 하나만 생성 가능: 추가적인 키를 기준으로 정렬하려면 보조 인덱스를 사용해야 한다.
-- employees 테이블 생성 시 기본키 설정
CREATETABLEemployees(idINTPRIMARYKEY,last_nameVARCHAR(50),first_nameVARCHAR(50));
보조 인덱스 (Secondary Index) 보조 인덱스는 기본 키 외의 열(Column)에 대해 생성되는 인덱스를 의미한다. 기본적으로 비클러스터형 인덱스로 구현되며, 데이터와 별도로 저장된다. 특징 1. 다양한 열에 생성 가능: 기본 키가 아닌 열에도 생성할 수 있다. 2. 포인터 사용: 보조 인덱스는 실제 데이터를 가리키는 포인터를 포함한다. 3. 데이터 정렬 없음: 보조 인덱스를 생성한다고 해서 데이터가 물리적으로 정렬되지는 않는다. 장점 1. 다양한 검색 조건 지원: 기본 키 외의 열을 기준으로 효율적인 검색이 가능하다. 2. 여러 개 생성 가능: 하나의 테이블에 여러 개의 보조 인덱스를 생성할 수 있다. 단점:
추가적인 저장 공간 필요: 보조 인덱스를 저장하기 위한 별도의 구조가 필요하다.
검색 속도 저하 가능성: 데이터를 검색할 때 포인터를 통해 실제 데이터를 참조해야 하므로 기본 인덱스보다 느릴 수 있다.
학번: 모든 학생의 학번에 대해 인덱스 생성
1001 → 레코드 위치 1
1002 → 레코드 위치 2
1003 → 레코드 위치 3
1004 → 레코드 위치 4
희소 인덱스 (Sparse Index) 희소 인덱스는 데이터 파일의 일부 레코드 또는 데이터 블록에 대해서만 인덱스 엔트리를 가지고 있는 인덱스이다. 특징: 1. 각 데이터 블록을 대표하는 키 값만 인덱스에 포함된다. 2. 인덱스 크기가 상대적으로 작다. 3. 데이터의 물리적 순서에 의존한다. 장점:
인덱스 크기가 작아 저장 공간을 적게 사용한다.
인덱스 갱신 비용이 낮다.
일반적으로 밀집 인덱스보다 인덱스 단계 수가 1정도 적어 디스크 접근 횟수가 줄어들 수 있다. 단점: