Database의 Data Load 과정
Page, Heap, Index 그리고 Disk I/O
1. Page
Page(페이지)는 Database에서 Data를 저장하고 관리하는 가장 작은 물리적 단위다.
Disk와 Memory 사이에서 Data가 이동할 때 핵심적인 역할을 하며, 다음과 같은 특징이 있다.
특징
첫째, Page는 일반적으로 고정 크기를 가진다.
그러나 이는 DBMS에 따라 설정된 고정크기가 다를 수 있다.
ex. PostgreSQL: 8KB | MySQL: 16K
둘째, 하나의 Page에는 여러 개의 행(Row) 이 들어간다.
즉, Page 하나가 많은 Data 행을 담고 있는 구조다.
셋째, Database는 Disk에서 Data를 읽거나 쓸 때 Page 단위로 작업한다.
-> Disk I/O 성능이 쿼리 성능에 직접적인 영향을 미치게 된다.
간단한 예시
다음과 같은 상황을 생각해보자.
- Data가 10,000개 있다.
- 각 Page는 5개의 Data를 저장할 수 있다.
- 따라서 총 2,000개의 Page가 존재한다.
- 이 Page들은 Disk에 Memory어 있다.
- 쿼리가 실행되면 필요한 Page를 메모리로 로드해서 사용한다.
즉, 특정 Data를 가져오기 위해 I/O가 발생하면 우리는 하나 이상의 Page를 읽어오게 된다.
I/O를 실행해서 특정 Data를 가져오고 싶다면, 결국 우리는 하나 이상의 Page를 읽어오게 된다.
2. Data 저장 방식
앞서 Page는 Database 성능에 직접적인 영향을 준다고 했다.
Page 사용 효율이 좋으면 Disk I/O를 줄일 수 있다.
그리고 이 비싼 I/O 비용을 줄이려면, 먼저 Data가 어떤 방식으로 저장되는지 이해해야 한다.
아래와 같은 테이블이 있다고 해보자.
| ID | Name | Age | Dpt |
|---|---|---|---|
| 1 | Alice | 25 | Engineering |
| 2 | Bob | 30 | Marketing |
| 3 | Charlie | 28 | Engineering |
행 기반 저장
행 기반 저장(Row-oriented storage)은 Data를 행 단위로 Page에 저장하는 방식이다.
일반적인 OLTP(Online Transaction Processing, 운영계) 시스템에서 주로 사용된다.
이 방식에서는 한 행의 모든 컬럼 값을 함께 저장한다.
예를 들면 다음과 같다.
{1, Alice, 25, Engineering}{2, Bob, 30, Marketing}{3, Charlie, 28, Engineering}
즉, 한 사람의 정보 전체를 한 덩어리처럼 저장하는 방식이다.
열 기반 저장
열 기반 저장(Column-oriented storage)은 Data를 열 단위로 저장하는 방식이다.
주로 OLAP(Online Analytical Processing, 분석계) 시스템에서 사용되며, 특정 컬럼만 읽는 분석 쿼리에 유리하다.
같은 테이블을 기준으로 보면 다음과 같이 저장된다고 볼 수 있다.
{1, 2, 3}{Alice, Bob, Charlie}{25, 30, 28}{Engineering, Marketing, Engineering}
즉, 같은 속성끼리 모아서 저장하는 구조다.
정리
Data 저장 방식에는 크게 두 가지가 있다.
- 행 기반 저장: 트랜잭션 처리에 유리
- 열 기반 저장: 분석 처리에 유리
그리고 일반적인 원본 Data를 다루는 트랜잭션 중심 시스템에서는 보통 행 기반 저장 방식이 사용된다는 점을 알고 있어야 한다.
3. Database의 Data 로드 과정
이제 Database가 I/O 작업을 통해 Disk의 페이지를 메모리로 로드한다는 것은 알고 있다.
또 Page의 특성상 여러 행을 한꺼번에 가져오게 된다는 점도 이해했다.
게다가 행 기반 저장 구조에서는 특정 열만 Disk에서 정교하게 떼어 오는 것이 아니라,
행 전체 정보가 함께 로드되는 경우가 많다.
즉, 우리가 원하는 건 특정 행의 특정 열일지라도,
실제로는 그보다 더 많은 Data가 같이 읽혀 들어오는 경우가 많다.
내부에서 일어나는 일
대략적인 흐름은 다음과 같다.
- Database가 Disk에서 Page 하나를 읽어온다.
- 메모리에서 byte 단위 Data를 해석한다.
- 역직렬화(deserialization)를 통해 구조화된 Data로 변환한다.
- 이후 필터링, 조건 비교, 결과 생성 등을 수행한다.
이 과정에서 역직렬화와 필터링도 비용이다.
그래서 Database 성능 최적화의 핵심은 결국 불필요한 I/O를 얼마나 줄이느냐에 가깝다.
Disk에서Memory 읽고, 메모리에서 덜 버리는 것.
이게 성능 최적화의 핵심이다.
참고
PostgreSQL 같은 일부 Database는 운영체제의 Page 캐시(OS Cache)에 크게 의존한다.
그래서 실제 환경에서는 Disk보다 캐시를 통해 더 자주 Data에 접근하는 경우도 많다.
4. Heap
보통 프로그래밍에서 말하는 Heap은 프로세스 메모리 영역을 뜻한다.
하지만 Database에서 말하는 Heap은 그와는 조금 다르게 봐야 한다.
Database에서의 Heap은 테이블 Data가 저장된 구조를 의미한다.
쉽게 말해, 테이블을 이루는 Page들의 집합이라고 보면 된다.
구조를 단순화하면 다음과 같다.
Disk
└Memory힙(테이블)
└── Page(Page)
└── Data(Row)
즉, Heap은 테이블 자체에 대한 실제 Data가 쌓여 있는 공간이다.
Database는 이 Page들에 접근하면서 Data를 읽고 쓴다.
왜 중요할까?
테이블의 모든 Data가 Heap에 저장되므로, Heap이 커질수록 읽어야 할 Page 수도 많아진다.
그만큼 I/O 비용도 증가한다.
그리고 바로 이 지점에서 등장하는 개념이 인덱스(Index) 다.
인덱스는 특정 조건을 만족하는 Data가 저장된 위치를 빠르게 찾도록 도와준다.
즉, Heap 전체를 뒤지는 비용을 줄이기 위한 장치라고 볼 수 있다.
5. Index
우선 인덱스는 자료구조라고 생각하면 된다.
이 자료구조는 Heap 안에서 원하는 Data가 어디 있는지 빠르게 찾도록 도와준다.
인덱스의 특징
-
포인터 역할
인덱스는 힙에서 Data가 저장된 위치(Page와 행)를 가리키는 정보를 가진다. -
구조
대부분의 DBMS는 인덱스를 B-Tree 구조로 관리한다.
경우에 따라 Hash 인덱스를 사용하기도 한다. -
저장 위치
인덱스도 결국 Disk에 저장되며, 필요할 때 메모리로 로드된다.
중요한 점
인덱스는 Data 조회 성능을 향상시키지만,
그 대신 다음과 같은 비용도 함께 가진다.
- 추가 저장 공간 필요
- 인덱스 유지 비용 발생
- INSERT / UPDATE / DELETE 시 관리 비용 증가
즉, 인덱스는 무조건 많다고 좋은 게 아니다.
다중 컬럼 인덱스
인덱스는 하나 이상의 컬럼으로 만들 수 있다.
즉, 다중 컬럼 인덱스도 가능하다.
다만 이 경우에는 컬럼 순서가 검색 효율에 큰 영향을 준다.
또한 인덱스도 Page 단위로 저장되고, I/O를 통해 메모리로 올라오므로
읽는 비용도 있고 유지하는 비용도 있다.
그래서 인덱스는 보통 다음 기준으로 똑똑하게 설계해야 한다.
- 크기가 너무 크지 않게
- 자주 변경되는 컬럼은 신중하게
- 실제 검색 조건에 맞게
6. 인덱스의 동작 방식
아래는 개념 이해를 위한 단순화된 설명이다.
실제 구현과 완전히 같지는 않지만 흐름을 이해하는 데는 충분하다.
Heap은 정렬되지 않은 테이블 Data라고 생각하자.
반면 Index는 정렬된 구조(B-Tree) 로 관리된다고 보자.
예를 들어 테이블의 ID 컬럼에 인덱스가 있다고 하자.
Heap에는 실제 Data 행이 저장되어 있고,
Index에는 다음과 같은 정보가 들어 있다.
10 -> (row: 1, page: 0)
20 -> (row: 2, page: 0)
30 -> (row: 3, page: 0)
즉, 30 -> (row: 3, page: 0) 은
ID가 30인 Data가 Heap의 Page 0의 3번째 행에 있다는 뜻이다.
예시
SELECT dpt
FROM person
WHERE id = 30;
이제 ID가 30인 사람의 부서(dpt)를 찾는 상황을 보자.
인덱스가 있는 경우
- 먼저 인덱스에서
ID = 30을 찾는다. - 인덱스는 이 Data가
(row: 3, page: 0)에 있다고 알려준다. - Database는 Heap에서
Page 0을 읽어온다. - 그 안에서
row: 3의 Data를 찾아dpt값을 반환한다.
여기서 중요한 점은,
Page는 통째로 읽혀 오기 때문에 row: 3만 딱 가져오는 건 아니다.
즉, ID = 1, ID = 2 Data도 함께 메모리로 올라오지만
최종적으로는 필요 없으니 버려진다.
인덱스가 없는 경우
인덱스가 없다면 Database는 Heap 테이블을 순차 스캔(Sequential Scan) 해야 한다.
즉,
- 첫 Page부터 차례대로 읽는다.
- 각 Page 안의 행들을 확인한다.
ID = 30이 나올 때까지 계속 스캔한다.
최악의 경우 마지막 Page까지 가야 할 수도 있다.
게다가 Page 단위로 읽기 때문에,
실제로 필요한 건 dpt 하나뿐이어도 name, age 같은 다른 정보도 함께 읽게 된다.
왜 인덱스가 빠를까?
인덱스가 없다면 Heap 전체를 훑어야 한다.
하지만 인덱스가 있다면 정렬된 B-Tree 구조를 이용해 원하는 위치를 훨씬 빠르게 찾을 수 있다.
즉,
- 인덱스 없음 → 전체를 순차 탐색
- 인덱스 있음 → 정렬된 구조를 따라 빠르게 탐색
이 차이 때문에 인덱스는 마치 마법처럼 성능을 끌어올리는 것처럼 보인다.
물론 그 마법에도 저장 비용과 유지 비용은 따라온다.
마무리
정리하면 다음과 같다.
- Database는 Page 단위로 Data를 읽고 쓴다.
- Page에는 여러 행이 담긴다.
- Heap은 테이블 Data가 저장된 실제 공간이다.
- Data 조회 시 불필요한 Page를 많이 읽을수록 I/O 비용이 커진다.
- Index는 원하는 Data의 위치를 빠르게 찾도록 도와준다.
- 다만 인덱스도 저장 공간과 유지 비용이 드는 구조다.
결국 Database 성능 최적화는
얼마나 적은 Page를 읽고, 얼마나 덜 버리게 만들 것인가의 문제라고 볼 수 있다.