Skip to main content

Command Palette

Search for a command to run...

PostgreSQL SharedBuffer Hashtable, Hashfunction 구성 직접 알아보기

Updated
6 min read

PostgreSQL ShareBuffer

웹 서버를 운영하거나 데이터베이스를 운용하게 되는 경우 최종적으로 물리적인 위치에 가는 것보다, 최대한 홉을 줄이는 등의 I/O 를 최소화하기 위한 방법들이 주로 이용된다. PostgreSQL 의 SharedBuffer 도 마찬가지로 물리적인 위치에 도달하여 Disk 에 I/O 를 일으키는 것보다 앞단의 Buffer 에 있으면 그값을 읽고 리턴하여 I/O 를 최소화 해주는 장치다.

Shared Buffer

SharedBuffer 는 기본적으로 Hash Table, Hash function, Buffer Descriptor, Buffer pool 로 되어있다. 오늘은 이 HashTable 과 Hash function 에 대해 직접 테이블을 만들고, 쿼리를 날려보며 어떻게 Hash Table 에 적재되는지 테스트 해보려고 한다. (이글은 SharedBuffer 의 존재에 대해 어느정도 알고있다고 가정하고 핸즈온 느낌으로 적어보려고 한다)

Hash function

/*
 * BufTableHashCode
 *      Compute the hash code associated with a BufferTag
 *
 * This must be passed to the lookup/insert/delete routines along with the
 * tag.  We do it like this because the callers need to know the hash code
 * in order to determine which buffer partition to lock, and we don't want
 * to do the hash computation twice (hash_any is a bit slow).
 */
uint32
BufTableHashCode(BufferTag *tagPtr)
{
    return get_hash_value(SharedBufHash, tagPtr);
}

hash function 은 기본적으로 Shared Buffers 에 있는 hash_table 에서 원하는 값을 가져오기 위한 key 값을 계산하기 위해 사용된다. 언어에 내장된 HashMap 같은 것을 공부하다 보면 보통 최소한으로 Lock 을 잡기 위해 Partioning 을 진행하고, 해당 파티션에만 락을 수행하게 되는데 PG 도 버전이 업그레이드 되면서 아래 처럼hash function 을 통해 필요한 영역에만 Lock 을 잡도록 되어 있다.

static inline LWLock *
BufMappingPartitionLock(uint32 hashcode)
{
    return &MainLWLockArray[BUFFER_MAPPING_LWLOCK_OFFSET +
                            BufTableHashPartition(hashcode)].lock;
}

그런데 처음으로 BufTableHashCode 함수를 보면 BufferTag 기반으로 hash key 값을 계산하는 것을 확인할 수 있다. BufferTag 는 무엇이고, 어떤 데이터길래 hash 함수 계산에 이용될까?

BufferTag

/*
 * Buffer tag identifies which disk block the buffer contains.
 *
 * Note: the BufferTag data must be sufficient to determine where to write the
 * block, without reference to pg_class or pg_tablespace entries.  It's
 * possible that the backend flushing the buffer doesn't even believe the
 * relation is visible yet (its xact may have started before the xact that
 * created the rel).  The storage manager must be able to cope anyway.
 *
 * Note: if there's any pad bytes in the struct, InitBufferTag will have
 * to be fixed to zero them, since this struct is used as a hash key.
 */
typedef struct buftag
{
    Oid         spcOid;         /* tablespace oid */
    Oid         dbOid;          /* database oid */
    RelFileNumber relNumber;    /* relation file number */
    ForkNumber  forkNum;        /* fork number */
    BlockNumber blockNum;       /* blknum relative to begin of reln */
} BufferTag;

설명하기 전에 소스코드를 먼져 보고 들어가면 buftag(BufferTag) 라는 구조체는 위와 같이 파일의 실제 위치정보를 가지고 있음을 확인할 수 있다. 위 주석에도 나와있다. "Buffer tag identifies which disk block the buffer contains the BufferTag data must be sufficient to determine where to write the block". 즉, BufferTag 를 통해서 데이터의 물리적인 File 위치를 알 수 있는 것이다.

조금만 더 쉽게 예시를 들어보자. 만약 유저가 SELECT * FROM users WHERE id = 1000; 이라는 쿼리를 날렸다고 해보자. 이 users 라는 테이블은 pg_default 스페이스에 그리고, myapp 이라는 db 에 있다고 해보겠다. 그렇다면 BufferTag 는 아래와 같이 구성될 것이다.

