(KO) CUBRID Heap Manager — 슬롯 페이지, 레코드 레이아웃, 연산, MVCC, 캐시
목차
- 학술적 배경
- DBMS 공통 설계 패턴 (Common DBMS Design)
- CUBRID의 구현
- 소스 코드 가이드
- 소스 검증 (2026-05-01 기준)
- CUBRID 너머 — 비교 설계와 연구 동향 (Beyond CUBRID — Comparative Designs & Research Frontiers)
- 출처
학술적 배경
섹션 제목: “학술적 배경”힙 파일 (heap file) 은 관계형 DBMS 엔진의 가장 아래층에 있 는 row-oriented 물리 저장소다. 정렬되지 않은 레코드들이 여러 페 이지에 흩어져 있고, 레코드 하나하나는 식별자 (RID, ROWID, OID) 로 색인된다. Database Internals (Petrov, 3장 File Formats, 4장 Implementing B-Trees) 는 힙 파일을 인덱스가 가리키는 저 장 영역 으로 정의한다. 인덱스 엔트리는 키와 OID 를 함께 들고 있고, 그 OID 를 따라가면 레코드가 실제로 저장된 힙 페이지에 도 달한다.
Petrov 는 slotted page (슬롯 단위로 공간을 나눈 페이지) 를 표준적으로 정의하는 출처이기도 하다. slotted page 는 “고정 크기 페이지 안에 가변 길이 레코드를 어떻게 저장할 것인가” 라는 문제 를 푸는 구조다. 페이지 한쪽 끝에서는 슬롯 이 거꾸로 자란다. 슬롯이란 레코드의 위치 (offset) 와 길이를 담는 고정 크기 엔트리 다. 페이지의 반대쪽 끝에서는 레코드 본문 이 가변 길이로 정방 향으로 자란다. 그 사이는 자유 공간 (free gap) 으로 남는다. 압축 (compaction) 은 이 자유 공간을 회수하는 작업이고, 레코드 삭제 는 슬롯에 지웠음 표시만 남길 뿐 데이터를 즉시 옮기지는 않는 다.
구현을 좌우하는 추가 요소가 두 가지 더 있다.
-
가변 길이 레코드와 oversized 레코드 처리. 레코드는 두 가 지 이유로 자기 슬롯을 초과할 수 있다. 첫째, UPDATE 가 문자열 을 늘려서 기존 레코드가 커진 경우. 둘째, BLOB 처럼 처음부터 어떤 페이지에도 들어갈 수 없는 크기인 경우. 고전적인 해결책 은 전달 포인터 (forwarding pointer) 를 두는 것이다. 원래 자리에는 이 레코드는 다른 곳에 있다 라는 표시를 남기고, 실 제 데이터는 같은 힙의 다른 페이지나 별도 overflow 파일에 둔 다. Garcia-Molina / Ullman / Widom Database Systems: The Complete Book §13.7 “Variable-Length Data and Records” 참 고.
-
MVCC 버전 체인을 힙 안에 둘 것인가. 엔진이 MVCC 를 도입 하면 (동반 문서
cubrid-mvcc.md참고), 레코드별 헤더에 필 드가 추가된다. insert MVCCID, delete MVCCID, 그리고 이전 버 전을 가리키는 back-pointer 가 그것이다. 가시성 술어 (visibility predicate) 는 필요할 때 이 back-pointer 를 따라 heap → log → undo segment 순으로 거슬러 올라간다. 이 모델에 서 힙 매니저와 MVCC 서브시스템은 같은 레코드 헤더를 공유하면 서도 책임은 분리된다. 힙 매니저는 물리적 레이아웃 을, MVCC 서브시스템은 가시성 판정 을 담당한다.
이 문서는 위 세 가지 — slotted page, 여러 형태의 레코드, 레코
드 헤더에 박혀 있는 MVCC 정보 — 가 src/storage/heap_file.{h,c}
와 src/storage/slotted_page.{h,c} 에서 어떻게 구현되는지를 차
례로 따라간다.
DBMS 공통 설계 패턴 (Common DBMS Design)
섹션 제목: “DBMS 공통 설계 패턴 (Common DBMS Design)”교재가 모델을 준다면, 이 섹션은 거의 모든 행 지향 엔진이 어떤
형태로든 채택하는 공학적 관행 의 이름을 모은다. PostgreSQL,
Oracle, MySQL InnoDB, SQL Server, CUBRID 가 모두 이 패턴을 공유
한다. 다음의 ## CUBRID의 구현 은 발명이 아니라, 이 공유 설계
공간 위에서 선택된 다이얼 조합으로 읽으면 된다.
페이지 단위의 공통 형태 — slotted page
섹션 제목: “페이지 단위의 공통 형태 — slotted page”row-oriented 힙의 모든 페이지는 slotted page 형태를 따른다. 페이지 위쪽에 작은 고정 헤더가 있고, 페이지 끝쪽에는 슬롯 디렉 토리가 거꾸로 자란다. 헤더 다음 자리부터 레코드 본문이 앞으로 자라고, 그 사이에 자유 공간이 남는다. 새 레코드는 이 자유 공간 안에만 들어갈 수 있다. 삭제하면 구멍이 남는데, 이 구멍은 정책 에 따라 곧장 다시 쓰이거나 (reusable-slot 모드) 나중에 압축 (compaction) 도중 회수된다. 가장 중요한 공통 속성은 슬롯이 안정적인 식별자 라는 것이다. 압축 도중 레코드 본문이 페이지 안에서 옮겨지더라도 슬롯 ID 로 만든 OID / TID / RID 는 바뀌지 않는다.
OID / TID / RID = (file, page, slot)
섹션 제목: “OID / TID / RID = (file, page, slot)”레코드별 식별자는 힙 파일·페이지·페이지 안 슬롯의 3-튜플이다.
인덱스 엔트리가 이 튜플을 들고 있고, 따라가면 슬롯에 도달한
다. 압축 도중 변할 수 있는 것은 슬롯의 오프셋 뿐이다.
PostgreSQL 은 (blocknumber, offsetnumber) 로 부르고
xmin / xmax 와 짝짓는다. Oracle 은 같은 모양을 ROWID 로 쓴
다. CUBRID 는 OID = (volid, pageid, slotid) 다.
페이지 안 전달 (relocation) — 조금 자란 경우
섹션 제목: “페이지 안 전달 (relocation) — 조금 자란 경우”UPDATE 가 새 이미지를 원래 슬롯에 다 넣지 못할 만큼 레코드를 키울 때, 엔진은 overflow 파일로 곧장 가기 전에 두 가지 저렴한 선택지를 가진다.
- 같은 페이지 안에서 이동. 페이지에 자유 공간이 충분하면 슬 롯 ID 는 그대로 두고 슬롯 안의 오프셋만 바꾼다.
- 같은 힙의 다른 페이지로 재배치 (relocation). 원래 슬롯에
새 위치를 가리키는 전달 레코드 (forwarding record) 를 남긴
다. 인덱스 엔트리는 그대로 원래 OID 로 찾아오고, 읽기 경로에
서 엔진이 전달 레코드를 투명하게 따라간다. PostgreSQL 은 페
이지 안에서는 HOT 체인, 페이지 사이에서는
line-pointer redirect 를 쓴다. Oracle 은 이 전달 레코드를
row migration 이라 부른다. CUBRID 는
REC_RELOCATION+REC_NEWHOME조합으로 표현한다.
overflow 파일 — 처음부터 너무 큰 경우
섹션 제목: “overflow 파일 — 처음부터 너무 큰 경우”페이지 페이로드 영역보다 큰 레코드는 힙 파일 안에 살 수 없다.
이런 레코드는 별도의 overflow 파일 에 페이지 단위 blob 으로
들어가고, 힙에는 overflow 위치를 가리키는 작은 참조 레코드만
남는다. CUBRID 에서는 REC_BIGONE 과 페이지 정렬 overflow 레
코드의 조합으로 구현된다.
자유 공간 힌트 캐시 (free-space hint cache)
섹션 제목: “자유 공간 힌트 캐시 (free-space hint cache)”이 레코드를 담을 만큼 자유 공간이 있는 페이지 를 찾는 비용은 계속 발생한다. 교재의 해법은 힙별 자유 공간 맵 (free-space map) 또는 best-fit 페이지의 작은 캐시다. 거의 모든 엔진이 비 슷한 구조를 둔다. PostgreSQL 은 FSM 을, Oracle 은 segment header free list 를, InnoDB 는 PAGE_LSN 기반 자유 공간 페이지를 둔다. CUBRID 는 N 개 페이지의 Best Space 캐시 와, 전체 스캔의 시 드 역할을 하는 second-best 힌트를 함께 유지한다. 이 캐시는 설 계상 근사값이다. 정확하게 추적하는 비용이 한 번 스캔하는 비용 보다 더 크기 때문이다.
스키마 표현 캐시 (schema-representation cache)
섹션 제목: “스키마 표현 캐시 (schema-representation cache)”원본 레코드의 속성을 해석하려면 엔진이 테이블 스키마 (컬럼 수,
타입, 오프셋, 인덱스) 를 알아야 한다. 그런데 스키마 자체가 다른
힙 (catalog / root class) 의 한 행이다. 따라서 단순한 read 한
번에도 카탈로그 행과 데이터 행을 두 번 파싱해야 한다. 표준 해
법은 클래스 OID 로 색인되는 클래스 표현 캐시 를 두는 것이
다. CUBRID 는 이를 HEAP_CLASSREPR_CACHE 라 부르고, PostgreSQL
은 relcache / syscache 라 부른다.
레코드 헤더 안의 MVCC 버전 메타데이터
섹션 제목: “레코드 헤더 안의 MVCC 버전 메타데이터”엔진이 MVCC 를 채택하면, 레코드별 헤더에 두 종류의 정보가 추가
된다. 하나는 (insert자, delete자, prev_version_pointer) 라는
표준 3중쌍이고, 다른 하나는 어떤 옵션 필드가 물리적으로 존재하
는지를 통제하는 플래그 바이트다. 가시성 술어
(cubrid-mvcc.md 참고) 는 스냅샷의 활성 집합을 insert자와
delete자를 검사하고, 현재 버전이 스냅샷에 비해 너무 새로우면
백포인터를 따라 버전 체인을 거슬러 올라간다. CUBRID 의
mvcc_rec_header 는 모든 REC_HOME 과 REC_NEWHOME 본문 안에
자리한다.
이론 ↔ CUBRID 명칭 매핑
섹션 제목: “이론 ↔ CUBRID 명칭 매핑”§학술적 배경 의 교재 개념과 CUBRID 의 명명된 엔티티가 다음과
같이 대응된다. ## CUBRID의 구현 섹션은 이 표의 각 행을 한 단
계씩 더 깊게 들여다본다.
| 이론 (Theory) | CUBRID 명칭 |
|---|---|
| 힙 파일 식별자 | HFID = {volume_file_id, header_page_id} |
| 레코드별 식별자 | OID = {volid, pageid, slotid} |
| 슬롯 페이지 헤더 | SPAGE_HEADER (slotted_page.h) |
| 슬롯 디렉토리 엔트리 | SPAGE_SLOT (offset:14 + length:14 + type:4 = 32비트) |
| 레코드 타입 (어휘) | 9 개 모드 INT16 enum: REC_HOME / REC_RELOCATION / REC_NEWHOME / REC_BIGONE / REC_ASSIGN_ADDRESS / REC_MVCC_NEXT_VERSION / REC_MARKDELETED / REC_DELETED_WILL_REUSE / REC_UNKNOWN |
| 페이지 안 전달 레코드 | REC_RELOCATION (헤더) + REC_NEWHOME (본문) |
| overflow 레코드 | REC_BIGONE (힙 참조) + 원본 overflow 레코드 (별도 파일) |
| 힙별 헤더 | HEAP_HDR_STATS (헤더 페이지의 슬롯 0) |
| 페이지별 체인 링크 | HEAP_CHAIN (모든 비헤더 페이지의 슬롯 0) |
| 레코드 안의 MVCC 버전 메타데이터 | mvcc_rec_header (mvcc.h), REC_HOME 본문에 임베드 |
| 자유 공간 힌트 캐시 (전역) | HEAP_STATS_BESTSPACE_CACHE |
| 스키마 표현 캐시 (전역) | HEAP_CLASSREPR_CACHE |
| 클래스 OID → HFID 캐시 (전역) | HEAP_HFID_TABLE (lock-free hash) |
| 스캔별 로컬 캐시 | HEAP_SCANCACHE (page latch + watcher + snapshot) |
| AttrInfo 로컬 캐시 | HEAP_CACHE_ATTRINFO (디코딩된 레코드 값) |
CUBRID의 구현
섹션 제목: “CUBRID의 구현”CUBRID 는 위의 관행을 두 계층으로 구체화한다. 아래층은 일반 **
슬롯 페이지** 추상화 (slotted_page.{h,c}) 다. 힙·카탈로그·
btree 등 다수가 이 추상화를 함께 쓴다. 위층은 그 위에 다중 형
태 레코드 어휘, MVCC 통합, 5 개 캐시를 더한 힙 파일 매니저
(heap_file.{h,c}) 다. 차별점은 네 가지다.
(1) 전달·overflow·MVCC tombstone 상태를 명시적이고 grep 가능하 게 드러내는 9 개 레코드 타입 어휘.
(2) 힙 헤더가 예약 바이트가 아니라 헤더 페이지의 슬롯 0 에 들
어 있는 실제 레코드 다. 일반 힙 페이지는 같은 슬롯 0 자리
에 HEAP_CHAIN 을 둔다. 형식이 통일되어 있다.
(3) HEAP_OPERATION_CONTEXT 구조체가 INSERT / UPDATE / DELETE
를 type 스위치 하나로 통합한다. 세 개의 독립된 코드 경로가
아니다.
(4) 핫 패스를 카탈로그와 전체 힙 스캔에서 떼어 놓는 5-캐시 아키텍처 (전역 3 + 로컬 2).

