Back to the Basics

[Database][PostgreSQL]Database Page - Postgres를 기준으로 본문

DATABASE

[Database][PostgreSQL]Database Page - Postgres를 기준으로

9Jaeng 2024. 4. 8. 01:13
728x90
반응형

Page 는 데이터베이스에서 데이터를 읽어오는 하나의 단위이다. SELECT query를 할 때 내가 원하는 행만 disk에서 가져오는 것이 아니라 page라는 단위로 disk에서 캐싱을 한다. 지금까지는 Database를 단순히 사용하기만 했었다. 하지만 느린 쿼리는 결국 문제를 발생시켰고 쿼리 최적화에 대해 고민을 하다보니 Index의 원리에 대해 알아야 했다. 그러다보니 Database에서 파일을 저장하고 관리하는 방법에 대해 알아야 하는 필요성이 생겼다.

그래서 이번 블로그에는 데이터베이스 페이지의 개념, 디스크에 읽기 및 쓰기 방법, 디스크에 저장되는 방법, Postgres에서 페이지 레이아웃에 대하여 정리해보았다.

[목차]

  1. Page란?
  2. Database에서 읽고 쓰기 과정
  3. Page에 들어가는 내용은 무엇일까?
  4. 소형 page VS 대형 page
  5. PostgreSQL Page Layout

1. Page란?

  • 페이지란 고정된 크기의 메모리 위치로 disk 위치로도 변환되는 일련의 바이트이다.
  • disk 상의 표현으로 블록이라 부른다. 0부터 순차적으로 번호가 지정되는데 이를 블록번호라고 한다.
  • 저장소 모델(행 저장소와 열 저장소)에 따라 행은 페이지에 저장되고 읽혀진다.
  • database는 한 번의 I/O를 통해 하나의 row를 읽는 것이 아닌 단일 또는 복수의 page를 읽는다. 이 page에는 많은 행이 들어있다.
  • 각 페이지에는 크기가 있다. (예: Postgres에서는 8KB, MySQL에서는 16KB)

테이블, 컬렉션, 행, 열, 인덱스, 시퀀스, 문서 등 모든 것이 결국 한 페이지에 바이트로 저장된다.
이렇게 함으로써 아래와 같은 이점이 있다

  1. 저장 엔진과 데이터베이스 프론트엔드 분리가능
  • 프론트엔드 : 사용자와 상호작용을 하고 SQL 쿼리를 처리하며 데이터 형식, 트랜잭션 처리 등의 기능을 제공한다.
  • 저장엔진 : 실제 데이터를 저장하고 관리하는 역할을 한다.
  • 페이지기반의 저장방식을 사용하면 저장엔진은 데이터 형식이나 API와는 분리되어 데이터를 관리하는데 집중할 수 있다.
  • 각각 독릭적으로 확장이 가능하며 보안 위험을 줄일 수 있다.
  1. 데이터 읽고 쓰기 및 캐싱이 용이하다.
  • 모든 데이터가 페이지로 저장이 된다면 데이터를 읽고 쓰거나 캐시하는 것이 쉬워진다.
  • 읽기 : 페이지 기반 저장방식을 사용하면 필요한 데이터만 읽을 수 있다. (ex) 테이블의 특정 행만 읽어야 하는 경우 해당 행이 포함된 페이지만 읽으면 된다.
  • 쓰기 : 데이터를 페이지 단위로 업데이트 할 수 있다. (ex) 해당 행이 포함된 페이지만 업데이트하면 된다.
  • 캐싱 : 자주 사용되는 데이터(페이지)를 캐시할 수 있다.

Database에서 읽고 쓰기 과정

database는 페이지에서 읽고 쓴다. 그리고 postgres process는 데이터를 읽고 쓸 때 database Buffer or Ram 사용하고 이를 Shared Buffer라고 한다. Buffer의 메모리 사이즈는 postgresql.conf의 shared_buffers 라는 파라미터에 의해 결정되며 서버가 시작될 때 할당된다.

  • 용어의 정의
    • 데이터베이스 버퍼: Shared Buffers 라고도 불리는 메모리 영역으로, 데이터베이스 페이지를 캐싱한다.
    • 페이지: 디스크에 저장된 데이터단뒤
    • 인덱스 범위 스캔: 특정 범위의 값을 가진 행을 찾는 쿼리