BufferTag tag = {
    .spcOid = 1663,      // pg_default 테이블스페이스
    .dbOid = 16385,      // myapp 데이터베이스  
    .relNumber = 16390,  // users 테이블
    .forkNum = 0,        // 메인 데이터 파일
    .blockNum = 100      // 101번째 페이지 (0부터 시작)
};

그리고 BufferTag 기반으로 해시키를 생성할 것이다.

uint32 hash = BufTableHashCode(&tag);

이렇게 생성된 hash 값을 통해 우리는 HashTable 에 값이 있는지 없는지 Lookup 해볼수 있다.

buffer_id = BufTableLookup(&tag, hash);
if (buffer_id >= 0) {
    // 있을 경우 바로 메모리에서 읽기
} else {
    // 없을 경우 실제 물리적인 위치로 이동해서 데이터 가져오기
}

실제로 테스트 해보기

일단 실습을 진행하기 위해서 postgreSQL 에 접속해준다.

postgres=# SELECT current_database();

그리고 일반적으로 pg 의 buffer 사용량을 체크하기 위해 주로 이용되는 pg_buffercache extension 을 설치해줘야 한다.

CREATE EXTENSION IF NOT EXISTS pg_buffercache;

이제 테스트를 위한 테이블을 하나 추가해보자.

postgres=# CREATE TABLE test_buffer AS
postgres-# SELECT generate_series(1, 1000) as id,
postgres-#        'data_' || generate_series(1, 1000) as name;

테이블을 추가하고 난 뒤에는 이제 BufferCache 에 잘 올라갔는지 확인해보아야 한다. 일단 전체 테이블을 조회하고 난뒤에 BufferCache 를 확인해보자.

postgres=# SELECT count(*) FROM test_buffer;
 count
-------
  1000
(1 row)


postgres=# SELECT count(*) FROM pg_buffercache WHERE relfilenode = (SELECT oid FROM pg_class WHERE relname = 'test_buffer');
 count
-------
     8
(1 row)

우리가 넣은 1000건이 잘 조회되었다. 그런데 BufferCache 를 보면 8건만 올라와있는 것을 확인할 수 있다. 아마 페이지에 저장되었기때문에 8개로 나오는 것이겠지만 실제로 한번 쿼리를 통해 확인해보도록 하자.

SELECT 
    bufferid,
    relfilenode, 
    reltablespace,
    reldatabase,
    relforknumber,
    relblocknumber,
    isdirty,
    usagecount
FROM pg_buffercache 
WHERE relfilenode = (SELECT oid FROM pg_class WHERE relname = 'test_buffer')
ORDER BY relblocknumber;

 bufferid | relfilenode | reltablespace | reldatabase | relforknumber | relblocknumber | isdirty | usagecount
----------+-------------+---------------+-------------+---------------+----------------+---------+------------
      247 |       24585 |          1663 |           5 |             0 |              0 | t       |          2
      248 |       24585 |          1663 |           5 |             0 |              1 | t       |          2
      249 |       24585 |          1663 |           5 |             0 |              2 | t       |          2
      250 |       24585 |          1663 |           5 |             0 |              3 | t       |          2
      251 |       24585 |          1663 |           5 |             0 |              4 | t       |          2
      252 |       24585 |          1663 |           5 |             0 |              5 | t       |          2
      253 |       24585 |          1663 |           5 |             0 |              6 | f       |          2
      254 |       24585 |          1663 |           5 |             0 |              7 | f       |          2
(8 rows)

결과를 보면 relblocknumber 가 0~7까지 인것을 확인할 수 있다. 즉 릴레이션 안의 페이지수가 8개인 것이다. 실제로 페이지에 값이 어떻게 되어 있는지도 PostgreSQL pageinspect extension 을 통해 확인 가능하다. 한번 설치해보도록 하자.

CREATE EXTENSION IF NOT EXISTS pageinspect;

이제 원하는 page block 내부에 튜플 정보를 확인해보기 위해서 heap_page_items 과 get_raw_page 라는 함수를 이용할 것이다.

SELECT 
    lp as item_number,
    lp_off as offset,
    lp_len as length,
    t_ctid 
FROM heap_page_items(get_raw_page('test_buffer', 0));

 item_number | offset | length | t_ctid