Figure 1 — Heap file layout. The first page (HFID.hpgid) is the
header page: slot 0 holds HEAP_HDR_STATS, slot 1+ may hold
records. Every subsequent page is a regular slotted page whose
slot 0 holds a HEAP_CHAIN (prev_vpid / next_vpid linkage +
max_mvccid for vacuum). All pages are double-linked into one
chain rooted at the header. Records too big for any heap page go
to a separate overflow file as raw REC_BIGONE payload; the
heap holds only the small forwarding record. (Source: original
“1. Heap Overview & Architecture” deck.)
힙 연산의 흐름 (How a heap operation flows)
섹션 제목: “힙 연산의 흐름 (How a heap operation flows)”flowchart LR
A["INSERT / UPDATE / DELETE / READ"] --> B["heap_create_<op>_context\n→ HEAP_OPERATION_CONTEXT"]
B --> C{"op type?"}
C -- "INSERT" --> I["heap_insert_logical\n• bestspace 캐시 참조\n• 슬롯 할당\n• 레코드 + MVCC 헤더 기록\n• redo/undo 로깅"]
C -- "UPDATE" --> U["heap_update_logical\n• 기존 read\n• MVCC 헤더 조정\n (prev_version_lsa)\n• 필요 시 relocation/overflow\n• 옛 이미지 undo 로깅"]
C -- "DELETE" --> D["heap_delete_logical\n• delete_mvccid 설정\n• overflow 특수 처리\n• redo/undo 로깅"]
C -- "READ" --> R["heap_get_visible_version\n• home 페이지 fix\n• 필요 시 REC_RELOCATION/\n REC_BIGONE 따라가기\n• mvcc_satisfies_snapshot\n• too-new면 prev_version_lsa 추적"]
I --> Z["결과 OID 반환\n또는 SCAN_CODE"]
U --> Z
D --> Z
R --> Z
각 박스는 아래 서브섹션에서 풀어서 본다. 박스 자체는 움직이지 않고, 단지 상세도가 깊어진다.
슬롯 페이지 포맷 — slotted_page.{h,c}
섹션 제목: “슬롯 페이지 포맷 — slotted_page.{h,c}”slotted page 는 페이지 내부의 기본 구조다. 위쪽에 작은 헤더가 있고, 페이지 끝쪽에는 슬롯 디렉토리가 거꾸로 자란다. 그 사이에 레코드 본문이 앞쪽에서부터 채워지고, 가운데에 자유 공간이 남 는다. CUBRID 의 구체적 형태는 다음과 같다.
// SPAGE_HEADER — src/storage/slotted_page.h (condensed)typedef struct spage_header SPAGE_HEADER;struct spage_header{ PGNSLOTS num_slots; /* total slots (including holes) */ PGNSLOTS num_records; /* slots actually holding records */ INT16 anchor_type; /* see Anchor types below */ unsigned short alignment; int total_free; /* total free bytes (incl. holes) */ int cont_free; /* contiguous free area in middle */ int offset_to_free_area; /* end of last record body */ int reserved1; int flags; unsigned int is_saving:1; /* undo-image preservation */ unsigned int need_update_best_hint:1; unsigned int reserved_bits:30;};
// SPAGE_SLOT — src/storage/slotted_page.htypedef struct spage_slot SPAGE_SLOT;struct spage_slot{ unsigned int offset_to_record:14; /* byte offset of body in page */ unsigned int record_length:14; unsigned int record_type:4; /* see Record types below */};각 슬롯은 32 비트다. 16 KB 페이지 (DB_PAGESIZE) 에서는 오프
셋과 길이가 각각 14 비트씩 필요하다 (16 K ≤ 2^14 이기 때문이
다). 나머지 4 비트짜리 record_type 이 9-상태 레코드 타입 어
휘를 담는다.
flowchart LR
subgraph Page["슬롯 페이지 (16 KB)"]
direction LR
H["SPAGE_HEADER\n(num_slots,\nnum_records,\nanchor_type,\ntotal_free,\noffset_to_free_area)"]
R0["record body 0"]
R1["record body 1"]
R2["record body 2"]
GAP["… 자유 공간 …"]
S2["slot 2"]
S1["slot 1"]
S0["slot 0"]
H --> R0 --> R1 --> R2 --> GAP --> S2 --> S1 --> S0
end
인덱스 k 의 슬롯 은 한 번 정해지면 절대 움직이지 않는다. 압
축 도중 본문이 옮겨져서 슬롯 안의 오프셋이 바뀔 수는 있지만,
슬롯 자체의 정체성은 그대로다. 이 안정성 덕에 OID
(volid, pageid, slotid) 가 압축을 내구성을 가진다.
앵커 타입 (Anchor types)
섹션 제목: “앵커 타입 (Anchor types)”SPAGE_HEADER.anchor_type 은 페이지의 슬롯 재사용 정책을 고른
다.
| 앵커 타입 | 슬롯 재사용? | 사용처 |
|---|---|---|
ANCHORED | 안 함 | FILE_HEAP 의 힙 페이지 (슬롯 ID = 디스크 OID, 재할당 금지) |
ANCHORED_DONT_REUSE_SLOTS | 안 함 | (별칭 / 명시적 형태) |
UNANCHORED_ANY_SEQUENCE | 함 | sort run, 쿼리 결과 — 슬롯 정체성 ephemeral |
UNANCHORED_KEEP_SEQUENCE | 함 (순서 유지) | 안정된 슬롯 순서가 필요한 catalog / index |
힙 페이지는 항상 anchored 다. 슬롯 ID 가 OID 의 일부로 외부에
알려지면 그 슬롯은 그 OID 에 영구적으로 묶이기 때문이다. DELETE
이후 슬롯은 두 가지 중 하나로 표시된다. 첫째는 REC_MARKDELETED
(보존), 둘째는 REC_DELETED_WILL_REUSE 다. 후자는 일부 시스템
테이블이 쓰는 FILE_HEAP_REUSE_SLOTS 파일 타입에서만 재사용이
허용된다.
힙 파일 구조 — HFID, HEAP_HDR_STATS, HEAP_CHAIN
섹션 제목: “힙 파일 구조 — HFID, HEAP_HDR_STATS, HEAP_CHAIN”HFID 는 힙 파일을 첫 페이지로 식별한다.
// HFID — src/storage/storage_common.h (sketch)typedef struct hfid HFID;struct hfid{ VFID vfid; /* file identifier (volume + file table id) */ INT32 hpgid; /* page id of the header page */};헤더 페이지는 일반 슬롯 페이지 형식을 그대로 따른다. 다만 슬 롯 0 에 힙 전체 통계 레코드를 둔다는 점이 다르다. 다른 모든 페이지도 슬롯 0 을 예약하지만, 그 자리는 페이지별 체인 레 코드를 담는 용도로 쓴다.
// HEAP_HDR_STATS — src/storage/heap_file.c (condensed)typedef struct heap_hdr_stats HEAP_HDR_STATS;struct heap_hdr_stats{ OID class_oid; /* the class whose rows live here */ VFID ovf_vfid; /* overflow file for REC_BIGONE records */ VPID next_vpid; /* first non-header heap page */ int unfill_space; /* per-page reserve kept free for UPDATEs */ struct { int num_pages; int num_recs; float recs_sumlen; int num_other_high_best; int num_high_best; int num_substitutions; int num_second_best; int head_second_best; int tail_second_best; int head; VPID last_vpid; VPID full_search_vpid; VPID second_best[HEAP_NUM_BEST_SPACESTATS]; HEAP_BESTSPACE best[HEAP_NUM_BEST_SPACESTATS]; } estimates; /* approximate heap-wide stats + best-space hint */ int reserved0_for_future; int reserved1_for_future; int reserved2_for_future;};
// HEAP_CHAIN — src/storage/heap_file.ctypedef struct heap_chain HEAP_CHAIN;struct heap_chain{ OID class_oid; VPID prev_vpid; VPID next_vpid; MVCCID max_mvccid; /* max MVCCID seen on this page (vacuum) */ INT32 flags;};이 레이아웃은 두 가지 사실을 말한다.
- 슬롯 0 은 항상 예약된다.
heap_file.h에HEAP_HEADER_AND_CHAIN_SLOTID = 0으로 못 박혀 있다. 사용자 데이터는 슬롯 1 부터 시작한다. 레코드를 스캔하는 코드는 슬 롯 0 을 명시적으로 건너뛴다. - 페이지 사이는 양방향으로 연결된다.
HEAP_CHAIN.prev_vpid와next_vpid가 힙 페이지를 양방향 연결 리스트로 묶는다. 이 리스트의 시작점은 늘 헤더 페이지다.heap_next는 scan -cache 힌트가 없을 때 이 리스트를 따라 페이지를 순회한다.
레코드 타입 어휘 (Record-type vocabulary)
섹션 제목: “레코드 타입 어휘 (Record-type vocabulary)”힙 레코드는 9 개 타입 중 하나이고, 슬롯의 4 비트 record_type
에 인코딩된다.
| 타입 | 바이트 위치 | 의미 |
|---|---|---|
REC_HOME | 이 슬롯에 인라인 | home 페이지의 일반 레코드. |
REC_RELOCATION | 이 슬롯 | 전달 포인터 (8 바이트, 대상 OID) — 본문은 다른 곳에 REC_NEWHOME 으로. |
REC_NEWHOME | 인라인 (다른 페이지) | 재배치된 레코드의 실제 본문. REC_RELOCATION 으로만 도달. |
REC_BIGONE | 이 슬롯 | overflow 파일로 가는 전달 포인터 (대상 VPID). |
REC_ASSIGN_ADDRESS | 이 슬롯 (zero-length 본문) | OID 는 만들어졌지만 레코드는 아직 없음 — 카탈로그 / root class 가 OID 를 먼저 필요로 할 때 쓰임. |
REC_MVCC_NEXT_VERSION | 이 슬롯 | (legacy MVCC version-chain 마커. 현재는 헤더의 prev_version_lsa 가 대체.) |
REC_MARKDELETED | tombstone | ANCHORED_DONT_REUSE_SLOTS 페이지에서 삭제된 슬롯 (OID 정체성 보존을 위해 영구 보존). |
REC_DELETED_WILL_REUSE | tombstone (재사용 가능) | ANCHORED 페이지에서 삭제된 슬롯 (FILE_HEAP_REUSE_SLOTS 파일 타입 — 재할당 가능). |
REC_UNKNOWN | — | 예약 / sentinel. |
이 가운데 일반 힙 read 에서 live 한 타입은 네 개다 —
REC_ASSIGN_ADDRESS, REC_HOME, REC_BIGONE, REC_RELOCATION.
REC_NEWHOME 은 REC_RELOCATION 으로만 도달할 수 있고,
heap_next 가 이를 걸러 주기 때문에 한 레코드가 스캔에서 두 번
반환되는 일은 없다.