그럼 행을 읽고 쓰기를 할 떄 Shared Buffer를 통해 어떤 과정을 거치는지 확인해보자

  1. 행을 읽을 때
     아래의 세 단계를 거친다.
    1) postgres process는 database의 Shared Buffers 에서 원하는 Page가 있는지 확인한다.
    2) Shared Buffer에서 찾을 수 없다면 OS에게 해당 오프셋의 파일로부터 페이지의 길이만큼 읽도록 요청한다.
    3) OS는 파일 시스템 캐시를 확인하고 필요한 데이터가 없는 경우 OS는 I/O를 발생시켜 Disk에서 페이지를 Shared Buffers로 캐싱한다.
    - page가 이 buffer pool에 배치되면 요청항 행 뿐만 아니라 page에 있는 다른 행에도 엑세스할 수 있다.
    - 특히 Index range scan으로 발생하는 읽기를 효율적으로 수행할 수 있다
    - 행이 작을수록 한 page에 더 많은 행이 들어가기 때문에 한 번의 I/O로 많은 행을 얻을 수 있다.

2. 행을 업데이트 할 때

       1) 위와 마찬가지로 데이터베이스는 행이 속한 페이지를 찾고 Shared Buffer로 가져온다.
       2) 행의 업데이트는 Shared Buffer 안에서 이루어진다.3) WAL 로그 기록4) 데이터 파일 및 WAL 파일 동기화

  • 이후 페이지는 메모리에 그대로 남아 있어 최종적으로 디스크로 플러시되기 전에 더 많은 쓰기를 수신할 수 있으므로 I/O 횟수를 최소화한다.
  • 삭제와 삽입 작업도 동일하게 작동한다.
  • 변경된 데이터는 일정 조건에 따라 데스크에 있는 데이터 파일로 동기화가 된다. - 동기화 조건 : 버퍼 공간 부족, 체크포인트 발생, WAL 로그 크기 증가, 특정 시간 간격 경과
  • 변경된 데이터는 Weite-Ahead-Logging(WAL)로그에도 기록된다. - WAL 로그는 데이터 변경 내용과 변경 순서를 기록하며 데이터 손실로부터 보호하는 역할을 한다.
  • Buffer는 메모리영역이기 때문에 디스크에비해 쓰기 속도가 훨씬 빠르다. - 버퍼에 기록된 변경된 데이터는 Dirty Data 라도고 불린다.

2. Page에 들어가는 내용은 무엇일까?

페이지에 저장되는 내용은 어떤 저장소 모델인지에 따라 다르다.

  1. Row-store 데이터베이스
    • 데이터를 행 단위로 저장하는 데이터베이스 관리 시스템(DBMS)이다.
    • 행과 해당 속성을 차례로 기록하여 OLTP 워크로드에 효율적이다.
    • OLTP(Online Transactional Processing - 동시에 발생하는 다수의 트랜잭션을 실행하는 데이터 처리 유형)
  2. Column-store 데이터베이스
    • 행을 열 단위로 페이지에 작성한다.
    • OLAP가 효율적이다. (Online analytical processing (온라인 분석 처리))
    • 단일 페이지 읽기는 한 열에서 값이 채워지므로, SUM과 같은 집계 기능에 훨씬 효과적이다.
  3. Document 데이터베이스
    • 문서를 압축하여 행 저장소와 같이 페이지에 저장한다.
  4. Graph 기반 데이터베이스
    • 그래프 탐색을 위해 연결성을 페이지에 기록하므로 페이지 읽기가 효율적이다.
    • 깊이,너비 검색에 대해 조정할 수도 있다.

어떤 저장모델이라도 결국 목표는 페이지에서 항목을 효과적으로 패킹하여 페이지 읽기가 효과적이도록 하는 것이다.
페이지는 클라이언트 측 작업에 도움이 되는 가능한 많은 유용한 정보를 제공해야 한다. 만약 작은 작업을 수행하기 위해 많은 페이지를 읽고 있는 경우 데이터 모델링을 재고해 보는 것이 좋다.

