콘텐츠로 이동

(KO) CUBRID Heap Manager — 슬롯 페이지, 레코드 레이아웃, 연산, MVCC, 캐시

목차

힙 파일 (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) 은 이 자유 공간을 회수하는 작업이고, 레코드 삭제 는 슬롯에 지웠음 표시만 남길 뿐 데이터를 즉시 옮기지는 않는 다.

구현을 좌우하는 추가 요소가 두 가지 더 있다.

  1. 가변 길이 레코드와 oversized 레코드 처리. 레코드는 두 가 지 이유로 자기 슬롯을 초과할 수 있다. 첫째, UPDATE 가 문자열 을 늘려서 기존 레코드가 커진 경우. 둘째, BLOB 처럼 처음부터 어떤 페이지에도 들어갈 수 없는 크기인 경우. 고전적인 해결책 은 전달 포인터 (forwarding pointer) 를 두는 것이다. 원래 자리에는 이 레코드는 다른 곳에 있다 라는 표시를 남기고, 실 제 데이터는 같은 힙의 다른 페이지나 별도 overflow 파일에 둔 다. Garcia-Molina / Ullman / Widom Database Systems: The Complete Book §13.7 “Variable-Length Data and Records” 참 고.

  2. 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 는 바뀌지 않는다.

레코드별 식별자는 힙 파일·페이지·페이지 안 슬롯의 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_HOMEREC_NEWHOME 본문 안에 자리한다.

§학술적 배경 의 교재 개념과 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 는 위의 관행을 두 계층으로 구체화한다. 아래층은 일반 ** 슬롯 페이지** 추상화 (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 architecture

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.h
typedef 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) 가 압축을 내구성을 가진다.

SPAGE_HEADER.anchor_type 은 페이지의 슬롯 재사용 정책을 고른 다.

앵커 타입슬롯 재사용?사용처
ANCHORED안 함FILE_HEAP 의 힙 페이지 (슬롯 ID = 디스크 OID, 재할당 금지)
ANCHORED_DONT_REUSE_SLOTS안 함(별칭 / 명시적 형태)
UNANCHORED_ANY_SEQUENCEsort 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.c
typedef 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;
};

이 레이아웃은 두 가지 사실을 말한다.

  1. 슬롯 0 은 항상 예약된다. heap_file.hHEAP_HEADER_AND_CHAIN_SLOTID = 0 으로 못 박혀 있다. 사용자 데이터는 슬롯 1 부터 시작한다. 레코드를 스캔하는 코드는 슬 롯 0 을 명시적으로 건너뛴다.
  2. 페이지 사이는 양방향으로 연결된다. HEAP_CHAIN.prev_vpidnext_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_MARKDELETEDtombstoneANCHORED_DONT_REUSE_SLOTS 페이지에서 삭제된 슬롯 (OID 정체성 보존을 위해 영구 보존).
REC_DELETED_WILL_REUSEtombstone (재사용 가능)ANCHORED 페이지에서 삭제된 슬롯 (FILE_HEAP_REUSE_SLOTS 파일 타입 — 재할당 가능).
REC_UNKNOWN예약 / sentinel.

이 가운데 일반 힙 read 에서 live 한 타입은 네 개다 — REC_ASSIGN_ADDRESS, REC_HOME, REC_BIGONE, REC_RELOCATION. REC_NEWHOMEREC_RELOCATION 으로만 도달할 수 있고, heap_next 가 이를 걸러 주기 때문에 한 레코드가 스캔에서 두 번 반환되는 일은 없다.

Figure 2 — Record types

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.)

// heap_insert_logical (sketch) — src/storage/heap_file.c
int
heap_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. */
}