-------------+--------+--------+---------
           1 |   8152 |     35 | (0,1)
           2 |   8112 |     35 | (0,2)
           3 |   8072 |     35 | (0,3)
           4 |   8032 |     35 | (0,4)
           5 |   7992 |     35 | (0,5)
           6 |   7952 |     35 | (0,6)
           7 |   7912 |     35 | (0,7)
           8 |   7872 |     35 | (0,8)
           9 |   7832 |     35 | (0,9)
          10 |   7792 |     36 | (0,10)

확인해보면 결과가 위와 같은 것을 확인할 수 있다. 다른 것들은 일단 스킵하고 t_ctid 를 일단 보면 된다. t_ctid(페이지 블럭번호, 아이템번호) 로 구성되어 있으며 이건 행의 물리적 주소를 나타낸다.

A word about t_ctid: whenever a new tuple is stored on disk, its t_ctid is initialized with its own TID (location).

즉, 우리가 만들었던 데이터(SELECT generate_series(1, 1000) as id, 'data_' || generate_series(1, 1000) as name;) 를 확인하기 위해서는 아래와 같이 id 와 name 값을 조회하면 확인해볼수있다.

SELECT 
    ctid,  -- 물리적 위치
    id,    -- 실제 데이터
    name   -- 실제 데이터
FROM test_buffer 
WHERE ctid = '(0,1)'

 ctid  | id |  name
-------+----+--------
 (0,1) |  1 | data_1
(1 row)

결과 또한 잘 나오는걸 확인해볼수 있다.

결론

BufferCache 에는 raw 단위가 아닌 페이지단위로 저장된다. 즉, 해당 페이지에서 부터는 O(N) 으로 접근하게 될 것이다.

More from this blog

RDB 에서 큰 컬럼을 인덱스로 잡으면 안되는 이유

B-Tree 는 기본적으로 페이지 사이즈 와 저장할 수 있는 원소의 개수를 고정값으로 사용한다. 하지만 우리가 실제로 페이지에 저장하는 값은 가변적인 크기를 가지고 있기 때문에 필연적으로 물리적으로 저장해야할 개수가 다 차기도 전에 페이지가 넘치는 상황에 부딪히게 된다. 예를 들어 100KB 를 저장하는 페이지에 위와 같이 데이터를 저장한 상태이다. 여

Feb 26, 20262 min read49

Slotted Page

데이터베이스와 관련된 기술을 보다보면 어떻게 데이터를 관리하고 저장하지? 특히 단편화(Fragmentation) 이 일어나는 것을 어떻게 통제하고 관리할까? 혹은 정렬된 자료구조 내부에서 데이터의 순서를 보존하기 위해 어떠한 행위들을 할까? 궁금해집니다. 오늘은 조금 더 데이터베이스 내부에 쓰이는 자료구조를 들여다보며 연관된 행위를 공부해보려고 합니다. F

Feb 22, 20264 min read63
Slotted Page

MCP 를 통한 workflow 자동화

AI native 최근에 LinkedIn 이나 여러 소셜 플랫폼들의 글을 보면 AI native 회사 라는 워딩들이 많이 보입니다. IBM 의 정의에 따르면 AI native 를 아래와 같이 정의한다고 하는데요. “AI를 사고와 업무 방식에 끊임없이 내재화하는 상태” 그렇다면 팀원들이 계속해서 AI 를 사고와 업무 방식에 끊임 없이 내재화 하려면 어떻게 해야할까요? 개발자들은 이미 Claude code 나 Codex 등 여러 AI Tool...

Feb 14, 20263 min read100

파이썬 톺아보기 2화 - Ast 와 바이트코드

식(Expression) 과 문장(Statement) 프로그래밍을 공부하다보면 위 두 단어를 반드시 마주하게 된다. 가끔 헷갈려하는 경우가 많은데 오늘은 python 에서 기본 모듈인 ast 모듈을 공부하며 이를 알아보도록 하자. 식(Expression) 기본적으로 식(Expression) 이란 평가되면 값이 나오는 코드 조각을 뜻한다. 파이썬에서는 어떠한 부분들이 있을까? 노드 타입설명예시 BinOp이항 연산a + b, x * y...

Feb 6, 20267 min read30
D

dev_roach

41 posts

Understanding PostgreSQL SharedBuffer Hashfunction