Figure 2 — Record types in a CUBRID heap. Slotted Page A
holds two REC_HOME records (orange) and one REC_RELOCATION
forwarding pointer (yellow) → its body lives as a REC_NEWHOME
(olive) on Slotted Page B. Records too big for any heap page
become a REC_BIGONE forwarding pointer (blue) → the actual body
lives as an unmarked overflow record in the overflow file
(HEAP_HDR_STATS.ovf_vfid). Index entries always point to the
original OID; CUBRID transparently follows the forwarding chain
on read. (Source: original 2. Heap Operations deck.)
Insert 흐름 — heap_insert_logical
섹션 제목: “Insert 흐름 — heap_insert_logical”// heap_insert_logical (sketch) — src/storage/heap_file.cintheap_insert_logical (THREAD_ENTRY *thread_p, HEAP_OPERATION_CONTEXT *context, PGBUF_WATCHER *home_hint_p){ /* 1. Adjust MVCC header on the record (set ins_mvccid). */ /* 2. If record too big → insert into overflow file first; build * REC_BIGONE forwarding record into context->recdes_p. */ /* 3. Find a target home page: * - check HEAP_STATS_BESTSPACE_CACHE * - else consult HEAP_HDR_STATS.estimates.best[] * - else scan up to min(20% of pages, 100 pages) starting from * full_search_vpid / second_best[] * - else allocate a new heap page (heap_alloc_new_page). * Once a slot is chosen, the resulting OID is determined and * its row lock can be acquired — the heap operation itself is * one of the few code paths that takes the row lock inside * rather than before the call (UPDATE / DELETE acquire upstream * in locator). */ /* 4. Splice the record into the slot. */ /* 5. Log redo/undo for crash recovery. */ /* 6. Post-process: bump statistics, update the bestspace cache, * flag the schema cache (CAS) if this insert is into a * catalog row. */}세 가지 핵심을 짚을 수 있다.
- MVCCID 는 레코드가 자리 잡기 전에 찍힌다. 레코드 헤더
의 플래그 비트가
mvcc_ins_id,mvcc_del_id,prev_version_lsa의 존재 여부를 알리도록 설정된다. - OID 는 슬롯이 결정한다. 사전에 요청해 받는 값이 아니다. 그래서 행 잠금 (row lock) 은 INSERT 도중 에 획득한다. UPDATE 와 DELETE 는 대상 OID 를 이미 알고 있어서 locator 단 계에서 미리 잡는다.
- Best 는 검색이 아니라 힌트다. 캐시와 estimate 가 자리 있는 페이지 쪽으로 편향되어 있고, 실패하면 제한된 스캔으로, 그래도 실패하면 새 페이지 할당으로 떨어진다. 비용은 흩어진 다 — 대부분의 INSERT 는 O(1) 캐시 hit 으로 끝난다.
Update 흐름 — heap_update_logical
섹션 제목: “Update 흐름 — heap_update_logical”UPDATE 는 INSERT 의 상위 집합에 옛 이미지 read 와 (필요하면) relocation 단계가 더해진 흐름이다.
flowchart TD
A["heap_update_logical(context)"] --> B["기존 레코드 읽기\n(home → REC_RELOCATION → REC_NEWHOME\n또는 home → REC_BIGONE 체인)"]
B --> C["heap_update_adjust_recdes_header\n• 새 ins_mvccid 찍기\n• undo write로부터 prev_version_lsa 채우기"]
C --> D{"새 크기가\n원래 슬롯에 맞는가?"}
D -- "예" --> E["in-place 덮어쓰기\n(REC_HOME → REC_HOME)"]
D -- "아니, 다른 힙 페이지에 맞음" --> F["대상 슬롯 할당,\nREC_NEWHOME splice,\nhome을 REC_RELOCATION으로 재기록"]
D -- "아니, 어떤 페이지에도 안 맞음" --> G["오버플로 파일에 insert,\nhome을 REC_BIGONE으로 재기록"]
E --> Z["undo + redo 로깅"]
F --> Z
G --> Z
현재 타입은 다른 타입으로 전이 할 수 있다. 한 UPDATE 가 만들
수 있는 합법적 전이를 정리하면 다음과 같다. 각 전이는 옛 타입
의 저장소를 정리하는 일도 함께 동반한다. 예를 들어
REC_RELOCATION 의 옛 REC_NEWHOME 슬롯은 deleted 로 표시된
다.
| 옛 타입 | 새 타입 후보 (우선순위 순) |
|---|---|
REC_HOME | REC_HOME (same slot) → REC_RELOCATION + REC_NEWHOME → REC_BIGONE + overflow body |
REC_RELOCATION + REC_NEWHOME | REC_HOME → REC_RELOCATION + REC_NEWHOME (same other-page) → REC_RELOCATION + REC_NEWHOME (different page) → REC_BIGONE + overflow |
REC_BIGONE + overflow body | REC_HOME → REC_RELOCATION + REC_NEWHOME → REC_BIGONE + overflow (same body, in place if fits) |
undo 이미지는 그 행의 이전 버전이 결국 prev_version_lsa 를
따라 도달하게 될 그 자리다. 그래서 undo 로그 엔트리의 LSN 이
새 레코드 헤더에 찍힌다.
Delete 흐름 — heap_delete_logical
섹션 제목: “Delete 흐름 — heap_delete_logical”MVCC 아래에서 DELETE 는 레코드를 물리적으로 제거하지 않는다.
대신 기존 레코드 헤더에 mvcc_del_id 를 세우고 연산을 로깅한
다. 슬롯을 실제로 풀어 주는 일은 vacuum
(heap_vacuum_all_objects) 이 맡는다. vacuum 은 mvcc_del_id
가 전역 oldest-visible MVCCID 보다 오래된 시점에 슬롯을 풀어
준다. 그래서 DELETE 흐름은 UPDATE 와 비슷해 보인다 — 레코드
를 읽고, 헤더를 조정하고, 로그를 남기는 순서가 같다.
특수 케이스가 두 가지 있다.
- overflow 레코드 (
REC_BIGONE). MVCC 헤더가 overflow 페 이지에 산다 (heap_set_mvcc_rec_header_on_overflow). delete 경로는 그 페이지의 레코드를 수정하지, 힙 페이지의 forwarder 를 수정하지 않는다. forwarder 는 overflow 레코드 자체가 풀릴 때만 갱신된다. - non-MVCC 클래스. 일부 시스템 카탈로그는 MVCC 비활성화
상태다 (
HEAP_SCANCACHE.mvcc_disabled_class = true). 이 경 우 DELETE 는 슬롯을 즉시REC_MARKDELETED/REC_DELETED_WILL_REUSE로 물리 표시한다.
Read 흐름 — heap_get_visible_version / heap_next
섹션 제목: “Read 흐름 — heap_get_visible_version / heap_next”진입점은 두 가지다. 하나는 OID 로 단일 레코드를 읽는 점 read
이고, 인덱스 lookup 뒤에 호출된다. 다른 하나는 SELECT * 같은
순차 스캔이다.
// heap_get_visible_version (sketch) — src/storage/heap_file.cSCAN_CODEheap_get_visible_version (THREAD_ENTRY *thread_p, const OID *oid, OID *class_oid, RECDES *recdes, HEAP_SCANCACHE *scan_cache, int ispeeking, int old_chn){ /* 1. heap_prepare_object_page: fix the home page (using * scan_cache->page_watcher to keep the latch across calls). */ /* 2. Inspect the slot type: * - REC_HOME → done, body is here. * - REC_RELOCATION → fix forward page, follow to REC_NEWHOME. * - REC_BIGONE → fix overflow page, read raw record. * - REC_ASSIGN_ADDRESS / REC_NEWHOME / REC_MARKDELETED / * REC_DELETED_WILL_REUSE / REC_UNKNOWN → not visible. */ /* 3. Read mvcc_rec_header. * Apply mvcc_satisfies_snapshot (see cubrid-mvcc.md): * - SNAPSHOT_SATISFIED → return record body. * - TOO_OLD_FOR_SNAPSHOT → return S_DOESNT_EXIST. * - TOO_NEW_FOR_SNAPSHOT → walk prev_version_lsa into the log * and re-evaluate. */}heap_next 는 스캔 변형이다. 페이지 체인을 걷고, 각 페이지 안
의 슬롯을 순회하면서 슬롯마다 heap_get_visible_version 을 호
출하고, non-live 레코드 타입 (REC_NEWHOME,
REC_ASSIGN_ADDRESS, header / chain, 빈 슬롯) 은 건너뛴다.
HEAP_SCANCACHE 가 호출 사이에 페이지 래치를 유지해 두므로
(cache_last_fix_page = true), 같은 페이지 안에서 연속해서
heap_next 를 부르면 re-fix 비용이 들지 않는다.
flowchart LR TX["스냅샷 가진 TX"] --> POINT["점 read\nheap_get_visible_version(oid)"] TX --> SCAN["순차 스캔\nheap_next(scan_cache)"] POINT --> FIX1["heap_prepare_object_page"] SCAN --> FIX2["페이지 체인 walk\n(HEAP_CHAIN.next_vpid)"] FIX1 --> SLOT FIX2 --> SLOT["record_type별 슬롯 dispatch"] SLOT -- "HOME" --> READ["in-place 본문 read"] SLOT -- "RELOCATION" --> NEW["→ REC_NEWHOME 따라가기"] SLOT -- "BIGONE" --> OVF["→ overflow file 따라가기"] NEW --> READ OVF --> READ READ --> VIS["mvcc_satisfies_snapshot"] VIS -- "SATISFIED" --> RET["레코드 반환"] VIS -- "TOO_NEW" --> CHAIN["prev_version_lsa로\n로그 walk"] VIS -- "TOO_OLD" --> SKIP["skip"] CHAIN --> VIS
MVCC 통합 — 레코드 헤더
섹션 제목: “MVCC 통합 — 레코드 헤더”모든 REC_HOME / REC_NEWHOME 본문과 모든 overflow 레코드는
mvcc_rec_header 를 들고 있다 (cubrid-mvcc.md 에서 명시).
// mvcc_rec_header — src/transaction/mvcc.h (recap)struct mvcc_rec_header{ INT32 mvcc_flag:8; /* which optional fields are present */ INT32 repid:24; /* representation id */ int chn; /* cache coherency number */ MVCCID mvcc_ins_id; /* set on insert / update */ MVCCID mvcc_del_id; /* set on delete */ LOG_LSA prev_version_lsa; /* back-pointer to previous version * (lives in undo log) */};
Figure 3 — A record updated twice without intervening DELETE. The
heap (top row) holds the current version (mvcc_ins_id3, prev_version_lsa3). Following prev_version_lsa3 lands on a log
record in the log file (bottom rows) carrying the previous
image with (mvcc_ins_id2, prev_version_lsa2), which in turn
points to the original (mvcc_ins_id1, prev_version_lsa1 = NULL).
The visibility predicate walks this chain when the heap version is
too new for the reading snapshot. (Source: original “3. MVCC in
Heap” deck.)
플래그 바이트가 물리 레이아웃을 통제한다. DELETE 된 적이 없는
레코드는 mvcc_del_id 필드를 가지지 않으므로 8 바이트를 절약
한다. UPDATE 된 적이 없는 레코드는 prev_version_lsa 도 없다.
heap_file.c 의 mvcc_header_size_lookup[8] 배열이 플래그 바
이트 값을 디스크 헤더 크기로 옮겨 준다.
페이지 단위 vacuum 추적은 HEAP_PAGE_VACUUM_STATUS
(HEAP_PAGE_VACUUM_NONE / _ONCE / _UNKNOWN) 에 자리한다.
이 상태는 HEAP_CHAIN.flags 안에 들어 있고, 이 페이지가 앞으
로 vacuum 방문을 더 필요로 하는지 예측해 페이지 해제 가능 여부
를 가른다.
캐시 — 전역 3 개, 로컬 2 개
섹션 제목: “캐시 — 전역 3 개, 로컬 2 개”CUBRID 는 힙 매니저 둘레에 다섯 개의 캐시를 둔다. 전역 과 로컬 이라는 분류는 큐레이터가 붙인 것이고, 소스에는 따로 그 런 구분이 없다.
flowchart TB
subgraph G["전역 캐시 (프로세스 와이드)"]
BS["HEAP_STATS_BESTSPACE_CACHE\nhfid → list of (vpid, freespace)\nINSERT 페이지 선택 시드"]
CR["HEAP_CLASSREPR_CACHE\nclass_oid → OR_CLASSREP\n(파싱된 스키마, attr 오프셋, btids)"]
HF["HEAP_HFID_TABLE\nclass_oid → HFID + classname\n(lock-free hash, size 1000)"]
end
subgraph L["로컬 캐시 (스캔/연산별)"]
SC["HEAP_SCANCACHE\n스캔 스코프\n(page_watcher, mvcc_snapshot,\ncache_last_fix_page, file_type)"]
AI["HEAP_CACHE_ATTRINFO\n레코드 디코딩 스크래치\n(read_classrepr, values[])"]
end
HF --> BS
HF --> CR
CR --> AI
SC --> AI
Best space cache — HEAP_STATS_BESTSPACE_CACHE
(heap_file.c). HFID 를 (VPID, freespace) 엔트리의 작은
배열로 매핑한다. INSERT / UPDATE 는 힙 스캔 전에 이 캐시를
본다. 디스크 등가물인 HEAP_HDR_STATS.estimates.best[] 는 메
모리 캐시가 cold 이거나 stale 일 때의 영구 폴백이다. 캐시·디스
크 sync 는 heap_stats_sync_bestspace 가 lazy 하게 수행한다.
second_best[] 배열은 힌트 로, 전체 스캔의 시작점이다
(head_second_best / tail_second_best 로 circular queue 처
럼 다룬다). num_substitutions 카운터는 1 000 번 INSERT 마다
한 번씩만 best[] 로 promotion 이 일어나도록 만든다. 한 페이
지에 INSERT 를 몰아넣지 않고 의도적으로 흩어 두기 위함이다.
Class-representation cache — HEAP_CLASSREPR_CACHE.
OR_CLASSREP 은 한 클래스의 파싱된 스키마다. 컬럼 타입, raw
record 안의 속성 오프셋, 인덱스 리스트 (BTID),
representation id 가 그 안에 들어 있다. 이 캐시가 없으면 매
heap_attrinfo_* 호출이 카탈로그 row 를 다시 read 하고 다시 파
싱해야 한다. eviction 은 heap_classrepr_decache 가 맡는다.
HFID 테이블 — HEAP_HFID_TABLE. 클래스 OID 에서 HFID 와
캐시된 classname 으로 가는 lock-free 해시 테이블이다. 해시 크
기는 HEAP_HFID_HASH_SIZE = 1000 으로 고정되어 있다. 거의 모
든 연산이 잠금이나 힙 순회를 위해 HFID 를 필요로 하므로, 이 캐
시가 카탈로그 lookup 을 크게 줄여 준다.
// HEAP_HFID_TABLE_ENTRY — src/storage/heap_file.hstruct heap_hfid_table_entry{ OID class_oid; /* key */ HEAP_HFID_TABLE_ENTRY *stack; HEAP_HFID_TABLE_ENTRY *next; UINT64 del_id; /* lock-free reclamation */
HFID hfid; /* value */ FILE_TYPE ftype; /* FILE_HEAP or FILE_HEAP_REUSE_SLOTS */ std::atomic<char*> classname;};Scan cache — HEAP_SCANCACHE. 스캔 단위로 유지되는 로컬 상
태다. 핵심 항목은 다음과 같다.
cache_last_fix_page— 연속heap_next호출 사이에 페이지 래치를 유지할지 여부.mvcc_snapshot— 가시성 평가에 쓸 스냅샷.page_latch— 새 페이지에 잡을LOCK모드 (보통은NULL_LOCK. 클래스가 이미 상위에서 S / X / SIX 로 잠겨 있기 때문이다).page_watcher— 호출 사이에 유지되는 물리 페이지 래치 핸 들.partition_list— 파티션 클래스에서 현재 스캔 중인 HFID 목 록.
AttrInfo cache — HEAP_CACHE_ATTRINFO. 한 레코드를 read /
write 하는 동안 OR_CLASSREP 과 디코딩된 HEAP_ATTRVALUE 배
열을 함께 들고 있는 스크래치 패드다. 디코딩은
heap_attrinfo_read_dbvalues 가, 인코딩은
heap_attrinfo_transform_to_disk 가 한다. last_classrepr 은
최신 representation 을 추적하고, read_classrepr 은 read 중
인 레코드가 실제로 사용한 representation 을 추적한다. 후자가
필요한 이유는, read 중인 데이터가 스키마 변경 전에 기록된 것일
수 있기 때문이다.
CHN — 클라이언트와 서버의 캐시 일관성
섹션 제목: “CHN — 클라이언트와 서버의 캐시 일관성”mvcc_rec_header.chn 은 cache coherency number 다. 클라이
언트 측 캐시가 사용하는 non-MVCC 카운터다. 쿼리가 레코드를
read 하면 서버는 chn 을 데이터와 함께 돌려준다. 그 뒤 클라이
언트가 같은 레코드를 다시 요청하면서 이전 chn 을 함께 보내
면, chn 이 바뀌지 않은 경우 서버는 본문을 다시 보내지 않을
수 있다.
MVCC 테이블에서는 chn 이 매 update 마다 증가하지 않는다. 그
역할은 mvcc_ins_id 가 대신한다. chn 은 root class 나 시스
템 카탈로그 같은 non-MVCC 테이블에서만 증가한다. HEAP_CHNGUESS
캐시는 소스에 TBU (to-be-used) 로 표시되어 부분 구현 상태이고,
핫 OID 에 대한 chn 비교를 단축할 용도로 마련되어 있다.
소스 코드 가이드
섹션 제목: “소스 코드 가이드”심볼명을 anchor 로 삼는다 — 라인 번호가 아니다. CUBRID 소스는 시간이 지나면 변한다. 그에 비해 함수명·struct 태그· enum 태그 같은 심볼은 거의 변하지 않는 안정된 식별자다. 현 재 위치는
git grep -n '<symbol>' src/storage/로 찾으면 된 다. 이 섹션 끝의 위치 힌트 표는 문서가 마지막으로updated:된 시점에 관찰한 값이며, 시간이 지나면 어긋날 수 있다.
타입 정의 (src/storage/)
섹션 제목: “타입 정의 (src/storage/)”struct hfid(storage_common.h) —(VFID, hpgid)힙 파일 식별자.struct spage_header(slotted_page.h) — 슬롯 페이지 헤더.struct spage_slot(slotted_page.h) — 32 비트 슬롯 디렉토 리 엔트리 (offset / length / type).struct heap_hdr_stats(heap_file.c) — 힙 전역 헤더 레코드 (헤더 페이지의 슬롯 0).struct heap_chain(heap_file.c) — 페이지별 체인 링크 (모 든 비헤더 페이지의 슬롯 0).struct heap_scancache(heap_file.h) — 스캔 스코프 로컬 상태.struct heap_operation_context(heap_file.h) — INSERT / UPDATE / DELETE 입출력 묶음.struct heap_get_context(heap_file.h) — read-by-OID 입출 력 묶음.struct heap_hfid_table/heap_hfid_table_entry(heap_file.h) — class OID → HFID lock-free hash.struct heap_cache_attrinfo(heap_attrinfo.h) — 레코드별 디코더 스크래치.enum HEAP_OPERATION_TYPE(heap_file.h) —INSERT/UPDATE/DELETE.enum HEAP_PAGE_VACUUM_STATUS(heap_file.h) —NONE/ONCE/UNKNOWN.- 레코드 타입 상수 (
REC_HOME,REC_RELOCATION,REC_NEWHOME,REC_BIGONE,REC_ASSIGN_ADDRESS,REC_MVCC_NEXT_VERSION,REC_MARKDELETED,REC_DELETED_WILL_REUSE,REC_UNKNOWN) (slotted_page.h). - 앵커 타입
ANCHORED,ANCHORED_DONT_REUSE_SLOTS,UNANCHORED_ANY_SEQUENCE,UNANCHORED_KEEP_SEQUENCE(slotted_page.h).
라이프사이클 (src/storage/heap_file.c)
섹션 제목: “라이프사이클 (src/storage/heap_file.c)”heap_manager_initialize— 모듈 초기화.heap_manager_finalize— 모듈 해제.heap_initialize_hfid_table/heap_finalize_hfid_table— HFID 캐시.heap_classrepr_restart_cache— CR 캐시 클리어 (스키마 변경 이벤트 후 사용).
연산 진입점 (src/storage/heap_file.c)
섹션 제목: “연산 진입점 (src/storage/heap_file.c)”heap_create_insert_context/heap_insert_logical.heap_create_update_context/heap_update_logical.heap_create_delete_context/heap_delete_logical.heap_assign_address— 레코드를 적기 전에 OID 를 미리 발급 (REC_ASSIGN_ADDRESS).heap_get_visible_version— MVCC 점 read.heap_get_last_version— 스냅샷과 무관하게 최신 커밋 버전을 돌려주는 점 read.heap_next/heap_prev/heap_first/heap_last— 스 캔 iterator.heap_scancache_start/heap_scancache_end— scan-cache 라이프사이클.
페이지 walk 와 overflow (src/storage/heap_file.c)
섹션 제목: “페이지 walk 와 overflow (src/storage/heap_file.c)”heap_vpid_next/heap_vpid_prev/heap_vpid_skip_next— 힙 페이지 체인 네비게이션.heap_alloc_new_page— 새 힙 페이지 할당과 연결.heap_ovf_find_vfid/heap_ovf_delete—REC_BIGONE을 위한 overflow 파일 관리.heap_set_mvcc_rec_header_on_overflow/heap_get_mvcc_rec_header_from_overflow— overflow 레코드 의 MVCC 헤더 조작.heap_remove_page_on_vacuum/heap_page_set_vacuum_status_none/heap_page_get_vacuum_status— 페이지 단위 vacuum 협조.
캐시 (src/storage/heap_file.c)
섹션 제목: “캐시 (src/storage/heap_file.c)”heap_classrepr_get/heap_classrepr_free/heap_classrepr_decache— class-representation 캐시.heap_get_class_info/heap_cache_class_info/heap_get_hfid_if_cached— HFID 테이블 lookup.heap_stats_update/heap_should_try_update_stat/heap_stats_sync_bestspace— bestspace 캐시 sync.heap_attrinfo_start/heap_attrinfo_end/heap_attrinfo_read_dbvalues/heap_attrinfo_transform_to_disk— AttrInfo 캐시.heap_chnguess_get/heap_chnguess_put/heap_chnguess_clear— CHN guess 캐시.
복구 (src/storage/heap_file.c)
섹션 제목: “복구 (src/storage/heap_file.c)”heap_rv_undo_insert/heap_rv_redo_insert/heap_rv_mvcc_redo_insert.heap_rv_undo_delete/heap_rv_redo_delete/heap_rv_mvcc_undo_delete/heap_rv_mvcc_redo_delete_*.heap_rv_undo_update/heap_rv_redo_update/heap_rv_undoredo_update/heap_rv_redo_update_and_update_chain.heap_rv_redo_newpage/heap_rv_redo_reuse_page.
이 개정 시점의 위치 힌트
섹션 제목: “이 개정 시점의 위치 힌트”이 라인 번호는 문서가 마지막으로 updated: 된 시점의 관찰값이
다. 다른 정의에 도착한다면 위쪽의 심볼명이 정본이다. 지나가는
길에 표를 갱신해 두면 된다.
| 심볼 | 파일 | 라인 |
|---|---|---|
struct spage_header | slotted_page.h | (varies) |
struct spage_slot | slotted_page.h | (varies) |
struct heap_hdr_stats | heap_file.c | (varies, near top) |
struct heap_chain | heap_file.c | (varies, near top) |
struct heap_scancache | heap_file.h | 143 |
struct heap_operation_context | heap_file.h | 267 |
struct heap_get_context | heap_file.h | 362 |
struct heap_hfid_table_entry | heap_file.h | 201 |
enum HEAP_PAGE_VACUUM_STATUS | heap_file.h | 354 |
heap_manager_initialize | heap_file.c | (varies) |
heap_insert_logical | heap_file.c | (varies) |
heap_update_logical | heap_file.c | (varies) |
heap_delete_logical | heap_file.c | (varies) |
heap_get_visible_version | heap_file.c | (varies) |
heap_next | heap_file.c | (varies) |
heap_classrepr_get | heap_file.c | (varies) |
heap_alloc_new_page | heap_file.c | (varies) |
라인 컬럼이 (varies) 로 비어 있는 것은 의도적이다.
heap_file.c 가 프로젝트 최대 소스 파일 (≈ 27 000 줄) 이라
서, 심볼 단위 git grep 이 권장되는 lookup 방식이다.
소스 검증 (2026-05-01 기준)
섹션 제목: “소스 검증 (2026-05-01 기준)”각 항목은 현재 소스에 대한 사실이고, 원본 분석 자료를 함 께 보지 않아도 그 자체로 읽힌다. 끝의 부연은 어떻게 검증되었 는지를 적고, 관련이 있을 때는 역사적 드리프트나 검증의 한계 도 적는다. 미해결 질문 은 큐레이터가 해결을 미루고 기록 해 둔 갭이다 — 이후 독자는 이를 알려진 버그가 아니라 시작점 으로 삼으면 된다.
섹션 헤딩 날짜는 frontmatter
updated:와 맞추기 위해 2026-05-01 로 갱신했다. 검증 자체는 2026-04-29 에 수행한 것 이고 그 이후 소스 드리프트는 없다 (이번 패스에서 재검증은 수 행하지 않았다). 각 사실 항목의 검증 일자는 2026-04-29 그대로 둔다.
검증된 사실
섹션 제목: “검증된 사실”-
HEAP_HEADER_AND_CHAIN_SLOTID = 0.heap_file.h에서 2026-04-29 검증. 슬롯 0 은 모든 힙 페이지에서 무조건 예약된 다 — 헤더 페이지에서는HEAP_HDR_STATS, 다른 모든 페이지에 서는HEAP_CHAIN자리다. 레코드를 스캔하는 코드는 슬롯 0 을 명시적으로 건너뛰고, 스캔 iterator (heap_next,heap_first) 는 슬롯 0 을 not-a-record 로 다룬다. -
HFID lock-free hash table 은 1 000 개 버킷.
heap_file.h의HEAP_HFID_HASH_SIZE = 1000, 2026-04-29 검 증. 하드코딩이며 런타임 파라미터가 아니다. -
페이지 여유 충분 임계는 페이지 크기의 30 %.
heap_file.h의HEAP_DROP_FREE_SPACE = (int)(DB_PAGESIZE * 0.3), 2026-04-29 검증. 기본 16 KB 페이지에서는 페이지가 bestspace 캐시에 들어가려면 약 4 915 바이트 이상이 free 여 야 한다. 이 임계는 INSERT 속도 (후보 페이지가 더 많아지고 스캔이 줄어듦) 와 저장 활용도 (부분만 채워진 페이지의 예약 공간이 놀게 됨) 사이의 trade-off 다. -
슬롯 필드는 14 / 14 / 4 비트.
spage_slot은offset_to_record:14 + record_length:14 + record_type:4= 32 비트다.slotted_page.h에서 2026-04-29 검증. 14 비트 오 프셋 상한이DB_PAGESIZE = 16384(2^14) 와 정확히 맞물린 다. 16 KB 를 넘는 페이지 크기는 이 필드를 넓혀야 한다. -
슬롯 0 만이 특수 슬롯은 아니다 — 앵커 타입도 중요하다.
ANCHORED_DONT_REUSE_SLOTS는 슬롯 테이블을 monotone-grow-only 로 만들고,UNANCHORED_*는 재사용을 허 용한다.FILE_HEAP의 힙 페이지는ANCHORED_DONT_REUSE_SLOTS를 써서 삭제 후 OID 가 alias 되지 않게 한다.FILE_HEAP_REUSE_SLOTS(일부 root-class 카탈로그용) 의 페이 지는ANCHORED를 써서 슬롯 재사용을 허용한다.slotted_page.c에서 2026-04-29 검증 (spage_slot_descriptor_setup과 FILE_HEAP_* 타입 매핑을 읽음). -
HEAP_OPERATION_CONTEXT는 4 개의PGBUF_WATCHER를 들고 있다.home_page_watcher,overflow_page_watcher,header_page_watcher,forward_page_watcher가 그 넷이다. 페이지 사이의 relocation 을 일으키는 UPDATE 는 동시에 최대 3 개 (home, forward, header) 를 들고 있을 수 있다. 네 번째 (overflow) 는REC_BIGONE경로용이다.heap_file.h에서 2026-04-29 검증. -
레코드 타입 어휘는 4 비트 = 16 값, 그중 9 개만 사용.
slotted_page.h에서 2026-04-29 검증. 9–15 는 예약이다. -
mvcc_header_size_lookup[8]은 플래그 바이트 값으로 색인 된다. MVCC 플래그 바이트는OR_MVCC_FLAG_MASK = 0x1f(cubrid-mvcc.md참고) 로 5 비트다. 32 가지 가능한 값이 있 지만, 문서화된 3 비트의 조합만 실제 발생하므로 8 엔트리 lookup 으로 충분하다.heap_file.c에서 2026-04-29 확인.
미해결 질문
섹션 제목: “미해결 질문”-
왜 정확히 1 000 인 해시 크기?
HEAP_HFID_HASH_SIZE = 1000이 명백한 튜닝 근거 없이 하드코딩되어 있다. 추적 경 로 — 이 값을 git history 로 추적; 1 000 개를 넘는 클래스를 가진 워크로드 (대형 스키마) 에서 핫 패스에 해시 충돌이 일어 나는지 확인;max_clients나num_classes에 비례해 스케 일해야 하는지 검토. -
num_substitutions = 1000— second-best promotion 이 왜 이 상수인가? bestspace 캐시는 1 000 INSERT 마다 한 번씩 페이지를second_best에서best로 promote 한다. 발표 자료는 인접 공간을 덜 채워서 INSERT 를 페이지 사이로 흩어 두기 위함이라고 설명한다. 추적 경로 — 다양한 워크로드에서 promotion 비율을 계측; 지역성 주장이 실증되는지 확인. -
REC_MVCC_NEXT_VERSION은 legacy 같다.slotted_page.h의 주석은 “legacy MVCC version-chain marker” 라고 적고 있 고, 현대 경로는 레코드 헤더의prev_version_lsa를 쓴다. 현재 코드 어딘가에서 실제로REC_MVCC_NEXT_VERSION을 생 성 하는가, 아니면 옛 데이터 파일에 대한 후방 호환성을 위해 소비 만 하는가? 추적 경로 —git grep REC_MVCC_NEXT_VERSION으로 producer 호출지 탐색. -
CHN guess 캐시 (
HEAP_CHNGUESS) 의 상태. 발표 자료는 TBU (to-be-used) 라고 부르고, 소스에는 구조는 있으나 사용 이 제한적이다. 완성·라이브인가, 부분 폐기인가, 스텁인가? 추적 경로 —heap_chnguess_get/heap_chnguess_put의 호출지 확인; CAS 워크로드를 돌려 캐시가 채워지는지 관찰. -
현대 빌드에서
unfill_space의 동작.HEAP_HDR_STATS는 미래 UPDATE 성장을 위한 페이지별unfill_space를 예약 해 둔다. 발표 자료는 정적 예약으로 설명하지만, 최근 커밋이 적응적으로 바꿨을 수 있다. 추적 경로 — 이 필드의 할당 경로 를 추적; vacuum 단계가 재분배를 하는지 확인. -
OOS (Out-of-row Overflow Storage) 와의 상호작용.
feat/oos브랜치가heap_file.c와 레코드 헤더를 변경 중 이고,OR_MVCC_FLAG_HAS_OOS를 추가하고 있다. 본 문서는develop을 대상으로 하며, OOS 가 land 된 뒤에는 여기에 적 힌 레코드 타입 어휘와 overflow 흐름이 후속 갱신을 필요로 한 다. (인접 OOS context 스킬에서 현재 상태를 확인할 수 있다.)
CUBRID 너머 — 비교 설계와 연구 동향 (Beyond CUBRID — Comparative Designs & Research Frontiers)
섹션 제목: “CUBRID 너머 — 비교 설계와 연구 동향 (Beyond CUBRID — Comparative Designs & Research Frontiers)”분석이 아닌 포인터 (pointers). 각 항목은 후속 문서의 시작점 이고, 깊이는 의도적으로 얕다.
- PostgreSQL HOT (Heap-Only Tuple). PostgreSQL 은 인덱스
컬럼을 건드리지 않는 UPDATE 에서 옛 버전을 같은 페이지에 인
라인으로 보존해 새 인덱스 엔트리를 만들지 않는다. CUBRID 의
in-place vs
REC_RELOCATION결정은 구조적으로 비슷하지만, 인덱스 터치 가 아니라 크기 가 결정 요소다. 직접 비교를 하면 어떤 워크로드 영역에서 어떤 정책이 최적인지 분명히 보일 것이다. - PostgreSQL TOAST (The Oversized-Attribute Storage
Technique) vs CUBRID
REC_BIGONE+ overflow 파일. 둘 다 큰 값을 힙 밖으로 밀어낸다. 그러나 TOAST 는 속성별·인라인 line -pointer 이고, CUBRID 는 레코드별·별도 파일이다. 선택적 스 캔 vs 풀 read 사이에서 trade-off 가 다르다. - InnoDB clustered-index storage. InnoDB 는 행을 PK 순으로 B-tree leaf 에 저장한다. 힙이 아니다. 힙 매니저 추상화가 없 고, 등가물은 clustered index 의 leaf 페이지 레이아웃이다. 비 교를 하면 CUBRID 가 행이 사는 곳 과 인덱스가 가리키는 곳 을 분리하는 이유가 분명해진다.
- In-place vs out-of-place 버전 체인. PostgreSQL 은 옛 버
전을 힙 인라인으로 두고, Oracle UNDO 와 CUBRID
prev_version_lsa는 옛 버전을 로그로 밀어낸다. Wu et al. In-Memory MVCC Empirical Evaluation (VLDB 2017) 이 이를 정량 비교한다. CUBRID 의 일반 OLTP 워크로드를 수치화할 가치가 있다. - MVCC 가비지 컬렉션 스케줄링. CUBRID 의
HEAP_PAGE_VACUUM_STATUS는 페이지 해제 전에 vacuum 방문이 더 필요한지 예측한다. PostgreSQL 의 visibility map / freeze map 과 Oracle 의 UNDO retention 튜닝과 비교해 볼 만하다. - 컬럼 스토어를 대안으로. 컬럼별 저장 엔진 (Vertica, MonetDB, Snowflake) 은 슬롯 페이지 메커니즘을 거의 무관하게 만든다. CUBRID 는 오늘 행 전용이다. 힙 매니저를 컬럼 스토어 설계 옆에 놓고 보면 컬럼 스토어 CUBRID 는 어떤 모습일까 라 는 대화의 시작점이 된다.
- 관련된 최근 연구 흐름. Wu et al., 실증 MVCC (VLDB 17); 본 지식 베이스의 OOS 기능 설계 문서 (진행 중); Stoica & Ailamaki, “Enabling Efficient OS Paging for Main-Memory OLTP Databases” (DaMoN 13) — buffer pool 과 힙의 상호작용.
이 섹션의 의도는 다음 문서의 씨앗을 뿌리는 것이지 여기서 분 석을 마치는 것이 아니다. 각 항목은 차례가 오면 자체 큐레이트 노트가 되어야 한다.
원본 분석 (raw/code-analysis/cubrid/storage/heap_manager/)
섹션 제목: “원본 분석 (raw/code-analysis/cubrid/storage/heap_manager/)”1._Heap_Overview_Architecture.pdf— 힙 파일 구조, 슬롯 페 이지 primer, 레코드 타입 개관.2._Heap_Operations.pdf— INSERT / UPDATE / DELETE / READ 흐름과 worked example, update 전이 매트릭스.3._MVCC_in_Heap.pdf— MVCC 레코드 헤더 레이아웃, 가시성 술 어, prev_version_lsa 체인.4._Caches_in_heap.pdf— bestspace 캐시, classrepr 캐시, HFID 테이블, scancache, attrinfo 캐시.5._Record_in_heap.pdf— 레코드 포맷 상세 (representation id, 플래그 비트, 고정 / 가변 속성).[코드분석]heap.pptx— 5 부 시리즈를 통합한 발표 자료. 슬 롯 페이지 레이아웃, 레코드 타입, update 전이, 캐시 아키텍처 다이어그램. 그림 위치를 찾기에 가장 유용한 텍스트 인덱스다.slotted_page.pdf/slotted page_min.pdf— 슬롯 페이지 포 맷 레퍼런스, 앵커 타입, 레코드 타입.CUBRID-Multiple_Page_Ordered_Fix.pdf— 힙 연산의PGBUF_WATCHER체인이 쓰는 다중 페이지 latching 프로토콜.TK-0811-037-CUBRID_Heap_File_Manager.pdf— 원본 기술 문서 로, 힙 파일 매니저의 구조적 레퍼런스.DML Log sequence.pdf— DML → 로그 레코드 시퀀스 레퍼런스.
Notion (CUBRID DEV WIKI)
섹션 제목: “Notion (CUBRID DEV WIKI)”- Storage – Concurrency 코드 분석 — Heap Manager 를 Lock Manager / MVCC / Vacuum 아래에 위치시키는 모듈 단위 컨텍스 트.
교재 챕터 (knowledge/research/dbms-general/)
섹션 제목: “교재 챕터 (knowledge/research/dbms-general/)”- Database Internals (Petrov), 3장 File Formats 와 4장 Implementing B-Trees — 슬롯 페이지, RID 의미론, 자유 공간 관리.
- Database Systems: The Complete Book (Garcia-Molina, Ullman, Widom), §13.7 “Variable-Length Data and Records” — forwarding, TOAST 류 overflow, 페이지 안 reorganization.
CUBRID 소스 (/data/hgryoo/references/cubrid/)
섹션 제목: “CUBRID 소스 (/data/hgryoo/references/cubrid/)”src/storage/heap_file.hsrc/storage/heap_file.csrc/storage/slotted_page.hsrc/storage/slotted_page.csrc/storage/heap_attrinfo.h(HEAP_CACHE_ATTRINFO 타입)src/transaction/mvcc.h(mvcc_rec_header 타입, 본 문서에서 요약)