세 가지 핵심을 짚을 수 있다.

  1. MVCCID 는 레코드가 자리 잡기 전에 찍힌다. 레코드 헤더 의 플래그 비트가 mvcc_ins_id, mvcc_del_id, prev_version_lsa 의 존재 여부를 알리도록 설정된다.
  2. OID 는 슬롯이 결정한다. 사전에 요청해 받는 값이 아니다. 그래서 행 잠금 (row lock) 은 INSERT 도중 에 획득한다. UPDATE 와 DELETE 는 대상 OID 를 이미 알고 있어서 locator 단 계에서 미리 잡는다.
  3. Best 는 검색이 아니라 힌트다. 캐시와 estimate 가 자리 있는 페이지 쪽으로 편향되어 있고, 실패하면 제한된 스캔으로, 그래도 실패하면 새 페이지 할당으로 떨어진다. 비용은 흩어진 다 — 대부분의 INSERT 는 O(1) 캐시 hit 으로 끝난다.

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_HOMEREC_HOME (same slot) → REC_RELOCATION + REC_NEWHOMEREC_BIGONE + overflow body
REC_RELOCATION + REC_NEWHOMEREC_HOMEREC_RELOCATION + REC_NEWHOME (same other-page) → REC_RELOCATION + REC_NEWHOME (different page) → REC_BIGONE + overflow
REC_BIGONE + overflow bodyREC_HOMEREC_RELOCATION + REC_NEWHOMEREC_BIGONE + overflow (same body, in place if fits)

undo 이미지는 그 행의 이전 버전이 결국 prev_version_lsa 를 따라 도달하게 될 그 자리다. 그래서 undo 로그 엔트리의 LSN 이 새 레코드 헤더에 찍힌다.

MVCC 아래에서 DELETE 는 레코드를 물리적으로 제거하지 않는다. 대신 기존 레코드 헤더에 mvcc_del_id 를 세우고 연산을 로깅한 다. 슬롯을 실제로 풀어 주는 일은 vacuum (heap_vacuum_all_objects) 이 맡는다. vacuum 은 mvcc_del_id 가 전역 oldest-visible MVCCID 보다 오래된 시점에 슬롯을 풀어 준다. 그래서 DELETE 흐름은 UPDATE 와 비슷해 보인다 — 레코드 를 읽고, 헤더를 조정하고, 로그를 남기는 순서가 같다.

특수 케이스가 두 가지 있다.

  1. overflow 레코드 (REC_BIGONE). MVCC 헤더가 overflow 페 이지에 산다 (heap_set_mvcc_rec_header_on_overflow). delete 경로는 페이지의 레코드를 수정하지, 힙 페이지의 forwarder 를 수정하지 않는다. forwarder 는 overflow 레코드 자체가 풀릴 때만 갱신된다.
  2. 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.c
SCAN_CODE
heap_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

모든 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 — MVCC version chain across heap and 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.cmvcc_header_size_lookup[8] 배열이 플래그 바 이트 값을 디스크 헤더 크기로 옮겨 준다.

페이지 단위 vacuum 추적은 HEAP_PAGE_VACUUM_STATUS (HEAP_PAGE_VACUUM_NONE / _ONCE / _UNKNOWN) 에 자리한다. 이 상태는 HEAP_CHAIN.flags 안에 들어 있고, 이 페이지가 앞으 로 vacuum 방문을 더 필요로 하는지 예측해 페이지 해제 가능 여부 를 가른다.

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.h
struct 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: 된 시점에 관찰한 값이며, 시간이 지나면 어긋날 수 있다.

  • 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_deleteREC_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 협조.
  • 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 캐시.
  • 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_headerslotted_page.h(varies)
struct spage_slotslotted_page.h(varies)
struct heap_hdr_statsheap_file.c(varies, near top)
struct heap_chainheap_file.c(varies, near top)
struct heap_scancacheheap_file.h143
struct heap_operation_contextheap_file.h267
struct heap_get_contextheap_file.h362
struct heap_hfid_table_entryheap_file.h201
enum HEAP_PAGE_VACUUM_STATUSheap_file.h354
heap_manager_initializeheap_file.c(varies)
heap_insert_logicalheap_file.c(varies)
heap_update_logicalheap_file.c(varies)
heap_delete_logicalheap_file.c(varies)
heap_get_visible_versionheap_file.c(varies)
heap_nextheap_file.c(varies)
heap_classrepr_getheap_file.c(varies)
heap_alloc_new_pageheap_file.c(varies)

