[DB-STUDY] 4주차 - 인덱스
2025.12.26·14분
Ureca 3기 - 백엔드 대면 교육을 들으며 따로 진행한 약 8주간의 데이터베이스 스터디 내용입니다.
GitHub - Study-Castle/Database_StudyContribute to Study-Castle/Database_Study development by creating an account on GitHub.github.com📘 1. 인덱스의 장단점
장점
- 데이터의 읽기 속도 향상
- 전반적인 시스템 부하 감소
단점
- - INSERT, UPDATE, DELETE 성능 희생
- 데이터의 저장속도를 얼마나 희생가능 + 읽기 속도를 얼마나 더 빠르게 만들어야 하는가에 따라 결정.
- 데이터의 읽기 속도 희생 CASE - 쓰기 지연보다 처리량 중시 ⇒ `INSERT` 성능 향상 중시
- 로그 / 센서 수집
- 트랜잭션 처리 - 금융 거래, 주문 처리
- 버퍼링 / 배치
- 데이터의 저장 속도 희생 CASE
- 검색 / 컨텐츠 서비스
- 분석 / 리포팅 서비스
📘 2. 인덱스 자료구조
B-Tree 인덱스

B-Tree
구성
- 루트 노드 : 최상위 노드
- 브랜치 노드 : 중간 노드, 루트↔리프 노드 간 데이터 경로 제공.
- 리프 노드 : 실제 데이터 레코드를 찾아가기 위한 주솟값.
- MyISAM 엔진 : 세컨더리 인덱스가 물리적 주소를 가진다.
- InnoDB 엔진 : PK(데이터의 논리적 주소) 저장. 인덱스를 통해 얻은 PK의 논리적 주소를 통해 클러스터드 테이블에서 실제 테이블 조회(즉, 1번의 추가 조회 발생)
Q. B-Tree 노드에는 어떤 값이 저장되는가?
A. 각 노드는 데이터를 가지는 것이 아닌 한 개의 `Page`(InnoDB 스토리지 엔진이 데이터를 저장하는 기본 단위 / Page내 여러 데이터 존재)를 가진다.

인덱스 키 추가
- 저장될 키 값 B-Tree에서 적절한 위치 검색
- 레코드의 키 값과 대상 레코드의 주소 정보 → 리프 노드에 저장.
인덱스 키 삭제
- 해당 키 값 저장된 리프노드 탐색
- 찾은 리프노드 삭제 마킹(그대로 유지 혹은 재활용 가능)
인덱스 키 변경
- 인덱스 키 삭제
- 인덱스 키 추가
인덱스 키 검색
- 루트 노드 시작
- 브랜치 노드 : 비교 작업
- 리프 노드 : 비교 작업
Q. 인덱스 순서대로 정렬되어 저장?
A. 대부분의 RDBMS의 레코드는 특정 기준으로 정렬되지 않고 임의의 순서로 저장. InnoDB 테이블에서의 레코드는 기본적으로 PK 순서대로 정렬
인덱스 성능에 영향을 미치는 요소
1. 인덱스 키 값의 크기
- 인덱스를 구성하는 키 값의 크기가 커질수록 한 Page 내에 저장 가능한 레코드의 수가 감소. ⇒ 디스크로 부터 읽어야 하는 횟수의 증가 ⇒ 성능 저하
인덱스 페이지에 저장 가능 개수 = Page 크기 / (인덱스 크기 + 자식 노드 주소)- 인덱스 A의 키 값 크기 :32KB / 인덱스 B의 키 값 크기 16KB 가정
- A의 한 페이지에서는 372개, B의 한 페이지에서는 585개가 저장됨
- 500개의 레코드를 READ 작업을 한다면 A는 최소 2번 이상 디스크 읽기 작업.
2. B-Tree 깊이
- 직접 제어 불가능.
- B-Tree의 깊이는 값을 검색할 때 몇 번이나 랜덤하게 디스크를 읽어야 하는지 문제로 직결.
- 인덱스 키 값 ⬆️ ⇒ 한 페이지에 담을 수 있는 키 값 개수⬇️ ⇒ B-Tree 깊이 ⬆️ ⇒ 디스크 읽기 ⬆️
3. 선택도 (기수성)
- 모든 인덱스 키 값 중 유니크한 값의 수
- 인덱스 키 값 중 중복된 값 ⬆️ ⇒ 기수성 ⬇️ / 선택도 ⬇️
- 선택도 ⬆️ ⇒ 검색 대상 ⬇️ ⇒ 디스크 읽기 ⬇️
4. 읽어야 하는 레코드의 건수
- 일반적으로 DBMS의 옵티마이저는 인덱스를 통해 레코드 1건 읽는 것이 테이블에서 직접 레코드 1건 읽는 것보다 4~5배 비용 많이 드는 작업으로 예측.
- 즉, 100만 건 레코드 중 50만 건을 읽는 작업에 대해서 옵티마이저는 인덱스를 타지 않고 직접 테이블을 모두 읽을 것.
- 총 레코드 수 x (20~25%) > 읽어야 하는 총 레코드 수 ⇒ 인덱스 추가 사용에 대한 고려 해봐야함.
📘 3. 인덱스를 이용하여 데이터 읽는 방법
인덱스 레인지 스캔
SELECT *
FROM employees
WHERE first_name BETWEEN 'Ebbe' AND 'God';
- 가장 대표적이면서 가장 빠른 방법
- 검색해야 할 인덱스 범위가 결정되었을 때 사용.
- 스캔 과정
- 루트 노드 비교 → 브랜치 노드 비교 → 리프 노드 비교 ⇒ 필요한 레코드 시작 지점 찾는다.
- 리프 노드의 레코드들만 스캔 (만약, 리프 노드를 끝까지 읽으면 리프 노드 간 링크를 통해 다음 리프 노드로 이동)
- 스캔 종료 지점 → 이제까지 읽은 레코드들 반환.
- 어떤 방식으로 스캔하든 관계없이 인덱스를 구성하는 칼럼의 정순 OR 역순 정렬된 레코드를 가지고 옴.
인덱스 풀 스캔