3. 소형 page VS 대형 page

  • 페이지의 크기는 Disk에서 읽고 쓰는 데이터 단위의 크기를 결정한다.
  • 페이지 크기가 클수록 I/O 작업 횟수가 줄오들고 성능이 향상되지만 메모리 사용량도 증가하고 다른 작업에 사용할 수 있는 메모리 공간이 줄어들 수 있다.
  • 소형 Page :
    • 장점
      • 읽기 및 쓰기가 빠르다 : 페이지 크기가 미디어 블록 크기와 비슷한 경우 I/O 작업이 최소화된다.
      • 메모리 사용량이 낮다 : 페이지 크기가 작으면 Buffer에 캐싱할 수 있는 page 수가 증가하여 메모리 사용량이 줄어든다.
    • 단점
      • 높은 메타데이터 오버헤드 : 페이지 크기가 작으면 페이지 헤더에 필요한 메타데이터 비율이 높아져 데이터 공간이 줄어든다.
      • 잦은 페이지 분할 : 행 크기가 페이지 크기를 초과하여 행이 여러 페이지에 분할되어 저장될 수 있다. -> 성능저하로 이어질 수 있다.
  • 대형 Page :
    • 장점
      • 낮은 메타데이터 오버헤드 : 페이지 크기가 크면 페이지 헤더에 필요한 메타데이터 비율이 낮아져 데이터 공간이 증가한다.
      • 낮은 페이지 분할 : 행 크기가 페이지 크기를 초과할 가능성이 낮아서 펲이지 분할이 줄어들고 성능이 향상될 수 있다.
    • 단점
      • 느린 읽기 쓰기 : 페이지 크기가 크면 disk I/O 작업량이 증가하여 읽기 및 쓰기 속도가 느려질 수 있다.
      • 높은 메모리 사용량 : 페이지 크기가 크면 Buffer에 캐싱할 수 있는 페이지 수가 줄어들고 메모리 사용량이 증가한다.
    • 페이지의 크기는 데이터베이스 작업 패턴에 맞춰 데이터베이스의 성능을 최대한 향상시킬 수 있도록 조정해야한다.
    • postgresql.conf 의 성정을 통해 조정할 수 있다.

4. PostgreSQL Page Layout

  • PostgreSQL의 모든 테이블과 Index는 고정 크기의 페이지 array로 이루어져있다. (크기는 일바적으로 8KB)
  • 위의 그림에서와 같이 PostgreSQL의의 페이지는 약 다섯 개의 세부 공간으로 분류된다.
    • PageHeaderData, ItemId, FreeSpace, Item, Sepcial space
    • ItemId : 실제 Item을 가리키는 Item 식별자 배열이다. 각 Item은 (오프셋, 길이) 쌍을 갖고. 항목당 4바이트이다.
    • FreeSpace : 할당되지 않은 공간으로 이 영역의 시작 부분부터 새 Item 식별자가 할당되고 끝 부분부터 새 Item이 할당된다.
    • Item : 실제 Item
    • Sepcial space : Index access method specific data. Different methods store different data. Empty in ordinary tables.

