일단 shared buffer 영역이 존재하는 이유는 매번 쿼리를 날릴때마다 DB는 디스크에 접근해서 데이터를 가져오기엔 비용과 부하가 발생한다.
이걸 해결하려면 메모리에 데이터를 미리 캐싱해서 올려두면 매번 디스크 IO를 발생시키지않고 응답속도와 비용절감을 가능케할 수 있다.
일단 책을 읽어보면서 buffer descrioptor는 무슨 역할을 하는지 궁금하게 했다.
순수하게 텍스트로 보면 그냥 버퍼 설명하는 주체?라고 생각하게 된다.
책에서는 Lock이라는 설명이 들어있었다 그래서 내가 아는 비관적/낙관적 락을 관리하는 주체인가? 해서 제미나이한테 물어보니 내가 아는 락이 아니라 shared pin이라고 shared buffer영역에서 사용되는 개념이였다. 말그대로 쿼리로 인해서 해당 테이블 데이터를 사용중이라는 lock 즉 pin을 건다는 개념인것이다.
그래서 예시를 들어서 개념을 확고하게 해보았다.
PostgreSQL은 보통 Clock Sweep이라는 알고리즘을 사용합니다. 시계 바늘이 돌면서 "너 요즘 자주 쓰이니?"라고 물어보고 다니는 방식이죠.
상황 가정
1번 버퍼 (1번 테이블): Usage Count = 11, Pin = 0 (아무도 안 쓰는 중)
2번 버퍼 (2번 테이블): Usage Count = 7, Pin = 1 (현재 질문자님이 읽는 중)
새로운 3번 테이블: 메모리로 들어오고 싶어 함 (자리 없음)
진행 과정
희생양 찾기: 시계 바늘이 돌면서 어떤 버퍼를 비울지 검사합니다.
2번 버퍼 검사: Usage Count가 7로 낮지만, Pin이 1인 것을 확인합니다.
결과: "오, 이건 누군가 지금 쓰고 있네? 건드리면 큰일 나겠다!" 하고 무조건 건너뜁니다. (Pin의 위력)
1번 버퍼 검사: Usage Count가 11로 높습니다. 시계 바늘은 이 카운트를 하나 깎습니다 ($11 -> 10$).
만약 모든 버퍼가 다 바쁘다면 바늘이 여러 바퀴 돌겠죠? 그러다 Usage Count가 0이 되고 Pin이 0인 녀석을 발견하면 그게 바로 희생양이 됩니다.
해시함수
간단히 이야기하면 어느 buffer descriptor에 있는지 태그해놓은 정보를 모아놓은 공간이다.
이렇게하면 모든 buffer descriptor에 있는 정보들을 확인하지 않아도 찾으려는 테이블의 페이지를 빨리 찾을 수 있다.
Hash(key) -> Mod(key, length) -> HashValue(result)
위의 공식을 이용해서 어떻게 해시값을 얻는지 정리해보자
key는 가변길이의 입력값, length는 해시 테이블의 길이라고 하자.
1. 유저가 abc라는 값을 저장하고 싶다. 그리고 해시 테이블의 길이는 100
2. 내부에서는 컴퓨터가 알아들을수 있도록 abc값을 hash함수를 이용해서 정수값으로 변환
3. 반환받은 정수값을 이용해서 Mod(-785388649, 100)로 계산하면 나머지값을 반환, 결과값: -49
4. 결과값이 음수일 경우라면 내부적으로 양수로 변환하여 49, 결국 해당 위치에 실제 데이터가 있다고 길을 알려줌
해시 테이블
해시 테이블의 내부 구조에 대해서 백화점의 보관함을 예시로 들어보자
# ? 도서관 수장고 비유로 보는 PostgreSQL 데이터 검색
### 1. 입력 키 (Key): "조선왕조실록 태조편"
* **상황:** 내가 찾고 싶은 책 제목입니다.
* **동작:** 이 제목을 '해시 함수'라는 자동 분류기에 넣으면 **256**이라는 숫자가 튀어나옵니다.
---
### 2. 해시 테이블 (Bucket [256])
* **상황:** 수장고에 있는 수만 개의 보관함 중 **256번 보관함**을 가리킵니다.
* **동작:** "태조편은 무조건 256번 함에 있다"라는 규칙에 따라 해당 위치로 바로 이동합니다.
---
### 3. 버퍼 파티션 (Buffer Partition)
* **상황:** 256번 보관함이 있는 **'제1구역'의 관리자**입니다.
* **동작:** 관리자에게 "256번 함을 열겠습니다"라고 허락을 구합니다(Lock).
* *덕분에 다른 구역(파티션)에서 책을 찾는 사람들과 서로 동선이 겹치지 않습니다.*
---
### 4. 디렉토리 및 해시 세그먼트
* **상황:** '제1구역' 안에 나열된 **보관함들의 복도**입니다.
* **동작:** 관리자의 허락 하에 복도를 따라 256번 보관함 앞에 도착합니다.
---
### 5. 해시 엘리먼트 (Buffer Tag)
* **상황:** 256번 보관함을 열었을 때 들어있는 **실제 책들과 이름표**입니다.
* **동작:** 보관함 안에 여러 권의 책이 있을 수 있으므로(해시 충돌), 책에 붙은 태그를 하나씩 확인합니다.
* 1번 책 태그: '고려사' (Pass)
* 2번 책 태그: **'조선왕조실록 태조편' (Match!)**
* **최종 결과:** 태그에 적힌 **Buffer ID**를 보고 메모리상의 실제 데이터 위치를 찾아냅니다.
---
위 예시에서 3번 동작에서 Lock을 건다라고 하는데 해당 락은 Light Weight Lock으로 내부 병목현상을 발생하지 않게하기 위해 경량화 락을 사용한다.
그리고 버퍼 파티션마다 LW Lock을 부여하여 동시에 접근요청이 오더라도 분산하여 관리하기 때문에 오랜 대기 및 부하를 줄이도록 하였다.
근데 지금까지보면 데이터를 금방 찾아오는 방법에 대해서 내부구현을 설명했다.
생각해보니 빨리 찾아오는거면 이건 결국 인덱스 라는 개념이지 않을까 하는 생각이 들었다.
제미나이에게 물어보니 인덱스와 해시 테이블은 결국 영구적인지 임시적인 매커니즘인지에 대한 차이라고 한다.
인덱스는 영구적인 데이터 위치 지도이고, 해시 테이블은 postgresql의 메모리에 있는 임시적인 데이터 위치 지도인것이다.
그래서 만약 메모리에 없으면 직접 디스크 IO를 발생시켜서 데이터를 가져오기때문에 비용문제와 함께 메모리에서 가져오는 방식보다는 시간이 걸릴것이다. 그래서 이런 구멍을 메우기 위해 인덱스 방식이 존재하는 것이다.
만약 ‘서울시’ 와 ‘부산시’가 같은 해시값을 반환하게 되어 같은 위치의 버킷에 할당하게 되면
해시 충돌이 일어난다. 만약 ‘서울시’ 라는 값이 해시 버킷에 저장되어있고 그 다음에 ‘부산시’가 같은 해시 버킷에 들어오면 ‘부산시’ 앨리먼트 ‘서울시’ 앨리먼트와 연결이 된다. 즉 ‘부산시’ 앨리먼트는 해시 버킷에 저장되지 않고 ‘서울시’ 앨리먼트와 연결이 되는걸 의미하는 단어로 chaining 이라고 부른다.
해시 엘리먼트
해시 엘리먼트는 버킷에 들어있는 실제 값을 의미하며 해시 엔트리라고도 불린다.
내부에는 hash value, 버퍼 태그, 버퍼 ID 구성되어있다.
hash value는 해시함수를 이용하여 생성된 값을 의미한다. 내부에서 고유한 값
버퍼 ID는 버퍼 풀이라는 거대한 보관함 전체를 의미한다고 하면 버퍼는 그 중의 칸 하나하나를 의미하고 버퍼 ID는 보관함 칸 한개의 ID를 생각하면 될것 같다.
버퍼 태그는 메모리에 올라와있는 데이터를 식별할수 있는 메타데이터라고 보면 될것같다. 즉, 실제 데이터가 어느페이지에 있는지 알 수 있다.
SELECT
b.bufferid AS "버퍼 ID",
b.reltablespace AS "SpcNode(OID)",
t.spcname AS "테이블스페이스명",
b.reldatabase AS "DbNode(OID)",
d.datname AS "데이터베이스명",
b.relfilenode AS "RelNode(OID)",
c.relname AS "테이블/인덱스명",
b.relforknumber AS "포크 번호",
b.relblocknumber AS "블록 번호"
FROM
pg_buffercache b
LEFT JOIN pg_database d ON b.reldatabase = d.oid
LEFT JOIN pg_class c ON b.relfilenode = c.relfilenode
LEFT JOIN pg_tablespace t ON b.reltablespace = t.oid
WHERE
b.reldatabase = (SELECT oid FROM pg_database WHERE datname = current_database())
AND c.relname IS NOT NULL and c.relname = '테이블명'
ORDER BY
b.bufferid
위 쿼리를 이용해서 해당 테이블 데이터가 메모리 어디에 할당받고 있는지 알수 있다.
| 값 | 항목명 | 의미와 해석 (비유) |
|---|---|---|
| 1286 | 버퍼 ID (Buffer Id) | “현재 이 데이터는 메모리(공유 버퍼)의 1286번 칸에 들어있습니다.” 가장 핵심이 되는 보관함 번호입니다. |
| 1663 pg_default |
테이블스페이스 OID / 테이블스페이스명 | “이 데이터의 하드디스크 상 저장 구역은 기본 구역(pg_default)입니다.” PostgreSQL이 설치될 때 기본적으로 만들어지는 디스크 저장 위치를 뜻합니다. |
| 5 postgres |
데이터베이스 OID / 데이터베이스명 | “이 데이터는 postgres라는 데이터베이스 소속입니다.” 건물로 치면 ‘postgres’라는 회사의 사무실(5번 방) 물건입니다. |
| 16512 users |
릴레이션 OID / 테이블·인덱스명 | “이 데이터는 users라는 테이블의 데이터입니다.” 사무실 안의 16512번 서랍장(users 테이블)에서 꺼내온 물건입니다. |
| 0 | 포크 번호 (ForkNum) | “이것은 테이블의 ‘진짜 본문 데이터(Main)’입니다.” PostgreSQL에서 0번은 실제 데이터 본문을 의미합니다. (참고로 1번은 빈 공간 정보, 2번은 데이터 표시 여부 등 관리용 정보입니다.) |
| 0 | 블록 번호 (BlockNum) | “이것은 users 테이블 파일의 가장 ‘첫 번째 조각(블록)’입니다.” 데이터베이스는 데이터를 8KB 크기의 블록으로 쪼개서 저장하는데, 그중 0번째(맨 처음) 페이지라는 뜻입니다. |