- 인덱스의 처음부터 끝까지 모두 읽는 방식.
- 쿼리 조건절에 사용된 칼럼이 인덱스의 첫 번쨰 칼럼이 아닌 경우
- 쿼리가 인덱스에 명시된 칼럼만으로 조건을 처리 가능한 경우
- 속도 : 인덱스 레인지 스캔 > 인덱스 풀 스캔 > 테이블 풀 스캔
루스 인덱스 스캔
- 듬성듬성하게 인덱스를 읽는 방법.
- 중간에 필요하지 않은 인덱스 키 값 SKIP
- GROUP BY / MAX() MIN() 함수 최적화 하는 경우 사용.
- dept_no 그룹 별로 1번째 레코드의 emp_no 값만 읽으면 됨.
- WHERE 조건 절 모두 스캔 필요❌ ⇒ 조건 만족하지 않으면 레코드 무시하고 SKIP
SELECT
dept_no, MIN(emp_no)
FROM
dept_emp
WHERE
dep_no BETWEEN 'd002' AND 'd004'
GROUP BY
dept_no;
=> (dept_no, emp_no) 인덱스 설정인덱스 스킵 스캔
- 루스 인덱스 스캔은 GROUP BY 작업을 처리하기 위한 인덱스 스캔 방식이라면 인덱스 스킵 스캔은 WHERE 조건절 검색을 위한 인덱스 스캔 방식.
- 단점
- WHERE 조건절에 조건 없는 인덱스의 선행 칼럼의 유니크 값 개수 적어야함.
- 쿼리가 인덱스에 존재하는 컬럼만으로 처리 가능해야함.
📘 4. 인덱스의 가용성과 효율성
비교 조건
SELECT * FROM dept_emp
WHERE dept_no = 'd002' AND emp_no >= 1004;- INDEX(dept_no, emp_no)
- dept_no와 emp_no 칼럼 ⇒ 작업 범위 결정 조건
- INDEX(emp_no,dept_no)
- emp_no 칼럼 ⇒ 작업 범위 결정 조건
- dept_no 칼럼 ⇒ 필터링 조건
- 작업 범위 결정 조건 🔼 ⇒ 쿼리 처리 성능 🔼
- 필터링 조건 ⇒ 쿼리 처리 성능❌
[ INDEX(dept_no, emp_no) ]
- dept_no 기준 정렬 ⇒ emp_no 기준 정렬
(dept_no, emp_no)
-----------------
(d001, 1001)
(d001, 1002)
(d001, 1003)
(d002, 1001)
(d002, 1002)
(d002, 1003)
(d002, 1004)
(d002, 1005)
(d002, 1006)
(d003, 1001)
(d003, 1002)
...- 인덱스에서 ‘d002’ 시작되는 구간 스캔
- 인덱스 상에서 (d002,…)로 시작하는 인덱스 시작 위치와 끝 위치 탐색.
- 줄여진 구간 내에서 (d002, 1004) 시작 위치 ⇒ (d002,MAX)까지 스캔.
[ INDEX(emp_no,dept_no) ]
(emp_no, dept_no)
-----------------
(1001, d001)
(1001, d002)
(1001, d003)
(1002, d001)
(1002, d002)
(1002, d003)
(1003, d001)
...
(1004, d001)
(1004, d002)
(1004, d003)
(1005, d001)
(1005, d002)
...- 인덱스에서 emp_no ≥ 1004시작 지점 ~ emp_no = MAX_VALUE 범위까지 스캔.
- 스캔 범위 내 레코드에서 dep_no = ‘d002’인 레코드 필터링 ⇒ 결국 1번에서의 스캔한 모든 레코드 탐색.
인덱스 가용성
INDEX(first_name)
- 주어진 인덱스 페이지
(first_name | 주소)
---------------------
(A a mer, | 1001)
(A a mod, | 1002)
(A b delaziz, | 1003)
(A b delghani,| 1001)
(A b delkadar,| 1002)
(A b delwaheb,| 1003)
(A b dulah, | 1004)
(A b dulla, | 1005)
(A c hilleas, | 1006)
...SELECT * FROM employee WHERE first_name LIKE '%mer';- 인덱스 사용❌
- 인덱스로 설정한 first_name에서 왼쪽부터 한 글자씩 비교하면서 일치 레코드를 찾아야함.
- But! %mer로 왼쪽 부분 고정❌ ⇒우선 순위가 낮은 뒷부분 값만으로 정렬됨 ⇒ 인덱스 효과 미미.
(dept_no | emp_no | 주소)
------------------------------
d001 | 10239 |
d001 | 10259 |
d002 | 10042 |
d002 | 10050. |
d002 |. 10059. |
d002 | 10080. |
d002 | 10132. |
d003 | 10005. |
d003 | 10013. |
...SELECT * FROM dept_emp WHERE emp_no >= 10144;- dept_no 컬럼 먼저 정렬 ⇒ emp_no 정렬
- dept_no 조건 없이 emp_no 값만으로 검색 ⇒ 인덱스 효율적 사용❌
인덱스를 사용할 수 없는 조건
- NOT-EQUAL 비교
- LIKE ‘%??’
- 스토어드 함수 / 다른 연산으로 인덱스 칼럼 변경
- NOT-DETERMINISTIC 속성의 스토어드 함수
- 데이터 타입이 서로 다른 비교(인덱스 컬럼을 변경시키는 경우)
- NULL 조건
📘 5. 다양한 인덱스
전문 검색 인덱스
- 컬럼에서 키워드로 검색하기 위한 방법
- 일부 검색에도 FullText 인덱스 사용 가능 ⇒ LIKE 조회보다 성능 우수
인덱싱 방식
- Stopword
- 내용을 공백, Tab, 문장 기호로 구분자 혹은 사용자 정의 문자열
- 나누어진 단어들에 대해 저장 ⇒ 조회 시 완전히 일치한 단어만 포함
ex)
1. 우리 집 강아지는 복슬강아지
2. 우리집 강아지는 복슬강아지
3. 우리집강아지는복슬강아지
=> "우리" 키워드 포함 조회 -> 1번만 조회됨.- N-gram
- 컬럼의 값을 변형해서 만들어진 값에 대해 인덱스 구축 시 사용.
함수 기반 인덱스
- 컬럼의 값을 변형해서 만들어진 값에 대해 인덱스 구축 시 사용.
인덱싱 방식
- 가상 칼럼
- 테이블 구조가 변경된다는 단점 존재
first_name + last_name 합쳐서 검색 상황
1. full_name 가상 컬럼 생성
2. 모든 레코드에 대해 full_name 업데이트
ALTER TABLE user
ADD full_name VARCHAR(30) AS (CONCAT(first_naem,' ',last_name)) VIRTUAL,
ADD INDEX ix_fullname (full_name);- 함수 이용
- 테이블 구조 변경 ❌
CREATE TABLE user (
user_id BIGINT,
..
INDEX ix_fullname((CONCAT(first_name,' ',last_name))
);멀티 밸류 인덱스
- 하나의 데이터 레코드가 여러 개의 키 값 가지는 형태
- MySQL 8.0부터 JSON 형식 사용 가능.
CREATE TABLE user (
user_id BIGINT,
...
credit_info JSON,
INDEX mx_creditscores ( (CAST(credit_info->'$.credit_scores' AS UNSIGNED ARRAY)) )INSERT INTO user VALUES (1, 'Matt', 'Lee', '{"credit_scores":[360,353,351]}');클러스터링 인덱스
- PK 값이 비슷한 레코드끼리 묶어서 저장하는 것.
- 테이블의 PK에 대해서만 적용
- PK 값으로 클러스터링 된 테이블은 PK에 대한 의존도 ❌
- PK 값 변경 시 페이징 이동 발생할 수도 있음.
| 장점 | 단점 |
|---|---|
| PK로 검색 시 성능 우수 | 클러스터링 키 값 크기 큰 경우 인덱스의 크기 커짐 |
| 테이블의 모든 세컨더리 인덱스가 클러스터링 키를 가지므로 인덱스만으로 처리되는 경우 많음. | 세컨더리 인덱스를 통해 검색 시 PK로 다시 한 번 검색 → 처리 성능 느림 |
| INSERT할 때 PK에 따라 레코드 위치 결정되므로 처리 성능 느림 |
댓글
GitHub Discussions에 저장됩니다