세부적으로 알아보자

  1. PageHeaderData - 24바이트
    • 24바이트의 고정 길이를 갖는다
    • Free Space pointer를 포함하여 페이지에 대한 일반 정보가 포함되어 있다.(Checksum, page의 다른 부분의 크기 등)
    • 즉 page 내의 내용을 설명하는 메타데이터이다. 
      필드 유형 길이 설명
      pd_lsn PageXLogRecPtr 8 바이트 - LSN: 마지막 변경 사항에 대한 WAL 레코드 바이트 다음 바이트, - 이 페이지와 관련된 가장 최근의 WAL 항목을 추적
      pd_checksum uint16 2 바이트 페이지 체크섬(if data checksums are enabled)
      pd_flags uint16 2 바이트 플래그 비트
      pd_lower LocationIndex 2 바이트 Free Space 시작에 대한 오프셋(페이지에서 허용하는 최대 위치)
      pd_upper LocationIndex 2 바이트 Free Space 끝까지의 오프셋(Insert되는 새로운 Tuple의 시작 위치)
      pd_special LocationIndex 2 바이트 Special 공강의 시작에 대한 오프셋
      pd_pagesize_version uint16 2 바이트 페이지 크기 및 레이아웃 버전 번호 정보
      pd_prune_xid TransactionId 4 바이트 페이지에서 제거되지 않은 가장 오래된 XMAX, 없는 경우 0
  • 위의 구조는 src/include/storage/bufpage.h에 정의되어있다.
  1. ItemId - 각 4바이트
    • 아이템 포인터의 배열이다.(아이템 자체가 아니다)
    • Line Pointer라고도 불린며 Data영역의 처음이고 Page Header 바로 뒤에 위치한다.
    • (offste, length)로 구성되며 해당 ItemId가 가리키는 Item의 물리적 주소를 의미한다.
    • PostgreSQL은 이러한 직접주소(해당 Item을 직접 가리키는)외에 간접 주소를 별도로 관리한다.
      • 간접주소는 (page numger, index of itermId)로 이루어졌고 iterm을 식별하기 위한 pointer CTID라고 부른다.
      • (n,m) 은 n번째 page의 m번째 ItermId를 가리키는 간접 주소이다.
      • 자세한 설명은 이 블로그를 참고해보자 아주 잘 설명되어있다..!
    • 이 포인터가 존재하기 때문에 HOT(Heap only Tuple)가 가능하다.
    • Update가 발생하면 새로운 tuple이 생성돠고 기존의 튜플과 같은 페이지에 튜플이 들어갈 경우 HOT 최적화는 기존 ItemId Pointer를 새로운 셔tuple을 가리키도록 변경한다. 이런 방식으로 index와 다른 데이터 구조는 여전히 이전 tupleId를 가리킬 수 있다.
    • 단점은, 각 ItemId Pointer가 4바이트를 차지하기 때문에 1000개의 Item을 저장할 수 있다면 page의 절반인 4KB가 헤더로 낭비되게 된다 (헤더는 ItemId배열을 포함한다)
  2. FreeSpace
    • ItemId와 Item 사이에 아무것도 할당되지 않은 빈 공간을 FreeSpace라고 부른다.
    • 새로운 ItemId는 이 공간의 시작 부분부터 할당이 되고 Item은 이 공간의 끝 부분부터 할당이 된다.

  3. Item

  • 메타 정보를 포함한 실제 Data를 저장
  • 할당되지 않은 FreeSpace 뒤쪽에서부터 저장된다.
  • Data란 table page의 경우 tuple, index page의 경우 index entry를 의미한다.
  • Tuple Layout
    • Header와 Data 영역으로 분류된다.
    • 23바이트의 고정 크기 헤더, 선택적 Null bitmap, Object Id, 사용자 데이터로 구성된다.
      • 고정 크기 헤더: 각 행은 행의 내부 관리에 필요한 메타데이터를 포함하는 고정 크기 헤더. 이 헤더는 일반적으로 23바이트를 차지한다.
      • NULL 비트맵: 열이 NULL 값을 가질 수 있는 경우, 헤더 다음에 NULL 비트맵이 존재. 이 비트맵은 행의 각 열에 대해 NULL 여부를 나타내는 비트(1: not-null, 0: null)를 가지고 있다.
      • 객체 ID: 특정 테이블에서 객체 ID가 사용되는 경우, 객체 ID 필드가 존재. 이 필드는 행 내부에서 해당 행을 고유하게 식별하는 값을 저장한다.
      • 사용자 데이터 : t_hoff로 지정된 오프셋부터 시작되며, 해당 머신 아키텍처의 MAXALIGN 간격을 고려하여 저장
    • header의 세부 정보는 아래와 같은 정보를 갖고 이를 통해 Tuple의 다중 버전 관리가 가능하다.

    • HeapTupleHeaderData의 구조는 src/include/access/htup_details.h 에 정의되어있다.

  1. Special Space
  • 페이지 끝 부분에 위치한다.
  • 일부 인덱스에서 추가 정보를 저장하는 데 사용하는 영역
  • Index 유형에 따라 다르다.

참고 사이트

728x90
반응형

'DATABASE' 카테고리의 다른 글

[Database][ElasticSearch] Score는 어떻게 계산이 될까?  (0) 2024.05.02
[Database] SQL 기본 - DDL  (0) 2022.01.06
Comments