Page
페이지(Page)는 데이터베이스에서 데이터를 저장하고 관리하는 가장 작은 물리적 단위다.
디스크와 메모리 간 데이터 이동에서 주요 역할을 하는데 이런 특징이 있다.
첫 째, 일반적으로 고정 크기를 갖는다. 가령 PostgreSQL에선 8KB고, MySQL에서는 16KB로 DBMS에 따른 차이가 있다.
둘 째, Page는 여러 행을 갖고 있다. 하나의 페이지에는 많~은 데이터 행을 포함하고 있는 것이다.
셋 째, 데이터베이스가 Disk에서 데이터를 읽거나 쓸 때 이 페이지 단위로 작업을 한다. 그러니 얘 때문에 Disk I/O 성능이 데이터베이스 쿼리 성능에도 직접적인 영향을 미치고 있다.
예를 들어 다음과 같은 과정이 있을 수 있다.
1. 데이터가 10000개가 있다.
2. 각 페이지는 5개의 데이터를 저장할 수 있다. 즉, 2000개의 페이지가 존재한다.
3. 각 페이지들은 Disk에 저장되며, 쿼리 실행 시 필요한 페이지를 가져와 Memory로 로드한다.
"I/O를 실행해서 특정 데이터를 가져오고 싶다면, 우리는 하나 이상의 페이지를 얻는다."
데이터 저장방식
앞서 페이지는 데이터베이스 성능에 직접적인 영향을 가져온다고 했다. 페이지 사용 효율이 좋으면 디스크 I/O를 줄일 수 있다. 이 비싼 비용인 I/O를 줄이기 위해 데이터의 저장방식부터 이해할 필요가 있다.
아래와 같은 테이블이 있다고 치자
ID | Name | Age | dpt
-----+-----------+-----+--------
1 | Alice | 25 | Engineering
2 | Bob | 30 | Marketing
3 | Charlie | 28 | Enginnering
✔️ 행 기반 저장
행 기반 저장부터 보자면, 이는 데이터를 행 단위로 페이지에 저장한다. 일반적인 OLTP(Online Transaction Processing, 운영계) 시스템에서 사용되는데 모든 열을 포함한 데이터를 함께 저장한다.
제시한 테이블을 예시로 들면 {1, Alice, 25, Enginnering}, {2, Bob, 30, Marketing}, {3, charlie, 28, Engineering} 이런 식이다.
✔️ 열 기반 저장
반대로 열 단위로 페이지에 저장하는 방식이다. 주로 OLAP(Online Analytical Processing, 분석계) 시스템에서 사용되고 특정 열만 읽는 분석 쿼리에 유리하다. 이 또한 위 테이블로 예시를 들면 {1, 2, 3}, {Alice, Bob, Charlie}, {25, 30, 28}, {Engineering, Marketing, Engineering} 임을 쉽게 유추할 수 있다.
이렇듯 두 가지의 방식이 존재하고, 이 중에선 원본 데이터를 다루는 일반적인 트랜잭션 중심 시스템에선 행 기반 저장이 동작함을 구조적으로 알고 있어야 한다.
데이터베이스의 데이터 로드 과정
이제 데이터베이스가 I/O 작업을 통해 Disk의 페이지를 Memory로 로드한다는 것을 알고 있다. 그리고 페이지의 기본 특성에 따라 많은 행을 함께 가져올 수 밖에 없음도 인지하고 있다. 거기다 각 행에는 원하는 열만 추출할 수 없고 모든 행 정보를 같이 가져오게 된다는 것도 이해하고 있다.
즉, 이 상황에 원하는 특정 데이터 행의 특정 열값만 가져오는 건 불가능하다.
그림을 그려봤다.
데이터베이스는 Disk에서 페이지를 하나 주워온다. 그 뒤, Memory에서 byte 단위로 역직렬화 작업을 수행하여 데이터를 구조화 하고 사용자가 요청한 결과를 생성해야 한다.
역직렬화, 필터링 작업은 데이터베이스의 처리 비용을 증가시키는 요인 중 하나로서 I/O 비용을 줄이는 것이 데이터베이스 성능 최적화의 핵심인 점을 함께 이해해야 한다.
(다만, Postgre와 같은 몇 데이터베이스는 OS의 Cache에 의존한다. 따라서 실상 Disk에 대한 I/O접근보다 캐시를 더 빈번하게 사용하기도 한다는 것을 참고로 알면 좋다.)
Heap
보통 프로그램 heap이라 하면 프로세스 메모리 공간인데, 그 Heap과 별개로 생각해야 한다.
데이터베이스에서 언급하는 Heap은 데이터베이스 내부 저장 구조와 관련된 용어로서, 데이터베이스 서버가 관리하는 Disk 혹은 memory Cache에 저장된 데이터 구조다. (캐시에 있다는 것은 디스크에서 가져온 데이터를 처리하는 임시 작업 공간일 때다.)
디스크
└── **힙 (테이블)**
└── 페이지 (Page)
└── 데이터 (Row)
간단히 구조화하면 이런데, Heap은 데이터 테이블을 구성하는 페이지의 묶음이다. 테이블 자체에 대한 모든 정보가 있다.
이 페이지 단위가 Disk에 저장되면 데이터 베이스가 이 페이지들에 접근해 데이터를 읽거나 쓰게 되어있다.
아무래도 테이블의 모든 데이터가 Heap에 저장되니 이 크기가 클수록 I/O 비용은 증가한다. 그래서 이를 위해 등장하는 개념이 '인덱스(Index)'이다. 인덱스는 특정 조건을 만족하는 데이터가 저장된 페이지를 빠르게 찾을 수 있도록 도와줄테니까.
Index
우선은 자료구조라고 생각해보자. 이 자료구는 Heap으로 이동할 위치를 파악할 수 있게 도와준다고 했다.
이 인덱스는 이런 특징이 있다
- 포인트: 인덱스는 힙에서 데이터가 저장된 위치(힙 테이블의 페이지와 행)을 가리키는 포인터를 가진다
- B-Tree: 대부분의 DBMS에서 B-Tree 혹은 Hash 구조를 지녔다
- 저장위치: 인덱스는 Disk에 저장되었닫가 쿼리를 통해 Memory로 로드될 예정이다
위의 특징으로 유추해보면, 인덱스는 데이터베이스의 성능을 향상시킴과 동시에 추가적인 저장 공간과 유지비용이 필요하다는 거다.
이는 뒤로하고 우리는 인덱스를 보통 하나 이상의 컬럼으로 생성한다. 즉 다중 컬럼 인덱스도 가능한데 이때는 컬럼 순서가 검색 효율에 영향을 미치고 있다. 더불어 이러한 인덱스는 자체적으론 페이지 단위로 저장되고 I/O 작업을 통해 Disk에서 Memory로 로드된다고 했다. 그러니.. 인덱스를 찾아오는 것도 비용이고 인덱스를 관리하는 것도 사용하는 것도 그 방식에 따라 다른 비용을 갖게 되니 똑똑하게 쓰게 되는게 좋다. 크기를 작게, 자주 변하지 않도록.
✔️ 동작방식
앞선 예시를 Heap과 Index로 대략 그렸다. 실제와는 다르며 그저 임시로 그렸다.
실제라면 Heap은 정렬되지 않은 상태로 저장된 테이블 구조(클러스터/비클러스터 얘기는 다음에)고, 인덱스는 B-Tree 형태의 정렬 상태를 지녔다.
이를 배경으로 그림에선 기존 테이블에 대해 ID가 10, 20, 30의 시퀀스로 나아갈 때 해당 ID를 Index의 기준으로 잡은 상황이다.
Heap은 보이는 것과 같이 각 행 1, 2, 3마다 데이터가 열 기반으로 나열되었다.
그에 비해 Index에서는 10(1, 0)과 같이 번호 뒤에 Tuple을 갖고 있는데, 이는 10이라는 ID의 데이터는 Heap내의 1번 행으로 Page0에 존재한다는 뜻이다. 즉 ID(row, pageNo) 로 볼 수 있다.
이 상황에서 원하는 것을 생각해보자.
SELECT dpt FROM person WHERE id = 30;
ID가 30인 사람의 직업이 궁금한테, 인덱스가 있을 경우 없을 경우 어떻게 될까?
💭 인덱스가 있다면
1. ID가 30인 데이터를 찾기 위해 인덱스에서 검색해본다
2. 인덱스를 가보니 이 데이터가 (row:3, page:0)에 위치한다고 한다
3. Heap에서 I/O를 수행해 Page0 ---> row:3 으로 향한다
이때, page 자체를 한꺼번에 가져오게 되고 그 뒤 row:3을 제외한 ID=1, 2 데이터는 쿨하게 버린다.
💭 인덱스가 없다면
인덱스가 없는 상황에 이 쿼리를 수행한다고 생각해보자.
데이터베이스가 무작위로 나열된 힙 테이블의 모든 페이지를 순차적으로 스캔한다. 최악의 경우 페이지 334 마지막 까지 갈 것이고, 이때 힙 테이블은 특정 열만 읽는 게 아니라 페이지 단위로 데이터를 읽다보니 필요치 않았던 name 정보도 읽어 나간다.
하지만 인덱스를 사용한다면 ID만으로 B-Tree 구조의 정렬된 상태를 이용해 로그 수준으로 빠르게 위치를 찾아나갈테니 인덱스가 왜 그리 마법같은 성능 향상을 가져오는 지 이해될 거다.
이제 클러스터를 얘기해야하는데...