라인 컬럼이 (varies) 로 비어 있는 것은 의도적이다. heap_file.c 가 프로젝트 최대 소스 파일 (≈ 27 000 줄) 이라 서, 심볼 단위 git grep 이 권장되는 lookup 방식이다.

각 항목은 현재 소스에 대한 사실이고, 원본 분석 자료를 함 께 보지 않아도 그 자체로 읽힌다. 끝의 부연은 어떻게 검증되었 는지를 적고, 관련이 있을 때는 역사적 드리프트나 검증의 한계 도 적는다. 미해결 질문 은 큐레이터가 해결을 미루고 기록 해 둔 갭이다 — 이후 독자는 이를 알려진 버그가 아니라 시작점 으로 삼으면 된다.

섹션 헤딩 날짜는 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.hHEAP_HFID_HASH_SIZE = 1000, 2026-04-29 검 증. 하드코딩이며 런타임 파라미터가 아니다.

  • 페이지 여유 충분 임계는 페이지 크기의 30 %. heap_file.hHEAP_DROP_FREE_SPACE = (int)(DB_PAGESIZE * 0.3), 2026-04-29 검증. 기본 16 KB 페이지에서는 페이지가 bestspace 캐시에 들어가려면 약 4 915 바이트 이상이 free 여 야 한다. 이 임계는 INSERT 속도 (후보 페이지가 더 많아지고 스캔이 줄어듦) 와 저장 활용도 (부분만 채워진 페이지의 예약 공간이 놀게 됨) 사이의 trade-off 다.

  • 슬롯 필드는 14 / 14 / 4 비트. spage_slotoffset_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. 왜 정확히 1 000 인 해시 크기? HEAP_HFID_HASH_SIZE = 1000 이 명백한 튜닝 근거 없이 하드코딩되어 있다. 추적 경 로 — 이 값을 git history 로 추적; 1 000 개를 넘는 클래스를 가진 워크로드 (대형 스키마) 에서 핫 패스에 해시 충돌이 일어 나는지 확인; max_clientsnum_classes 에 비례해 스케 일해야 하는지 검토.

  2. num_substitutions = 1000 — second-best promotion 이 왜 이 상수인가? bestspace 캐시는 1 000 INSERT 마다 한 번씩 페이지를 second_best 에서 best 로 promote 한다. 발표 자료는 인접 공간을 덜 채워서 INSERT 를 페이지 사이로 흩어 두기 위함이라고 설명한다. 추적 경로 — 다양한 워크로드에서 promotion 비율을 계측; 지역성 주장이 실증되는지 확인.

  3. 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 호출지 탐색.

  4. CHN guess 캐시 (HEAP_CHNGUESS) 의 상태. 발표 자료는 TBU (to-be-used) 라고 부르고, 소스에는 구조는 있으나 사용 이 제한적이다. 완성·라이브인가, 부분 폐기인가, 스텁인가? 추적 경로 — heap_chnguess_get / heap_chnguess_put 의 호출지 확인; CAS 워크로드를 돌려 캐시가 채워지는지 관찰.

  5. 현대 빌드에서 unfill_space 의 동작. HEAP_HDR_STATS 는 미래 UPDATE 성장을 위한 페이지별 unfill_space 를 예약 해 둔다. 발표 자료는 정적 예약으로 설명하지만, 최근 커밋이 적응적으로 바꿨을 수 있다. 추적 경로 — 이 필드의 할당 경로 를 추적; vacuum 단계가 재분배를 하는지 확인.

  6. 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 → 로그 레코드 시퀀스 레퍼런스.
  • 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.h
  • src/storage/heap_file.c
  • src/storage/slotted_page.h
  • src/storage/slotted_page.c
  • src/storage/heap_attrinfo.h (HEAP_CACHE_ATTRINFO 타입)
  • src/transaction/mvcc.h (mvcc_rec_header 타입, 본 문서에서 요약)