CUBRID B+Tree — 코드 수준 심층 분석
이 문서의 위치: 상위 분석서
cubrid-btree.md가 설계 의도와 이론적 배경을 다룬다면, 이 문서는 코드 수준에서 모든 분기와 필드를 추적하는 심층 분석서다. 각 챕터는 독립적으로 읽을 수 있지만, 순서대로 읽으면 인덱스 키 하나가 커널 안에서 거치는 전체 생애주기를 따라갈 수 있다.
목차:
| Ch | 제목 | 상태 |
|---|---|---|
| 1 | 자료구조 전체 지도 | ✅ |
| 2 | 레코드 인코딩·디코딩과 모듈 초기화 | ✅ |
| 3 | 하강과 래치 커플링 | ✅ |
| 4 | 새 키 삽입 | ✅ |
| 5 | OID 추가와 유니크 제약 강제 | ✅ |
| 6 | 노드 분할과 분리자 승격 | ✅ |
| 7 | 범위 스캔과 LSA 기반 재개 | ✅ |
| 8 | 객체 삭제 — 논리적 삭제와 물리적 삭제 | ✅ |
| 9 | 병합과 미달 노드 재균형 | ✅ |
| 10 | 특수 경로와 엣지 경로 | ✅ |
Chapter 1: 자료구조 전체 지도
섹션 제목: “Chapter 1: 자료구조 전체 지도”질문: B+Tree 노드, 레코드, 키/OID 셀을 기술하는 메모리 내 구조체와 디스크 위 구조체는 무엇이며, 각 필드는 어떻게 연결되는가? 이론적 배경은 cubrid-btree.md(### Slotted-page nodes, ### Non-leaf vs. leaf record split, ### OID-list suffix in non-unique leaves)에 있다. 이 챕터는 필드를 구체적으로 짚는다. 계층 구조: slotted page -> 노드 헤더(슬롯 HEADER = 0) -> 키별 레코드(NON_LEAF_REC/LEAF_REC 고정 접두사 + 가변 페이로드).
1.1 슬롯 페이지 기반 구조: spage_header와 spage_slot
섹션 제목: “1.1 슬롯 페이지 기반 구조: spage_header와 spage_slot”// spage_header -- src/storage/slotted_page.hstruct spage_header{ PGNSLOTS num_slots; PGNSLOTS num_records; INT16 anchor_type; /* ANCHORED / ANCHORED_DONT_REUSE_SLOTS / UNANCHORED_* */ unsigned short alignment; int total_free; int cont_free; int offset_to_free_area; /* records grow up to here */ int reserved1; int flags; /* always SPAGE_HEADER_FLAG_NONE today */ unsigned int is_saving:1; /* space saving for recovery (undo) */ unsigned int need_update_best_hint:1; unsigned int reserved_bits:30;};| 필드 | 역할 | 존재 이유 |
|---|---|---|
num_slots | 할당된 슬롯 항목 수(유효 + 재사용 가능 포함) | 슬롯 배열 길이; PGSLOTID 인덱스 공간 |
num_records | 현재 레코드를 보유한 슬롯 수 | 유효 슬롯과 해제/재사용 가능 슬롯을 구분 |
anchor_type | 슬롯 안정성 정책; ANCHORED, ANCHORED_DONT_REUSE_SLOTS, UNANCHORED_ANY_SEQUENCE, UNANCHORED_KEEP_SEQUENCE | B+Tree는 UNANCHORED_KEEP_SEQUENCE 사용. 슬롯 id가 노드 내 키의 순서 위치가 되며, 삽입/삭제 시 이후 슬롯이 이동해 순서를 유지한다 (Ch 3) |
alignment | 레코드 시작 정렬값 | BTREE_MAX_ALIGN 덕분에 MVCCID/OID 읽기가 정렬됨 |
total_free | 전체 여유 바이트, 단편화될 수 있음 | 삽입 전 가능 여부 사전 검사 |
cont_free | offset_to_free_area에서 연속된 여유 공간 | <= cont_free면 제자리 삽입, 아니면 압축 필요 |
offset_to_free_area | 레코드/여유 공간 경계 | 새 바이트가 여기에 기록되고 값이 전진 |
reserved1, flags, reserved_bits | 패딩 / 미래 확장용 | 8바이트 정렬, 하위 호환 |
is_saving | 복구 공간 예약 활성 상태 | 롤백 시 해제된 공간을 복원할 수 있음 |
need_update_best_hint | 힙 통계 힌트 | B+Tree에서는 사용하지 않음 |
// spage_slot -- src/storage/slotted_page.hstruct spage_slot{ unsigned int offset_to_record:14; unsigned int record_length:14; unsigned int record_type:4; /* REC_HOME, REC_NEWHOME, ... */};| 필드 | 역할 | 존재 이유 |
|---|---|---|
offset_to_record | 레코드 바이트가 시작되는 위치 | 간접 참조 덕분에 압축 시 레코드를 이동해도 슬롯 id가 바뀌지 않음 |
record_length | 레코드 바이트 길이 | B+Tree 코드에 전달되는 RECDES의 경계를 결정 |
record_type | REC_* 식별자 | B+Tree 레코드는 REC_HOME; 힙 재사용을 위해 필드가 존재 |
불변식 — 슬롯/레코드 계수.
num_records <= num_slots이며,offset_to_free_area + cont_free가 앞쪽으로 성장하는 슬롯 배열을 넘어서면 안 된다.spage_insert*가 강제하며, 위반 시SP_DOESNT_FIT-> 분할(Ch 6)로 이어진다.
불변식 — 슬롯 순서는 키 순서. B+Tree 페이지는
UNANCHORED_KEEP_SEQUENCE로 초기화된다(btree.c의spage_initialize호출). 슬롯 id가 곧 노드 내 키의 순서 위치이며,spage_insert_at/spage_delete가 이후 슬롯을 이동(spage_shift_slot_up/_down)해 그 순서를 유지한다. 덕분에 하강 탐색은 슬롯 id로 직접 이진 탐색할 수 있다(Ch 3). 반대급부로PGSLOTID는 위치 기반 값이므로, 페이지 래치를 해제한 스캔은 재사용 전에 반드시 페이지 LSA로 유효성을 재확인해야 한다(Ch 7). 압축은 레코드 바이트를 이동시키되 슬롯 항목은 건드리지 않는다.
1.2 B+Tree 노드 헤더와 루트 헤더
섹션 제목: “1.2 B+Tree 노드 헤더와 루트 헤더”슬롯 HEADER(= 0)가 노드 헤더다. 루트가 아닌 노드는 btree_node_header를 담고, 루트는 btree_root_header를 담는다. btree_root_header의 **첫 번째 필드가 내장된 btree_node_header**이므로, 두 구조체 모두 노드 수준 접근자를 공유한다.
// btree_node_header -- src/storage/btree_load.hstruct btree_node_header{ BTREE_NODE_SPLIT_INFO split_info; VPID prev_vpid; /* leaf prev pointer */ VPID next_vpid; /* leaf next pointer */ short node_level; /* Leaf == 1, Non_leaf > 1 */ short max_key_len; int common_prefix;};| 필드 | 역할 | 존재 이유 |
|---|---|---|
split_info | {float pivot; int index;} | 다음 분할 지점을 관찰된 접근 패턴 쪽으로 편향시킨다(Ch 6). 항상 50/50이 아님 |
prev_vpid | 왼쪽 형제 페이지 id (리프) | 리프 이중 연결 체인으로 범위 스캔이 이동(Ch 7) |
next_vpid | 오른쪽 형제 페이지 id (리프) | B-link “next”; 방금 분할된 페이지 위의 스캔이 오른쪽으로 따라감 |
node_level | 노드 높이, 리프 == 1 | 곳곳에서 리프/비리프 판별자로 사용 |
max_key_len | 서브트리 내 최대 키 길이 | 하강하지 않고 크기 추정 / 분할 예측 |
common_prefix | 이 노드의 공통 접두사 길이 | 접두사 압축 (COMMON_PREFIX_UNKNOWN = -1은 미계산 상태) |
// btree_node_split_info -- src/storage/storage_common.hstruct btree_node_split_info { float pivot; int index; }; /* pivot = split_slot_id/num_keys; index = insert slot after split */// btree_root_header -- src/storage/btree_load.hstruct btree_root_header{ BTREE_NODE_HEADER node; /* root is also a node */ INT64 num_oids; INT64 num_nulls; INT64 num_keys; OID topclass_oid; /* topclass oid, or NULL OID for non-unique */ int unique_pk; struct { int rev_level:16; int deduplicate_key_idx:16; } _32; VFID ovfid; /* overflow (key) file */ MVCCID creator_mvccid; char packed_key_domain[1]; /* key type for the index — ALWAYS LAST */};
// BTREE_CONSTRAINT_TYPE -- src/storage/storage_common.htypedef enum { BTREE_CONSTRAINT_UNIQUE = 0x01, BTREE_CONSTRAINT_PRIMARY_KEY = 0x02 } BTREE_CONSTRAINT_TYPE;| 필드 | 역할 | 존재 이유 |
|---|---|---|
node | 내장된 btree_node_header | 루트도 실제 노드이므로 btree_get_node_header가 그대로 동작 |
num_oids | 인덱싱된 총 객체 수 | 유니크 통계 / 추정치; 델타 로깅으로 유지 |
num_nulls | NULL 키 수 (저장되지 않음) | 유니크 제약은 NULL을 무시하며 별도로 집계 |
num_keys | 고유 키 수 | 정상적인 유니크 인덱스에서 num_oids == num_keys |
topclass_oid | 소유 클래스 OID, 또는 NULL | 유니크 단일 클래스 vs. 비유니크 다중 클래스 구분 |
unique_pk | 비트마스크 BTREE_CONSTRAINT_UNIQUE (0x01), BTREE_CONSTRAINT_PRIMARY_KEY (0x02) | 유니크 강제 + 고정 객체 크기 결정 (Ch 5) |
_32.rev_level | 디스크 리비전 | 포맷 게이트 (BTREE_CURRENT_REV_LEVEL) |
_32.deduplicate_key_idx | 중복 제거 컬럼 인덱스, idx+1로 저장 | SUPPORT_DEDUPLICATE_KEY_MODE; +1은 0이 미설정임을 나타냄 |
ovfid | 오버플로 키 파일 id | 너무 긴 키 처리 (Ch 10) |
creator_mvccid | 인덱스 생성 트랜잭션 | 온라인 로드 중 인덱스 가시성 |
packed_key_domain | 직렬화된 키 TP_DOMAIN | 키 타입; 반드시 마지막 (가변 길이) |
불변식 —
packed_key_domain은 맨 끝에. 유연 배열 멤버로서 그 뒤에 아무것도 올 수 없다.btree_init_root_header(마지막에 팩)와btree_glean_root_header_info(언팩)가 강제한다. 이 필드 뒤에 다른 필드를 추가하면 도메인 바이트가 덮어써져 키 비교가 깨진다.
불변식 — 유니크 카운터. 유니크 인덱스의 모든 커밋된 변이 이후
num_oids == num_keys가 성립해야 한다 (NULL은num_nulls에).btree_change_root_header_delta가 구조적 변경과 함께 세 값을 원자적으로 조정해 이를 보장한다(Ch 5).
1.3 키별 레코드 고정 접두사: NON_LEAF_REC과 LEAF_REC
섹션 제목: “1.3 키별 레코드 고정 접두사: NON_LEAF_REC과 LEAF_REC”// non_leaf_rec -- src/storage/btree.hstruct non_leaf_rec { VPID pnt; short key_len; }; /* pnt = child page pointer */// leaf_rec -- src/storage/btree.hstruct leaf_rec { VPID ovfl; short key_len; }; /* ovfl = overflow-OID page */| 구조체 | 필드 | 역할 | 존재 이유 |
|---|---|---|---|
non_leaf_rec | pnt | 분리자 키 >= 에 대한 자식 VPID | 하강(Ch 3)이 이를 따라 내려감 |
non_leaf_rec | key_len | 분리자 키 길이 (-1이면 오버플로) | pnt 뒤의 키 바이트 길이; 디스크 접두사 NON_LEAF_RECORD_SIZE = DISK_VPID_ALIGNED_SIZE |
leaf_rec | ovfl | 첫 번째 오버플로 OID 페이지, 없으면 NULL_VPID | 레코드 용량을 초과하는 OID 넘침 처리 (Ch 5) |
leaf_rec | key_len | 인라인 키 길이 (-1이면 오버플로 키) | OID 목록 앞 키 바이트를 경계 짓는다; LEAF_RECORD_SIZE = 0 — 고정 접두사 없음, 첫 번째 OID가 시작점이며 leaf_rec은 파싱 대상일 뿐 |
1.4 리프 레코드 전체 바이트 레이아웃
섹션 제목: “1.4 리프 레코드 전체 바이트 레이아웃”왼쪽에서 오른쪽으로: 첫 번째 OID (인라인) -> 클래스 OID (BTREE_LEAF_RECORD_CLASS_OID인 경우만) -> 삽입 MVCCID (첫 번째 OID의 volid에 BTREE_OID_HAS_MVCC_INSID가 있는 경우만) -> 삭제 MVCCID (BTREE_OID_HAS_MVCC_DELID가 있는 경우만) -> 키 바이트 (leaf_rec.key_len 길이, 오버플로 키면 VPID) -> 후속 OID 목록(각 OID마다 자체 MVCC 필드 포함 가능) -> 정렬된 오버플로 VPID 꼬리 (BTREE_LEAF_RECORD_OVERFLOW_OIDS인 경우만). 판별 비트는 첫 번째 OID의 재사용된 두 하위 필드에 산다: 레코드 플래그는 slotid, OID별 MVCC 플래그는 volid.
// leaf record flags -- src/storage/btree.c (in first OID's slotid)#define BTREE_LEAF_RECORD_FENCE ((short) 0x1000)#define BTREE_LEAF_RECORD_OVERFLOW_OIDS ((short) 0x2000)#define BTREE_LEAF_RECORD_OVERFLOW_KEY ((short) 0x4000)#define BTREE_LEAF_RECORD_CLASS_OID ((short) 0x8000)#define BTREE_LEAF_RECORD_MASK ((short) 0xF000)| 플래그 | 세트 시 의미 | 소비자 |
|---|---|---|
BTREE_LEAF_RECORD_FENCE | 펜스 레코드(분할 경계)로, 실제 객체가 아님 | 스캔이 건너뜀(Ch 7); 분할 시 생성(Ch 6) |
BTREE_LEAF_RECORD_OVERFLOW_OIDS | leaf_rec.ovfl 설정됨; 정렬된 꼬리 VPID 존재 | BTREE_RECORD_OR_BUF_INIT이 파싱 길이를 DB_ALIGN(DISK_VPID_SIZE, BTREE_MAX_ALIGN)만큼 줄여 꼬리 VPID를 OID로 잘못 읽지 않게 함 |
BTREE_LEAF_RECORD_OVERFLOW_KEY | 키가 ovfid를 가리키는 VPID로 대체됨 | 키 읽기/비교 시 오버플로 페이지를 가져옴(Ch 10) |
BTREE_LEAF_RECORD_CLASS_OID | 첫 번째 OID 뒤에 클래스 OID 저장됨 | 한 레코드의 객체들이 여러 클래스에 걸침(비유니크 다중 클래스) |
// per-OID MVCC flags -- src/storage/btree.c (in each OID's volid)#define BTREE_OID_HAS_MVCC_INSID ((short) 0x4000)#define BTREE_OID_HAS_MVCC_DELID ((short) 0x8000)#define BTREE_OID_MVCC_FLAGS_MASK ((short) 0xC000)BTREE_GET_MVCC_INFO_SIZE_FROM_FLAGS는 바이트 수를 도출한다: 둘 다 세트 -> 2 * OR_MVCCID_SIZE, 하나만 -> OR_MVCCID_SIZE, 없음 -> 0.
불변식 — 플래그 네임스페이스 분리. 레코드 플래그는
slotid비트0xF000; MVCC 플래그는volid비트0xC000.OVERFLOW_KEY와HAS_MVCC_INSID는 서로 다른 하위 필드에서 둘 다 0x4000이다.BTREE_OID_GET_RECORD_FLAGS(slotid)와BTREE_OID_GET_MVCC_FLAGS(volid)를 사용해야 하며, 혼용하면 오파싱이 발생한다.
불변식 — MVCC 필드는 위치 기반 디코딩. 길이 접두사가 없으며, 존재 여부는 OID의
volid플래그로만 알 수 있다. 따라서 정확히 그만큼의 바이트를 소비한 뒤 다음 필드로 넘어가야 한다.BTREE_RECORD_OR_BUF_INIT+ 플래그 매크로가 강제하며, 임의 산술을 사용하면 첫 번째 가변 MVCC 객체에서 동기가 깨진다.
flowchart TD
A["start: first OID"] --> B{"slotid has<br/>CLASS_OID?"}
B -->|yes| C["read class OID"]
B -->|no| D
C --> D{"volid has<br/>HAS_MVCC_INSID?"}
D -->|yes| E["read insert_mvccid"]
D -->|no| F
E --> F{"volid has<br/>HAS_MVCC_DELID?"}
F -->|yes| G["read delete_mvccid"]
F -->|no| H
G --> H{"slotid has<br/>OVERFLOW_KEY?"}
H -->|yes| I["key field = VPID into ovfid"]
H -->|no| J["read key_len key bytes"]
I --> K["trailing OID list"]
J --> K
K --> L{"slotid has<br/>OVERFLOW_OIDS?"}
L -->|yes| M["leaf_rec.ovfl = aligned tail VPID"]
L -->|no| N["done"]
M --> N
Figure 1-1. 리프 레코드 하나를 분기 완전하게 파싱하는 흐름도.
1.5 BTID_INT: 메모리 내 인덱스 디스크립터
섹션 제목: “1.5 BTID_INT: 메모리 내 인덱스 디스크립터”btree_glean_root_header_info가 루트 헤더에서 추출하며, 거의 모든 B+Tree 함수에 전달된다.
// btid_int -- src/storage/btree.hstruct btid_int{ BTID *sys_btid; int unique_pk; int part_key_desc; /* last partial-key domain is descending */ TP_DOMAIN *key_type; TP_DOMAIN *nonleaf_key_type; /* differs from key_type under prefix keys */ VFID ovfid; char *copy_buf; /* key copy buffer, from INDX_SCAN_ID */ int copy_buf_len; int rev_level; int deduplicate_key_idx; OID topclass_oid;};| 필드 | 역할 | 존재 이유 |
|---|---|---|
sys_btid | 영속적인 BTID (파일 + 루트 페이지 id) | 디스크 인덱스 식별자 |
unique_pk | 유니크/PK 비트마스크 | 유니크 검사(Ch 5) + 고정 객체 크기 결정 |
part_key_desc | 마지막 부분 키 컬럼이 DESC | 비교 부호를 반전(BTREE_IS_PART_KEY_DESC) |
key_type | 리프 키 TP_DOMAIN | 전체 키 비교 / 직렬화 |
nonleaf_key_type | 비리프 도메인, 접두사 키에서만 다름 | 고정 문자 separator가 varying 대응 타입으로 저장됨; 비리프 비교는 이것을 사용 |
ovfid | 오버플로 키 파일 | 너무 긴 키; 루트 ovfid를 미러링 |
copy_buf / copy_buf_len | 키 구체화 스크래치 버퍼 | 스캔 내 키별 malloc을 피함; 스캔 id에서 빌림 |
rev_level | 디스크 포맷 리비전 | 포맷 의존 경로를 보호 |
deduplicate_key_idx | 중복 제거 컬럼 인덱스 | 중복 제거 모드 키 처리 |
topclass_oid | 소유 클래스 OID | 클래스 검사; 비유니크 다중 클래스는 NULL |
불변식 —
key_typevs.nonleaf_key_type. 접두사 인덱스가 아니면 같은 도메인이다. 고정 문자 접두사 인덱스에서는nonleaf_key_type이btree_generate_prefix_domain의 varying 대응 타입이 되며, separator는 반드시 이것으로 비교해야 한다(key_type사용 불가). 잘못된 도메인을 쓰면 하강 순서가 틀어진다(Ch 6).
1.6 BTREE_NODE_TYPE과 BTREE_OP_PURPOSE
섹션 제목: “1.6 BTREE_NODE_TYPE과 BTREE_OP_PURPOSE”// BTREE_NODE_TYPE -- src/storage/btree.htypedef enum { BTREE_LEAF_NODE = 0, BTREE_NON_LEAF_NODE, BTREE_OVERFLOW_NODE } BTREE_NODE_TYPE;| 값 | 페이지 종류 | 레코드 레이아웃 |
|---|---|---|
BTREE_LEAF_NODE | 리프 (node_level == 1) | §1.4의 리프 레코드 레이아웃 |
BTREE_NON_LEAF_NODE | 내부 노드 (node_level > 1) | NON_LEAF_REC + 분리자 키 |
BTREE_OVERFLOW_NODE | 오버플로 OID 페이지 | btree_overflow_header + 고정 크기 OID 패킹 |
BTREE_OP_PURPOSE는 단일 공유 엔진(btree_insert_internal/btree_delete_internal)의 디스패치 키다:
// btree_op_purpose -- src/storage/btree.henum btree_op_purpose{ BTREE_OP_NO_OP, BTREE_OP_INSERT_NEW_OBJECT, BTREE_OP_INSERT_MVCC_DELID, BTREE_OP_INSERT_MARK_DELETED, BTREE_OP_INSERT_UNDO_PHYSICAL_DELETE, BTREE_OP_DELETE_OBJECT_PHYSICAL, BTREE_OP_DELETE_OBJECT_PHYSICAL_POSTPONED, BTREE_OP_DELETE_UNDO_INSERT, BTREE_OP_DELETE_UNDO_INSERT_UNQ_MULTIUPD, BTREE_OP_DELETE_UNDO_INSERT_DELID, BTREE_OP_DELETE_VACUUM_OBJECT, BTREE_OP_DELETE_VACUUM_INSID, BTREE_OP_NOTIFY_VACUUM, BTREE_OP_ONLINE_INDEX_IB_INSERT, BTREE_OP_ONLINE_INDEX_IB_DELETE, BTREE_OP_ONLINE_INDEX_TRAN_INSERT, BTREE_OP_ONLINE_INDEX_TRAN_INSERT_DF, BTREE_OP_ONLINE_INDEX_UNDO_TRAN_INSERT, BTREE_OP_ONLINE_INDEX_TRAN_DELETE, BTREE_OP_ONLINE_INDEX_UNDO_TRAN_DELETE};다섯 계열: 삽입, 물리/논리 삭제, vacuum, notify-vacuum, 온라인 인덱스 로드. Ch 8에서 중요한 구분으로 INSERT_MVCC_DELID(MVCC 삭제 시 삭제 MVCCID 추가) vs. INSERT_MARK_DELETED(비MVCC 유니크, vacuum 제외)가 있고, vacuum에서는 DELETE_VACUUM_OBJECT(완전 제거) vs. DELETE_VACUUM_INSID(삽입 MVCCID만 제거)가 구분된다.
1.7 BTREE_MVCC_INFO와 BTREE_OBJECT_INFO: 메모리 내 셀
섹션 제목: “1.7 BTREE_MVCC_INFO와 BTREE_OBJECT_INFO: 메모리 내 셀”§1.4 레이아웃을 파싱하면 객체당 BTREE_OBJECT_INFO 하나가 나온다. 그 안의 MVCC 하위 레코드가 BTREE_MVCC_INFO다:
// btree_mvcc_info -- src/storage/btree.hstruct btree_mvcc_info { short flags; MVCCID insert_mvccid; MVCCID delete_mvccid; };#define BTREE_MVCC_INFO_INITIALIZER { 0, MVCCID_ALL_VISIBLE, MVCCID_NULL }// btree_object_info -- src/storage/btree.hstruct btree_object_info { OID oid; OID class_oid; BTREE_MVCC_INFO mvcc_info; };| 구조체 | 필드 | 역할 | 존재 이유 |
|---|---|---|---|
btree_mvcc_info | flags | 어떤 MVCCID가 존재하는지 | OID별 volid 플래그의 메모리 내 미러 |
btree_mvcc_info | insert_mvccid | 생성 트랜잭션 id (또는 MVCCID_ALL_VISIBLE) | 읽기 가시성; 모두 보임 상태가 되면 vacuum이 제거 |
btree_mvcc_info | delete_mvccid | 삭제 트랜잭션 id (또는 MVCCID_NULL) | 논리적 삭제 마커; 물리적 제거는 vacuum을 기다림 |
btree_object_info | oid | 인덱싱된 인스턴스 | 조회 결과 |
btree_object_info | class_oid | 소유 클래스 | 다중 클래스 비유니크 인덱스 + 락 타겟팅 |
btree_object_info | mvcc_info | 객체 MVCC 상태 | 스캔 가시성(Ch 7) |
불변식 —
flags는 저장된 비트를 미러링.BTREE_MVCC_INFO.flags는 레코드에 패킹된 OID별 플래그와 반드시 일치해야 한다. 공인된 변경자BTREE_MVCC_INFO_SET_FIXED_SIZE/_CLEAR_FIXED_SIZE가 강제한다. 불일치 시 계산된 크기가 틀려 다음 객체를 잘못된 오프셋에서 읽게 된다. 고정 크기 규칙(BTREE_OBJECT_FIXED_SIZE,btree_load.h)은 오버플로 페이지 객체, 리프의 첫 번째 이후 객체, 오버플로 페이지를 가진 레코드의 첫 번째 객체의flags를 전부 채운다 — OID [+ 유니크인 경우 클래스 OID] + 두 MVCCID — 그 결과 오버플로 페이지가 고정 보폭 배열로 주소 지정된다.
포함 관계 요약: BTID_INT는 btree_root_header에서 추출된다(btree_node_header를 내장하고 btree_node_split_info를 내장); 노드 레코드는 non_leaf_rec(pnt로 자식에 하강) 또는 leaf_rec(ovfl로 고정 보폭 OID를 담는 btree_overflow_header 페이지에 체인)이고; 각 리프 레코드는 btree_object_info 목록으로 디코딩되며, 각각이 btree_mvcc_info를 갖는다.
1.8 챕터 요약 — 핵심 정리
섹션 제목: “1.8 챕터 요약 — 핵심 정리”- 세 계층. 각 페이지는 슬롯 페이지(
spage_header+spage_slot[])이며, 슬롯 0(HEADER)이btree_node_header(비루트) 또는btree_root_header(루트, 노드 헤더 내장)를 담는다; 슬롯 1..N이 키별 레코드를 담는다. LEAF_RECORD_SIZE는 0. 리프 레코드에는 고정 디스크 접두사가 없다 — 첫 번째 OID가 시작점이다;leaf_rec/non_leaf_rec은 파싱 대상이며, 실제 디스크 크기(NON_LEAF_RECORD_SIZE)를 갖는 것은non_leaf_rec뿐이다.- 플래그는 OID 하위 필드에 재사용된다. 레코드 플래그(
BTREE_LEAF_RECORD_*, 마스크0xF000)는 첫 번째 OID의slotid에; MVCC 플래그(BTREE_OID_HAS_MVCC_*, 마스크0xC000)는 각 OID의volid에. 0x4000이 필드마다 다른 의미이므로 올바른 하위 필드를 마스킹해야 한다. - 리프 레코드는 위치 기반으로 디코딩된다. 선택적 클래스 OID, 삽입/삭제 MVCCID, 키(인라인 또는 오버플로 VPID), 후속 OID 목록, 선택적 정렬 오버플로 VPID — 플래그 유무로 결정되며 길이 접두사가 없다; 안전한 탐색은
BTREE_RECORD_OR_BUF_INIT+ 플래그 매크로로만 가능하다. BTID_INT는 전달되는 디스크립터. 루트 헤더에서 추출되며unique_pk, 분리된key_type/nonleaf_key_type(접두사 키에서만 달라짐),ovfid, 중복 제거 메타데이터를 거의 모든 연산에 전달한다.BTREE_OP_PURPOSE가 디스패치 키. 하나의 삽입/삭제 엔진이 다섯 계열(삽입, 삭제, vacuum, notify, 온라인 로드)을 처리하며, purpose가 MVCC 삭제와 비MVCC 표시 삭제를, 완전 제거와 insid 전용 제거를 구분한다.- 카운터와 플래그는 일관성을 유지한다. 루트 헤더
num_oids/num_keys/num_nulls는 델타 로깅으로 일관되게 유지되고,BTREE_MVCC_INFO.flags는 저장된 OID 플래그를 미러링해야 한다 — 따라서 크래시나 동시 읽기를 거쳐도 레코드 크기와 유니크 산술이 틀어지지 않는다.
Chapter 2: 레코드 인코딩·디코딩과 모듈 초기화
섹션 제목: “Chapter 2: 레코드 인코딩·디코딩과 모듈 초기화”Chapter 1에서는 인메모리 구조체(BTID_INT, LEAF_REC, NON_LEAF_REC, BTREE_MVCC_INFO, BTREE_OBJECT_INFO)를 살펴봤다. 이번 장은 논리적인 (key, OID, class_oid, MVCC info) 튜플이 슬롯 페이지 레코드의 원시 바이트로 변환되었다가 다시 읽히는 과정을 추적하고, 이후 모든 챕터의 호출 스택을 관통하는 두 개의 연산별 임시 구조체(btree_insert_helper, btree_delete_helper)와 오버플로 키 파일의 일회성 초기화를 다룬다. 오버플로 키가 존재하는 이유나 OID 목록이 키 뒤에 붙는 구조, 유니크·비유니크 레이아웃의 이론은 노드 레이아웃과 유니크 키 처리를 참고하라. 이 장은 이론이 아니라 바이트 수준을 추적한다.
2.1 디스크 레코드 문법
섹션 제목: “2.1 디스크 레코드 문법”리프 레코드의 파싱 제어 비트는 별도 필드가 아니다 — 첫 번째 객체의 slot-id 하프워드(OR_OID_SLOTID)의 상위 니블에 탑재된다:
// BTREE_LEAF_RECORD_* — src/storage/btree.c#define BTREE_LEAF_RECORD_FENCE ((short) 0x1000) /* fence key, not a real object */#define BTREE_LEAF_RECORD_OVERFLOW_OIDS ((short) 0x2000) /* OID list spills to overflow page */#define BTREE_LEAF_RECORD_OVERFLOW_KEY ((short) 0x4000) /* key spilled to overflow-key file */#define BTREE_LEAF_RECORD_CLASS_OID ((short) 0x8000) /* subclass OID present after instance OID */#define BTREE_LEAF_RECORD_MASK ((short) 0xF000)두 번째 독립적인 쌍은 첫 번째 객체의 volid 하프워드(OR_OID_VOLID)에 탑재되어 객체별 MVCC 필드를 제어한다: BTREE_OID_HAS_MVCC_INSID (0x4000), BTREE_OID_HAS_MVCC_DELID (0x8000). OID는 8바이트(pageid(4) @0 | slotid(2) @4 | volid(2) @6)이며, 디스크 주소는 slotid/volid의 하위 비트만 사용하므로 각 상위 니블이 비어 있다. 이것이 핵심 인코딩 기법이다: 첫 번째 객체의 OID가 레코드 헤더를 겸한다.
flowchart LR
subgraph First["첫 번째 객체 (레코드 헤더 겸임)"]
O1["instance OID 8B<br/>slotid 상위 니블 = 레코드 플래그<br/>volid 상위 니블 = MVCC 플래그"]
C1["class OID 8B<br/>CLASS_OID 플래그가 있을 때"]
I1["insert MVCCID 8B<br/>HAS_MVCC_INSID가 있을 때"]
D1["delete MVCCID 8B<br/>HAS_MVCC_DELID가 있을 때"]
end
KEY["키 바이트<br/>(OVERFLOW_KEY이면 6B 오버플로 VPID)"]
REST["객체 2..n<br/>OID + 선택적 MVCCID"]
OVF["오버플로-OIDs VPID 6B<br/>OVERFLOW_OIDS가 있을 때"]
O1 --> C1 --> I1 --> D1 --> KEY --> REST --> OVF
Figure 2-1. 리프 레코드 바이트 문법. 첫 번째 객체가 키보다 앞에 온다. 추가 객체들은 align32 패딩 이후에 온다. 맨 뒤의 오버플로-OIDs VPID는 BTREE_RECORD_OR_BUF_INIT이 객체 순회에서 제외한다(§2.5). 비리프 레코드에는 객체가 없다: NON_LEAF_REC 고정 부분(자식 VPID + key_len)에 키가 뒤따르며, 음수 key_len은 OVERFLOW_KEY의 비리프 등가물이다.
2.2 키 크기 측정: btree_get_disk_size_of_key
섹션 제목: “2.2 키 크기 측정: btree_get_disk_size_of_key”호출자는 버퍼 크기를 정하고 오버플로 여부를 결정하기 위해 키의 패킹된 길이가 필요하다. btree_get_disk_size_of_key는 얇은 래퍼다:
// btree_get_disk_size_of_key — src/storage/btree.cint btree_get_disk_size_of_key (DB_VALUE * key) { if (key == NULL || DB_IS_NULL (key)) { assert (key != NULL && !DB_IS_NULL (key)); /* <- NULL keys never reach here in release */ return 0; } return pr_index_writeval_disk_size (key); /* <- delegates to the type's packer */}분기는 NULL 검사 하나뿐이다. NULL 키는 레코드의 부재(루트 헤더의 num_nulls에 계산됨)를 뜻하며, 길이 0인 셀이 아니므로 여기에 도달하면 버그다. 반환 값은 btree_write_record의 or_init 크기 산정과 §2.9의 오버플로 임계값 판정에 쓰인다.
불변식 — 키 크기는 한 번 계산해 재사용된다. 삽입 경로는
BTREE_GET_KEY_LEN_IN_PAGE(>= BTREE_MAX_KEYLEN_INPAGE인 키는DISK_VPID_SIZE로 압축) 처리 후 그 길이를btree_insert_helper::key_len_in_page에 저장한다. 이 측정값과 실제index_writeval결과가 어긋나면 슬롯 크기가 맞지 않아 인접 레코드가 오염된다. 양쪽 모두 동일한pr_type패커를 거치도록 해서 일치를 강제한다.
2.3 레코드 플래그 설정: btree_leaf_set_flag
섹션 제목: “2.3 레코드 플래그 설정: btree_leaf_set_flag”모든 레코드 플래그 쓰기는 하나의 병목 함수를 통과한다:
// btree_leaf_set_flag — src/storage/btree.cstatic void btree_leaf_set_flag (RECDES * recp, short record_flag) { short slot_id; assert ((short) (record_flag & ~BTREE_LEAF_RECORD_MASK) == 0); /* <- only the F000 nibble allowed */ slot_id = OR_GET_SHORT (recp->data + OR_OID_SLOTID); /* <- read first object's slotid+flags */ OR_PUT_SHORT (recp->data + OR_OID_SLOTID, slot_id | record_flag); /* <- OR in, preserve slotid */}OR_OID_SLOTID 위치의 워드를 읽고, 플래그를 OR하여 되돌려 쓴다. assert는 잘못된 비트(또는 volid 워드에 쓰여야 할 MVCC 0x4000/0x8000)가 첫 번째 OID의 slot-id를 오염시켜 객체 포인터가 조용히 잘못된 위치를 가리키는 상황을 잡는다. 실제 slot-id는 상위 니블을 쓰지 않으므로 하위 비트에 대한 쓰기는 비파괴적이다. MVCC 플래그는 형제 함수 btree_record_object_set_mvcc_flags가 동일한 방식으로 OR_OID_VOLID에 적용한다.
2.4 레코드 직렬화: btree_write_record
섹션 제목: “2.4 레코드 직렬화: btree_write_record”btree_write_record는 두 노드 유형 모두, 호출자가 제공한 RECDES에 레코드를 구성하며, OR_BUF 커서로 쓰면서 플래그를 설정한다. Figure 2-2는 모든 분기를 추적한다.
flowchart TD
A["or_init buf over rec->data"] --> B{node_type == LEAF?}
B -- "non-leaf" --> NL["write fixed NON_LEAF_REC"]
B -- "leaf" --> L1["or_put_oid instance OID"]
L1 --> L2{UNIQUE and class != topclass?}
L2 -- yes --> L3["or_put_oid class OID<br/>set CLASS_OID flag"]
L2 -- no --> L4
L3 --> L4{mvcc_info != NULL?}
L4 -- yes --> L7["or_put_mvccid insid if HAS_INSID<br/>or_put_mvccid delid if HAS_DELID<br/>set MVCC flags in volid"]
L4 -- no --> KT
L7 --> KT
NL --> KT{NORMAL key?}
KT -- normal --> K1["pr_type->index_writeval key"]
KT -- overflow --> K3["leaf: set OVERFLOW_KEY flag<br/>btree_store_overflow_key, write 6B VPID"]
K1 --> AL["or_put_align32"]
K3 --> AL
AL --> FIN["rec->length = ptr-buffer<br/>rec->type = REC_HOME"]
Figure 2-2. btree_write_record의 제어 흐름 — 모든 분기.
모든 or_put_* 호출 다음에는 if (error_code != NO_ERROR) { assert_release (false); return error_code; }가 따라온다 — OR_BUF 오버런은 런타임 조건이 아니라 크기 산정 버그다. 리프 경로 분기별 설명:
- 첫 번째 객체 —
or_put_oid가 instance OID를 쓴다. 이것이 동시에 레코드 헤더다(§2.1). - Class OID — 유니크 이고 객체의 클래스가
btid->topclass_oid와 다를 때만or_put_oid+btree_leaf_set_flag (rec, BTREE_LEAF_RECORD_CLASS_OID). 기본 테이블의 PK는 이를 저장하지 않아 8바이트를 아낀다. 플래그와 바이트는 동시에 움직인다. - MVCC —
mvcc_info != NULL로 보호된다.HAS_INSID/HAS_DELID는 독립적인or_put_mvccid호출이다.btree_record_object_set_mvcc_flags (rec->data, mvcc_info->flags)는 바이트를 쓴 후에 volid 니블을 확정하므로, 불완전한 레코드가 없는 필드를 가진 척하는 일이 없다.mvcc_info == NULL이면 바이트도 플래그도 쓰지 않는다(비-MVCC 벌크 경로).
비리프 분기는 btree_write_fixed_portion_of_non_leaf_record_to_orbuf를 내보내고 oid/class_oid/mvcc_info를 무시한다. 이후 공유 키 쓰기가 실행된다: NORMAL은 pr_type->index_writeval을 호출하고, OVERFLOW는 OVERFLOW_KEY를 설정하고(리프 전용 — 비리프는 음수 key_len으로 표시), btree_store_overflow_key로 키를 연결한 뒤 6바이트 VPID를 그 자리에 쓴다. 마지막으로 or_put_align32가 4바이트 경계로 패딩하고(파서가 이에 의존 — §2.6), rec->length는 커서 이동량, rec->type = REC_HOME이 된다.
불변식 — 직렬화 시 설정한 플래그는 파서가 건너뛸 필드를 정확히 예언해야 한다.
btree_write_record가 유일한 생산자이고,btree_read_record_without_decompression이 유일한 소비자다. 둘은 플래그 비트 외에 상태를 공유하지 않는다. 만약 쓰기 측이OVERFLOW_KEY를 설정하고 키를 저장하지 않았다면(또는 반대라면), 읽기 측의or_advance연산이 키 영역으로 걸어 들어간다. 따라서 각 플래그는 해당or_put에 바로 인접하여 설정되며, 절대 일괄 처리되지 않는다.
2.5 BTREE_RECORD_OR_BUF_INIT — 순회 경계
섹션 제목: “2.5 BTREE_RECORD_OR_BUF_INIT — 순회 경계”객체 루프는 레코드 전체를 맹목적으로 읽지 않는다. BTREE_RECORD_OR_BUF_INIT은 맨 뒤의 오버플로-OIDs VPID를 제외한 endptr을 가진 OR_BUF를 설정한다:
// BTREE_RECORD_OR_BUF_INIT — src/storage/btree.c#define BTREE_RECORD_OR_BUF_INIT(buf, btree_rec) \ do { int size = (btree_rec)->length; \ if (btree_leaf_is_flaged (btree_rec, BTREE_LEAF_RECORD_OVERFLOW_OIDS)) \ { size -= DB_ALIGN (DISK_VPID_SIZE, BTREE_MAX_ALIGN); } /* <- hide ovfl pointer from loops */ \ or_init (&buf, (btree_rec)->data, size); } while (false)이로써 §2.6과 §2.7은 6바이트 오버플로 VPID를 객체로 오인하지 않고 while (buf.ptr < buf.endptr) 루프를 돌 수 있다.
2.6 레코드 파싱: btree_read_record와 without_decompression 코어
섹션 제목: “2.6 레코드 파싱: btree_read_record와 without_decompression 코어”btree_read_record는 두 단계 셸이다: btree_read_record_without_decompression을 호출하여 필드를 파싱하고 키 위치를 찾은 뒤, 펜스가 아닌 인페이지 리프 키에 대해서만 노드의 공통 프리픽스를 다시 붙인다. 코어는 바이트를 순회하며 §2.4가 기록했다고 주장한 필드들을 같은 조건으로 건너뛴다:
// btree_read_record_without_decompression — src/storage/btree.c (leaf field-skip, condensed)rc = or_advance (&buf, OR_OID_SIZE); /* skip instance oid */if (BTREE_IS_UNIQUE (btid->unique_pk) && btree_leaf_is_flaged (rec, BTREE_LEAF_RECORD_CLASS_OID)) { rc = or_advance (&buf, OR_OID_SIZE); } /* skip class oid (UNIQUE && flag) */if (btree_record_object_is_flagged (rec->data, BTREE_OID_HAS_MVCC_INSID)) { rc = or_advance (&buf, OR_MVCCID_SIZE); } /* skip insert mvccid */if (btree_record_object_is_flagged (rec->data, BTREE_OID_HAS_MVCC_DELID)) { rc = or_advance (&buf, OR_MVCCID_SIZE); } /* skip delete mvccid */if (btree_leaf_is_flaged (rec, BTREE_LEAF_RECORD_OVERFLOW_KEY)) { key_type = BTREE_OVERFLOW_KEY; }if (btree_leaf_is_flaged (rec, BTREE_LEAF_RECORD_OVERFLOW_OIDS)) { btree_leaf_get_vpid_for_overflow_oids (rec, &leaf_rec->ovfl); }else { VPID_SET_NULL (&leaf_rec->ovfl); }레코드 플래그는 slot-id 니블에서(btree_leaf_is_flaged), 객체별 MVCC 플래그는 volid 니블에서(btree_record_object_is_flagged) 가져온다. class OID 건너뜀은 BTREE_IS_UNIQUE 와 플래그가 모두 필요해 §2.4와 일치한다. 각 or_advance는 자체 if (rc != NO_ERROR) return rc;를 가진다(생략됨). 비리프 분기는 고정 NON_LEAF_REC 부분을 읽고 key_len < 0을 BTREE_OVERFLOW_KEY로 처리한다. 공유 키 읽기 분기:
- NORMAL 키.
clear_key는key != NULL && copy_key == COPY_KEY_VALUE일 때만true다. MIDXKEY/char/bit string 복사는 malloc을 피하기 위해btid->copy_buf를 경유한다.index_readval이 커서를 전진시키고leaf_rec->key_len이 이동량으로 설정된다.key == NULL을 전달하면index_readval이 키를 구체화하지 않고 건너뛴다 — 스캔이 OID 리스트에 도달하기 위해 사용하는 저비용 경로다. - OVERFLOW 키. 6바이트 VPID를 읽는다.
key != NULL이면btree_load_overflow_key로 체인을 따라가며*clear_key = true를 강제하고, 그렇지 않으면*clear_key = false.or_get_short실패 시 assert하고 키를 null로 만든다.
후처리에서 after_key_offset을 계산한다: key->need_clear 오버라이드가 자기-정리 타입에는 *clear_key = true를 강제하고, buf.ptr = PTR_ALIGN (buf.ptr, OR_INT_SIZE)가 쓰기 측의 align32를 건너뛰며, *offset = CAST_BUFLEN (buf.ptr - buf.buffer)가 키 바로 다음 바이트를 기록한다. 이 *offset이 모듈에서 가장 많이 재사용되는 숫자 — OID 리스트가 시작하는 위치로, after_key_offset이라는 이름으로 도처에 전달된다. btree_read_record는 이후 리프이고 오버플로가 아니며 펜스도 아닌 키에만 압축 해제를 수행한다: btree_node_get_common_prefix가 n_prefix > 0을 반환하면 슬롯 1(펜스)을 읽고 pr_midxkey_add_prefix가 프리픽스와 서픽스를 *key에 붙인다(n_prefix < 0이면 assert). 프리픽스 메커니즘은 companion 문서의 노드 레이아웃 절을 참고하라.
2.7 객체 수 세기: btree_leaf_get_first_object와 btree_record_get_num_oids
섹션 제목: “2.7 객체 수 세기: btree_leaf_get_first_object와 btree_record_get_num_oids”btree_leaf_get_first_object는 헤더를 겸하는 객체를 반환한다: BTREE_RECORD_OR_BUF_INIT (record_buffer, recp) 후 btree_or_get_object (..., BTREE_LEAF_NODE, dummy_offset = 0, oidp, class_oid, mvcc_info)를 실행하며, NO_ERROR를 assert한다(첫 번째 객체는 반드시 파싱되어야 한다). btree_record_get_num_oids는 전체 개수를 계산하며, 세 분기가 스토리지 방식을 인코딩한다:
// btree_record_get_num_oids — src/storage/btree.c (condensed; the three branches)if (node_type == BTREE_LEAF_NODE) { rec_oid_cnt = 1; /* the object before the key */ BTREE_RECORD_OR_BUF_INIT (buf, rec); or_seek (&buf, after_key_offset); /* jump past the key */ if (BTREE_IS_UNIQUE (btid_int->unique_pk)) { /* unique: fixed-size objects */ fixed_object_size = BTREE_OBJECT_FIXED_SIZE (btid_int); assert ((CAST_BUFLEN (buf.endptr - buf.ptr) % fixed_object_size) == 0); return rec_oid_cnt + CAST_BUFLEN (buf.endptr - buf.ptr) / fixed_object_size; } while (buf.ptr < buf.endptr) { /* non-unique: variable size, walk it */ mvcc_flag = btree_record_object_get_mvcc_flags (buf.ptr); buf.ptr += OR_OID_SIZE + BTREE_GET_MVCC_INFO_SIZE_FROM_FLAGS (mvcc_flag); rec_oid_cnt++; } return rec_oid_cnt;}return CEIL_PTVDIV (rec->length, BTREE_OBJECT_FIXED_SIZE (btid_int)); /* overflow node: all fixed */세 가지 방식: 유니크 리프 — 첫 번째 이후 객체들은 고정 크기(BTREE_OBJECT_FIXED_SIZE = 2*OID + 2*MVCCID). 키 이후 스팬을 나누면 개수가 나오고, 나머지 assert가 오염 트립와이어다. 비유니크 리프 — 가변 크기 객체(class OID 없음, MVCCID 선택적)이므로 OR_OID_SIZE + BTREE_GET_MVCC_INFO_SIZE_FROM_FLAGS(...)씩 전진하는 루프를 쓴다. 첫 번째 객체는 앞에서 +1로 더해진다. 오버플로 노드 — 키 없이 전부 고정 크기: CEIL_PTVDIV로 rec->length / fixed_size.
불변식 —
after_key_offset은 키 바로 다음 바이트를 정확히 가리켜야 한다. 세 가지 리프 전략 모두or_seek (&buf, after_key_offset)이 첫 번째 키 이후 객체에 정확히 착지하는 것에 의존한다. 해당 오프셋은 §2.6의*offset그대로다. 유니크 리프의 나머지 assert는 오프셋이 1바이트라도 어긋나기 전에 잡아낸다.
2.8 연산별 헬퍼 구조체
섹션 제목: “2.8 연산별 헬퍼 구조체”모든 변형 연산은 두 힙-프리 임시 구조체 중 하나를 거친다. 최상위 호출에서 한 번 생성되어 재귀 아래로 전달되며, 그렇지 않으면 열두 개의 매개변수가 됐을 것들을 하나로 묶는다.
btree_insert_helper — 전체 필드
섹션 제목: “btree_insert_helper — 전체 필드”| 필드 | 역할 | 존재 이유 |
|---|---|---|
obj_info | 삽입 중인 BTREE_OBJECT_INFO | 페이로드 (OID + class OID + MVCC info) |
purpose | BTREE_OP_PURPOSE 판별자 | 신규 삽입 / MVCC-delid / 온라인 인덱스 / undo 동작 선택 |
op_type | 단건 vs 다건 삽입/수정 | unique_stats_info 수집을 제어 |
unique_stats_info | btree_unique_stats * 누산기 | 유니크 카운트 유지 보수를 배치 플러시로 지연 |
key_len_in_page | 패킹된 키 길이 (BTREE_GET_KEY_LEN_IN_PAGE 이후) | 재측정 없이 분할 크기 산정 |
nonleaf_latch_mode | 하강용 PGBUF_LATCH_MODE | 기본은 READ 낙관적, 분할 예상 시 WRITE (Ch 3) |
is_first_try | 첫 번째 루트 픽스일 때 true | B-트리 정보를 루트에서 한 번만 로드 |
need_update_max_key_len | 아래로 전파할 max-key-len 증가 | 자식이 늘어난 최댓값을 따라야 함 |
is_crt_node_write_latched | 현재 노드가 이미 쓰기 래치됨 | 래치 승격 건너뜀 |
is_root | 현재 노드가 루트 | 루트는 분할/병합 처리가 특수함 |
is_unique_key_added_or_deleted | 키 수가 실제로 변경됨 | 키 생성 vs OID 추가를 통계 목적으로 구분 |
is_unique_multi_update | 유니크 인덱스의 다행 업데이트 | 구문 중간에 가시적 객체가 1개 초과여도 허용 |
is_ha_enabled | HA 복제 활성 | 일시적 유니크 위반 금지 |
log_operations | er_log 추적 토글 | 디버그 계측 |
is_null | 키가 SQL NULL | NULL은 계산되지만 저장되지 않음 |
printed_key / printed_key_sha1 | 읽기 가능한 키 + SHA1 | 대형 키 진단용 |
insert_list | btree_insert_list * | 배치 다중 키 삽입 드라이버 |
leaf_addr | 대상 리프의 LOG_DATA_ADDR | redo 로깅 앵커 |
rcvindex | LOG_RCVINDEX id | redo 핸들러 선택 |
rv_keyval_data / _length | 패킹된 key+value undo 이미지 | 논리적 undo 페이로드 |
rv_redo_data / _ptr | redo 버퍼 + 쓰기 커서 | 물리적 redo 이미지, 점진적으로 구성 |
compensate_undo_nxlsa | 다음 undo LSA | undo 중 CLR 생성 |
is_system_op_started | 중첩 시스템 op 열려 있음 | 대기 중인 log_sysop_* commit/abort 추적 |
time_track | PERF_UTIME_TRACKER | 성능 통계 |
saved_locked_oid / _class_oid (SERVER_MODE) | 유니크 검사 중 잠긴 객체 | 래치 해제 후 재시도까지 잠금 유지 |
기억해 둘 생성자 하강 기본값: purpose (BTREE_OP_NO_OP), nonleaf_latch_mode (PGBUF_LATCH_READ) (낙관적 시작), is_first_try (true) (btree info 한 번만 로드), is_crt_node_write_latched (false), is_root (false), is_unique_key_added_or_deleted (true) (달리 증명될 때까지 키 수가 변한다고 가정), is_system_op_started (false).
btree_delete_helper — 전체 필드
섹션 제목: “btree_delete_helper — 전체 필드”| 필드 | 역할 | 존재 이유 |
|---|---|---|
object_info | 제거할 BTREE_OBJECT_INFO | 삭제 대상 |
second_object_info | 두 번째 객체 정보 | 유니크 인덱스 삽입 undo 시 밀려난 객체 필요 |
purpose | BTREE_OP_PURPOSE 판별자 | 삽입 헬퍼와 동일한 다중화 역할 |
nonleaf_latch_mode | 하강 래치 모드 | READ 낙관적 기본값 |
op_type | 연산 유형 (기본 SINGLE_ROW_DELETE) | unique_stats_info 수집 제어 |
unique_stats_info | btree_unique_stats * | 배치 유니크 카운트 유지 보수 |
match_mvccinfo | 매칭할 BTREE_MVCC_INFO | 키의 OID 리스트에서 특정 객체 버전을 찾음 |
buffered_key | 키의 OR_BUF * | 사전 패킹된 키로 검색, 재인코딩 불필요 |
printed_key / printed_key_sha1 | 읽기 가능한 키 + SHA1 | 대형 키 진단용 |
log_operations | 추적 토글 | 디버그 로깅 |
is_root | 현재 노드가 루트 | 루트 전용 병합 처리 |
is_first_search | 첫 번째 탐색 | 초기 하강과 재시작 후 재시도 구분 |
check_key_deleted | 가시적 객체가 1개 초과 가능 (MULTI_ROW_UPDATE) | 키가 실제로 사라졌는지 검증 여부 결정 |
is_key_deleted | 키가 실제로 비어짐 | 키가 살아남았다면 수집된 통계 수정 |
leaf_addr | 리프의 LOG_DATA_ADDR | redo 앵커 |
rv_keyval_data / _length | undo 이미지 | 삭제의 논리적 undo |
rv_redo_data / _ptr | redo 버퍼 + 커서 | 물리적 redo 이미지 |
reference_lsa | 참조 LSA | 복구/보상 기준점 |
is_system_op_started | 중첩 시스템 op 열려 있음 | 동일한 commit/abort 북키핑 |
time_track | PERF_UTIME_TRACKER | 성능 통계 |
두 구조체는 복구 블록(leaf_addr, rv_keyval_data*, rv_redo_data*, is_system_op_started)을 서로 거울처럼 공유한다. btree_insert_helper_to_delete_helper와 역방향 함수는 한 연산이 모드를 전환할 때(예: 삭제 후 삽입하는 MVCC 업데이트) 이 블록을 복사한다.
불변식 —
is_system_op_started는 모든 종료 경로에서 균형을 맞춰야 한다. 코드가log_sysop_start하고(§2.9처럼) 이 플래그를 설정했다면, 모든return/goto error에서log_sysop_commit또는log_sysop_abort를 호출해야 한다. 시스템 op가 누수되면 로그 어펜드 고정이 유지되어 복구가 멈춘다. 이 플래그 덕분에 오류 경로가 “내가 하나 열었나?”를 묻고 언와인드할 수 있다.
2.9 오버플로 키 파일 최초 초기화
섹션 제목: “2.9 오버플로 키 파일 최초 초기화”오버플로 키 파일은 키가 처음으로 BTREE_MAX_KEYLEN_INPAGE(= DB_PAGESIZE / 8)를 초과할 때 지연 생성된다. 삽입 루트 준비 단계는 이중 검사 패턴으로 생성을 보호한다: 읽기 래치 아래에서 key_len >= BTREE_MAX_KEYLEN_INPAGE && VFID_ISNULL (&btid_int->ovfid)를 검사하고, 참이면 루트를 WRITE로 pgbuf_promote_read_latch한다(ER_PAGE_LATCH_PROMOTE_FAIL이면 재픽스). insert_helper->is_crt_node_write_latched = true를 설정한 뒤 다시 VFID_ISNULL을 검사한다 — 래치 창에서 다른 삽입자가 파일을 이미 만들었을 수 있기 때문이다. 그때서야 log_sysop_start하고 btree_create_overflow_key_file을 호출한다(실패 시 sysop을 중단하고 goto error). 시스템 op은 주변 삽입이 롤백되더라도 파일을 영속하게 한다 — 트랜잭션 데이터가 아니라 영구 메타데이터이기 때문이다.
// btree_create_overflow_key_file — src/storage/btree.c (condensed)VFID_SET_NULL (&btid->ovfid);des.btree_key_overflow.btid = *btid->sys_btid; /* structure copy */des.btree_key_overflow.class_oid = btid->topclass_oid;assert (!OID_ISNULL (&des.btree_key_overflow.class_oid));error_code = file_create_with_npages (thread_p, FILE_BTREE_OVERFLOW_KEY, 3, &des, &btid->ovfid);if (error_code != NO_ERROR) { return error_code; }error_code = heap_get_class_tde_algorithm (thread_p, &btid->topclass_oid, &tde_algo);if (error_code != NO_ERROR) { VFID_SET_NULL (&btid->ovfid); return error_code; } /* <- roll back VFID */error_code = file_apply_tde_algorithm (thread_p, &btid->ovfid, tde_algo);if (error_code != NO_ERROR) { VFID_SET_NULL (&btid->ovfid); return error_code; } /* <- roll back VFID */return error_code;분기 완전성: 먼저 ovfid를 null로 만들고, 인덱스의 BTID와 최상위 클래스 OID로 태그된 3페이지짜리 FILE_BTREE_OVERFLOW_KEY 파일을 생성한 뒤, 클래스의 TDE(투명 데이터 암호화) 알고리즘을 적용한다 — 암호화된 테이블은 오버플로 키도 암호화된다. 파일 생성 이후의 모든 오류 경로는 ovfid를 다시 null로 만든다 — 반쯤 초기화된 파일이 BTID_INT에 유효한 VFID를 남기지 않아, 재시도 시 호출자의 VFID_ISNULL 재검사가 올바르게 작동한다. 성공한 VFID는 이후 루트 헤더에 영속된다. class_oid assert는 소유 클래스 없이 고아 오버플로 파일이 생기는 상황을 막는다.
2.10 챕터 요약 — 핵심 정리
섹션 제목: “2.10 챕터 요약 — 핵심 정리”- 첫 번째 객체의 OID가 레코드 헤더다 — 레코드 플래그는
slotid니블에(btree_leaf_set_flag), MVCC 플래그는volid니블에(btree_record_object_set_mvcc_flags). 하위 비트는 여전히 유효한 디스크 주소다. btree_write_record와btree_read_record_without_decompression은 플래그 비트만을 접점으로 하는 생산자·소비자 쌍이다. 쓰기 측은 각 플래그를 해당 필드 바로 옆에 설정하고, 읽기 측은 정확히 그 필드들만 건너뛴다. 따라서 플래그는 절대 일괄 처리되지 않으며, 모든or_put/or_advance는 자체 오류 출구를 가진다.*offset/after_key_offset이 핵심 숫자다 —PTR_ALIGN이후 한 번 계산되어,btree_record_get_num_oids와 이후 반복자들이or_seek으로 이 값을 사용한다.BTREE_RECORD_OR_BUF_INIT은 맨 뒤의 오버플로 VPID를 그 루프들에서 숨긴다.- 객체 수 세기에는 세 가지 방식이 있다: 유니크 리프(나눗셈 하나, 나머지 assert), 비유니크 리프(
BTREE_GET_MVCC_INFO_SIZE_FROM_FLAGS를 이용한 객체별 순회), 오버플로(rec->length / fixed_size). 리프 경우에서 +1 첫 번째 객체는 명시적으로 더해진다. btree_insert_helper/btree_delete_helper는 힙-프리 생성자-초기화 임시 구조체다. 기본값(PGBUF_LATCH_READ,is_first_try = true)은 Ch 3의 낙관적 하강을 인코딩하고, 거울 복구 블록은 로깅 챕터에 공급된다.- 오버플로 키 파일은 지연 생성되며 정확히 한 번 만들어진다. 시스템 op 내부에서 이중 검사
VFID_ISNULL로 보호되어 롤백을 생존한다.btree_create_overflow_key_file의 모든 오류 경로는ovfid를 재null화하여 재검사가 정직하게 동작하게 한다.
Chapter 3: 하강과 래치 커플링
섹션 제목: “Chapter 3: 하강과 래치 커플링”이 챕터는 “주어진 키가 어느 리프에 속하는가, 그리고 동시성 환경에서 트리를 망가뜨리지 않고 어떻게 그 리프까지 내려가는가”라는 질문에 답한다. 하강 경로는 삽입·삭제·스캔 탐색·유니크 프로브 모두가 공유한다. 래치 커플링 이론과 “루트부터 재시작” 주장은 companion 문서의 ### Latch-coupling on descent를 참고하라. 이 챕터는 그것을 수정한다: 쓰기 경로는 하강 도중 비관적으로 분할하며(Ch 6), 재시작은 래치 승격이 실패할 때만 발생한다.
3.1 하강 드라이버와 세 개의 콜백
섹션 제목: “3.1 하강 드라이버와 세 개의 콜백”모든 순회는 btree_search_key_and_apply_functions로 흐른다 — 호출자가 세 개의 콜백(fix-root / advance / process-leaf)을 꽂아 넣는 템플릿이다.
// btree_search_key_and_apply_functions -- src/storage/btree.cstart_btree_traversal: restart = false; is_leaf = false; root_function (..., &crt_page, &is_leaf, search_key, &stop, &restart, root_args); /* 1. fix root, load BTID_INT 1st try */ if (stop) goto end; if (restart) goto start_btree_traversal; /* stop: NULL/vacuum. restart: promote failed */ while (!is_leaf) /* 2. ADVANCE until leaf reached */ { advance_function (..., &crt_page, &advance_page, &is_leaf, search_key, &stop, &restart, advance_args); if (stop) goto end; if (restart) { if (advance_page) pgbuf_unfix_and_init (thread_p, advance_page); goto start_btree_traversal; } if (!is_leaf) /* latch-couple: release parent only AFTER child fixed */ { if (crt_page) pgbuf_unfix (thread_p, crt_page); crt_page = advance_page; advance_page = NULL; } } if (key_function) { key_function (...); if (restart) goto start_btree_traversal; } /* 3. operate on leaf */콜백 삼중쌍(root / advance / key)의 구성: 읽기 = btree_get_root_with_key / btree_advance_and_find_key / 없음; 삽입(Ch 4–6)과 삭제(Ch 8–9)는 각각 btree_fix_root_for_insert / _for_delete를 루트 콜백으로, btree_split_node_and_advance / btree_merge_node_and_advance를 advance 콜백으로, btree_key_insert_new_object / btree_key_delete_*를 키 함수로 바꿔 끼운다.
불변식 — 재시작 시 페이지 누수 없음.
restart시 엔진은start_btree_traversal로 점프하기 전에advance_page를 unfix하고, 루프 상단에서crt_page를 unfix한다. 각 advance 콜백 역시restart = true를 설정하기 전에crt_page와child_page를 모두 unfix한다. unfix를 빠뜨리면 버퍼 fix 카운터가 영원히 올라간 채로 남는다.
Figure 3-1. 하강 뼈대.
stateDiagram-v2
[*] --> FixRoot : start_btree_traversal
FixRoot --> End : stop true
FixRoot --> FixRoot : restart true
FixRoot --> Advance : 루트 고정, 리프 아님
FixRoot --> ProcessLeaf : 루트가 곧 리프
Advance --> End : stop true
Advance --> FixRoot : restart true\nchild unfix
Advance --> Advance : child 고정\n부모 해제 후 하강
Advance --> ProcessLeaf : 리프 도달
ProcessLeaf --> FixRoot : restart true
ProcessLeaf --> End : 완료
End --> [*]
3.2 BTREE_SEARCH_KEY_HELPER — 결과 전달자
섹션 제목: “3.2 BTREE_SEARCH_KEY_HELPER — 결과 전달자”페이지 수준 탐색은 판정 결과를 작은 구조체 하나에 기록한 뒤 포인터로 하강 경로 전체에 넘긴다.
// struct btree_search_key_helper -- src/storage/btree.cstruct btree_search_key_helper{ enum fence_key_presence { NO_FENCE_KEY = 0, HAS_FENCE_KEY }; BTREE_SEARCH result; /* Result of key search. */ PGSLOTID slotid; /* Slot ID of found key or slot ID of the biggest key smaller then key (if not found). */ fence_key_presence has_fence_key;};| 필드 | 역할 | 존재 이유 |
|---|---|---|
result | BTREE_SEARCH 값(아래 매트릭스). 핵심 판정. | 호출자가 found/absent로 분기한다. |
slotid | BTREE_KEY_FOUND이면 키를 담은 슬롯; 없으면 검색 키보다 작은 것 중 가장 큰 키의 슬롯; 미초기화 상태나 DB_UNK이면 NULL_SLOTID. | ”찾았다, 여기 있다”와 “없다, 여기에 넣어야 한다” 두 경우를 하나의 구조체로 처리한다. |
has_fence_key | 마지막으로 검사한 레코드에 BTREE_LEAF_RECORD_FENCE가 달려 있으면 HAS_FENCE_KEY. | 착지한 슬롯이 실제 데이터가 아닌 합성 경계이므로 스캔 시 건너뛴다(§3.6). |
btree_search_*_page가 설정할 수 있는 값의 역할 매트릭스 (storage_common.h의 7개짜리 BTREE_SEARCH enum 중 일부. BTREE_ERROR_OCCURRED와 BTREE_ACTIVE_KEY_FOUND는 Ch 5/8의 OID/버전 탐색에 속하며 여기서는 발생하지 않는다):
| 값 | 설정 조건 / slotid / 반응 |
|---|---|
BTREE_KEY_FOUND | 펜스가 아닌 정확한 일치; slotid = 일치 슬롯; 읽기/수정 |
BTREE_KEY_NOTFOUND | DB_UNK 비교; slotid = NULL_SLOTID; 오류 전파 |
BTREE_KEY_SMALLER | 키 < 전체, 슬롯 1; slotid = 1; 이전 형제로 이동 |
BTREE_KEY_BIGGER | 키 > 전체, 오른쪽 끝 초과; slotid = key_cnt + 1; 다음 형제로 이동 |
BTREE_KEY_BETWEEN | 없음, [min,max] 내부; slotid = 다음으로 큰 슬롯; 소유 리프, 여기에 삽입 |
BTREE_SEARCH_KEY_HELPER_INITIALIZER는 { BTREE_KEY_NOTFOUND, NULL_SLOTID, NO_FENCE_KEY }로 초기화한다. btree_advance_and_find_key는 노드 유형에 따라 분기한다(Figure 3-2): 리프 탐색은 세 필드를 모두 채우고, 비리프 탐색은 slotid만 채우며 helper는 건드리지 않는다.
Figure 3-2. helper를 채우는 주체.
flowchart LR A["btree_advance_and_find_key"] -->|leaf| LP["btree_search_leaf_page\nfills result+slotid+has_fence_key"] A -->|non-leaf| NP["btree_search_nonleaf_page\nfills only slotid"] LP --> H["BTREE_SEARCH_KEY_HELPER"]
3.3 루트 고정
섹션 제목: “3.3 루트 고정”세 루트 콜백 모두 btree_fix_root_with_info를 호출한다. 이 함수는 latch_mode로 루트를 pgbuf_fix하고, 헤더를 읽어(btree_get_root_header) btid_int_p != NULL일 때만 btree_glean_root_header_info로 메타데이터를 수집한다. BTID_INT(키 타입, 유니크 플래그, 오버플로 키 VFID, topclass OID 등)는 첫 번째 fix 때만 적재된다. 재시작 시 콜백은 btid_int_p = NULL을 넘겨 기존 값을 재사용한다.
// btree_fix_root_for_insert -- src/storage/btree.cif (insert_helper->is_first_try) *root_page = btree_fix_root_with_info (..., insert_helper->nonleaf_latch_mode, NULL, &root_header, btid_int);else { /* restart: re-fix, reset latch flags, skip all first-try-only work */ *root_page = btree_fix_root_with_info (..., insert_helper->nonleaf_latch_mode, NULL, NULL, NULL); insert_helper->is_crt_node_write_latched = false; insert_helper->need_update_max_key_len = false; return NO_ERROR; }insert_helper->is_first_try = false; /* <- never do the below twice */btree_fix_root_for_insert에서 첫 시도에만 실행되는 분기(is_first_try로 보호되어 재시작 시 반복되지 않는다):
purpose == UNDO_PHYSICAL_DELETE/ONLINE_INDEX_UNDO_TRAN_DELETE→ 유니크용 클래스 OID를 채우고key_len_in_page를 계산한 뒤 반환.- 유니크 인덱스 →
btree_unique_stats incr빌드(delete_*vsinsert_*, purpose에 따라 결정);unique_stats_info(다중 행) 또는logtb_tran_update_unique_stats에 누적; 오류 시goto error. is_null→*stop = true, 루트 unfix 후 반환;INSERT_MVCC_DELID/INSERT_MARK_DELETED→ 반환.- 그 외(실제 삽입) →
key_len >= BTREE_MAX_KEYLEN_INPAGE이고 오버플로 키 파일이 없으면 루트를 WRITE로 승격하여 파일을 생성(§3.4);key_len_in_page를 설정하고 반환.
btree_fix_root_for_delete는 이를 그대로 거울 반영한다 — 첫 탐색에서 BTID_INT를 적재하고, 재시작 시 재고정 후 반환. 첫 fix의 delete_helper->purpose 분기(코드 발췌 대신 산문 요약): BTREE_OP_DELETE_VACUUM_* 두 purpose는 즉시 return NO_ERROR; 다섯 undo purpose(_UNDO_INSERT*, _OBJECT_PHYSICAL_POSTPONED, _ONLINE_INDEX_UNDO_TRAN_INSERT)는 btid_int->topclass_oid를 NULL unique BTREE_DELETE_CLASS_OID에 COPY_OID한 뒤 return NO_ERROR; 그 외(btree_is_delete_object_purpose 단언)는 incr.delete_*로 유니크 통계를 갱신하고 is_null이면 *stop = true로 설정.
두 콜백 모두 nonleaf_latch_mode로 루트를 고정한다. 이 값은 기본적으로 PGBUF_LATCH_READ 다 — 쓰기는 낙관적으로 시작하며, 노드를 변경해야 할 때만 WRITE로 승격한다.
3.4 래치 커플링과 READ에서 WRITE로의 전환
섹션 제목: “3.4 래치 커플링과 READ에서 WRITE로의 전환”- 읽기 경로 (
btree_advance_and_find_key): 루트와 각 자식은PGBUF_LATCH_READ로 고정된다. 자식이 고정된 후에야 부모를 해제한다. 승격은 없다. - 쓰기 경로 (split/merge advance): 역시
READ로 시작하며, 해당 노드를 수정해야 하는 위치에서만 WRITE로 승격한다.
btree_split_node_and_advance(Ch 6)는 자식이 리프의 부모이거나 max-key-len을 늘려야 할 때 자식을 즉시 WRITE로 고정하고, 그 외에는 nonleaf_latch_mode를 유지한다. 분할이 필요한데 현재 노드가 READ 래치 상태이면 제자리에서 승격을 시도하고, 실패 시 WRITE 모드로 재시작한다.
// btree_split_node_and_advance -- src/storage/btree.cif (need_split && insert_helper->nonleaf_latch_mode == PGBUF_LATCH_READ && !insert_helper->is_crt_node_write_latched) { error_code = pgbuf_promote_read_latch (thread_p, crt_page, PGBUF_PROMOTE_ONLY_READER); /* always ONLY_READER */ if (error_code == ER_PAGE_LATCH_PROMOTE_FAIL) { /* could not promote -> unfix both pages, switch to WRITE-mode, restart from root */ insert_helper->nonleaf_latch_mode = PGBUF_LATCH_WRITE; *restart = true; pgbuf_unfix_and_init (thread_p, child_page); pgbuf_unfix_and_init (thread_p, *crt_page); insert_helper->is_crt_node_write_latched = false; return NO_ERROR; } insert_helper->is_crt_node_write_latched = true; /* promoted in place: keep descending */ }형제 노드에 대한 나중 승격은 pgbuf_promote_read_latch (..., &child_page, PGBUF_PROMOTE_SHARED_READER)로 자식 노드를 대상으로 한다. 두 승격 모두 실패 시 동일하게 재시작한다.
불변식 — 래치 업그레이드 교착 없음. 현재 페이지 승격은
PGBUF_PROMOTE_ONLY_READER, 자식 페이지 승격은PGBUF_PROMOTE_SHARED_READER를 사용한다 — 두 경우 모두 다른 리더가 해당 페이지를 보유하면 대기 대신 실패하며, 이후 스레드는 모든 래치를 해제하고 WRITE 모드로 재시작한다. READ 래치를 보유한 채 WRITE 업그레이드를 기다리는 것이 교과서적인 래치 업그레이드 교착이다. 실패-재시작 방식이 이를 원천적으로 차단한다.
이것이 companion의 “루트부터 재시작”을 수정하는 내용이다: 쓰기 경로는 분할마다 재시작하지 않는다 — 하강 도중 비관적으로 분할하며(Ch 6), 재시작은 승격 실패 시에만 발생하고, 그 이후 WRITE 모드 하강은 루프하지 않는다.
3.5 btree_search_nonleaf_page — 이진 탐색과 자식 선택
섹션 제목: “3.5 btree_search_nonleaf_page — 이진 탐색과 자식 선택”슬롯 1은 자식 포인터만 페이로드로 갖는 더미 음의 무한대 키다.
// btree_search_nonleaf_page -- src/storage/btree.ckey_cnt = btree_node_number_of_keys (thread_p, page_ptr);if (key_cnt <= 0) return ER_FAILED; /* underflow guard */if (key_cnt == 1) /* only the dummy neg-inf key -> follow its pointer */ { *slot_id = 1; *child_vpid = non_leaf_rec.pnt; return NO_ERROR; }left = 2; right = key_cnt; /* <- start at 2: skip dummy slot 1, then narrow as §3.6 */while (left <= right) { middle = CEIL_PTVDIV ((left + right), 2); c = btree_compare_key (key, &temp_key, btid->key_type, 1, 1, &start_col); if (c == DB_UNK) return ER_FAILED; /* incomparable -> error */ if (c == 0) { *slot_id = middle; *child_vpid = non_leaf_rec.pnt; return NO_ERROR; } /* exact separator hit */ else if (c < 0) right = middle - 1; else left = middle + 1; /* (+ remember start_col) */ }루프 종료 후(left > right): c < 0 → 자식은 middle의 왼쪽 (*slot_id = middle - 1, 해당 pnt 재읽기); c >= 0 → 자식은 middle (*child_vpid가 이미 non_leaf_rec.pnt를 보유). page_bounds(update_boundary_* 호출)는 읽기 하강에서는 NULL이다. start_col은 midxkey 열 건너뛰기 커서다. 비리프 탐색은 slot_id/child_vpid만 출력하며, helper는 절대 채우지 않는다(btree_advance_and_find_key가 search_key->slotid를 임시 값으로 빌려 쓴다).
3.6 btree_search_leaf_page — 발견·미발견·펜스 키 건너뛰기
섹션 제목: “3.6 btree_search_leaf_page — 발견·미발견·펜스 키 건너뛰기”리프에서의 사후 조건은 더 풍부하다. 펜스 키가 동등 비교를 복잡하게 만든다.
// btree_search_leaf_page -- src/storage/btree.cleft = 1; right = key_cnt; /* search_key zeroed to NOTFOUND/NULL_SLOTID/NO_FENCE first */while (left <= right) /* middle = CEIL_PTVDIV((left+right),2); c = btree_compare_key(...) */ { if (c == DB_UNK) { search_key->result = BTREE_KEY_NOTFOUND; ... return error; } if (c == DB_EQ) { if (btree_leaf_is_flaged (&rec, BTREE_LEAF_RECORD_FENCE)) /* matched a FENCE record */ { search_key->has_fence_key = HAS_FENCE_KEY; if (middle == 1) c = DB_GT; /* lower fence -> "go right" */ else if (middle == key_cnt) /* upper fence -> key is past end */ { search_key->result = BTREE_KEY_BIGGER; search_key->slotid = key_cnt; return NO_ERROR; } } else { search_key->result = BTREE_KEY_FOUND; search_key->slotid = middle; return NO_ERROR; } } if (c < 0) right = middle - 1; else left = middle + 1; /* (narrow + remember start_col, as §3.5) */ }펜스 키는 접두사 압축의 경계를 한정하기 위해 이웃 노드 경계를 합성하여 복사한 것이다(companion의 ### Node layout). 실제 일치로 보고해서는 안 되기 때문에, 루프 내 분기에서 하위 펜스 일치는 “오른쪽으로 이동”(c = DB_GT)으로 전환하고, 상위 펜스 일치는 BTREE_KEY_BIGGER를 반환한다.
루프 종료 후(키 없음): 마지막 레코드가 펜스이면 has_fence_key를 표시하고, §3.2 매트릭스에 따라 판정을 설정한다. c < 0 → middle == 1이면 SMALLER, 아니면 BETWEEN, slotid = middle; c >= 0 → middle == key_cnt이면 BIGGER, 아니면 BETWEEN, slotid = middle + 1. 헤더 주석에 따르면, 키가 상위 펜스와 동등한 경우는 unfix/refix 후 스캔이 재개될 때만 발생한다(Ch 7). #ifndef NDEBUG 블록이 btree_leaf_is_key_between_min_max로 교차 검증한다.
3.7 btree_locate_key — 단일 키 하강 진입점
섹션 제목: “3.7 btree_locate_key — 단일 키 하강 진입점”이 일회성 진입점은 기본 루트 함수와 btree_advance_and_find_key를 써서 엔진을 호출하며, reuse_btid_int = true를 넘겨 BTID_INT를 재수집하지 않는다. 두 가지 분기: 오류 조기 반환(*leaf_page_out = NULL); 성공 경로에서는 *found_p = (search_key.result == BTREE_KEY_FOUND)와 *slot_id = search_key.slotid를 설정하고, 리프를 READ 래치가 걸린 채로 반환한다 — 유니크 프로브(Ch 5)와 삭제(Ch 8)의 기본 빌딩 블록이다. *found_p는 BTREE_KEY_FOUND일 때만 true이며, 나머지 경우는 found = false에 slot_id가 삽입 지점을 가리킨다.
3.8 btree_compare_key — 비교 기본 연산
섹션 제목: “3.8 btree_compare_key — 비교 기본 연산”모든 이진 탐색의 최하단에 있는 함수로, DB_LT/DB_EQ/DB_GT/DB_UNK를 반환한다.
// btree_compare_key -- src/storage/btree.cif (DB_IS_NULL (key1)) { if (DB_IS_NULL (key2)) { assert (false); return DB_UNK; } return DB_LT; } /* NULL sorts low; both-NULL impossible */if (DB_IS_NULL (key2)) return DB_GT;if (dom_type == DB_TYPE_MIDXKEY) { c = pr_midxkey_compare (..., start_colp, &dummy_diff_column, dom_is_desc, NULL); if (dom_is_desc[0]) c = ((c == DB_GT) ? DB_LT : (c == DB_LT) ? DB_GT : c); /* DESC flips leading col */ }NULL과 DESC 처리 외에, 비교 불가 타입이나 콜레이션 불일치는 DB_UNK를 반환하며, 이는 모든 탐색에서 하드 오류다. start_colp는 midxkey 열 건너뛰기 커서로, 단일 컬럼 키에서는 사용되지 않는다.
3.9 챕터 요약 — 핵심 정리
섹션 제목: “3.9 챕터 요약 — 핵심 정리”- 엔진 하나, 콜백 셋 —
btree_search_key_and_apply_functions는 fix-root → advance → process-leaf 순서로 실행된다. 연산 간 차이는 콜백에만 있다(읽기:btree_get_root_with_key+btree_advance_and_find_key; 쓰기: split/merge advance). BTREE_SEARCH_KEY_HELPER는 범용 판정자 —result,slotid(일치 또는 삽입 지점),has_fence_key.BTID_INT는 첫 번째 루트 고정 때 딱 한 번 적재된다 — 재시작 시btid_int_p = NULL로 재고정하며, 첫 시도 전용 블록은is_first_try로 보호된다.- 하강은 래치 커플링 방식, 낙관적 READ — 자식을 고정한 후 부모를 해제하며, 노드를 변경해야 할 때만 승격한다.
- 재시작은 예외 — 쓰기 경로는 하강 도중 비관적으로 분할한다(Ch 6). 재시작은
ER_PAGE_LATCH_PROMOTE_FAIL시에만 발생하며, 이후 WRITE 모드 실행이 래치 업그레이드 교착을 방지한다. - 비리프 탐색과 리프 탐색의 출력이 다르다 —
btree_search_nonleaf_page는 더미 슬롯 1을 건너뛰고(slot_id, child_vpid)를 출력한다.btree_search_leaf_page는 완전한 helper를 출력하고 펜스 키를 특별 처리한다. btree_compare_key는 결정론적 — NULL은 낮게 정렬되고, DESC는 선행 컬럼을 뒤집으며, 비교 불가 타입은DB_UNK(하드 오류)를 반환한다.
Chapter 4: 새 키 삽입
섹션 제목: “Chapter 4: 새 키 삽입”이 챕터는 하나의 질문에 답한다. 하강이 리프에 도달했을 때 키가 아직 없다면, 새 (key, first-OID) 레코드를 어떻게 구성하고 리프에 끼워 넣으며, 헤더 카운터와 복구 레코드는 어떻게 갱신되는가? btree_insert_internal의 흐름, btree_split_node_and_advance가 리프에 제어를 넘기는 방식, btree_key_insert_new_object의 분기, 그리고 btree_key_insert_new_key 전체를 추적한다. 기존 키에 OID를 덧붙이는 경우는 Chapter 5, 분할 경로는 Chapter 6에서 다룬다. 레코드 레이아웃, 래치 커플링 하강, 유니크 통계 모델의 개념적 설명은 동반 문서 cubrid-btree.md(“Record encoding”, “Latch coupling”)를 참고하며, 여기서 다시 유도하지 않는다. 구조체 해부는 Chapter 1에서 끝났다. 이 챕터에서는 이 경로가 변경하는 보조 필드만 짚는다.
4.1 진입점 오케스트레이션: btree_insert_internal
섹션 제목: “4.1 진입점 오케스트레이션: btree_insert_internal”세 가지 공개 삽입 진입점은 모두 btree_insert_internal로 수렴한다. 이 함수는 페이지를 직접 건드리지 않는 얇은 설정-및-분기 계층이다. 스택에 BTREE_INSERT_HELPER 하나를 만들고, purpose에 따라 키별 함수를 선택한 뒤, btree_search_key_and_apply_functions에 하강을 위임한다. purpose별 함수 선택이 첫 번째 분기점이다.
// btree_insert_internal -- src/storage/btree.cswitch (purpose) { case BTREE_OP_INSERT_UNDO_PHYSICAL_DELETE: LSA_COPY (&insert_helper.compensate_undo_nxlsa, undo_nxlsa); [[fallthrough]]; case BTREE_OP_INSERT_NEW_OBJECT: key_insert_func = btree_key_insert_new_object; break; /* <- our path */ case BTREE_OP_INSERT_MVCC_DELID: case BTREE_OP_INSERT_MARK_DELETED: key_insert_func = btree_key_find_and_insert_delete_mvccid; break; /* <- logical delete */ default: assert (false); return ER_FAILED;}새 키를 삽입하는 경우 purpose는 BTREE_OP_INSERT_NEW_OBJECT다. NULL 키 플래그는 switch 이전에 계산되며 결정적이다 — NULL 키는 절대 저장하지 않는다: insert_helper.is_null = key == NULL || DB_IS_NULL (key) || btree_multicol_key_is_null (key);. 실제 작업은 세 콜백(루트 고정, 하강/분할, 리프 처리)을 범용 워커 btree_search_key_and_apply_functions에 꿰어 넣는 호출 하나로 수행된다. 반환 후에는 세 가지 후처리가 실행된다. (1) 저장된 락 해제 (SERVER_MODE) — 유니크 키 잠금 과정에서 insert_helper.saved_locked_oid에 기록된 선행 OID를 성공·실패와 관계없이 해제한다. (2) 에러 단락, 그렇지 않으면 PSTAT_BT_NUM_INSERTS를 증가시키고 *unique를 설정한다. (3) 멀티-업데이트 통계 보정 — HA가 비활성화된 유니크 인덱스에서 MULTI_ROW_UPDATE 중 키가 추가·삭제되지 않은 경우(!is_unique_key_added_or_deleted), 투기적 하강 델타를 되돌린다(delete_key_and_row() 되돌리기, add_row() 재적용). 투기적 키 카운트 델타가 취소되는 유일한 지점이다.
flowchart TD
A["btree_insert public entry"] --> B["btree_insert_internal\nbuild BTREE_INSERT_HELPER"]
B --> C{"purpose?"}
C -->|INSERT_NEW_OBJECT / UNDO_PHYSICAL_DELETE| D["key_insert_func =\nbtree_key_insert_new_object"]
C -->|MVCC_DELID / MARK_DELETED| E["key_insert_func =\nbtree_key_find_and_insert_delete_mvccid"]
D --> F["btree_search_key_and_apply_functions"]
E --> F
F --> G["btree_fix_root_for_insert\nunique stats, NULL gate"]
G --> H["btree_split_node_and_advance\ndescent + split"]
H --> I["leaf reached -> key_insert_func"]
I --> J["post: release lock, stats correction"]
Figure 4-1. btree_insert_internal 오케스트레이션과 콜백 연결.
4.2 유니크 확인 게이트와 NULL 키 조기 종료
섹션 제목: “4.2 유니크 확인 게이트와 NULL 키 조기 종료”btree_fix_root_for_insert는 하강당 한 번만 실행된다(is_first_try로 보호). 루트를 고정하는 것 외에 유니크 통계 계산과 key_len_in_page 산출(Chapter 6 분할 수식에 필요한 패킹 키 길이)을 담당한다. 이 함수에는 새 키 경로를 결정짓는 두 개의 게이트가 있다.
유니크 통계 게이트는 BTREE_IS_UNIQUE (btid_int->unique_pk) 하나만 확인한다. 근처에 이 게이트처럼 보이는 매크로가 하나 있지만 실제로는 그렇지 않다.
// BTREE_NEED_UNIQUE_CHECK -- src/storage/btree.h (defined but never referenced)#define BTREE_NEED_UNIQUE_CHECK(thread_p, op) \ (logtb_is_current_active (thread_p) \ && (op == SINGLE_ROW_INSERT || op == MULTI_ROW_INSERT || op == SINGLE_ROW_UPDATE))이 매크로의 의도된 의미 — 활성 트랜잭션(롤백·복구는 라이브 검사를 건너뜀)이면서 삽입 형태의 op 타입 — 는 설계 의도를 기록하고 있지만, src/ 내 어느 호출자도 이를 참조하지 않는다. 즉, 이 매크로는 데드 코드다. 실제 통계 증가는 BTREE_IS_UNIQUE만으로 게이트되며, NULL 여부에 따라 증가 방식이 결정된다.
// btree_fix_root_for_insert -- src/storage/btree.cif (BTREE_IS_UNIQUE (btid_int->unique_pk)) { btree_unique_stats incr; /* ... MVCC-delete branch omitted ... */ if (insert_helper->is_null) incr.insert_null_and_row (); /* <- NULL: a null + a row, never a key */ else incr.insert_key_and_row (); /* <- speculative: assumes a new key WILL be added */ if (BTREE_IS_MULTI_ROW_OP (insert_helper->op_type)) (*insert_helper->unique_stats_info) += incr; /* multi-row sink */ else if (!btree_is_online_index_loading (insert_helper->purpose)) logtb_tran_update_unique_stats (thread_p, *btid, incr, true); /* single-row tran stats */ }불변식 — 투기적 키 카운트는 반드시 보정된다, 틀린 채로 방치되지 않는다. 위반 시: 중복 키 삽입마다 유령 키가 보고되어 옵티마이저 카디널리티가 오염된다.
insert_key_and_row()는 하강이 키 존재 여부를 알기 전에 적용된다. 리프에 이미 키가 있으면(Chapter 5) 4.1의 멀티-로우 보정이 이를 되돌리고, 단일 행 롤백은logtb_tran_update_unique_stats델타를 역전시킨다.
NULL 키 조기 종료 — NULL 키는 결코 리프에 도달하지 않는다.
// btree_fix_root_for_insert -- src/storage/btree.cif (insert_helper->is_null) { *stop = true; /* <- halt walker at root */ pgbuf_unfix_and_init (thread_p, *root_page); return NO_ERROR; }통계는 이미 계산되었으므로 NULL은 집계되지만 저장되지는 않는다. *stop은 워커가 리프 콜백 없이 반환하도록 만들기 때문에, btree_key_insert_new_key는 non-NULL 키에 대해서만 도달한다 — 함수 자체의 assert (key != NULL && ...)이 이를 인코딩한다. 마지막으로 이 함수는 insert_helper->key_len_in_page = BTREE_GET_KEY_LEN_IN_PAGE (btree_get_disk_size_of_key (key))를 설정하며, key_len >= BTREE_MAX_KEYLEN_INPAGE인 경우 오버플로 키 파일을 생성할 수 있다.
4.3 리프 도달: btree_split_node_and_advance
섹션 제목: “4.3 리프 도달: btree_split_node_and_advance”재귀 드라이버의 전체 해부는 Chapter 6에서 다룬다. 이 챕터에서는 리프에 도달하는 끝부분만 중요하다. 자식 분할을 위한 공간을 확보한 뒤(Chapter 6) 노드 타입을 검사한다.
// btree_split_node_and_advance -- src/storage/btree.cif (node_type == BTREE_LEAF_NODE) { assert (pgbuf_get_latch_mode (*crt_page) == PGBUF_LATCH_WRITE); /* <- leaves always WRITE-latched */ /* No other child. Notify called this is a leaf node and return the slot of new key. */ error_code = btree_search_leaf_page (thread_p, btid_int, *crt_page, key, search_key); if (error_code != NO_ERROR) { ASSERT_ERROR (); goto error; } *is_leaf = true; /* <- walker now calls key_insert_func instead of recursing */ return NO_ERROR; }*is_leaf가 true로 설정되면 워커는 재귀를 멈추고 *crt_page에서 키 함수를 호출한다. btree_search_leaf_page의 핵심 출력은 search_key->slotid(이진 탐색으로 키가 위치해야 할 슬롯)와 search_key->result(BTREE_KEY_FOUND이면 존재, 아니면 BTREE_KEY_NOTFOUND / BTREE_KEY_BETWEEN) — 다음 함수가 분기하는 피벗이다. 리프는 도달 시 항상 WRITE 래치 상태이므로 리프 경로에서는 래치 승격이 발생하지 않는다.
4.4 리프 분기: btree_key_insert_new_object
섹션 제목: “4.4 리프 분기: btree_key_insert_new_object”이 리프 콜백은 복구 비계(scaffolding)를 설정한 뒤 새 키 경로(이 챕터)와 덧붙이기 경로(Chapter 5)로 분기한다. 먼저 insert_helper->leaf_addr(offset = search_key->slotid, pgptr = *leaf_page, vfid)를 기록하고, undo keyval 블롭 — 논리적 undo 이미지(키 + 클래스 OID + OID + MVCC 정보) — 을 준비한다.
// btree_key_insert_new_object -- src/storage/btree.cif (insert_helper->purpose == BTREE_OP_INSERT_NEW_OBJECT /* || ONLINE_INDEX_TRAN_INSERT ... */) { insert_helper->rcvindex = BTREE_MVCC_INFO_IS_INSID_NOT_ALL_VISIBLE (BTREE_INSERT_MVCC_INFO (insert_helper)) ? RVBT_MVCC_INSERT_OBJECT : RVBT_NON_MVCC_INSERT_OBJECT; /* <- MVCC vs non-MVCC */ error_code = btree_rv_save_keyval_for_undo (btid_int, key, ..., &insert_helper->rv_keyval_data, ...);}else /* BTREE_OP_INSERT_UNDO_PHYSICAL_DELETE */ insert_helper->rcvindex = RVBT_RECORD_MODIFY_COMPENSATE; /* <- compensation, no undo keyval */그런 다음 핵심 분기 — result != BTREE_KEY_FOUND이 이 챕터의 새 키 경로이고, else는 Chapter 5의 덧붙이기 경로로 이어진다.
// btree_key_insert_new_object -- src/storage/btree.cif (search_key->result != BTREE_KEY_FOUND) { /* Key doesn't exist. Insert new key. */ error_code = btree_key_insert_new_key (thread_p, btid_int, key, *leaf_page, insert_helper, search_key); /* ... free undo blob if reallocated, track time ... */ return NO_ERROR; }/* Key was found. Append new object to existing key. */if (BTREE_IS_UNIQUE (btid_int->unique_pk) && insert_helper->purpose == BTREE_OP_INSERT_NEW_OBJECT) { error_code = btree_key_lock_and_append_object_unique (..., restart, ...); /* <- Chapter 5 */ if (error_code != NO_ERROR) { ASSERT_ERROR (); goto error; } if (*restart == true) { goto exit; } /* <- re-descend: leaf changed under the lock wait */ }else { /* btree_key_append_object_non_unique (...) -- Chapter 5 */ }found-key 유니크 경로는 *restart를 설정하고 goto exit하여 루트부터 다시 하강할 수 있다. btree_key_lock_and_append_object_unique가 락 대기 중에 리프가 재구성되었다면 워커에게 재하강을 요청한다 — 이 분기에서 탈출하는 네 번째 방식이며, 동작 원리는 Chapter 5에서 다룬다. 덧붙이기 분기는 공용 exit:/error: 에필로그를 거쳐 반환되며, 힙에 재할당된 undo 버퍼를 해제하고 성능 타이머를 기록한다. 새 키 분기는 이에 상응하는 정리를 인라인으로 수행하고 exit:에 도달하지 않고 직접 반환한다. 우리의 관심사는 result != BTREE_KEY_FOUND 분기뿐이다.
4.5 셀 구성과 삽입: btree_key_insert_new_key
섹션 제목: “4.5 셀 구성과 삽입: btree_key_insert_new_key”챕터의 핵심: 키와 첫 번째 OID를 담은 리프 레코드를 구성하고, search_key->slotid에 삽입하며, 헤더 카운터를 갱신하고, undo/redo 로그 레코드 하나를 방출한다.
4.5.1 — 추가 표시. insert_helper->is_unique_key_added_or_deleted = true; 로 4.2의 투기적 +key가 올바름을 확인한다(키가 없을 때만 도달하므로), 이후 보정은 발생하지 않는다.
4.5.2 — 오버플로 키 vs 인페이지 키 (세 하위 분기).
// btree_key_insert_new_key -- src/storage/btree.ckey_len = btree_get_disk_size_of_key (key);if (key_len >= BTREE_MAX_KEYLEN_INPAGE) { key_type = BTREE_OVERFLOW_KEY; log_sysop_start (thread_p); /* <- (a) overflow key needs a nested system op */ insert_helper->is_system_op_started = true; }else { int diff_column = btree_node_get_common_prefix (thread_p, btid_int, leaf_page); if (diff_column > 0) /* <- (b) prefix compression */ { new_key = &local_key; pr_clone_value (key, new_key); pr_midxkey_remove_prefix (new_key, diff_column); } else if (diff_column < 0) { ASSERT_ERROR (); return diff_column; } /* <- (c) error: bad prefix, early return before any sysop */ }(a) 크기 초과 → 오버플로 키, 시스템 op 아래에서 오버플로 페이지 할당이 원자적으로 커밋/어보트된다. (b) 공통 접두사 > 0 → local_key에 복제한 뒤 접두사 컬럼을 제거한다. (c) diff_column < 0 → 에러 전파(시스템 op 이전에 반환하므로 어보트 불필요).
4.5.3 — btree_write_record로 REC_HOME 디스크립터에 인코딩.
// btree_key_insert_new_key -- src/storage/btree.crecord.type = REC_HOME; record.data = PTR_ALIGN (data_buffer, MAX_ALIGNMENT); record.area_size = DB_PAGESIZE;error_code = btree_write_record (thread_p, btid_int, NULL /* no non-leaf node_rec */, new_key, BTREE_LEAF_NODE, key_type, key_len, false, BTREE_INSERT_CLASS_OID (insert_helper), BTREE_INSERT_OID (insert_helper), BTREE_INSERT_MVCC_INFO (insert_helper), &record);if (new_key == &local_key) pr_clear_value (&local_key); /* <- free cloned key on BOTH paths, before err check */if (error_code != NO_ERROR) { ASSERT_ERROR (); goto error; }리프에서 이 함수는 첫 번째 OID, 선택적으로 서브클래스 OID(유니크 인덱스에서 클래스가 최상위 클래스와 다를 경우, BTREE_LEAF_RECORD_CLASS_OID 설정), MVCC 정보, 그리고 패킹된 키 본체(또는 오버플로 페이지 VPID)를 기록한다. local_key는 에러 검사 이전에 정리되므로 복제본이 절대 누수되지 않는다.
4.5.4 — 슬롯에 끼워 넣기: spage_insert_at.
// btree_key_insert_new_key -- src/storage/btree.cnode_header = btree_get_node_header (thread_p, leaf_page); /* ... NULL-check -> goto error ... */FI_TEST (thread_p, FI_TEST_BTREE_MANAGER_RANDOM_EXIT, 0);/* Nothing should fail after spage_insert_at! */if (spage_insert_at (thread_p, leaf_page, search_key->slotid, &record) != SP_SUCCESS) { assert_release (false); error_code = ER_FAILED; goto error; }불변식 —
spage_insert_at은 돌아올 수 없는 지점이다. 위반 시: redo가 바로 직후에 방출되기 때문에, 절반만 적용된 삽입을 깨끗하게 롤백할 수 없다. Chapter 6의 분할 패스가 이미btree_get_max_new_data_size바이트를 확보해 두었으므로SP_DOESNT_FIT은 발생할 수 없다. 남은 실패는 로직 버그뿐이다 (assert_release).
내부에서 spage_insert_at은 슬롯 id를 검증한 뒤 spage_find_empty_slot_at을 분기한다. num_slots에서는 spage_add_new_slot으로 덧붙이고, 기존 키들 사이에서는 spage_take_slot_in_use를 호출하여 slot_id부터 슬롯 디렉터리 항목을 하나씩 밀어 공간을 만든다. 두 경우 모두 페이지 헤더 카운터를 증가시키고 spage_insert_data가 바이트를 복사한다(Chapter 1). search_key->slotid에 삽입하면 정렬 순서가 유지된다.
4.5.5 — 노드 헤더 갱신: max_key_len과 split_info.
// btree_key_insert_new_key -- src/storage/btree.ckey_cnt = btree_node_number_of_keys (thread_p, leaf_page);key_len = BTREE_GET_KEY_LEN_IN_PAGE (key_len);if (key_len > node_header->max_key_len) { update_max_key_length = true; node_header->max_key_len = key_len; } /* <- redo must know too */btree_split_next_pivot (&node_header->split_info, (float) search_key->slotid / key_cnt, key_cnt);키 카운트는 여기서 기록하지 않는다 — 슬롯 카운트에서 파생되며, spage_insert_at이 이미 증가시켰다. 이 함수가 명시적으로 관리하는 것은 다음 두 가지다.
max_key_len— 새 키가 더 긴 경우에만 증가한다. 로컬 불리언update_max_key_length는 redo 인코딩(4.5.6)에게 새 키 길이를 패킹하도록 알려 복구 시 헤더를 동일하게 재구성할 수 있게 한다. 이는 리프 성장이며, 조상 체인은 하강 중(Chapter 6)에 갱신되었다.split_info.pivot—btree_split_next_pivot은slotid / key_cnt를 누적 평균에 반영하여, 미래 분할이 삽입이 집중되는 위치를 선호하도록 한다(Chapter 6).
4.5.6 — 복구 레코드 방출 (헬퍼의 더 작은 redo 버퍼가 전체 레코드와 디버그 정보를 담지 못할 수 있으므로 로컬 버퍼에 기록).
// btree_key_insert_new_key -- src/storage/btree.cLOG_RV_RECORD_SET_MODIFY_MODE (&insert_helper->leaf_addr, LOG_RV_RECORD_INSERT); /* <- redo = "insert a slot" */if (update_max_key_length) { rv_redo_data_ptr = or_pack_int (rv_redo_data_ptr, key_len); /* <- replay the header growth */ BTREE_RV_SET_UPDATE_MAX_KEY_LEN (&insert_helper->leaf_addr); }memcpy (rv_redo_data_ptr, record.data, record.length); rv_redo_data_ptr += record.length;btree_rv_log_insert_object (thread_p, *insert_helper, insert_helper->leaf_addr, 0, rv_redo_data_length, NULL, rv_redo_data);Redo 데이터는 선택적 패킹 key_len(BTREE_RV_SET_UPDATE_MAX_KEY_LEN 주소 플래그로 게이트, 헤더가 증가했을 때만 포함)에 이어 원시 레코드가 온다. 대응하는 undo는 4.4에서 준비한 논리적 keyval 블롭이다. btree_rv_log_insert_object는 purpose에 따라 로그 형태를 선택한다.
// btree_rv_log_insert_object -- src/storage/btree.cif (insert_helper.is_system_op_started) log_append_undoredo_data (thread_p, RVBT_RECORD_MODIFY_UNDOREDO, &addr, undo_length, redo_length, undo_data, redo_data);else switch (insert_helper.purpose) { case BTREE_OP_INSERT_NEW_OBJECT: /* undo logical, redo physical */ log_append_undoredo_data (thread_p, insert_helper.rcvindex, &addr, insert_helper.rv_keyval_data_length, redo_length, insert_helper.rv_keyval_data, redo_data); break; /* ... online-index and compensate cases ... */ }불변식 — 일반 새 키 삽입 케이스에서 undo는 논리적이고 redo는 물리적이다. 위반 시: undo가 재구성된 페이지를 오염시키거나 오버플로 할당을 누수시킨다. 롤백은 원시 바이트를 복원하는 대신 객체를 재파생하여 물리적으로 제거한다 — 동시 스레드가 그 바이트를 이미 재구성했을 수 있기 때문이다. 예외: 오버플로 키 시스템 op(
is_system_op_started)은 sysop으로 괄호처리된 물리적RVBT_RECORD_MODIFY_UNDOREDO를 기록한다.
4.5.7 — 시스템 op 종료 및 마무리.
// btree_key_insert_new_key -- src/storage/btree.cif (insert_helper->is_system_op_started) btree_insert_sysop_end (thread_p, insert_helper);pgbuf_set_dirty (thread_p, leaf_page, DONT_FREE); /* <- keep latch, flag for flush */return NO_ERROR;
error: if (insert_helper->is_system_op_started) { log_sysop_abort (thread_p); insert_helper->is_system_op_started = false; } /* <- roll back overflow alloc */ return error_code;error: 레이블은 단일 정리 지점이며, 실질적인 작업은 시작된 sysop을 어보트하는 것뿐이다(오버플로 키 케이스). 돌아올 수 없는 지점 불변식이 error:에서 도달 가능한 슬롯 페이지 변경은 없음을 보장한다.
flowchart TD
A["btree_key_insert_new_key"] --> B["is_unique_key_added_or_deleted = true"]
B --> C{"key_len >= MAX_KEYLEN_INPAGE?"}
C -->|yes| D["key_type = OVERFLOW\nlog_sysop_start"]
C -->|no| E{"common prefix?"}
E -->|diff_column > 0| F["clone + strip prefix into local_key"]
E -->|diff_column < 0| G["return error"]
E -->|0| H["use key as-is"]
D --> I["btree_write_record"]
F --> I
H --> I
I -->|error| Z["goto error"]
I -->|ok| J["spage_insert_at at slotid"]
J -->|not SP_SUCCESS| Z
J -->|SP_SUCCESS| K["update max_key_len if grown\nbtree_split_next_pivot"]
K --> L["pack redo: optional key_len + record"]
L --> M["btree_rv_log_insert_object\nundo logical / redo physical"]
M --> N{"sysop started?"}
N -->|yes| O["btree_insert_sysop_end"]
N -->|no| P["pgbuf_set_dirty; return NO_ERROR"]
O --> P
Z --> Q{"sysop started?"}
Q -->|yes| R["log_sysop_abort"]
Q -->|no| S["return error_code"]
R --> S
Figure 4-2. btree_key_insert_new_key 분기 완전 제어 흐름.
4.6 챕터 요약 — 핵심 정리
섹션 제목: “4.6 챕터 요약 — 핵심 정리”btree_insert_internal은 순수한 설정이다. 헬퍼를 구성하고,btree_key_insert_new_object를 선택하며,is_null을 계산하고, 워커에 위임한다. 이후 작업은 저장된 락 해제와 멀티-업데이트 무연산 보정뿐이다.- 유니크 통계는
btree_fix_root_for_insert에서 리프가 키 존재 여부를 알기 전에 낙관적으로 증가된다(insert_key_and_row). 실제 값은 4.1 보정 또는 undo 로그로 복원된다. NULL 키는 집계되지만(insert_null_and_row)*stop = true로 루트에서 중단된다. btree_split_node_and_advance는btree_search_leaf_page후*is_leaf = true를 설정하여 리프에 도달한다. 리프는 항상 WRITE 래치 상태이며,search_key->result가 분기 피벗이다.btree_key_insert_new_object에는 네 가지 탈출 경로가 있다. 새 키 삽입, 유니크 덧붙이기(*restart시goto exit으로 재하강, Chapter 5), 비유니크 덧붙이기, 에러 경로.btree_key_insert_new_key는 세 하위 분기에서btree_write_record로 셀을 인코딩한다. 크기 초과 → 시스템 op 아래 오버플로 키, 접두사 압축 가능 → 복제 후 제거, 일반 → 그대로.spage_insert_at이 돌아올 수 없는 지점이다(덧붙이기 vsspage_take_slot_in_use). Chapter 6 분할 패스가 공간을 보장하므로SP_DOESNT_FIT은 버그를 의미한다. 헤더 관리는max_key_len(더 길 때만 증가,update_max_key_length로 redo에 반영)과split_info.pivot이며, 키 카운트는 슬롯 카운트에서 암묵적으로 파생된다.- 일반 새 키 삽입의 복구는 undo 논리적, redo 물리적이다. 오버플로 키 sysop은 대신 sysop으로 괄호처리된 물리적
RVBT_RECORD_MODIFY_UNDOREDO쌍을 기록한다.
Chapter 5: OID 추가와 유니크 제약 강제
섹션 제목: “Chapter 5: OID 추가와 유니크 제약 강제”Chapter 4가 키의 첫 번째 객체를 다뤘다면, 이 챕터는 키가 이미 존재하는 상황을
다룬다. 이미 존재하는 키에 두 번째 OID를 꿰어 넣어야 한다. 비유니크 인덱스라면
순수한 추가(append)로 끝나지만, 유니크 인덱스라면 ER_BTREE_UNIQUE_FAILED로
끝날 수 있는 잠금 프로토콜이 선행된다. 세 가지 하위 문제가 뒤섞인다. 새 OID가
어디에 놓이는가(인라인 상한까지는 인라인, 그 이후는 LEAF_REC::ovfl 오버플로
체인), 유니크 추가가 허용되는가(현재 가시 객체를 잠그고 잠금 하에 재판단), 그리고
“가시”의 기준이 무엇인가(purpose에 따라 다르다). 슬롯 페이지 레이아웃, key || OID
인코딩, 오버플로 파일 구조는 cubrid-btree.md §“Node layout”과 §“Unique-key
handling”을 참고하라.
5.1 두 개의 추가 진입점
섹션 제목: “5.1 두 개의 추가 진입점”리프 키 디스패처는 BTREE_IS_UNIQUE (btid_int->unique_pk) 결과에 따라 경로를
나눈다. 비유니크 경로는 잠금도 가시성 검사도 없이 그냥 추가하고, 유니크 경로는
기존 가시 객체를 잠근 뒤 삽입이 유니크 제약을 위반하는지 판단한 다음 추가한다.
인라인 목록이 꽉 찼을 때는 두 경로 모두 btree_key_append_object_into_ovf에서
합류한다.
5.2 btree_key_append_object_non_unique — 그냥 추가
섹션 제목: “5.2 btree_key_append_object_non_unique — 그냥 추가”// btree_key_append_object_non_unique -- src/storage/btree.cn_objects = btree_record_get_num_oids (..., leaf_record, offset_after_key, BTREE_LEAF_NODE);if (n_objects < BTREE_MAX_OIDCOUNT_IN_LEAF_RECORD (btid_int)) /* <- branch A: room inline */ { btree_record_append_object (..., leaf_record, BTREE_LEAF_NODE, btree_obj, NULL, &..rv_redo_data_ptr); spage_update (...); return NO_ERROR; /* writes at tail; logical-undo + physical-redo log */ }BTREE_MVCC_INFO_SET_FIXED_SIZE (&btree_obj->mvcc_info); /* <- branch B: overflow objects are fixed size */error_code = btree_key_append_object_into_ovf (..., leaf_record, leaf_info, insert_helper, btree_obj);상한 BTREE_MAX_OIDCOUNT_IN_LEAF_RECORD(btree_load.h)는
BTREE_MAX_OIDLEN_INPAGE / BTREE_OBJECT_FIXED_SIZE다. Branch A는 레코드
꼬리에 쓴다(하강 시 이미 리프 여유 공간이 보장되므로 분할 없음). Branch B는
고정 크기를 강제한다 — 오버플로 객체는 MVCCID 슬롯 두 개를 모두 담아야 페이지가
이진 탐색 가능해진다(§5.9).
불변식: 인라인 OID 목록은 BTREE_MAX_OIDCOUNT_IN_LEAF_RECORD를 초과하지
않는다 — n_objects < n_objects_limit 게이트(그리고 §5.7의 대칭 assert (==))
로 강제된다. 위반하면 레코드가 무한정 커져서 분할 지점 계산이 깨진다(분할 계산은
레코드 크기가 유계임을 전제한다).
5.3 btree_append_oid와 btree_record_append_object
섹션 제목: “5.3 btree_append_oid와 btree_record_append_object”btree_append_oid는 기계적인 쓰기 함수다(로더에서도 사용됨). rec->data + rec->length 위치에 OR_PUT_OID로 8바이트 OID를 쓰고, rec->length += OR_OID_SIZE로 길이를 늘리는 것이 전부다. MVCC/class/recovery 데이터는 없다.
풍부한 버전인 btree_record_append_object는 전체 객체를 패킹한다. 리프 레코드가
이미 BTREE_LEAF_RECORD_OVERFLOW_OIDS를 갖고 있으면, 먼저 append_at을
DISK_VPID_ALIGNED_SIZE만큼 뒤로 물리고 꼬리 VPID를 저장한 뒤 객체를 패킹하고
VPID를 다시 붙인다.
// btree_record_append_object -- src/storage/btree.cif (node_type == BTREE_LEAF_NODE && record->length > 0 && btree_leaf_is_flaged (record, BTREE_LEAF_RECORD_OVERFLOW_OIDS)) { append_at -= DISK_VPID_ALIGNED_SIZE; OR_GET_VPID (append_at, &ovf_vpid); } /* <- preserve ovfl link */따라서 새 인라인 객체는 꼬리 LEAF_REC::ovfl 링크 앞에 삽입되고, 링크는 항상
레코드 꼬리의 고정된 위치를 유지한다.
5.4 인라인에서 오버플로로의 전환
섹션 제목: “5.4 인라인에서 오버플로로의 전환”btree_key_append_object_into_ovf는 체인 페이지 중 비어 있는 공간이 있으면 재사용하고,
없으면 새로 할당한다.
// btree_key_append_object_into_ovf -- src/storage/btree.cbtree_find_free_overflow_oids_page (..., &leaf_record_info->ovfl, &overflow_page);if (overflow_page == NULL) /* <- no free page in chain */ error_code = btree_key_append_object_as_new_overflow (...); /* allocate + prepend new page */else /* <- a chain page has room */ { btree_key_append_object_to_overflow (..., overflow_page, append_object, insert_helper); pgbuf_unfix_and_init (thread_p, overflow_page); }새 페이지 분기는 btree_start_overflow_page를 호출해 페이지를 할당하고 초기화한 뒤,
btree_leaf_record_change_overflow_link로 리프 링크에 페이지를 끼워 넣고
BTREE_LEAF_RECORD_OVERFLOW_OIDS를 설정한다.
// btree_start_overflow_page -- src/storage/btree.c*new_page_ptr = btree_get_new_page (thread_p, btid_int, new_vpid, near_vpid);VPID_COPY (&ovf_header_info.next_vpid, first_overflow_vpid); /* <- prepend: new page -> old head */btree_init_overflow_header (...); spage_insert_at (..., *new_page_ptr, 1, &rec); /* one object, slot 1 */// ... redo-only log (RVBT_RECORD_MODIFY_NO_UNDO); undo handled logically by caller's sysop ...선두 삽입 방식이다(leaf -> new -> old head -> ...). 새 페이지의 next_vpid가
리프 링크가 재작성되기 전의 현재 헤드를 가리킨다.
불변식 — 오버플로 링크와
BTREE_LEAF_RECORD_OVERFLOW_OIDS플래그는 시스템 오퍼레이션 하에 함께 설정된다.btree_key_append_object_as_new_overflow는 할당과 링크 재작성을log_sysop_start/btree_insert_sysop_end로 감싼다. 따라서 두 작업 사이에 크래시가 발생해도 논리적으로 취소된다. 플래그와 링크가 불일치하면btree_record_append_object가 꼬리에서 쓰레기 VPID를 읽게 된다.
5.5 유니크 프로토콜 — btree_key_lock_and_append_object_unique
섹션 제목: “5.5 유니크 프로토콜 — btree_key_lock_and_append_object_unique”유니크 인덱스는 단순히 추가할 수 없다. MVCC 환경에서 “중복” 키는 이미 논리적으로
삭제된 가시 버전을 가질 수 있으며, 그 경우 새 버전 삽입은 합법이다. 프로토콜은
현재 첫 번째 객체를 잠근 뒤 재판단한다. 리프 LSA를 저장하고,
find_unique_helper.locked_oid에 이전 saved_locked_oid를 복사한 상태에서
X_LOCK으로 btree_key_find_and_lock_unique(§5.6)를 호출하고, 다음 분기들을
순서대로 탄다.
flowchart TB
FAL["find_and_lock_unique"] --> E1{"error / *restart?"}
E1 -- yes --> RET["return / restart"]
E1 -- no --> NF{"result != KEY_FOUND\nor SERVER LSA split?"}
NF -- yes --> NEWK["restart, or insert_new_key\nkey vacuumed away"]
NF -- no --> FO{"found_object and\nINSERT_NEW_OBJECT?"}
FO -- no --> AP["append_object_unique"]
FO -- yes --> MU{"!multi_update or\nnum_visible > 1?"}
MU -- yes --> UV["unique violation"]
MU -- no --> HA{"is_ha_enabled?"}
HA -- yes --> UV
HA -- no --> AP
Figure 5-1. btree_key_lock_and_append_object_unique의 모든 분기 흐름
(!found_object RR-격리 분기도 unique violation으로 이어진다).
위반 판단 분기를 직접 인용한다.
// btree_key_lock_and_append_object_unique -- src/storage/btree.cif (find_unique_helper.found_object && insert_helper->purpose == BTREE_OP_INSERT_NEW_OBJECT) { if (insert_helper->is_unique_multi_update) // ... btree_get_num_visible_from_leaf_and_ovf with dirty snapshot -> num_visible ... if (!insert_helper->is_unique_multi_update || num_visible > 1) { /* <- real violation */ if (prm_get_bool_value (PRM_ID_UNIQUE_ERROR_KEY_VALUE)) return ER_UNIQUE_VIOLATION_WITHKEY; BTREE_SET_UNIQUE_VIOLATION_ERROR (...); return ER_BTREE_UNIQUE_FAILED; } else if (insert_helper->is_ha_enabled) return ER_REPL_MULTI_UPDATE_UNIQUE_VIOLATION; /* HA strictness */ insert_helper->is_unique_key_added_or_deleted = false; /* <- multi-update allowed */ OID_SET_NULL (&insert_helper->saved_locked_oid); /* keep lock on relocated object */} else insert_helper->is_unique_key_added_or_deleted = true; /* all deleted -> a new key */살아 있는 가시 객체가 존재하는데 일반 삽입을 시도하면 위반이다 —
ER_BTREE_UNIQUE_FAILED, 또는 PRM_ID_UNIQUE_ERROR_KEY_VALUE가 설정돼 있으면
키 값을 포함한 ER_UNIQUE_VIOLATION_WITHKEY. 다중 업데이트 예외는 더티
스냅샷 기준 num_visible <= 1일 때만 일시적인 두 번째 버전을 허용한다. HA 환경에서는
그 경우마저도 ER_REPL_MULTI_UPDATE_UNIQUE_VIOLATION이 된다. RR 분기
(!found_object이지만 반복 가능 읽기 스냅샷이 가시 객체를 여전히 카운트하는 경우)도
동일한 에러 쌍을 발생시킨다. 위반이 아닌 것으로 판명되면, MVCC 삽입(rcvindex == RVBT_MVCC_INSERT_OBJECT)이면서 키에 새로운 가시 객체가 추가되지 않는 경우
(!is_unique_key_added_or_deleted) 또는 첫 번째 객체가 같은 트랜잭션에 의해
삭제된 경우(삭제 MVCCID == 삽입 MVCCID)에 한해 undo를 위해 두 객체를 저장한다.
그러면 btree_rv_save_keyval_for_undo_two_objects가 두 버전을 패킹하고
rcvindex가 RVBT_MVCC_INSERT_OBJECT_UNQ로 바뀌어 undo 시 밀려난 객체가 복원된다.
불변식: 유니크 인덱스에서 키당 가시 객체는 최대 하나다(다중 행 업데이트 중에는
일시적으로 둘, HA 환경에서는 절대 둘 없음) — num_visible > 1 분기와
is_ha_enabled 분기가 강제한다. 위반하면 커밋된 두 행이 동일한 유니크 키를
공유하게 된다.
5.6 btree_key_find_and_lock_unique와 잠금 루프
섹션 제목: “5.6 btree_key_find_and_lock_unique와 잠금 루프”btree_key_find_and_lock_unique는 BTREE_IS_UNIQUE에 따라 두 구현으로 분기한다.
_of_unique는 첫 번째 객체만 검사한다(btree_leaf_get_first_object).
_of_non_unique는 OID 목록을 물리적으로 저장하지만 유니크로 선언된 시스템
카탈로그 인덱스를 위한 것이다. 주 경로를 인용한다.
// btree_key_find_and_lock_unique_of_unique -- src/storage/btree.cif (search_key->result != BTREE_KEY_FOUND) goto error_or_not_found; /* key gone */while (true) { btree_leaf_get_first_object (btid_int, &record, &unique_oid, &unique_class_oid, &mvcc_info); if (class filter mismatch) goto error_or_not_found; /* partition-scoped lookup */ if (locked_oid set and != unique_oid) /* first object changed */ { lock_unlock_object_donot_move_to_non2pl (...); OID_SET_NULL (&locked_oid); } switch (mvcc_satisfies_delete (thread_p, &mvcc_header)) { case INSERT_IN_PROGRESS: case DELETE_IN_PROGRESS: /* SA: assert_release(false); SERVER: fallthrough */ case CAN_DELETE: /* already locked -> found; else cond-lock; on fail unfix+uncond, *restart/break */ case DELETED: case SELF_DELETED: goto error_or_not_found; /* gone -> caller may insert */ default: assert_release (false); error_code = ER_FAILED; goto error_or_not_found; } }_of_non_unique는 실질적으로 다르다. 레코드가 목록을 담고 있으므로, 첫
번째 객체에서 멈추지 않고 리프 레코드 전체를 스캔한 뒤 모든 오버플로 페이지까지
순회한다.
// btree_key_find_and_lock_unique_of_non_unique -- src/storage/btree.cwhile (true) { if (start_reading_leaf_record) { /* read leaf record; first object via btree_or_get_object */ } else if (buf.ptr == buf.endptr) { /* <- exhausted current record */ if (VPID_ISNULL (&next_overflow_vpid)) goto error_or_not_found; /* end of chain: not found */ overflow_page = pgbuf_fix (..., &next_overflow_vpid, OLD_PAGE, PGBUF_LATCH_READ, ...); if (prev_overflow_page) pgbuf_unfix_and_init (thread_p, prev_overflow_page); btree_get_next_overflow_vpid (..., &next_overflow_vpid); /* step the chain */ } btree_or_get_object (&buf, ..., &unique_oid, ...); /* next object */ if (class mismatch) continue; /* <- advance, NOT not-found */ switch (mvcc_satisfies_delete (...)) { /* same 5 cases as _of_unique */ } }_of_unique와 달리, 클래스 불일치 시 continue로 다음 목록 객체로 이동하고,
삭제된 객체도 스캔을 계속한다. not-found는 buf.ptr == buf.endptr이면서
VPID_ISNULL(next_overflow_vpid)일 때만 보고된다. 페이지는 PGBUF_LATCH_READ로
고정되고, 스캔이 진행되면서 이전/현재 쌍이 해제된다.
불변식 — 유니크 리프 레코드의 첫 번째 객체는 가장 최신의 가시 버전이며, 잠금 대상이 바로 이것이다. §5.5와
btree_leaf_change_first_object(§5.8)가 이를 유지한다._of_unique는 이 불변식에 의존한다. 만약 첫 번째 버전이 낡은 것이라면, 삽입자가 잘못된 객체를 잠그고 실제 충돌을 놓치게 된다.
BTREE_FIND_UNIQUE_HELPER가 이 함수들 사이를 관통한다.
| 필드 | 역할 | 존재 이유 |
|---|---|---|
oid | 발견된 객체의 OID (출력) | 어떤 객체가 가시였는지 |
match_class_oid | 이 클래스만 허용; NULL이면 아무 클래스나 | 파티션 범위 조회; 불일치 = not-found |
lock_mode | 획득할 잠금; 삽입 = X_LOCK, FK = S_LOCK | 읽기 전용 조회와 삽입 구분 |
snapshot | MVCC 필터; NULL = 없음 | FK/find는 설정; 잠금 삽입은 NULL 유지 |
found_object | 발견 및 잠금 성공 여부 | 성공 플래그 (§5.5) |
time_track | PSTAT_BT_FIND_UNIQUE*용 PERF_UTIME_TRACKER | 단계별 성능 카운터 |
locked_oid (SERVER_MODE) | 현재 여기서 잠근 객체 | unfix/refix 후에도 생존; 첫 번째 객체 변경 감지 |
locked_class_oid (SERVER_MODE) | locked_oid의 클래스 OID | lock_unlock_object_*에 필요 |
locked_oid/locked_class_oid는 SA_MODE에는 없다. 삽입(유니크)은 X_LOCK +
NULL 스냅샷을 사용한다(살아 있는 객체가 위반 판단을 주도한다). FK/find는
S_LOCK + 스냅샷을 사용한다(가시 객체가 존재하는지만 확인, 배제 불필요).
5.7 btree_key_append_object_unique — 최신 항목을 맨 앞에
섹션 제목: “5.7 btree_key_append_object_unique — 최신 항목을 맨 앞에”새 객체가 첫 번째 객체가 되어야 하므로, 현재 첫 번째 객체는 꼬리로 밀려난다.
// btree_key_append_object_unique -- src/storage/btree.cif (btree_record_get_num_oids (...) >= BTREE_MAX_OIDCOUNT_IN_LEAF_RECORD (btid_int)) error_code = btree_key_relocate_last_into_ovf (...); /* full: evict LAST inline object first */BTREE_MVCC_INFO_SET_FIXED_SIZE (&first_object->mvcc_info);btree_record_append_object (..., first_object, NULL, &..rv_redo_data_ptr); /* append OLD first at tail */btree_leaf_change_first_object (..., BTREE_INSERT_OID (insert_helper), ...); /* overwrite slot zero */spage_update (...);// log_append_undoredo_data with insert_helper->rcvindex (MVCC / NON_MVCC / *_UNQ); logical undo.꽉 찬 경우, btree_key_relocate_last_into_ovf가 자체 시스템 op 아래 마지막
인라인 객체를 오버플로로 퇴거시킨다. 여유가 있으면 퇴거는 건너뛴다.
2단계 분리는 의도적이다 — 로깅 시스템이 sysop과 논리적 undo를 혼합할 수 없으므로,
1단계는 물리적 undo/redo를 사용하는 sysop으로, 2단계는 논리적 undo로 처리한다.
btree_key_relocate_last_into_ovf는 log_sysop_start를 열고,
btree_key_append_object_into_ovf로 마지막 객체를 밀어 넣은 뒤,
btree_record_remove_last_object로 리프에서 제거하고
RVBT_RECORD_MODIFY_UNDOREDO를 기록한다. 미묘한 분기는 재읽기다.
// btree_key_relocate_last_into_ovf -- src/storage/btree.cif (VPID_ISNULL (&leaf_record_info->ovfl) && btree_leaf_is_flaged (leaf_record, BTREE_LEAF_RECORD_OVERFLOW_OIDS)) { btree_read_record (...); btree_record_get_last_object (...); } /* <- first ovf page just added: re-read */오버플로 추가가 첫 번째 오버플로 페이지를 방금 생성했을 수 있으므로,
btree_leaf_record_change_overflow_link가 리프 레코드를 재작성하여 캐시된
offset_to_last_object가 낡아졌다. btree_record_remove_last_object 호출 전에
다시 계산해야 한다. 그렇지 않으면 잘못된 바이트가 제거된다.
5.8 btree_leaf_change_first_object — 슬롯 제로 덮어쓰기
섹션 제목: “5.8 btree_leaf_change_first_object — 슬롯 제로 덮어쓰기”이 기본 연산은 레코드 내 첫 번째 객체를 교체한다. 객체의 크기 등급이 바뀌면 레코드가 늘어나거나 줄어든다.
// btree_leaf_change_first_object -- src/storage/btree.cif (old_rec_flag & BTREE_LEAF_RECORD_OVERFLOW_OIDS) { /* <- overflow -> FORCE fixed size */ old_object_size += 2 * OR_MVCCID_SIZE; new_object_size += 2 * OR_MVCCID_SIZE; BTREE_MVCC_INFO_SET_FIXED_SIZE (mvcc_info); if (BTREE_IS_UNIQUE (...) && !new_has_class_oid) /* unique + overflow forces a class OID too */ { new_rec_flag |= BTREE_LEAF_RECORD_CLASS_OID; new_object_size += OR_OID_SIZE; } }// else: no overflow -> variable size. Then RECORD_MOVE_DATA(recp, new_object_size, old_object_size).오버플로 없음 상태에서는 첫 번째 객체가 가변 크기다. 오버플로 있음 상태에서는
고정 크기가 강제되고, 유니크 인덱스라면 클래스 OID까지 포함된다. 그래야
btree_leaf_get_first_object와 오버플로 이진 탐색이 균일한 폭에 합의할 수 있다.
RECORD_MOVE_DATA는 (new - old) 만큼 키와 인라인 OID들을 슬라이드하며,
이동량을 *key_offset으로 보고한다.
불변식 — 리프 레코드에 오버플로 체인이 있으면, 첫 번째 객체는 고정 폭이다 (유니크인 경우 클래스 OID 포함). 여기와 각 추가 함수의
BTREE_MVCC_INFO_SET_FIXED_SIZE가 강제한다. 위반하면btree_find_oid_from_ovfl의 이진 탐색(BTREE_OBJECT_FIXED_SIZE로 나눔)이 객체 중간을 읽게 된다.
5.9 정확한 (key, OID) 위치 찾기 — find-OID 함수군
섹션 제목: “5.9 정확한 (key, OID) 위치 찾기 — find-OID 함수군”유니크 검사와 삭제 경로 모두 *“이 purpose의 가시성 기준으로 이 정확한 (key, OID)는
어느 페이지에 있는가?”*를 묻는다. btree_find_oid_and_its_page는 리프를
선형으로 스캔하고(btree_find_oid_from_leaf, 가변 폭, 객체 0 이후 패킹된 키를
건너뜀), 이후 각 오버플로 페이지를 이진 탐색한다(btree_find_oid_from_ovfl,
고정 폭, OID 정렬).
// btree_find_oid_and_its_page -- src/storage/btree.cbtree_find_oid_from_leaf (..., oid, match_mvccinfo, purpose, offset_to_object, ...);if (*offset_to_object != NOT_FOUND) { *found_page = leaf_page; return NO_ERROR; } /* in leaf */if (VPID_ISNULL (&leaf_rec_info->ovfl)) return NO_ERROR; /* no chain: not found */do { overflow_page = pgbuf_fix (..., PGBUF_LATCH_WRITE, ...); /* walk chain, tracking *prev_page */ btree_find_oid_from_ovfl (..., overflow_page, oid, purpose, match_mvccinfo, offset_to_object, ...);} while (*offset_to_object == NOT_FOUND && !VPID_ISNULL (&overflow_vpid)); /* step via next_overflow_vpid */// found: *found_page = overflow_page; else unfix all + not found페이지는 PGBUF_LATCH_WRITE로 고정된다(변경하는 호출자 때문). prev_page는
제거 후 재연결을 위해 추적된다. not-found와 에러 경로에서는 모든 페이지가 해제된다.
두 finder의 MVCC 게이트는 btree_find_oid_does_mvcc_info_match이며,
purpose에 따라 달라진다.
BTREE_OP_PURPOSE | 일치 규칙 |
|---|---|
DELETE_VACUUM_INSID | insert MVCCID가 match_mvccinfo->insert_mvccid와 같음 |
DELETE_VACUUM_OBJECT, DELETE_OBJECT_PHYSICAL_POSTPONED | delete MVCCID가 match_mvccinfo->delete_mvccid와 같음 (재사용된 OID 회피) |
DELETE_UNDO_INSERT, DELETE_UNDO_INSERT_UNQ_MULTIUPD | insert MVCCID가 롤백 중인 것과 일치 |
DELETE_UNDO_INSERT_DELID | delete MVCCID 일치; 설정돼 있으면 insert MVCCID도 교차 검증 |
DELETE_OBJECT_PHYSICAL, INSERT_MVCC_DELID, INSERT_MARK_DELETED, default | 객체가 유효한 delete MVCCID를 갖지 않음 (살아 있음) |
purpose가 어떤 MVCC 필드로 일치를 결정할지 선택하므로, finder 하나가 삽입·삭제· vacuum을 모두 처리한다. 기본 분기(“유효한 delete MVCCID 없음”)는 유니크 삽입과 물리적 삭제를 구동하므로, 삭제됐지만 아직 vacuum되지 않은 재사용 OID의 이전 버전은 올바르게 건너뛰어진다.
5.10 챕터 요약 — 핵심 정리
섹션 제목: “5.10 챕터 요약 — 핵심 정리”BTREE_IS_UNIQUE에 따른 두 진입점:btree_key_append_object_non_unique는 무조건 추가하고,btree_key_lock_and_append_object_unique는 잠금 후 검증한다.BTREE_MAX_OIDCOUNT_IN_LEAF_RECORD까지 인라인, 이후 오버플로 —btree_key_append_object_into_ovf경유. 체인 페이지를 재사용하거나 새 페이지를 선두에 삽입(btree_start_overflow_page; 플래그와 링크는 시스템 op 아래 함께 설정).- 유니크 = 먼저 잠금, 그다음 판단.
_of_unique는 첫 번째 객체에 X-lock을 걸고,_of_non_unique는 전체 목록을 스캔한다. 살아 있는 가시 객체가 있으면ER_BTREE_UNIQUE_FAILED.MULTI-ROW-UPDATE는 일시적 추가 버전을 하나까지 허용하지만, HA 환경에서는 절대 허용 없으며, 둘도 허용되지 않는다. - 최신 가시 객체는 항상 맨 앞 —
btree_key_append_object_unique가 이전 첫 번째 객체를 꼬리로 옮기고(꽉 찼으면 마지막 인라인 객체를 먼저 오버플로로 퇴거), 이후btree_leaf_change_first_object를 호출한다. - 오버플로는 고정 폭 강제 (MVCCID 슬롯 둘 모두, 유니크면 클래스 OID 포함) —
btree_find_oid_from_ovfl의 이진 탐색 전제 조건. - finder 하나, 다양한 purpose —
btree_find_oid_does_mvcc_info_match가 동일한(key, OID)를BTREE_OP_PURPOSE에 따라 일치 또는 불일치로 판정하므로, 삽입/삭제/undo/vacuum이btree_find_oid_and_its_page를 공유한다. - 재시작과 재읽기는 1급 시민:
*restart는 루트로 돌아가고, LSA 재확인은 분할 재시작을 강제하며, “첫 오버플로 페이지 방금 생성” 재읽기는btree_key_relocate_last_into_ovf를 정직하게 만든다.
Chapter 6: 노드 분할과 분리자 승격
섹션 제목: “Chapter 6: 노드 분할과 분리자 승격”대상 리프 — 또는 하강 경로 상의 어느 조상 노드 — 에 여유 공간이 없을 때, CUBRID는 비관적 하강 중 분할(pessimistic split-on-descent) 로 트리를 확장한다. 루트에서 재시작하지 않고도 공간 부족 문제를 안전하게 해결하는 방식이다. 왜 CUBRID가 시스템 op 아래에서 선제적으로 분할하는지는 cubrid-btree.md의 §“Descent” 및 Figure 2–3에서 다룬다. 이 챕터는 분기별로 어떻게 동작하는지를 해부한다. Chapter 3은 아래 함수를 호출하는 하강 경로를, Chapter 4는 공간이 보장된 뒤의 리프 삽입을 다뤘다.
6.1 비관적 계약: 전진 전에 분할한다
섹션 제목: “6.1 비관적 계약: 전진 전에 분할한다”하강 드라이버 btree_search_key_and_apply_functions는 레벨마다 전진(advance) 함수를 하나씩 호출한다. 삽입 경로에서는 btree_split_node_and_advance가 그 역할을 맡는다.
불변식 — “전진해 들어가는 자식에는 언제나 여유 공간이 있다.”
btree_split_node_and_advance는 자식 페이지를 반환하기 전에 그 자식이 최악 크기의 새 항목을 흡수할 수 있는지 확인하고, 부족하면 먼저 분할한다. 연쇄 분할(자식 분할이 부모 분할을 부르고, 그 부모가 다시 제 부모의 분할을 부르는 상황)은 일어날 수 없다: 제어가 어떤 노드에 도달한 시점에 그 부모는 직전 반복에서 이미 빈 슬롯 하나를 보장받았기 때문이다. 마지막에 방문하는 리프도 같은 귀납으로 공간을 보장받는다.
이 계약은 매 레벨에서 연산이 추가할 수 있는 최대 항목 크기를 빈 공간과 비교함으로써 강제된다.
// btree_split_node_and_advance -- src/storage/btree.cmax_new_data_size = btree_get_max_new_data_size (..., node_type, max_key_len, insert_helper, false /*known*/);need_split = max_new_data_size > spage_get_free_space_without_saving (thread_p, child_page, NULL);max_key_len은 need_update_max_key_len이 참일 때는 insert_helper->key_len_in_page, 그렇지 않으면 node_header->max_key_len이다. known_to_be_found = false로 전달하는 이유는 하강 시점에 해당 키의 존재 여부를 아직 알 수 없으므로 항상 신규 항목 공간을 예약하기 위해서다 — 이것이 비관적 추정이다.
6.2 btree_get_max_new_data_size — 최악 경우 추정기
섹션 제목: “6.2 btree_get_max_new_data_size — 최악 경우 추정기”과소 추정은 §6.1 불변식을 깨뜨린다. 함수 본체는 노드 타입 확인 후 switch (helper->purpose)로 분기한다.
| 분기 | 반환 크기 | 이유 |
|---|---|---|
| 비리프 노드 | NON_LEAF_ENTRY_MAX_SIZE(key_len) + SPAGE_SLOT_SIZE | 자식 분할이 여기에 분리자 하나를 승격시킨다. 슬롯 디렉터리도 하나 늘어난다. |
| 리프, 키 발견 | BTREE_OBJECT_FIXED_SIZE | OID 추가 / 첫 객체 업그레이드 / 오버플로 링크 추가(Ch 5). 새 슬롯 없음. |
| 리프, 키 미발견 | LEAF_ENTRY_MAX_SIZE(key_len) + SPAGE_SLOT_SIZE | 신규 키 레코드와 그 슬롯(Ch 4). |
INSERT_MVCC_DELID / MARK_DELETED | OR_MVCCID_SIZE | 논리 삭제는 기존 객체에 delete MVCCID만 도장 찍는다(Ch 8). |
| default | assert_release(false), ER_FAILED | 처리되지 않은 목적 — 프로그래밍 오류. |
하강은 리프에 known_to_be_found = false를 전달하므로, 리프 추정값은 삽입 경우 중 가장 큰 신규 키 비용이 된다.
6.3 btree_node_split_info — 쏠림 누산기
섹션 제목: “6.3 btree_node_split_info — 쏠림 누산기”모든 노드 헤더는 BTREE_NODE_SPLIT_INFO를 갖는다. 이 구조체는 연속된 분할을 핫 사이드 쪽으로 편향시킨다 — 단조 증가(자동 증가) 키 스트림에 대한 최적화로, 50/50 분할을 그대로 쓰면 왼쪽 절반이 항상 반쯤 비어 있게 된다.
// btree_node_split_info -- src/storage/storage_common.hstruct btree_node_split_info{ float pivot; /* pivot = split_slot_id / num_keys */ int index; /* number of key insert after node split */};| 필드 | 역할 | 존재 이유 |
|---|---|---|
pivot | 과거 분할이 착지한 위치의 누적 이동 평균([0,1]). 0.5이면 균형; 1.0에 가까우면 오른쪽 쏠림(오름차순); 0.0에 가까우면 왼쪽 쏠림. | btree_split_find_pivot이 기하학적 중간이 아닌 핫 사이드를 조준하게 하여 냉각된 절반이 낭비되지 않게 한다. |
index | 평균에 반영된 분할 횟수(키 개수 상한). CMA 분모. | 하나의 이상치 삽입이 pivot을 급격히 흔들지 못하도록 평균을 완충한다. |
btree_write_default_split_info는 새 노드를 pivot = BTREE_SPLIT_DEFAULT_PIVOT (0.5), index = 1로 초기화한다 — 균형 상태로 시작.
btree_split_find_pivot 은 평균을 목표 바이트 크기로 변환하며 두 경로로 나뉜다.
- 균형 경로 —
pivot == 0(미초기화) 또는BTREE_SPLIT_LOWER_BOUND < pivot < BTREE_SPLIT_UPPER_BOUND:CEIL_PTVDIV(total, 2)를 목표로 삼는다.[0.20, 0.80]데드밴드는 미세한 잡음이 편향을 일으키지 않게 막는다. - 쏠림 경로 —
pivot ≤ 0.20또는≥ 0.80:(int)(total * pivot)을 목표로 삼되,[0.05, 0.95](MIN_PIVOT/MAX_PIVOT)로 클램핑하여 각 사이드에 최소 5% 이상이 남도록 보장한다.
btree_split_next_pivot 은 새 분할을 평균에 편입한다. index = MIN(index+1, max_index); pivot == 0이면 new_value를 직접 대입하고, 그렇지 않으면 new_pivot = pivot + (new_value - pivot)/index (CMA), [0,1]로 클램핑. new_value = p_slot_id / key_cnt는 방금 승격된 분리자가 착지한 위치다. 오름차순 삽입은 1.0에 가까운 값을 계속 공급하므로 pivot이 위로 끌려가고, 이후 분할은 밀집된 오른쪽을 유지하면서 희소한 왼쪽 절반을 형제에게 넘기게 된다.
6.4 btree_find_split_point — 크기 균형 분할점
섹션 제목: “6.4 btree_find_split_point — 크기 균형 분할점”btree_split_node/btree_split_root는 어디서 자를지를 btree_find_split_point에 위임한다. 이 함수는 분리자 키를 반환하고 *mid_slot — 왼쪽에 유지되는 마지막 슬롯 번호 — 을 기록한다. 분할은 크기 기반(spage_get_space_for_record 바이트 단위, 키 개수 아님)이므로 크기가 큰 키도 올바르게 처리된다. 소스 주석에서 도출되는 세 가지 규칙: (1) mid_size에 최대한 근접할 것, (2) 양쪽 모두 신규 항목과 펜스 키를 담을 공간을 남겨둘 것, (3) 양쪽 모두 비펜스 레코드를 ≥1개 유지할 것. 이를 뒷받침하는 세 예산: left_max_size/right_max_size = BTREE_NODE_MAX_SPLIT_SIZE - new_ent_size -(리프에 한해) new_fence_size; mid_size = btree_split_find_pivot(tot_rec, …); left_min_size = tot_rec - right_max_size.
루프는 슬롯을 왼쪽에서 오른쪽으로 순회하면서 left_size를 누적한다. 각 실제 레코드의 바이트를 더하되, 리프 삽입 슬롯에서는 new_ent_size(키 없음) 또는 existing + new_ent_size(키 있음, 제자리 추가)를 더한다. 레코드별 처리:
left_size < left_min_size(오른쪽이 아직 예산 초과)인 동안:continue;if (left_size > MIN(left_max_size, mid_size)) { *mid_slot = i-1; break; }— 오버플로 직전 슬롯에서 자른다;if (i == stop_at) { *mid_slot = i; break; }— 펜스를 제외한 전체 페이지가 왼쪽에 남는다.
루프 이후 두 가지 후처리 검사로 규칙 #3을 강제한다. *mid_slot == start_with - 1(왼쪽 비어있음) → ++; *mid_slot == stop_at(오른쪽 비어있음) → --. 각각 노드 타입 / is_key_added_to_left / found에 의해 조건부 실행된다.
펜스 키 비용. 리프의 경우 예산은 new_fence_size = LEAF_FENCE_MAX_SIZE(max_key_len)+SPAGE_SLOT_SIZE를 양쪽 모두에서 미리 뺀다. 분할 시 왼쪽 리프에는 상단 펜스, 오른쪽에는 하단 펜스가 추가되기 때문이다(BTREE_LEAF_RECORD_FENCE로 표시된 분리자 복사본으로, 범위 스캔이 경계를 저렴하게 감지할 수 있게 한다 — Ch 7). 이 사전 공제로 펜스가 실체화될 때 노드가 넘치지 않는다.
분리자 선택. *mid_slot이 확정되면 mid_key(왼쪽 마지막)와 next_key(오른쪽 첫 번째)를 읽는다. 리프는 btree_get_prefix_separator로 가장 짧은 구분 접두사를 계산한다 — 단, 어느 쪽이든 오버플로 키이면 next_key 전체를 복사한다(접두사가 인페이지 최대치를 초과할 수 있기 때문이다). 비리프는 항상 next_key를 복사한다.
불변식 — “분리자는 엄격하게 분리한다.” 왼쪽 노드의 모든 키는
< separator ≤오른쪽 노드의 모든 키를 만족한다.btree_get_prefix_separator(mid_key, next_key)는> mid_key이고≤ next_key인 최솟값을 반환한다. 이 조건을 위반하면 검색이 잘못된 서브트리로 라우팅된다.
6.5 btree_split_node — 루트가 아닌 노드 Q를 Q와 R로 분할
섹션 제목: “6.5 btree_split_node — 루트가 아닌 노드 Q를 Q와 R로 분할”btree_split_node는 부모 P, 가득 찬 노드 Q, 새로 할당된 형제 R, P에서 Q를 가리키는 슬롯 번호, 그리고 하강 키를 인자로 받는다(세 페이지 모두 쓰기 래치 상태임이 앞에서 검증된다). Q는 [1..leftcnt] + 상단 펜스를 유지하고, R은 하단 펜스 + [leftcnt+1..key_cnt]를 받으며, P에는 sep → R 슬롯이 추가되고, (리프의 경우) Q.next → R로 재연결된다.
Figure 6-1 — btree_split_node 리프 체인 재연결
flowchart LR Q["Q: [1..leftcnt]"] R["R: [leftcnt+1..]"] N["old Q.next"] Q -->|"next_vpid"| R R -->|"next_vpid"| N R -->|"prev_vpid"| Q N -->|"prev_vpid (step 8)"| R
분기별 상세 진행:
- 분할점 탐색.
key_cnt = btree_node_number_of_keys(Q);≤ 0→goto exit_on_error.btree_find_split_point→sep_key,leftcnt;sep_key가 NULL/DB_NULL이면 로그 기록 후 오류 반환.rightcnt = key_cnt - leftcnt. - 펜스 레코드(리프 전용). 리프이고
sep_key_len < BTREE_MAX_KEYLEN_INPAGE && ≤ qheader->max_key_len이면,sep_key로부터 리프 레코드를 작성하고BTREE_LEAF_RECORD_FENCE를 설정하며flag_fence_insert = true. 오버플로 키 분리자이거나PRM_ID_USE_BTREE_FENCE_KEY = false이면flag_fence_insert = false로 강제된다. - Q undo 로그 및 헤더 설정.
log_append_undo_data2(RVBT_COPYPAGE, …)로 구 Q 전체를 바이트 단위로 스냅샷한다(바이트 단위 롤백용).right_next_vpid = qheader->next_vpid를 저장. 형제 링크는 리프 레벨에만 존재한다. 리프는qheader->next_vpid = *R_vpid,rheader->prev_vpid = *Q_vpid로 설정; 비리프 Q의next_vpid는 null(VPID_SET_NULL)로 처리하고 R의 링크 필드는btree_init_node_header기본값을 유지(R 헤더 redo 로그됨). R은 항상rheader->next_vpid = right_next_vpid를 상속한다. - 상위 절반 이동 및 Q 펜스 처리. R 슬롯 1에 선택적 하단 펜스를 추가한 뒤,
i = 1..rightcnt동안 이동 대상은leftcnt+1로 고정된다: Q의 슬롯leftcnt+1을 peek →spage_insert_at(R, j, …)→spage_delete(Q, leftcnt+1).flag_fence_insert가 참이면 Q의 상단 펜스를leftcnt+1에 삽입.btree_compress_node(Q)후 Q를 redo 로그(RVBT_COPYPAGE). - R redo 로그.
btree_compress_node(R)후 R을 redo 로그(RVBT_COPYPAGE). R에는 undo 로그가 없다 — 시스템 op에서 새로 할당되었으므로 중단 시 단순 해제된다. - 분리자를 P로 승격.
nleaf_rec.pnt = *R_vpid; 키가 너무 길면key_type = BTREE_OVERFLOW_KEY/key_len = -1, 그렇지 않으면BTREE_NORMAL_KEY.p_slot_id++(Q를 가리키는 슬롯 뒤에 삽입),spage_insert_at(P, p_slot_id, &rec), undo/redoRVBT_NDRECORD_INS. P 헤더를 undo 로그한 뒤btree_split_next_pivot(&pheader->split_info, (float) p_slot_id / key_cnt, key_cnt)로 착지 슬롯을 반영하고,max_key_len을 갱신하며, 헤더를 redo 로그한다. - 자식 선택.
btree_compare_key(key, sep_key, …):DB_UNK→ 단언 실패;< 0→*advance_vpid = *Q_vpid; 그 외 →*R_vpid. - 리프 이웃 재연결. R이 리프일 때만 실행: 구 Q 다음이었던 페이지의
prev_vpid를 R로 재지정한다(btree_get_next_page(R)+btree_set_vpid_previous_vpid— §6.7). exit_on_error에서sep_key를 해제하고 반환한다. 호출자의 시스템 op 안에서log_sysop_abort가 모든 페이지 변경을 되돌린다.
불변식 — “이중 연결 리프 체인은 일관성을 유지한다.” 리프 분할 후 네 링크가 모두 성립한다:
Q.next = R,R.prev = Q,R.next = old-next,old-next.prev = R(step 3이 처음 세 개를, step 8이 네 번째를 설정). step 8을 생략하면prev_vpid가 여전히 넘친 Q를 가리키는 리프가 남아 역방향 범위 스캔이 손상된다.
6.6 btree_split_root — 높이 증가 특수 경로
섹션 제목: “6.6 btree_split_root — 높이 증가 특수 경로”루트는 페이지 정체성을 유지해야 한다(인덱스 카탈로그에 VPID로 등록되어 있다). 따라서 형제 하나를 생성하는 방식으로는 분할할 수 없다. btree_split_root는 자식 Q와 R 두 개를 할당하고, 루트 P를 두 개의 분리자가 있는 상위 레벨 노드로 재작성한다(레벨 L → L+1, 슬롯 1 = neg-inf → Q, 슬롯 2 = sep → R). §6.5와의 차이점:
- 루트 전체 undo 로그(
RVBT_COPYPAGE);key_cnt읽기(≤0→ 오류);split_info스냅샷,split_info.index = 1로 리셋. - 분할점 및 펜스 탐색 — §6.5와 동일.
neg_inf_key = sep_key— 분리자를 왼쪽 포인터의 더미 음의 무한대 키로 재사용한다. - 레벨 증가.
pheader->node.node_level++,max_key_len갱신, 루트 split-info를 기본값으로 리셋. 자식들은node_level = new_level - 1을 받고max_key_len/split_info를 상속. 형제 링크 규칙은 §6.5 step 3과 동일(리프 전용). - R 채우기(상위 절반). 선택적 하단 펜스 추가 후,
rightcnt개 레코드를 이동(P의leftcnt+1을 peek/insert/delete 반복). R을 redo 로그RVBT_INS_PGRECORDS(R은 신규). - Q 채우기(하위 절반). P의 슬롯 1을 peek하고 삭제하는 방식으로
leftcnt개 레코드를 이동(P가 앞에서 줄어듦). 펜스를 원하면 Q의 상단 펜스를 추가하고, 그렇지 않으면i--. Q를 redo 로그RVBT_INS_PGRECORDS. - 루트 P 재구성. 전체 삭제 redo 로그(
RVBT_DEL_PGRECORDS,rec_cnt = key_cnt), 이후 슬롯 1 =neg_inf_key → Q_vpid, 슬롯 2 =sep_key → R_vpid작성(각각RVBT_NDRECORD_INS; 키가 너무 길면BTREE_OVERFLOW_KEY/key_len = -1). - 자식 선택. §6.5와 동일한
btree_compare_key3방향 분기:DB_UNK→ 오류;< 0→ Q; 그 외 → R. exit_on_error에서sep_key해제; 시스템 op가 중단되고 Q와 R이 해제되며,RVBT_COPYPAGEundo가 P를 복원한다.
불변식 — “루트는 페이지 정체성을 바꾸지 않고, 가장 왼쪽 분리자는 음의 무한대다.”
btree_split_root는 P에 새 VPID를 주지 않고 제자리에서 다시 쓰며, 슬롯 1에는 항상 더미neg_inf_key를 설치한다.btree_compare_key는 슬롯 1과는 비교하지 않는다 —< sep_key인 탐색은 무조건 Q로 향한다 — 그래서 가장 왼쪽 포인터가 실제 분리자보다 작은 모든 키를 담당한다. 페이지 복사가 아니라 전체 삭제 후 두 건 삽입이라서 redo를 거쳐도 P의 정체성이 유지된다.
로깅 비대칭: btree_split_node는 Q와 R에 전체 페이지 RVBT_COPYPAGE를 사용하고, btree_split_root는 레코드 집합 로깅(RVBT_INS_PGRECORDS / RVBT_DEL_PGRECORDS / RVBT_NDRECORD_INS)을 사용한다. 두 경우 모두 단일 시스템 op 브라켓을 공유하므로 중단 처리는 동일하다.
6.7 btree_set_vpid_previous_vpid — 역방향 링크 재작성
섹션 제목: “6.7 btree_set_vpid_previous_vpid — 역방향 링크 재작성”이 작은 함수는 자체 undo/redo를 갖추고 있어 분할 롤백 시 이웃 페이지의 prev_vpid가 복원된다.
// btree_set_vpid_previous_vpid -- src/storage/btree.cif (page_p == NULL) return NO_ERROR; /* <- chain tail, no-op */header = btree_get_node_header (thread_p, page_p);if (header == NULL) return ER_FAILED;btree_node_header_undo_log (thread_p, &btid->sys_btid->vfid, page_p);header->prev_vpid = *prev; /* <- repoint backward link, then redo-log */세 분기: NULL 페이지(체인 끝) → no-op 성공; 헤더 읽기 실패 → ER_FAILED; 그 외 → 전체 undo/redo와 pgbuf_set_dirty로 역방향 링크를 재작성. 리프 병합(Ch 9)에서도 재사용되므로 독립 함수로 분리되어 있다.
6.8 btree_split_node_and_advance — 전체 분기 지도
섹션 제목: “6.8 btree_split_node_and_advance — 전체 분기 지도”전진 함수는 need_split 블록 이상의 역할을 한다. node_type, need_split, need_update_max_key_len이 계산된(§6.1) 뒤, 반환 전에 다음 분기들을 통과한다. 아래에서 루트 재시작은 ER_PAGE_LATCH_PROMOTE_FAIL 발생 시 nonleaf_latch_mode = WRITE, *restart = true, 자식+부모 unfix, is_crt_node_write_latched = false, NO_ERROR 반환을 의미한다(다른 promote 오류 → goto error). 소스 순서 기준 아홉 분기:
- 루트 디스패치(
insert_helper->is_root): 루트 경로는 비루트 흐름과 병렬 블록으로 구성되지만(자체 시스템 op, 승격, 전진 분기), 분할은btree_split_root(페이지 두 개 신규, §6.6)를 사용한다. *crt_page승격(need_split && nonleaf_latch_mode == READ && !is_crt_node_write_latched): 부모의 읽기→쓰기 래치를 승격(PGBUF_PROMOTE_ONLY_READER); 실패 시 루트 재시작.- 자식 승격(
(need_split || need_update_max_key_len) && nonleaf_latch_mode == READ && !need_update_max_key_len && !is_child_leaf): 자식을 승격(PGBUF_PROMOTE_SHARED_READER); 실패 시 루트 재시작. need_update_max_key_len(규칙parent->max_key_len >= child->max_key_len위반): 자식max_key_len = key_len_in_page로 설정,btree_node_header_redo_log+pgbuf_set_dirty,need_update_max_key_len = true전파,is_crt_node_write_latched = true로 설정하여 모든 하위 노드도 갱신하게 한다.- 분할 + 자식으로 전진(
need_split,VPID_EQ(&advance_vpid, &child_vpid)):new_page1unfix,log_sysop_commit,*advance_to_page = child_page,is_crt_node_write_latched = true, 반환. - 분할 + 새 페이지로 전진(
need_split,VPID_EQ(&advance_vpid, &new_page_vpid1)):child_pageunfix,log_sysop_commit,*advance_to_page = new_page1,is_crt_node_write_latched = true, 반환. - 분할 + 불가능(
need_split, VPID 모두 불일치):assert_release(false), 세 페이지 모두 unfix,error_code = ER_FAILED,goto error. - 비분할 폴스루(
!need_split && !need_update_max_key_len && nonleaf_latch_mode == READ): 읽기 래치 상태로 수정 없음.is_crt_node_write_latched = false,*advance_to_page = child_page, 반환. error:레이블(모든goto error):new_page1/new_page2unfix 먼저, 이후 시스템 op가 열려 있으면log_sysop_abort, 마지막으로child_pageunfix.
두 승격 조기 반환이 루트에서 재시작하는 유일한 경로이며, 재시작 원인은 래치 경쟁이고 절대로 용량이 아니다. 분할 브라켓은 need_split 블록 안에서 log_sysop_start(is_system_op_started = true)로 열리고, 전진 분기별 log_sysop_commit 또는 error: 레이블로 닫힌다.
// btree_split_node_and_advance -- src/storage/btree.c (error: label)if (new_page1 != NULL) pgbuf_unfix_and_init (thread_p, new_page1);if (new_page2 != NULL) pgbuf_unfix_and_init (thread_p, new_page2);if (is_system_op_started) log_sysop_abort (thread_p); /* <- unfix new pages FIRST so abort can deallocate them */if (child_page != NULL) pgbuf_unfix_and_init (thread_p, child_page);순서가 핵심이다. log_sysop_abort 이전에 새 페이지를 먼저 unfix해야 한다 — 중단 시 새 페이지가 해제되는데, fix count가 0이어야 해제가 가능하기 때문이다.
6.9 챕터 요약 — 핵심 정리
섹션 제목: “6.9 챕터 요약 — 핵심 정리”- 비관적 하강 중 분할은 최악 경우 항목을 담지 못할 가능성이 있는 노드를 전진 전에 분할하므로, 리프는 항상 공간을 보장받으며 연쇄 분할은 구조적으로 불가능하다.
btree_get_max_new_data_size는 이 보장의 배후 추정기로, 노드 타입과 삽입 목적에 따라 분기한다. 하강은known_to_be_found = false를 전달하여 신규 키의 전체 비용을 예약한다.btree_node_split_info{pivot, index} 는 CMA 누산기다.btree_split_find_pivot이 이를 목표 바이트 크기로 변환하고([0.20,0.80]구간에서는 균형,[0.05,0.95]로 클램핑),btree_split_next_pivot이 각 분리자의 착지 슬롯으로 갱신한다.btree_find_split_point는 크기 균형 방식이다. 양쪽 모두 신규 항목과 펜스 공간을 예약하고, 각 사이드에 비펜스 레코드 ≥1개를 유지하며, 접두사 단축 분리자를 반환한다.btree_split_node는 Q 전체를 undo 로그(RVBT_COPYPAGE)하고, 상위 절반을 R로 이동하고, 펜스를 작성하며, 양쪽 모두 redo 로그하고, 분리자를 P로 승격(RVBT_NDRECORD_INS)하며, 리프 체인을 재연결한다(§6.7).btree_split_root는 높이를 제자리에서 증가시킨다. 자식 두 개, 슬롯 1에 더미 neg-inf 분리자와 슬롯 2에 실제 분리자,node_level증가, 레코드 집합 redo 방식을 사용한다.btree_split_node_and_advance는 아홉 분기 항목에 걸쳐 여덟 개의 활성 종료 경로를 갖는다(error:레이블은 공유 중단 싱크). 두 래치 승격 루트 재시작, max-key-len 전파, 두 분할 커밋,assert_release(false)불가능 경로, 비분할 읽기 래치 폴스루 — 모두 단일 시스템 op 브라켓 안에 있으며,error:레이블은log_sysop_abort전에 새 페이지를 먼저 unfix한다.
Chapter 7: 범위 스캔과 LSA 기반 재개
섹션 제목: “Chapter 7: 범위 스캔과 LSA 기반 재개”이 챕터는 읽기 경로의 핵심 질문에 답한다. 데이터가 디스크에 있을 때, 범위 스캔은 어떻게 리프와 오버플로 페이지를 키 단위로 순회하며, 클라이언트 왕복 사이에 모든 래치를 해제한 뒤 안전하게 재개하는가? 답은 단일 범용 드라이버 btree_range_scan이며, 키 처리 콜백(BTREE_RANGE_SCAN_PROCESS_KEY_FUNC)으로 매개변수화된다. 드라이버는 위치 결정·래치·전진·페이지 LSA 기반 재개를 담당하고, 콜백은 키별 작업(SELECT용 가시 OID 수집, FK 검사용 존재 확인)을 담당한다. 영속 커서 상태는 모두 BTREE_SCAN(BTS) 하나에 담기므로, 스캔은 반복 사이에 모든 래치를 내려놓고 이전 위치에서 재개할 수 있다. 래치 커플링 강하(descent)에 대해서는 cubrid-btree.md §“Descent — latch-coupling on read”를 참고하라.
7.1 커서 구조체 — btree_scan과 btree_keyrange
섹션 제목: “7.1 커서 구조체 — btree_scan과 btree_keyrange”btree_keyrange(src/storage/btree.h)는 “무엇을 스캔할지” 기술자이고, btree_scan(BTS)은 “지금 어디에 있는지” 전체 상태를 담는 대형 커서다. 헤더에 /* TO BE REMOVED */로 표기된 필드는 레거시 전용이며 legacy로 표기한다.
btree_keyrange 필드 | 역할 | 존재 이유 |
|---|---|---|
range | RANGE enum — 각 끝점을 열림/닫힘/무한으로 표현 | 모든 경계 조합을 담는다; 위치 결정 분기가 이 값만을 기준으로 선택된다 |
lower_key | 하한 DB_VALUE, 또는 NULL | NULL = 하한 없음; btree_range_scan_start가 이 값의 null 여부로 분기 |
upper_key | 상한 DB_VALUE, 또는 NULL | NULL = 상한 없음; 범위 검사가 상단에서 중단되지 않는다 |
num_index_term | 바인드된 선행 인덱스 컬럼 수 | MIDXKEY가 접두어 기준으로 범위를 지정할 때 비교기가 참조할 컬럼 수를 알려준다 |
아래 표는 모든 btree_scan 멤버의 기준 설명이다.
| 필드 | 역할 | 존재 이유 |
|---|---|---|
btid_int | 리졸브된 트리 id + 키 도메인 | 키마다 루트 헤더를 다시 읽지 않아도 된다 (Ch 1) |
read_uncommitted | 레거시 dirty-read 플래그 | legacy — btree_prepare_next_search와 함께 사라질 예정 |
P_vpid/P_page | 이전 리프 vpid/ptr | legacy — btree_find_next_index_record와 함께 사라질 예정 |
C_vpid | 현재 리프 VPID | unfix 후에도 유지 — 재개 시 재고정의 앵커 |
O_vpid | 현재 오버플로 VPID | 키의 OID가 하나의 버퍼를 초과할 때 중간 재개 지점 |
C_page | 현재 리프 포인터 | 래치를 보유하지 않을 때 NULL; 순회 중에만 설정 |
slot_id | C_page의 현재 슬롯 | 래치 재획득 후 재검증 |
oid_pos | 레거시 OID 커서 | legacy — 콜백 OID 순회로 대체됨 |
cur_key | 커서가 파킹된 키 | slot_id를 신뢰할 수 없을 때 재개 앵커 |
clear_cur_key | cur_key 소유권 플래그 | btree_scan_clear_key가 해제 여부를 결정 |
cur_common_prefix_page_vpid/_lsa | 공통 접두어가 캐시된 페이지의 VPID+LSA (디버그 빌드 전용) | CHECK_VERIFY_COMMON_PREFIX_PAGE_INFO 하에서만 존재; 캐시된 접두어가 현재 페이지에 여전히 속하는지 단언 |
common_prefix_size | 페이지 공통 접두어 길이 | 계산 전까지는 COMMON_PREFIX_UNKNOWN |
common_prefix_key | 페이지의 공유 접두어 바이트 | 압축된 cur_key와 결합하여 전체 키를 복원 |
clear_common_prefix_key | 접두어 소유권 플래그 | 페이지가 바뀔 때 접두어 복사본을 해제 |
is_cur_key_compressed | cur_key가 접두어 제거된 상태 | true이면 btree_check_decompress_key가 unfix 전에 접두어를 재붙임 |
attid_idxs | attr-id → 컬럼 위치 맵 | attr-id로 참조 기저 컬럼을 필터링 |
is_cur_key_copied | cur_key가 힙 복사본을 소유 | C_page unfix 후에도 복사본이 살아남는다; §7.8에서 설정 |
key_range/key_filter | 스캔 범위 / 키별 술어 | 범위 위치 결정; 필터가 범위 내 실패를 걸러냄 |
key_filter_storage | 뒷받침 FILTER_INFO | 구조체를 소유하여 key_filter가 반복 전반에 걸쳐 안정적으로 유지 |
use_desc_index | 내림차순 스캔 플래그 | prev_vpid + 조건부 이전 리프 고정 경로를 선택 |
restart_scan | 레거시 재시작 카운터 | legacy |
read_keys/qualified_keys | 쿼리 추적 카운터 | §7.7에서 증가; EXPLAIN/trace 통계에 사용 |
key_range_max_value_equal | 상한이 정확히 일치 | 다음 전진 시 또 다른 키를 읽지 않고 스캔을 종료 |
cur_leaf_lsa | 마지막 unfix 시 C_page의 LSA | 재개 시 재사용 vs. 재위치 결정 |
lock_mode | S_LOCK 또는 X_LOCK | 스캔의 격리 요구에서 결정 |
key_record | 피크된 키의 RECDES | OID 순회 콜백에서 슬롯을 다시 읽지 않도록 |
need_to_check_null | 범위가 SQL NULL과 일치 가능 | 범위 검증의 NULL 분기를 토글 |
leaf_rec_info | 디코딩된 LEAF_REC | .ovfl이 키의 OID 오버플로 체인 첫 번째 페이지 |
node_type | 항상 BTREE_LEAF_NODE | 커서가 비리프에 파킹되지 않음을 단언 |
offset | 첫 번째 OID까지의 바이트 오프셋 | key_record에서 OID 처리를 시작하는 위치 |
key_status | NOT_VERIFIED / VERIFIED / CONSUMED | 키별 상태 기계 (§7.2) |
end_scan | 전체 범위 소진 | 드라이버 종료; C_vpid 리셋, 키 클리어 |
end_one_iteration | OID 버퍼 가득 참 | 부분 결과 반환, C_vpid/cur_leaf_lsa 유지 |
is_interrupted | 콜백이 키 중간에 반환 | 전진 없이 내부 루프 탈출 |
is_key_partially_processed | 키의 OID가 반복에 걸쳐 분할됨 | 재개 시 리프가 아닌 O_vpid부터 콜백 재진입 |
n_oids_read/..._last_iteration | 누적 / 반복별 카운트 | 반복별 값이 btree_keyval_search의 반환값; key_limit_upper에서 차감 |
oid_ptr | 호출자 버퍼로의 쓰기 헤드 | 반복마다 oid_list->oidp로 리셋 |
match_class_oid | 계층적 유니크용 클래스 필터 | 파티션 계층에서 다른 클래스의 OID를 건너뜀 |
key_limit_lower/_upper | LIMIT/OFFSET 푸시다운 | _upper는 종료 시 반복별 카운트로 감소 |
index_scan_idp | 소유 인덱스 스캔 | OID 버퍼·스냅샷·최적화 모드(ISS/MRO/커버링)의 원천 |
is_btid_int_valid | btid_int가 리졸브됨 | 첫 사용 시 재리졸브를 막는 가드 |
is_scan_started | 시작 vs. 재개 선택자 | 시작 성공 전까지 false; 이후 재개 경로 사용 |
force_restart_from_root | 내림차순 빠른 경로 실패 | 다음 재개 시 전체 재위치 강제 |
is_fk_remake | 중복 제거 모드 FK 재시도 | 재구성된 키로 FK 존재 재시도 |
time_track | 타이밍 | PSTAT_BT_* 에 기여 |
bts_other | 목적별 추가 상태 | FK: BTREE_FIND_FK_OBJECT 결과 구조체를 가리킴 |
7.2 key_status 상태 기계
섹션 제목: “7.2 key_status 상태 기계”BTS_KEY_STATUS(bts_key_status, src/storage/btree.h)는 세 값을 갖는다. BTS_KEY_IS_NOT_VERIFIED(슬롯에 도달했으나 범위/필터 미검증), BTS_KEY_IS_VERIFIED(범위와 필터 모두 통과, 처리 준비 완료), BTS_KEY_IS_CONSUMED(완전히 처리됨, 다음 슬롯으로 전진).
stateDiagram-v2 [*] --> NOT_VERIFIED : 슬롯에 위치 NOT_VERIFIED --> VERIFIED : 범위 ok, 필터 ok NOT_VERIFIED --> NOT_VERIFIED : 펜스 또는 필터 실패\n슬롯 전진 VERIFIED --> CONSUMED : key_func가 모든 OID 읽음 CONSUMED --> NOT_VERIFIED : 다음 슬롯으로 전진 VERIFIED --> VERIFIED : 재개 시 동일 레코드 재읽기 CONSUMED --> [*] : 범위 소진\nend_scan
Figure 7-1. key_status 전이. advance_over_filtered_keys가 NOT_VERIFIED에서 VERIFIED로 전환하고, 콜백이 VERIFIED에서 CONSUMED로 전환한다.
핵심 불변식: 드라이버는 key_status == BTS_KEY_IS_VERIFIED일 때만 key_func를 호출한다. btree_range_scan의 콜백 직전에 단언된다. 콜백에 이르는 모든 경로는 먼저 btree_range_scan_advance_over_filtered_keys를 거치며, 이 함수는 범위와 필터를 모두 통과한 뒤에만 VERIFIED로 설정한다. 이 규칙을 어기면 콜백이 펜스 키나 범위 밖 키에 대한 OID를 내보낼 수 있고, 결과 집합에 속하지 않는 행이 섞여 들어온다.
7.3 단일 키 래퍼 — btree_keyval_search와 btree_scan_update_range
섹션 제목: “7.3 단일 키 래퍼 — btree_keyval_search와 btree_scan_update_range”포인트 조회(WHERE k = 5)는 퇴화된 범위 검색이다.
// btree_keyval_search — src/storage/btree.crc = btree_prepare_bts (thread_p, bts, btid, isidp, kv_range, filter, ...);rc = btree_range_scan (thread_p, bts, btree_range_scan_select_visible_oids);return bts->n_oids_read_last_iteration; /* <- OID count, iterative */“같은 키로 GE_LE 범위 검색을 하는 것”에 불과하다. n_oids_read_last_iteration을 반환함으로써 호출자가 !BTREE_END_OF_SCAN(bts) 동안 루프를 돌며, 버퍼 가득 참을 여러 번 거쳐 OID가 분산된 키를 완전히 소진할 수 있다. btree_scan_update_range는 기존 BTS에 새 범위를 설치한다(다중 범위 최적화). range를 검증하고, SQL NULL 경계를 null 처리하며, 내림차순 스캔일 때 경계를 교환한다.
// btree_scan_update_range — src/storage/btree.cif ((bts->use_desc_index && !BTREE_IS_PART_KEY_DESC (&bts->btid_int)) || (!bts->use_desc_index && BTREE_IS_PART_KEY_DESC (&bts->btid_int))) { /* XOR: scan-desc != column-desc */ range_reverse (bts->key_range.range); /* GE_LE becomes LE_GE etc. */ swap_key = bts->key_range.lower_key; /* <- swap bounds */ bts->key_range.lower_key = bts->key_range.upper_key; bts->key_range.upper_key = swap_key; }교환은 “스캔 내림차순”과 “컬럼 내림차순 저장” 중 정확히 하나만 성립할 때 발동한다. 둘 다 성립하면 물리적 순회가 이미 순방향이므로 교환이 필요 없다.
7.4 스캔 시작 — btree_range_scan_start
섹션 제목: “7.4 스캔 시작 — btree_range_scan_start”is_scan_started == false 상태에서만 호출된다. 첫 번째 유효 키에 위치를 잡는다. /* (X) */ 마커는 분기 지도다.
// btree_range_scan_start — src/storage/btree.cbts->key_status = BTS_KEY_IS_NOT_VERIFIED;if (bts->key_range.lower_key == NULL) { /* (A) no lower bound: jump to lowest leaf */ btree_find_lower_bound_leaf (thread_p, bts, NULL); if (bts->end_scan) { return NO_ERROR; } /* empty index */ }else { /* (B) lower bound: descend to leaf */ btree_locate_key (..., bts->key_range.lower_key, ..., &found); if (!found) { if (bts->use_desc_index) { bts->slot_id--; } } /* (B1) desc: first-smaller */ else if (bts->key_range.range == GT_LT || bts->key_range.range == GT_LE || bts->key_range.range == GT_INF) { bts->key_status = BTS_KEY_IS_CONSUMED; } /* (B2) strict GT_*: skip exact */ }btree_range_scan_advance_over_filtered_keys (thread_p, bts);if (bts->force_restart_from_root) { /* (C) desc couldn't position: restart */ assert (bts->use_desc_index); bts->key_status = BTS_KEY_IS_NOT_VERIFIED; return NO_ERROR; /* is_scan_started stays false -> driver loops */ }bts->is_scan_started = true; /* (D) success */return NO_ERROR;네 가지 결과 모두 advance_over_filtered_keys로 합류한다. (C) 만이 is_scan_started를 false로 남긴다 — 이전 리프 고정 경로에서 진전 없이 끝난 내림차순 시작으로, 드라이버가 다시 시도한다.
7.5 범용 드라이버 — btree_range_scan
섹션 제목: “7.5 범용 드라이버 — btree_range_scan”외부 루프는 end_scan(범위 완료) 또는 end_one_iteration(버퍼 가득 참, 클라이언트 반환)까지 실행된다. 내부 루프는 키를 소진하며, VERIFIED 키마다 콜백을 호출한다.
flowchart TB
START["reset end_scan/end_one_iteration\nn_oids_read_last_iteration=0\noid_ptr=oid_list.oidp"] --> OUTER{"!end_scan and\n!end_one_iteration?"}
OUTER -->|no| EXIT["save cur_leaf_lsa, unfix pages\ndebit key_limit_upper"]
OUTER -->|yes| POS{"is_scan_started?"}
POS -->|no| STARTF["btree_range_scan_start"]
POS -->|yes| RESUME["btree_range_scan_resume"]
STARTF --> CHK
RESUME --> CHK{"end_scan?"}
CHK -->|yes| EXIT
CHK -->|no| FRR{"force_restart_from_root?"}
FRR -->|yes| UNFIXC["unfix C_page, continue outer"]
UNFIXC --> OUTER
FRR -->|no| INNER["INNER: key_func(bts)"]
INNER --> ICHK{"interrupted /\nend_one_iteration /\nend_scan?"}
ICHK -->|yes| EXIT
ICHK -->|no| ADV["advance_over_filtered_keys"]
ADV --> ACHK{"end_scan?"}
ACHK -->|yes| EXIT
ACHK -->|no| AFRR{"force_restart_from_root?"}
AFRR -->|yes| UNFIXC
AFRR -->|no| INNER
Figure 7-2. btree_range_scan 제어 흐름. 외부 루프는 시작과 재개 중 하나를 선택하고, 내부 루프는 key_func를 호출한 뒤 전진한다. force_restart_from_root 경로는 양쪽 모두 C_page를 unfix하고 외부 루프로 재진입한다.
// btree_range_scan — src/storage/btree.cwhile (true) { assert (bts->key_status == BTS_KEY_IS_VERIFIED); /* invariant 7.2 */ error_code = key_func (thread_p, bts); /* <- the callback */ if (bts->is_interrupted || bts->end_one_iteration || bts->end_scan) { break; } assert (bts->key_status == BTS_KEY_IS_CONSUMED); btree_range_scan_advance_over_filtered_keys (thread_p, bts); if (bts->force_restart_from_root) { /* unfix C_page, decompress cur_key */ break; } }end: 레이블에서 드라이버는 반드시, 순서대로 다음을 수행한다. unfix 전에 cur_leaf_lsa = pgbuf_get_lsa(C_page)를 캡처하고; end_one_iteration이면 cur_key를 압축 해제하고; C_page/P_page를 unfix하며; end_scan이면 C_vpid를 null로 만들고 is_scan_started와 키를 클리어하며; key_limit_upper에서 n_oids_read_last_iteration을 차감한다.
불변식 —
cur_leaf_lsa는C_page를 unfix하는 그 순간 캡처된다. LSA를 먼저 복사하지 않고 unfix하면, 이후 재개 시 낡은 LSA로 “변경 없음”이라고 잘못 판단해 이미 다른 키를 가리키는slot_id를 그대로 재사용하게 된다.end:블록이 복사-후-unfix 순서를 보장하며, 페이지가 없는 경로에서는cur_leaf_lsa를 null로 만들어 재개 시 반드시 루트부터 재위치를 강제한다.
7.6 재개 — btree_range_scan_resume과 LSA 결정
섹션 제목: “7.6 재개 — btree_range_scan_resume과 LSA 결정”래치를 계속 보유하지 않고 위치를 재확립한다. 결정은 cur_leaf_lsa에 달려 있으며, /* (X) */는 분기 지도다.
// btree_range_scan_resume — src/storage/btree.cif (!bts->force_restart_from_root) { pgbuf_fix_if_not_deallocated (..., &bts->C_vpid, ..., &bts->C_page); if (bts->C_page != NULL) { if (LSA_EQ (&bts->cur_leaf_lsa, pgbuf_get_lsa (bts->C_page))) { return btree_range_scan_advance_over_filtered_keys (...); } /* (1) UNCHANGED */ if (BTREE_IS_PAGE_VALID_LEAF (thread_p, bts->C_page)) { /* (2) still a leaf but changed */ btree_leaf_is_key_between_min_max (..., &bts->cur_key, &search_key); if (search_key.result == BTREE_KEY_BETWEEN) { btree_search_leaf_page (..., &bts->cur_key, &search_key); } switch (search_key.result) { case BTREE_KEY_FOUND: /* (2a) key still here: reset slot */ bts->slot_id = search_key.slotid; return btree_range_scan_advance_over_filtered_keys (...); case BTREE_KEY_BETWEEN: /* (2b) key deleted, neighbor here */ bts->slot_id = bts->use_desc_index ? search_key.slotid - 1 : search_key.slotid; bts->key_status = BTS_KEY_IS_NOT_VERIFIED; return btree_range_scan_advance_over_filtered_keys (...); case BTREE_KEY_SMALLER: case BTREE_KEY_BIGGER: case BTREE_KEY_NOTFOUND: break; /* (2c) migrated off page: re-locate */ } } pgbuf_unfix_and_init (thread_p, bts->C_page); /* (3) reused as non-leaf */ } /* (4) C_page deallocated */ }bts->force_restart_from_root = false;btree_locate_key (..., &bts->cur_key, ..., &found);if (found) { return btree_range_scan_advance_over_filtered_keys (...); }if (btree_node_number_of_keys (thread_p, bts->C_page) < 1) { bts->end_scan = true; return NO_ERROR; } /* (5) index emptied under us */if (bts->use_desc_index) { bts->slot_id--; }bts->key_status = BTS_KEY_IS_NOT_VERIFIED;return btree_range_scan_advance_over_filtered_keys (...);(1) 빠른 경로 — 리프가 바이트 단위로 그대로이므로 slot_id/key_status를 그대로 사용한다. (2a)/(2b) 변경됐지만 여전히 리프인 페이지에서 cur_key를 검색해 슬롯을 재도출한다. (2c)/(3)/(4)/(5) btree_locate_key로 루트부터 재강하하여 전진하거나, 종료하거나, 삽입 위치에 파킹한다.
불변식 — 재개는
slot_id만으로가 아니라cur_key를 기준으로 앵커를 다시 잡는다.slot_id는 LSA가 변경 없음을 증명한 분기 (1)에서만 신뢰된다. 그 외에서는cur_key를 검색해 슬롯을 재도출한다. 낡은 슬롯을 그대로 쓰면 동시적 분할/병합으로 슬롯 번호가 밀릴 때 키를 건너뛰거나 중복 방출하는 일이 생긴다.
7.7 펜스 키와 필터 실패 키 건너뛰기 — btree_range_scan_advance_over_filtered_keys
섹션 제목: “7.7 펜스 키와 필터 실패 키 건너뛰기 — btree_range_scan_advance_over_filtered_keys”“어딘가에 위치한 상태”를 “다음 VERIFIED 키에 위치한 상태, 또는 end_scan”으로 전환한다. 세 단계로 나뉜다.
재개-VERIFIED 조기 반환 — 이미 VERIFIED이면(페이지 재사용, 재검증 없이 재고정) 다시 피크하고 반환한다. 그렇지 않으면 방향을 설정하고 CONSUMED 슬롯을 한 칸 전진시킨다.
// btree_range_scan_advance_over_filtered_keys — src/storage/btree.cif (bts->key_status == BTS_KEY_IS_VERIFIED) { spage_get_record (..., bts->slot_id, &bts->key_record, PEEK); btree_range_scan_read_record (thread_p, bts); /* page may have moved bytes */ return NO_ERROR; /* keep current key */ }if (bts->key_range_max_value_equal) { bts->end_scan = true; return NO_ERROR; } /* exact upper bound returned */inc_slot = bts->use_desc_index ? -1 : 1;if (bts->key_status == BTS_KEY_IS_CONSUMED) { bts->slot_id += inc_slot; }next_vpid = bts->use_desc_index ? node_header->prev_vpid : node_header->next_vpid;페이지 경계 루프 — slot_id가 페이지를 벗어나면 다음 리프를 고정한다. VPID_ISNULL이면 스캔을 종료한다. 오름차순은 직접 래치 커플링하고, 내림차순은 §7.8에 위임한다.
while (bts->slot_id <= 0 || bts->slot_id > key_count || key_count == 0) { if (VPID_ISNULL (&next_vpid)) { bts->end_scan = true; return NO_ERROR; } /* consumed */ if (bts->use_desc_index) { btree_range_scan_descending_fix_prev_leaf (..., &key_count, &node_header, &next_vpid); if (bts->force_restart_from_root) { return NO_ERROR; } } else { next_node_page = pgbuf_fix (..., &next_vpid, ...); pgbuf_unfix (thread_p, bts->C_page); /* latch-couple to next */ bts->C_page = next_node_page; VPID_COPY (&bts->C_vpid, &next_vpid); bts->slot_id = 1; next_vpid = node_header->next_vpid; } }키별 검증 — 피크하고; 펜스는 건너뛰며; 아니면 읽어서 카운트하고 범위+필터를 적용한다.
spage_get_record (..., bts->slot_id, &bts->key_record, PEEK);if (!btree_leaf_is_flaged (&bts->key_record, BTREE_LEAF_RECORD_FENCE)) { btree_range_scan_read_record (thread_p, bts); bts->read_keys++; btree_apply_key_range_and_filter (..., &is_range_satisfied, &is_filter_satisfied); if (!is_range_satisfied) { bts->end_scan = true; return NO_ERROR; } /* past range */ if (is_filter_satisfied) { bts->qualified_keys++; bts->key_status = BTS_KEY_IS_VERIFIED; /* <- only success exit */ return NO_ERROR; } /* filter failed: fall through */ }bts->slot_id += inc_slot; /* fence/filter-fail: try next slot */펜스 키는 페이지 경계 센티널로(cubrid-btree.md §“Node layout” 참고), 묵묵히 건너뛴다. 범위 검사 실패는 스캔을 종료하고, 필터 실패는 전진 후 루프를 계속한다. 내림차순 페이지 경계 이동은 §7.8에서 force_restart_from_root를 설정할 수 있다.
7.8 레코드 읽기와 내림차순 이전 리프 고정
섹션 제목: “7.8 레코드 읽기와 내림차순 이전 리프 고정”btree_range_scan_read_record는 피크된 슬롯을 cur_key로 디코딩한다. 가변 길이 텍스트/비트/midxkey는 PEEK_KEY_VALUE를 사용하며(압축 해제 시 할당이 발생할 수 있고 is_cur_key_copied로 추적된다), 고정 길이 타입은 COPY_KEY_VALUE를 사용한다.
// btree_range_scan_read_record — src/storage/btree.cswitch (TP_DOMAIN_TYPE (bts->btid_int.key_type)) { case DB_TYPE_MIDXKEY: case DB_TYPE_VARCHAR: case DB_TYPE_CHAR: case DB_TYPE_BIT: case DB_TYPE_VARBIT: { int ret = btree_read_record_in_leafpage (thread_p, bts->C_page, PEEK_KEY_VALUE, bts); bts->is_cur_key_copied = bts->cur_key.need_clear; /* compressed -> copied */ return ret; } default: break; }bts->is_cur_key_copied = true;return btree_read_record_in_leafpage (thread_p, bts->C_page, COPY_KEY_VALUE, bts);복사되거나 압축 해제된 키는 C_page unfix 후에도 살아남는다. 그렇기 때문에 드라이버는 end_one_iteration 시 unfix 전에 btree_check_decompress_key를 실행한다. 재개 시 완전한 cur_key로 페이지를 검색해야 하기 때문이다.
btree_range_scan_descending_fix_prev_leaf는 내림차순 스캔이 순방향 형제 링크를 거슬러 올라가야 하기 때문에 존재한다. 조건 없는 래치를 사용하면 순방향 스캔과 교착 상태를 일으킨다. 이 함수는 이전 리프에 조건부 래치를 시도하고, 실패하면 현재 래치를 해제한 뒤 안전한 순서로 재획득한다.
// btree_range_scan_descending_fix_prev_leaf — src/storage/btree.cprev_leaf = pgbuf_fix (..., &prev_leaf_vpid, ..., PGBUF_CONDITIONAL_LATCH);if (prev_leaf != NULL) { /* (A) conditional latch won: common case */ pgbuf_unfix_and_init (thread_p, bts->C_page); bts->C_page = prev_leaf; bts->slot_id = *key_count = btree_node_number_of_keys (...); /* last key of prev */ VPID_COPY (next_vpid, &(*node_header_ptr)->prev_vpid); return NO_ERROR; }/* (B) conditional failed: drop current, re-fix prev unconditionally */btree_check_decompress_key (bts); /* cur_key must survive the unfix */pgbuf_unfix_and_init (thread_p, bts->C_page);pgbuf_fix_if_not_deallocated (..., &prev_leaf_vpid, ..., &prev_leaf);if (prev_leaf == NULL) { bts->force_restart_from_root = true; return NO_ERROR; } /* (B1) freed */if (!BTREE_IS_PAGE_VALID_LEAF (thread_p, prev_leaf)) { bts->force_restart_from_root = true; ...; return NO_ERROR; } /* (B2) reused non-leaf */*node_header_ptr = btree_get_node_header (thread_p, prev_leaf);if (!VPID_EQ (&(*node_header_ptr)->next_vpid, &bts->C_vpid)) { bts->force_restart_from_root = true; ...; return NO_ERROR; } /* (B3) relinked */(A) 대기 없이 성공하여 이전 페이지의 마지막 키에 위치한다. (B) 현재 래치를 해제한 뒤(미리 cur_key 압축 해제) 무조건부로 재고정한다. 래치 없는 상태에서 발생할 수 있는 세 가지 위험 상황 각각이 루트 재시작을 강제한다 — (B1) 페이지 해제, (B2) 비리프로 재사용, (B3) 링크 재연결.
불변식 — 내림차순 전진은 순방향 링크 방향으로 두 리프 래치를 동시에 보유하지 않는다. 이전 페이지에 조건부 래치를 시도하거나, 실패 시 현재 페이지 래치를 먼저 해제한 뒤 대기한다. 이것이 내림차순 스캔이
next_vpid순서로 래치를 잡는 오름차순 스캔·쓰기와 교착 상태를 일으키지 않는 규칙이다. 안전하게 진행할 수 없을 때는force_restart_from_root를 설정하고 루트부터 재강하한다 — 느리지만 안전하다.
7.9 SELECT 콜백 — btree_range_scan_select_visible_oids
섹션 제목: “7.9 SELECT 콜백 — btree_range_scan_select_visible_oids”SELECT용 BTREE_RANGE_SCAN_PROCESS_KEY_FUNC다. 현재 VERIFIED 키의 OID 목록(리프 인라인 OID, 이후 오버플로 체인)을 순회하며 MVCC 가시 OID만 호출자 버퍼에 복사한다. 키의 OID 목록이 버퍼를 초과할 수 있으므로, O_vpid와 is_key_partially_processed로 키 중간 재개를 지원한다.
flowchart TB
ENTER["key_func 진입 (VERIFIED 키)"] --> ISS{"ISS distinct-key op?"}
ISS -->|yes| EOUT["키 설정, n=1, end_scan"]
ISS -->|no| PART{"is_key_partially_processed?"}
PART -->|yes| RESOVF["O_vpid 재고정\n다음 오버플로 vpid 획득"]
PART -->|no| SOFT{"소프트 캡 초과\n리프+1ovf 기준?"}
SOFT -->|yes| ONEIT["end_one_iteration"]
SOFT -->|no| LEAF["리프 OID 처리"]
LEAF --> OVF["오버플로 루프"]
RESOVF --> OVF
OVF --> HARD{"하드 캡 초과\n이 ovf 페이지?"}
HARD -->|yes| SAVE["O_vpid=last_visible\npartial=true, end_one_iteration"]
HARD -->|no| PROC["페이지 OID 처리"]
PROC --> MORE{"오버플로 페이지 더 있음?"}
MORE -->|yes| OVF
MORE -->|no| DONE["O_vpid=NULL\nkey_status=CONSUMED"]
Figure 7-3. btree_range_scan_select_visible_oids. 소프트 캡은 버퍼가 거의 찰 때 키 앞에서 멈추며(end_one_iteration), 하드 캡은 키의 오버플로 체인 내부에서 일시 중단하고 다음 반복의 키 중간 재개를 위해 O_vpid를 저장한다.
리프와 오버플로 OID는 btree_record_process_objects가 btree_select_visible_object_for_range_scan으로 처리한다. 이 함수는 MVCC 헤더를 구성하고, 스냅샷을 확인하며, match_class_oid와 키 제한 필터를 적용하고, BTS_SAVE_OID_IN_BUFFER로 가시 OID를 복사한다.
7.10 FK 콜백 — btree_range_scan_find_fk_any_object
섹션 제목: “7.10 FK 콜백 — btree_range_scan_find_fk_any_object”FK 검사는 참조된 행이 하나라도 존재하는지만 알면 된다. 따라서 FK 콜백은 첫 번째 적격 객체에서 멈추며, 결과는 bts->bts_other(BTREE_FIND_FK_OBJECT)에 저장한다.
// btree_range_scan_find_fk_any_object — src/storage/btree.cfk_arg.bts = bts; fk_arg.ovfl_page = NULL;btree_record_process_objects (..., BTREE_LEAF_NODE, ..., &stop, btree_fk_object_does_exist, &fk_arg); /* leaf */if (stop == true) { return NO_ERROR; } /* found -> done, no overflow */VPID_COPY (&ovf_vpid, &bts->leaf_rec_info.ovfl);while (!VPID_ISNULL (&ovf_vpid)) { /* hand-over-hand chain walk */ fk_arg.ovfl_page = pgbuf_fix (..., &ovf_vpid, ...); btree_record_process_objects (..., BTREE_OVERFLOW_NODE, ..., btree_fk_object_does_exist, &fk_arg); if (stop) { break; } /* found -> done */ btree_get_next_overflow_vpid (thread_p, fk_arg.ovfl_page, &ovf_vpid); }if (bts->end_scan) { return NO_ERROR; }if (!bts->is_interrupted) { /* fully scanned, none found */ assert (OID_ISNULL (&((BTREE_FIND_FK_OBJECT *) bts->bts_other)->found_oid)); if (bts->is_fk_remake) { /* deduplicate-mode retry with rebuilt key */ } }리프에서 일치하면 stop을 설정하고, 체인을 건드리지 않은 채 반환한다. 일치하지 않으면 hand-over-hand 방식으로 체인을 순회하며 첫 번째 일치에서 멈춘다. 완전히 스캔했는데 없고 인터럽트도 없으면 found_oid는 NULL이다. 중복 제거 키 모드(is_fk_remake)이면 재구성된 키로 재시도한다. FK 콜백은 end_one_iteration을 설정하지 않는다 — FK 검사는 하나의 키 범위 내에서 완결되므로 키 중간 재개가 불필요하다.
7.11 챕터 요약 — 핵심 정리
섹션 제목: “7.11 챕터 요약 — 핵심 정리”- 하나의 범용 드라이버, 두 개의 콜백.
btree_range_scan이 위치 결정·래치·전진·재개를 담당하고,btree_range_scan_select_visible_oids가 SELECT 키별 작업을,btree_range_scan_find_fk_any_object가 FK 존재 확인을 담당한다.btree_keyval_search는 SELECT 콜백을 사용하는 단일 키 범위 검색에 불과하다. cur_leaf_lsa가 재개 계약이다.C_page를 unfix하는 그 순간 캡처된다. 재개 시 재고정된 페이지와의LSA_EQ비교가 빠른 경로이며, 불일치하면cur_key를 다시 찾거나 루트부터 재위치한다.key_status는 엄격한 세 상태 기계다. 모든 콜백 직전에 VERIFIED를 단언하고, 직후에 CONSUMED를 단언한다.advance_over_filtered_keys만이 NOT_VERIFIED에서 VERIFIED로 승격시키며, 범위와 필터를 모두 통과한 후에만 가능하다.- 재개는
slot_id가 아닌cur_key를 기준으로 앵커를 다시 잡는다. 슬롯 번호는 LSA가 페이지 변경 없음을 증명할 때만 신뢰된다. 그 외에서는 키를 검색해 슬롯을 재도출하므로, 동시적 분할/병합이 스캔을 중복이나 누락으로 이끌지 않는다. - 내림차순 스캔은 조건부 이전 리프 래치로 교착 상태를 피한다.
btree_range_scan_descending_fix_prev_leaf가 조건부 래치를 시도하고, 실패하면 현재 래치를 해제한 뒤 안전한 순서로 재획득한다. 리프 체인이 변경됐으면force_restart_from_root를 강제한다. - 두 개의 용량 제한이 대형 키를 반복에 걸쳐 분할한다. 소프트 제한은 버퍼가 거의 찰 때 키 앞에서 멈추며(
end_one_iteration), 하드 제한은 키의 오버플로 체인 내부에서 일시 중단하고O_vpid와is_key_partially_processed를 저장하여 다음 반복이 키 중간부터 재개하도록 한다 — 래치를 왕복에 걸쳐 보유하지 않고 수백만 OID를 스트리밍한다. - 펜스 키는 스캔에 보이지 않는다.
advance_over_filtered_keys가BTREE_LEAF_RECORD_FENCE슬롯을 건너뛰므로, Ch 6의 분할에서 생성된 페이지 경계 센티널이 결과 집합으로 누출되지 않는다.
Chapter 8: 객체 삭제 — 논리적 삭제와 물리적 삭제
섹션 제목: “Chapter 8: 객체 삭제 — 논리적 삭제와 물리적 삭제”하나의 키에서 OID 하나를 어떻게 빼내는가? 논리적 삭제는 delete MVCCID를
찍어두는 것으로 끝난다 — 이전 스냅샷을 가진 독자는 여전히 그 객체를 볼 수
있다. 물리적 삭제는 해당 객체 바이트를 레코드에서 잘라내며, 마지막 OID가
빠져나가면 키 전체를 제거한다. 이 두 경로를 모두 지휘하는 오케스트레이터가
btree_delete_internal이다. undo(재삽입) 경로와 vacuum 경로도 동일한
오케스트레이터가 담당하며, BTREE_OP_PURPOSE 값에 따라 하위 작업자를
선택한다. 레코드 레이아웃(key‖OID 구조, unique first-object 규칙, 오버플로
체인)은 cubrid-btree.md § “CUBRID’s Approach”를 참고하라.
8.1 오케스트레이터 하나, 일곱 가지 목적
섹션 제목: “8.1 오케스트레이터 하나, 일곱 가지 목적”btree_delete_internal은 BTREE_DELETE_HELPER를 구성하고 switch (purpose)로
키별 작업자를 선택한 뒤, 삽입 챕터(Chapter 3)에서 설명한 공용 하강 머신
btree_search_key_and_apply_functions에 넘긴다.
// btree_delete_internal -- src/storage/btree.cswitch (purpose) { case BTREE_OP_DELETE_OBJECT_PHYSICAL_POSTPONED: case BTREE_OP_DELETE_UNDO_INSERT: LSA_COPY (&delete_helper.reference_lsa, ref_lsa); /* <- CLR anchor */ [[fallthrough]]; case BTREE_OP_DELETE_OBJECT_PHYSICAL: case BTREE_OP_DELETE_VACUUM_OBJECT: key_func = btree_key_delete_remove_object; break; /* <- physical path */ case BTREE_OP_DELETE_UNDO_INSERT_UNQ_MULTIUPD: LSA_COPY (&delete_helper.reference_lsa, ref_lsa); key_func = btree_key_remove_object_and_keep_visible_first; break; case BTREE_OP_DELETE_VACUUM_INSID: key_func = btree_key_remove_insert_mvccid; break; /* <- vacuum insid */ case BTREE_OP_DELETE_UNDO_INSERT_DELID: LSA_COPY (&delete_helper.reference_lsa, ref_lsa); key_func = btree_key_remove_delete_mvccid; break; /* <- logical undo */ default: assert_release (false); return ER_FAILED; }일곱 개 케이스는 네 개의 리프 작업자로 압축된다.
key_func | 목적 | 효과 |
|---|---|---|
btree_key_delete_remove_object | PHYSICAL, PHYSICAL_POSTPONED, UNDO_INSERT, VACUUM_OBJECT | OID를 잘라내고, 마지막 OID면 키도 제거. PHYSICAL은 전체 undo를 기록하고, 나머지는 CLR 또는 vacuum |
btree_key_remove_object_and_keep_visible_first | UNDO_INSERT_UNQ_MULTIUPD | 잘라내기 + 가시적 첫 번째 객체 복원 |
btree_key_remove_delete_mvccid | UNDO_INSERT_DELID | 논리적: delete MVCCID 제거 |
btree_key_remove_insert_mvccid | VACUUM_INSID | 논리적: insert MVCCID 제거 |
하강 완료 후 처리: printed_key 해제, 오류 시 로그 및 반환, 정상이면
PSTAT_BT_NUM_DELETES 증가, *unique 게시, 다음의 미묘한 되돌림 분기 실행.
// btree_delete_internal -- src/storage/btree.cif (delete_helper.check_key_deleted && !delete_helper.is_key_deleted) { /* <- key still has a visible object: undo the optimistic decrement */ delete_helper.unique_stats_info->insert_key_and_row (); /* <- revert key */ delete_helper.unique_stats_info->delete_row (); /* <- keep row delta */ }flowchart TB
A["btree_delete_internal\nswitch(purpose)"] --> B["btree_search_key_and_apply_functions"]
B --> C["btree_fix_root_for_delete"]
C --> D["btree_merge_node_and_advance\n하강 및 미달 노드 병합 -- Ch 9"]
D --> E{"key_func"}
E -->|physical| F["btree_key_delete_remove_object"]
E -->|undo delid| G["btree_key_remove_delete_mvccid"]
E -->|vacuum insid| H["btree_key_remove_insert_mvccid"]
E -->|unq multiupd| I["btree_key_remove_object_and_keep_visible_first"]
Figure 8-1. 오케스트레이션. purpose가 리프 작업자를 결정하고, 하강 머신은 삽입 경로와 공유된다.
8.2 리프 노드까지 하강
섹션 제목: “8.2 리프 노드까지 하강”btree_fix_root_for_delete는 루트를 고정할 때마다 한 번씩 실행된다. 첫
탐색에서는 B-트리 정보를 로드(btree_fix_root_with_info)하고, 재시작 시에는
재고정 후 조기 반환한다. 키를 언팩하고, 도메인을 설정하며, is_null을 계산한다.
일부 목적은 여기서 멈춘다: vacuum 목적 두 가지는 통계 처리 전에
return NO_ERROR하고, undo/postpone 목적은 class OID가 NULL인 경우
topclass_oid로 패치한 뒤 반환한다(undo 이미지에서 topclass와 동일하면 생략된
다). PHYSICAL 목적만 unique 통계 처리까지 내려오며, MULTI_ROW_UPDATE가
check_key_deleted/is_key_deleted 쌍을 활성화한다(§8.1). NULL 키는
*stop을 설정한다.
btree_merge_node_and_advance는 전진 콜백이다(병합 논리는 Ch 9). 리프 도달
분기: node_level == 1에서 btree_search_leaf_page를 호출하고,
pgbuf_promote_read_latch로 루트 읽기 래치를 승격한다.
ER_PAGE_LATCH_PROMOTE_FAIL이 발생하면 *restart를 설정하고
nonleaf_latch_mode를 PGBUF_LATCH_WRITE로 바꿔 재시도 시 쓰기 래치 상태로
하강하게 한다. 성공하면 *is_leaf를 설정하고 쓰기 래치 상태 그대로
key_func에 전달된다.
불변식 — 리프 노드는 변경 전에 반드시 쓰기 래치 상태여야 한다. 모든
key_func은pgbuf_get_latch_mode (*leaf_page) >= PGBUF_LATCH_WRITE를 assert한다. 승격-또는-재시작 분기가 이를 보장한다. 읽기 래치 상태의 작업자는 동시 쓰기 작업과 충돌해 페이지를 손상시킨다.
8.3 물리적 삭제 디스패치 — btree_key_delete_remove_object
섹션 제목: “8.3 물리적 삭제 디스패치 — btree_key_delete_remove_object”객체를 찾고, 로깅을 준비하며, 제거하고, 재집계한다.
- 키 발견 여부:
result == BTREE_KEY_FOUND이면 레코드를 복사하고,btree_read_record와btree_find_oid_and_its_page로 OID를 찾아found_page/prev_found_page/offset_to_object에 저장한다(오버플로 페이지일 수 있음). 그렇지 않으면 offset은NOT_FOUND. - 객체 미발견:
VACUUM_OBJECT의 경우 양성(이미 vacuum됨) — 경고 후goto exit. 그 외는assert_release(false)+ER_BTREE_UNKNOWN_KEY. - 발견:
SERVER_MODEassert로 락 보유를 확인한다(vacuum/recovery/ 자체 삽입의 비 unique는 락 없이 동작). 두 번째 assert로 unique first-object 규칙을 점검한다(§8.7). - Undo 로깅:
PHYSICAL목적만btree_rv_save_keyval_for_undo로 키 값 이미지를rv_keyval_data에 저장한다(롤백 시 OID 재삽입에 사용). vacuum/ undo-of-insert는 보상 레코드이므로= NULL. - 제거:
node_type = (*leaf_page == found_page) ? LEAF : OVERFLOW를 설정하고btree_key_remove_object(§8.4)를 호출한다. 나머지 페이지는 unfix. - 재집계:
MULTI_ROW_UPDATE의 경우 리프를 다시 읽고btree_get_num_visible_from_leaf_and_ovf로 dirty 스냅샷 카운트.num_visible_oids > 0이면is_key_deleted = false로 설정(§8.1이 통계 수정). exit: 두 페이지 모두 idempotent unfix,rv_keyval_data해제.
8.4 btree_key_remove_object — 리프 vs. 오버플로 분기
섹션 제목: “8.4 btree_key_remove_object — 리프 vs. 오버플로 분기”두 줄짜리 디스패처다(Figure 8-2): 리프 → btree_leaf_remove_object,
오버플로 → btree_overflow_remove_object. 리프 레코드는 절대로 빈
상태로 남아서는 안 된다(unique first-object 규칙). 반면 오버플로 페이지는
완전히 빌 수 있다.
flowchart TB
K["btree_key_remove_object\nnode_type?"] --> L{"BTREE_LEAF_NODE?"}
L -->|yes| LO["btree_leaf_remove_object"]
L -->|no| OO["btree_overflow_remove_object"]
LO --> LO0{"offset_to_object == 0?"}
LO0 -->|no| RR["btree_record_remove_object\n비첫 번째 객체 제거"]
LO0 -->|yes, only object, no ovfl| DK["btree_delete_key_from_leaf\n키 전체 제거"]
LO0 -->|yes, only object, has ovfl| RP["btree_replace_first_oid_with_ovfl_oid"]
LO0 -->|yes, more leaf objects| RL["btree_leaf_record_replace_first_with_last"]
OO --> OO1{"overflow has 1 object?"}
OO1 -->|yes| DEA["페이지 해제 + 재연결"]
OO1 -->|no| RR2["btree_record_remove_object"]
Figure 8-2. 물리적 제거의 모든 분기.
8.5 btree_leaf_remove_object — 리프의 네 가지 분기
섹션 제목: “8.5 btree_leaf_remove_object — 리프의 네 가지 분기”unique first-object 불변식을 강제 적용한다. 키 자체를 제거하면
check_key_deleted가 초기화된다. 네 가지 분기:
// btree_leaf_remove_object -- src/storage/btree.cif (offset_to_object == 0) { btree_record_get_last_object (..., &last_oid, ..., &offset_to_last_object); if (offset_to_last_object == 0) /* <- only object */ { if (VPID_ISNULL (&leaf_rec_info->ovfl)) btree_delete_key_from_leaf (...); /* <- remove the key (§8.8) */ else btree_replace_first_oid_with_ovfl_oid (...); /* <- pull from overflow (§8.6) */ } else btree_leaf_record_replace_first_with_last (...); /* <- first := last */ }else btree_record_remove_object (...); /* <- splice non-first */불변식 — unique 리프 레코드의 첫 번째 객체는 가시적 객체이며, OID가 남아 있는 한 레코드는 절대로 비어 있지 않다. 첫 번째 슬롯은 별도의 인코딩을 사용하므로(class OID와 고정 크기 MVCC 정보를 포함할 수 있음), 첫 번째 객체를 제거할 때는 반드시 교체 방식으로 처리해야 한다. 단순 splicing을 적용하면 스캔이 잘못된 형식의 첫 번째 객체를 읽거나, 오버플로 링크는 살아있지만 OID가 없는 키를 만나게 된다.
8.6 오버플로에서 OID 끌어올리기 — btree_replace_first_oid_with_ovfl_oid
섹션 제목: “8.6 오버플로에서 OID 끌어올리기 — btree_replace_first_oid_with_ovfl_oid”리프의 유일한 객체를 제거했는데 오버플로 OID가 남아 있다면, 오버플로
객체 하나가 새 리프 첫 번째 객체가 되어야 한다. system-op 샌드위치 구조로
처리한다: btree_leaf_change_first_object가 오버플로의 마지막 객체를 리프
첫 번째 슬롯에 복사하고, btree_overflow_record_replace_object가 삭제할
객체를 비워진 오버플로 슬롯으로 옮긴다. 커밋 후 btree_overflow_remove_object
(§8.7)로 그 슬롯을 제거한다. exit_on_error는 system op을 중단한다. 순수
undo 목적은 이 경로에서 실패해서는 안 된다.
8.7 오버플로 제거 — btree_overflow_remove_object와 관련 함수
섹션 제목: “8.7 오버플로 제거 — btree_overflow_remove_object와 관련 함수”오버플로 레코드(슬롯 1)를 읽고 객체가 홀로인지 여부로 분기한다.
// btree_overflow_remove_object -- src/storage/btree.cif (overflow_record.length == BTREE_OBJECT_FIXED_SIZE (btid_int)) { assert (offset_to_object == 0); /* <- only first removable */ btree_get_next_overflow_vpid (thread_p, *overflow_page, &next_overflow_vpid); pgbuf_unfix_and_init (thread_p, *overflow_page); if (!delete_helper->is_system_op_started) { log_sysop_start (...); ... } file_dealloc (..., &overflow_vpid, FILE_BTREE); if (prev_page == leaf_page) btree_modify_leaf_ovfl_vpid (...); /* <- relink from leaf */ else btree_modify_overflow_link (...); /* <- relink from prev ovfl */ }else btree_record_remove_object (..., BTREE_OVERFLOW_NODE, offset_to_object, &ovf_addr);error: 레이블은 자체 시작한 system op을 종료하고, no-undo가 아닌 목적임을
assert한다.
btree_record_remove_object / _internal은 두 경로가 공유하는 바이트 수준
splicing 함수다. _internal은 인코딩된 크기(OR_OID_SIZE + unique이면 두
번째 OID + 플래그에 따른 MVCC 바이트)를 계산하고, 제거된 바이트를 undo용으로
팩킹하며, RECORD_MOVE_DATA로 공백을 메우고 redo를 팩킹한다. 래퍼는
spage_update와 btree_rv_log_delete_object를 호출한다.
불변식 — 오버플로 페이지의 첫 번째 객체만이 페이지 해제 대상이 될 수 있다.
_internal은node_type == BTREE_OVERFLOW_NODE || offset_to_object > 0을 assert한다. 리프의 첫 번째 객체를 직접 splice하는 것을 거부하며, 그 경우는 교체 경로(§8.5/§8.6)를 거쳐야 한다.
8.8 키 전체 제거 — btree_delete_key_from_leaf
섹션 제목: “8.8 키 전체 제거 — btree_delete_key_from_leaf”마지막 OID가 빠져나가면 키 레코드 자체를 삭제한다. leafrec_pnt->key_len < 0
이면 키 값이 오버플로 키 페이지에 있다는 뜻이므로, 먼저 system op을 시작하고
btree_delete_overflow_key를 호출한다. system op이 열린 상태라면 삭제 전에
레코드를 undo용으로 복사해둔다. 이후:
// btree_delete_key_from_leaf -- src/storage/btree.cif (spage_delete (thread_p, leaf_pg, search_key->slotid) != search_key->slotid) goto exit_on_error;key_cnt = btree_node_number_of_keys (thread_p, leaf_pg);if (key_cnt == 0) header->max_key_len = 0; /* <- no keys left */LOG_RV_RECORD_SET_MODIFY_MODE (&delete_helper->leaf_addr, LOG_RV_RECORD_DELETE);btree_rv_log_delete_object (thread_p, *delete_helper, delete_helper->leaf_addr, leaf_record.length, 0, leaf_record.data, NULL); /* <- undo = record */exit_on_error는 열린 system op을 중단한다. 여기서는 페이지 로컬
key_cnt만 갱신된다. 인덱스 전체의 num_keys/num_oids 델타는
unique-stats 경로(delete_key_and_row/delete_row, §8.2/§8.1)가
반영한다.
8.9 논리적 삭제 — btree_key_remove_delete_mvccid
섹션 제목: “8.9 논리적 삭제 — btree_key_remove_delete_mvccid”논리적 절반(BTREE_OP_DELETE_UNDO_INSERT_DELID)은 MVCC delete를 롤백한다.
객체는 그대로 남고 delete MVCCID만 제거된다. 미발견은 hard 오류
ER_BTREE_UNKNOWN_KEY다 — undo는 반드시 찾아야 한다. 유일성 여부에 따라
분기한다.
- unique (
_unique→btree_remove_delete_mvccid_unique_internal): 가시적 객체는 반드시 첫 번째여야 하므로 undo 시 위치를 되돌릴 수 있다. 이미 첫 번째라면 단순히btree_record_remove_delid를 호출하고, 그렇지 않으면 현재 첫 번째(고정 크기)와 교환한다 — 오버플로 페이지에 undoredo를 기록하는 시스템 op 아래에서 수행된다. compensate로 기록(log_append_compensate_with_undo_nxlsa,reference_lsa에 앵커됨), system op이 열린 경우는 undoredo로 기록. - non-unique (
_non_unique): 순서 제약 없음 — 제자리에서btree_record_remove_delid를 수행하고spage_update후log_append_compensate_with_undo_nxlsa(RVBT_RECORD_MODIFY_COMPENSATE,reference_lsa에 앵커됨).
btree_record_remove_delid는 has_fixed_size에 따라 분기한다. 고정 크기
객체(오버플로, 또는 오버플로가 있는 첫 번째 객체)는 너비를 유지하면서
MVCCID를 null화한다(btree_set_mvccid로 MVCCID_NULL 설정). 가변 크기
객체는 MVCCID 바이트를 제거하고 BTREE_OID_HAS_MVCC_DELID 플래그를
해제한다(btree_remove_mvccid).
8.10 논리적 vacuum — btree_key_remove_insert_mvccid
섹션 제목: “8.10 논리적 vacuum — btree_key_remove_insert_mvccid”BTREE_OP_DELETE_VACUUM_INSID는 반대편 작업이다. 객체가 모든 트랜잭션에
가시적이 된 시점에 vacuum이 insert MVCCID를 제거한다 — undo 없음.
분기: 객체 미발견 → 양성, 경고 후 return NO_ERROR. 발견 → INSID가 전체
가시적이 아님을 assert하고, btree_record_remove_insid로 바이트를 제거하며,
spage_update 후 log_append_redo_data를 redo-only 모드
RVBT_RECORD_MODIFY_NO_UNDO로 기록한다.
불변식 — vacuum은 절대로 undo를 생성하지 않는다. 두 vacuum 작업자 (여기서의
_insert_mvccid와 물리적 경로의VACUUM_OBJECT) 모두 redo만 기록한다. vacuum이 undo를 기록한다면, 복구 도중 충돌 시 “un-vacuum”이 발생해 죽은 버전이 다시 노출되고 가시성이 깨진다.
8.11 챕터 요약 — 핵심 정리
섹션 제목: “8.11 챕터 요약 — 핵심 정리”- 오케스트레이터 하나, 일곱 가지 목적.
btree_delete_internal의switch (purpose)가 네 개의 리프 작업자 중 하나를 선택한다. 하강 경로 (btree_fix_root_for_delete→btree_merge_node_and_advance)는 삽입과 공유되며, 리프에 도달할 때 쓰기 래치 상태를 보장한다. - 논리적 경로와 물리적 경로는 서로 다르다. 논리적 경로
(
_remove_delete_mvccidundo,_remove_insert_mvccidvacuum)는 MVCCID를 제자리에서 제거한다. 물리적 경로(_delete_remove_object)는 OID 바이트를 splicing하며 필요 시 키를 제거한다. - unique first-object 규칙이 리프 분기를 결정한다.
btree_leaf_remove_object는 첫 번째 객체를 절대로 직접 splice하지 않는다. 키 전체를 제거하거나(btree_delete_key_from_leaf), 오버플로에서 끌어올리거나(btree_replace_first_oid_with_ovfl_oid), 마지막 객체와 교환한다. 오버플로 제거는 단독/비단독 여부로 분기한다(해제 + 재연결, 또는 splice). - 로깅은 의도를 인코딩한다. 물리적 삭제는 재삽입을 위한 키 값 undo
이미지를 함께 기록한다. vacuum은 redo-only다. delete-undo는
reference_lsa에 앵커된 compensate 레코드를 기록한다.check_key_deleted/is_key_deleted쌍은MULTI_ROW_UPDATE가 가시적 객체를 재집계해 성급한 통계 감소를 되돌릴 수 있게 한다.
Chapter 9: 병합과 미달 노드 재균형
섹션 제목: “Chapter 9: 병합과 미달 노드 재균형”이 챕터는 Chapter 6의 거울 이미지다. 병합은 삭제 이전의 하강 경로에서 아래 방향으로 구동된다. 하강 루틴은 btree_merge_node_and_advance로 내려가며, 비리프 홉마다 “지금 진입하려는 자식을 형제에 합칠 수 있는가?”를 묻는다. 가능하다면 두 자식을 왼쪽으로 접어 넣고, 부모에서 사이 키 분리자를 삭제하고, 오른쪽 페이지를 반납한다. 높이가 2보다 크고 키가 두 개뿐인 퇴화 루트는 btree_merge_root로 위임되어 두 자식을 루트 프레임으로 끌어올리고 트리 높이를 한 단계 줄인다.
병합 이론(미달 임계값, 분리자 접기, 높이 수축)은 cubrid-btree.md의 “Deletion and rebalancing” 절을 참고하라. 이 챕터는 CUBRID 메커니즘만 추적한다. 분할 로깅과의 대응은 Chapter 6에 있으며, 병합은 동일한 RVBT_COPYPAGE / RVBT_INS_PGRECORDS redo 동사와 RVBT_MARK_DEALLOC_PAGE undo 동사를 역방향으로 재사용한다.
9.1 하강 중 병합 드라이버: btree_merge_node_and_advance
섹션 제목: “9.1 하강 중 병합 드라이버: btree_merge_node_and_advance”btree_merge_node_and_advance는 삭제 경로의 BTREE_ADVANCE_WITH_KEY_FUNCTION으로, btree_delete_internal을 위해 btree_search_key_and_apply_functions가 노드별로 디스패치한다. 역할은 이중적이다: 리프 방향으로 한 레벨 전진하면서, 진입하는 자식을 기회적으로 병합한다. 세 영역으로 분기하며 각각 return으로 끝난다 — 리프 (위치 찾기, 병합 없음); 키 2개에 레벨 > 2인 루트 (btree_merge_root 위임); 일반 비리프 (key를 따라가는 자식을 선택하고, btree_merge_node로 오른쪽 이웃과의 병합 시도). Figure 9-1은 전체 골격이며, 표시된 모든 엣지가 실제 분기다.
flowchart TD
A["merge_node_and_advance"] --> B{"node_level == 1 ?"}
B -->|leaf| C{"is_root and latch not WRITE ?"}
C -->|yes| D["promote SHARED_READER"]
D -->|FAIL| E["restart, latch:=WRITE, return"]
D -->|ok| F["is_leaf=true, return"]
C -->|no| F
B -->|non-leaf| G{"is_root and level>2 and keys==2 ?"}
G -->|yes| H["fix L+R, compute need/force_root_merge"]
H --> I{"need_root_merge ?"}
I -->|no| J["search child, advance, fall to tail"]
I -->|yes| LM{"latch == READ ?"}
LM -->|already WRITE| M["sysop, merge_root, dealloc Q+R, commit"]
LM -->|READ| K{"promote all 3 ?"}
K -->|FAIL and force| L["restart, latch:=WRITE, return"]
K -->|FAIL not force| J
K -->|ok| M
M --> N["advance_to_page=root, crt_page=NULL"]
G -->|no| O["search child, fix child_page"]
J --> O
O --> P{"slotid<key_count and free>PAGESIZE/2 ?"}
P -->|no| Z["advance_to_page=child, return"]
P -->|yes| Q["fix right neighbor"]
Q --> R{"right fixed ?"}
R -->|not in buffer| Z
R -->|yes| S["btree_node_mergeable"]
S --> T{"status != NO ?"}
T -->|no| Z
T -->|yes| U{"latch READ ? promote all 3"}
U -->|already WRITE| W
U -->|FAIL and FORCE| V["restart, latch:=WRITE, return"]
U -->|FAIL and TRY| Z
U -->|ok| W["sysop, merge_node, dealloc right, commit"]
Figure 9-1. btree_merge_node_and_advance의 제어 흐름 — already-WRITE 승격 생략 엣지 포함.
리프 분기. node_level == 1이면 btree_search_leaf_page로 search_key를 위치시킨 뒤, WRITE 래치를 보장한다. READ로 하강했고 이 리프가 루트이기도 하다면 (단일 페이지 트리), 승격을 시도한다. ER_PAGE_LATCH_PROMOTE_FAIL이 발생하면 *restart를 설정하고 nonleaf_latch_mode를 WRITE로 올려 하강이 독점 모드로 재시도되게 한다. 그 외의 경우 리프는 이미 WRITE를 보유하고 *is_leaf = true를 반환한다.
// btree_merge_node_and_advance -- src/storage/btree.cif (node_header->node_level == 1) { error_code = btree_search_leaf_page (thread_p, btid_int, *crt_page, key, search_key); if (delete_helper->is_root && delete_helper->nonleaf_latch_mode != PGBUF_LATCH_WRITE) { error_code = pgbuf_promote_read_latch (thread_p, crt_page, PGBUF_PROMOTE_SHARED_READER); if (error_code == ER_PAGE_LATCH_PROMOTE_FAIL) { *restart = true; delete_helper->nonleaf_latch_mode = PGBUF_LATCH_WRITE; return NO_ERROR; } } *is_leaf = true; return NO_ERROR; }루트 축소 사전 점검: is_root && node_level > 2 && btree_node_number_of_keys(...) == 2. node_level > 2 조건은 트리가 단일 페이지로 붕괴하지 않도록 막는다. 두 자식이 고정되고, 사용 바이트가 DB_PAGESIZE - spage_get_free_space(...)로 계산되며, 접혀 들어올 분리자의 여유 공간을 max_key_len으로 방어적으로 허용한 두 임계값이 도출된다:
// btree_merge_node_and_advance -- src/storage/btree.cneed_root_merge = (left_used + right_used + CAN_MERGE_WHEN_EMPTY + root_max_key_length) < DB_PAGESIZE;force_root_merge = (left_used + right_used + FORCE_MERGE_WHEN_EMPTY + root_max_key_length) < DB_PAGESIZE;CAN_MERGE_WHEN_EMPTY는 MAX(DB_PAGESIZE*0.33, ...), FORCE_MERGE_WHEN_EMPTY는 MAX(*0.66, ...)다. need는 병합 후 페이지가 최대 ~67% 가득 참, force는 최대 ~34%. 승격은 nonleaf_latch_mode == PGBUF_LATCH_READ일 때만 시도된다. 하강이 이미 독점 모드라면(재시작 경로) 승격을 건너뛰고 바로 병합을 실행한다.
루트 병합 결과 분기 표 (need_root_merge 조건 진입 후):
| 래치 / 승격 결과 | force_root_merge | 동작 |
|---|---|---|
nonleaf_latch_mode 이미 WRITE | — | 승격 생략, 병합으로 낙하 (else: “Pages already latched exclusively”) |
| READ → 3개 모두 래치 승격 성공 | — | log_sysop_start, btree_merge_root, 두 자식 dealloc, log_sysop_commit, *advance_to_page = *crt_page, *crt_page = NULL |
승격 → PROMOTE_FAIL | true | *restart = true, latch:=WRITE, 자식 unfix, return |
승격 → PROMOTE_FAIL | false | 낙하, 병합 생략, 한 자식으로 전진 |
| 기타 오류 | — | goto error (시스템 op 시작된 경우 abort) |
불변식 — 루트 병합은 루트로 재진입한다.
btree_merge_root가 성공하면 하강하는 대신*advance_to_page = *crt_page,*crt_page = NULL로 설정해 동일한 루트에 콜백을 재실행한다. 이로써 짧아진 루트에 두 개의 병합 가능한 자식이 또 있을 경우 두 번째 축소가 실행된다. 이 처리를 생략하면 두 레벨을 줄여야 할 트리가 한 레벨만 줄어들고 루트가 일시적으로 미달 상태가 된다.
일반 비리프 꼬리 처리. btree_search_nonleaf_page가 search_key->slotid와 자식 VPID를 선택한다. child_latch는 자식이 리프(node_level == 2)일 때 PGBUF_PROMOTE_ONLY_READER와 함께 WRITE이고, 그 외에는 PGBUF_PROMOTE_SHARED_READER와 함께 nonleaf_latch_mode를 상속한다 — 레벨 2에서의 단일 독자 승격은 Chapter 6의 btree_split_node_and_advance와 대응된다. 오른쪽 이웃 사전 점검이 독자 질문이 초점을 맞추는 게이트다:
// btree_merge_node_and_advance -- src/storage/btree.cif (search_key->slotid < key_count && spage_get_free_space (thread_p, child_page) > DB_PAGESIZE / 2)두 조건 모두 필요하다: slotid < key_count는 오른쪽 이웃이 존재함을 의미하고, 절반 이상 여유 공간은 미달 상태일 가능성을 나타낸다. SERVER_MODE IO 부하 시 neighbor_fetch_mode는 OLD_PAGE_IF_IN_BUFFER로 강등되어 vacuum이 차가운 형제 때문에 멈추지 않는다 — 이웃이 없으면 right_page가 NULL이 되고, er_clear가 이 무해한 누락을 가리며, 코드는 단순 전진으로 넘어간다. 마찬가지로 이미 WRITE 하강 시에는 승격 블록을 건너뛰고 직접 병합한다. 왼쪽 이웃 분기는 없다: 오른쪽 형제만 시도하며, 하강이 모든 노드를 방문하므로 각 쌍은 왼쪽 멤버에서 확인된다.
9.2 병합 가능성 분류기: btree_node_mergeable
섹션 제목: “9.2 병합 가능성 분류기: btree_node_mergeable”btree_node_mergeable은 세 값의 BTREE_MERGE_STATUS를 반환한다: BTREE_MERGE_NO (생략), BTREE_MERGE_TRY (래치 승격이 저비용이면 병합), BTREE_MERGE_FORCE (필수; 승격 실패 시 독점으로 재시작). Figure 9-2는 모든 반환 경로를 열거한다.
flowchart TD
A["node_mergeable(L,R)"] --> B{"L_cnt == 0 ?"}
B -->|yes| F1["FORCE: left empty"]
B -->|no| C{"L non-fence == 0 ?"}
C -->|yes| D["R_used = uncompressed(R)"]
D --> D1{"R + FORCE_EMPTY < PAGESIZE"}
D1 -->|yes| F2["FORCE"]
D1 -->|no| D2{"R + CAN_EMPTY < PAGESIZE"}
D2 -->|yes| T1["TRY"]
D2 -->|no| N1["NO"]
C -->|no| E{"R_cnt == 0 ?"}
E -->|yes| F3["FORCE: right empty"]
E -->|no| G{"R non-fence == 0 ?"}
G -->|yes| H["symmetric on L_used"]
G -->|no| I{"L==1 and R==1 key ?"}
I -->|yes| F4["FORCE: both single-key"]
I -->|no| J["used bytes"]
J --> K{"L+R+CAN_EMPTY < PAGESIZE ?"}
K -->|no| N2["NO"]
K -->|yes| Lr["recompute uncompressed for LEAF"]
Lr --> Mm{"uncompressed still fits ?"}
Mm -->|no| N3["NO"]
Mm -->|yes| O{"L+R+FORCE_EMPTY < PAGESIZE ?"}
O -->|yes| F5["FORCE"]
O -->|no| T2["TRY"]
Figure 9-2. btree_node_mergeable의 반환 분류.
btree_node_number_of_keys로 키를 세고, btree_is_fence_key가 하단 펜스(슬롯 1)와 상단 펜스(슬롯 L_cnt)를 보고하면 빼서 비펜스 카운트를 구한다. 세 가지 경우:
- 한쪽이 비어 있거나 펜스만 있는 경우.
L_cnt == 0→FORCE. L에 펜스만 있다면(L_non_fence_cnt == 0), R의 비압축 크기(btree_node_size_uncompressed, 병합 후 펜스를 재도출)로 결정한다:FORCE_MERGE_WHEN_EMPTY여유로 맞으면 →FORCE;CAN_MERGE_WHEN_EMPTY로만 맞으면 →TRY; 그 외 →NO. R도 대칭적. - 양쪽이 키 하나.
L_non_fence_cnt == 1 && R_non_fence_cnt == 1→FORCE. - 크기 검사. 사용 바이트로
L_used + R_used + CAN_MERGE_WHEN_EMPTY < DB_PAGESIZE이면 후보다. 리프 노드는 비압축 크기로 재확인한다(병합 시 공유 펜스 접두사가 제거되므로 병합 후 바이트가 단순 합산을 초과할 수 있다):
// btree_node_mergeable -- src/storage/btree.cif (l_node_type == BTREE_LEAF_NODE) { L_used = btree_node_size_uncompressed (thread_p, btid, L_page); if (L_used < 0) { return BTREE_MERGE_NO; } if (L_used + R_used + CAN_MERGE_WHEN_EMPTY >= DB_PAGESIZE) { return BTREE_MERGE_NO; } R_used = btree_node_size_uncompressed (thread_p, btid, R_page); if (L_used + R_used + CAN_MERGE_WHEN_EMPTY >= DB_PAGESIZE) { return BTREE_MERGE_NO; } }if (L_used + R_used + FORCE_MERGE_WHEN_EMPTY < DB_PAGESIZE) { return BTREE_MERGE_FORCE; }return BTREE_MERGE_TRY;TRY/FORCE 비대칭은 0.33/0.66 분할에서 비롯된다. 크기 검사에 실패한 경우는 최종 return BTREE_MERGE_NO에 도달한다.
9.3 일반 병합: btree_merge_node
섹션 제목: “9.3 일반 병합: btree_merge_node”btree_merge_node(P, left_pg, right_pg, p_slot_id, child_vpid, status)는 right_pg를 left_pg로 접어 넣고, 부모 슬롯 p_slot_id의 분리자(오른쪽 페이지를 가리키는 슬롯)를 삭제하며, 오른쪽 페이지를 반납하고, 살아남은 VPID를 *child_vpid에 반환한다(항상 left_vpid). 12개의 STEP 주석이 복구 이야기에 대응한다. Figure 9-3은 흐름이다.
flowchart TD A["btree_merge_node"] --> B["STEP 0-1: classify fences\nkeep left lower + right upper"] B --> C["STEP 2-3: gather rec[] in merge_buf\nrecompress if merged_prefix changed"] C --> D["STEP 4: undo-log left RVBT_COPYPAGE"] D --> E["STEP 5-7: remove upper fence,\nspage_insert rec[], append fence"] E --> F["STEP 8: RVBT_NDRECORD_DEL +\nspage_delete parent slot, child_vpid=left"] F --> G["STEP 9-10: left next_vpid past right,\nmax_key_len, redo-log left"] G --> H["STEP 11: right node_level=-1\nunder RVBT_MARK_DEALLOC_PAGE"] H --> I["STEP 12: btree_get_next_page +\nset_vpid_previous_vpid -> left"] E -->|spage failure| X["assert_release, goto exit_on_error"] F -->|delete != slot| X
Figure 9-3. btree_merge_node의 STEP 흐름과 오류 탈출.
STEP 0–3. 리프 페이지는 BTREE_LEAF_RECORD_FENCE 플래그가 설정된 경우 하단 펜스(슬롯 1)와 상단 펜스(슬롯 L_cnt)를 갖는다. 병합된 페이지는 왼쪽 하단 펜스와 오른쪽 상단 펜스를 유지한다. 왼쪽 상단 펜스와 오른쪽 하단 펜스(사라지는 경계)는 버린다. 오른쪽 상단 펜스는 오른쪽 페이지가 반납되기 전에 미리 복사해 둔다(merged_upper_fence_record). 레코드들은 merge_buf 기반의 스크래치 rec[]에 수집되며(left_used + right_used + MAX_MERGE_ALIGN_WASTE가 스택 버퍼를 초과하면 db_private_alloc으로 확장), 병합된 접두사가 원본과 달라지면 각각 btree_recompress_record로 재압축한다.
불변식 — 접두사 단조성. 병합된 접두사는 어느 원본 접두사보다도 길어질 수 없다. 양쪽 펜스를 가진 MIDXKEY 인덱스의 경우
merged_prefix = pr_midxkey_common_prefix(left_fence_key, right_fence_key)이고, 그 외에는0이다(재압축 생략 빠른 경로). 코드는 레코드를 재압축(확장)만 한다.merged_prefix보다 긴 접두사로 압축하면 키 바이트가 손상된다.
STEP 4 — 왼쪽의 undo 이미지. 왼쪽을 변경하기 전에 전체 이미지를 undo 로그에 기록한다:
// btree_merge_node -- src/storage/btree.c (STEP 4)log_append_undo_data2 (thread_p, RVBT_COPYPAGE, &btid->sys_btid->vfid, left_pg, -1, DB_PAGESIZE, left_pg);이 물리적 undo 절반은 STEP 10의 redo와 쌍을 이루므로, 롤백된 삭제는 병합 전 왼쪽 페이지를 바이트 단위로 재구성할 수 있다(§9.5).
STEP 5–7 — 왼쪽 재작성. 왼쪽 상단 펜스를 제거한다(존재할 경우). 접두사가 변경됐으면 왼쪽 비펜스 레코드를 모두 삭제 후 재삽입하고, 그렇지 않으면 유지한다. 수집된 rec[]를 spage_insert하고, 병합된 상단 펜스를 덧붙인다. 각 spage_* 실패는 assert_release(false) + goto exit_on_error다.
STEP 8 — 부모에서 분리자 제거. 구조적 핵심이다. 부모의 p_slot_id 레코드를 읽는다. 오버플로 키(nleaf_pnt.key_len < 0)가 있으면 btree_delete_overflow_key로 페이지를 해제하고, 분리자를 로그에 기록한 뒤 삭제한다:
// btree_merge_node -- src/storage/btree.c (STEP 8)btree_rv_write_log_record (recset_data, &recset_length, &peek_rec, BTREE_NON_LEAF_NODE);log_append_undoredo_data2 (thread_p, RVBT_NDRECORD_DEL, &btid->sys_btid->vfid, P, p_slot_id, recset_length, sizeof (p_slot_id), recset_data, &p_slot_id);if (spage_delete (thread_p, P, p_slot_id) != p_slot_id) { /* ... goto exit_on_error ... */ }*child_vpid = *left_vpid; /* <- surviving child is always the left page */오른쪽 페이지의 슬롯을 삭제하면 왼쪽 페이지의 슬롯이 여전히 left_pg를 가리키며, 이제 두 구 범위를 모두 커버한다. RVBT_NDRECORD_DEL은 이전 레코드(undo: 재삽입)와 슬롯 id(redo: 삭제)를 담는다.
STEP 9–11 — 왼쪽 헤더, redo, 오른쪽 사망 표시. 왼쪽 next_vpid를 오른쪽 페이지 너머(= right_header->next_vpid)로 재지정하고, max_key_len을 최댓값으로, split-info를 초기화하고, pgbuf_set_dirty 전에 log_append_redo_data2(RVBT_COPYPAGE, left_pg, ...)로 redo 이미지를 기록한다. 오른쪽 페이지는 여기서 해제되지 않는다. node_level이 -1로 설정되어 RVBT_MARK_DEALLOC_PAGE 아래에 기록되므로, 동시에 재고정하는 읽기가 죽은 페이지를 감지할 수 있다(BTREE_IS_PAGE_VALID_LEAF). file_dealloc은 호출자에서 수행한다.
STEP 12 — 다음 노드의 prev_vpid 재연결. 오른쪽 페이지 다음에 있는 노드는 아직 반납된 오른쪽 페이지를 prev_vpid로 가리킨다. btree_get_next_page로 그 노드를 가져오고(체인 끝이면 NULL), btree_set_vpid_previous_vpid로 역방향 링크를 왼쪽으로 재작성한다. 이것이 네 번째 페이지를 건드리는 유일한 분기다. 따라서 병합은 순간적으로 P, 왼쪽, 오른쪽, 다음 페이지를 함께 보유할 수 있다. exit_on_error 꼬리는 힙 할당된 경우 merge_buf_ptr를 해제하고, 시스템 op abort(§9.5)가 이후 모든 것을 되돌린다.
9.4 루트 축소: btree_merge_root
섹션 제목: “9.4 루트 축소: btree_merge_root”btree_merge_root(P, Q, R)은 btree_merge_node보다 단순하다. P의 분리자를 형제로 옮기는 것이 아니라 두 자식의 레코드를 P 자체로 쏟아 넣기 때문이다. Q와 R은 dealloc 표시 외에는 건드리지 않으며, 루트의 페이지 정체성이 바뀌지 않으므로 b-tree 헤더 포인터가 그대로 유효하다.
Q의 상단 펜스(btree_leaf_is_flaged(..., BTREE_LEAF_RECORD_FENCE))가 있으면 Q_end를 감소시켜 제외하고, R의 하단 펜스는 R_start를 증가시켜 건너뛴다(루트 레벨 > 2이므로 자식은 보통 비리프이라 펜스가 없지만, 코드는 방어적으로 처리한다). 두 개의 루트 분리자는 btree_delete_meta_record로 제거된다 — 슬롯 2부터, 그 다음 슬롯 1 순서로, 번호가 밀리지 않게 한다. Q의 레코드는 btree_rv_util_save_page_records로 저장되고, RVBT_INS_PGRECORDS로 한 번에 기록되면서 spage_insert_at으로 P의 슬롯 1..Q_end에 삽입된다. 반복 중 오류 경로는 실제로 안착한 레코드만 기록한다:
// btree_merge_root -- src/storage/btree.cfor (i = 1; i <= Q_end; i++) { if (spage_get_record (...) != S_SUCCESS || spage_insert_at (...) != SP_SUCCESS) { if (i > 1) { recset_header.rec_cnt = i - 1; /* undo-log the i-1 records already inserted */ } goto exit_on_error; } }R의 레코드는 이어서 슬롯 left_cnt + 1 ..에 같은 방식으로 기록된다. 두 블록이 모두 안착하면 루트 헤더를 undo 로그에 기록하고, prev_vpid/next_vpid를 null로, split-info를 기본값으로 초기화한 뒤 node_level을 하나 감소시킨다:
// btree_merge_root -- src/storage/btree.cbtree_node_header_undo_log (thread_p, &btid->sys_btid->vfid, P);VPID_SET_NULL (&root_header->node.prev_vpid);VPID_SET_NULL (&root_header->node.next_vpid);btree_write_default_split_info (&(root_header->node.split_info));root_header->node.node_level--;assert_release (root_header->node.node_level > 1);btree_node_header_redo_log (thread_p, &btid->sys_btid->vfid, P);불변식 — 트리 높이를 줄이는 곳은 루트뿐이다. 루트 프레임의
node_level--이 높이가 감소하는 유일한 지점이다.assert_release(node_level > 1)은 축소 후 루트가 비리프임을 유지한다 —node_level > 2사전 점검과 함께, 트리는 레벨 3 루트를 한 번에 리프로 축소하거나 단일 페이지로 만들지 않는다.#ifndef NDEBUG블록도 루트의max_key_len이MAX(q, r max_key_len)과 같음을 어설트한다. 자식 레코드를 먼저 복사하지 않고node_level을 감소시키면 하강이 분리자 없는 레벨을 인덱싱해 쓰레기 슬롯을 읽게 된다.
Q와 R은 각각 사망 표시(node_level = -1, RVBT_MARK_DEALLOC_PAGE로 undo 로그)되지만 여기서 해제되지는 않는다. file_dealloc은 btree_merge_root 반환 후 호출자가 수행한다.
9.5 역방향 링크 수리와 시스템 op / 논리적 undo 괄호
섹션 제목: “9.5 역방향 링크 수리와 시스템 op / 논리적 undo 괄호”btree_set_vpid_previous_vpid는 병합이 오른쪽 페이지를 제거한 뒤 이중 연결 리프 체인을 일관되게 유지한다 — 분할이 수행하는 next-link 재작성의 정확한 대응물이다.
// btree_set_vpid_previous_vpid -- src/storage/btree.cheader = btree_get_node_header (thread_p, page_p);if (header == NULL) { return ER_FAILED; }btree_node_header_undo_log (thread_p, &btid->sys_btid->vfid, page_p);header->prev_vpid = *prev; /* <- repoint back-link to surviving left page */btree_node_header_redo_log (thread_p, &btid->sys_btid->vfid, page_p);pgbuf_set_dirty (thread_p, page_p, DONT_FREE);앞에 if (page_p == NULL) return NO_ERROR;가 있어, 병합된 왼쪽 페이지가 체인의 마지막일 때는 no-op이 된다. 이 프리미티브는 분할과 병합 모두에 쓰인다.
괄호 구조. 각 병합은 log_sysop_start / log_sysop_commit 안에서 실행되며, 모든 물리적 변경을 포함한다 — 왼쪽의 RVBT_COPYPAGE, 루트의 RVBT_INS_PGRECORDS, 부모의 RVBT_NDRECORD_DEL, 반납된 페이지의 RVBT_MARK_DEALLOC_PAGE, 그리고 file_dealloc:
// btree_merge_node_and_advance -- src/storage/btree.clog_sysop_start (thread_p);is_system_op_started = true;/* ... btree_merge_node / btree_merge_root, then file_dealloc of the recycled page(s) ... */log_sysop_commit (thread_p);is_system_op_started = false;불변식 — 구조적 병합은 원자적이며 삭제 내에서 자기 undo된다. start와 commit 사이의 어떤
goto error에서든,error:레이블이log_sysop_abort를 실행해 undo 레코드를 역순으로 재실행한다: 반납된 페이지가 해제 취소되고, 부모 분리자가 재삽입되고, 왼쪽 페이지가RVBT_COPYPAGE이미지에서 복원된다 — 병합 전 형태로 재분할하여 삭제가 재시도된다. sysop은 리프 수준의 객체 제거 전에 커밋하므로, 병합은 구조적(중첩 최상위) 동작이다: 포함된 논리적 삭제가 롤백되더라도 커밋된 병합은 절대 논리적으로 undo되지 않아 병합된 형태가 유지된다.file_dealloc을 시스템 op 밖에 두면 커밋과 dealloc 사이 충돌 시 오른쪽 페이지가 누출된다. 레코드 이동 없이 dealloc을 커밋하면 부모가 해제된 페이지를 가리킨다.
Chapter 6 상호 참조: 분할은 새로운 오른쪽 페이지에 RVBT_COPYPAGE redo를, 부모에 RVBT_NDRECORD_INS를 기록한다. 병합은 대칭적으로 살아남은 왼쪽 페이지에 RVBT_COPYPAGE undo+redo를, 부모에 RVBT_NDRECORD_DEL을 기록한다 — 하나의 undo가 구조적으로 다른 하나다.
9.6 챕터 요약 — 핵심 정리
섹션 제목: “9.6 챕터 요약 — 핵심 정리”- 병합은 하강 중 오른쪽 형제만 대상으로 구동된다.
btree_merge_node_and_advance는 내려가면서 각 자식을 오른쪽 이웃과 비교한다. 왼쪽 병합 분기는 없으며, 사전 점검은 오른쪽 이웃의 존재(slotid < key_count)와 자식의 미달 상태(free > PAGESIZE/2) 모두를 요구한다. - 병합 가능성은 세 값이다.
btree_node_mergeable은CAN_MERGE_WHEN_EMPTY(~33%)와FORCE_MERGE_WHEN_EMPTY(~66%)를 기준으로NO/TRY/FORCE를 반환한다. 비어 있거나 펜스만 있는 노드, 그리고 키가 하나인 쌍은 항상FORCE이며, 리프 쌍은 비압축 크기로 재확인한다.TRY는 승격 실패 시 생략되고FORCE는 독점 모드로 하강을 재시작한다. - 하강이 이미 독점 모드이면 승격을 생략한다. 두 병합 블록 모두
nonleaf_latch_mode == PGBUF_LATCH_READ일 때만 래치 승격을 시도한다. 재시작(이미 WRITE) 경로에서는 직접 병합을 실행한다. - 일반 병합은 부모에서 분리자를 제거한다.
btree_merge_node는 오른쪽을 왼쪽으로 복사하고, 오른쪽 페이지를 가리키는 부모 슬롯을 삭제(RVBT_NDRECORD_DEL)하고,left_header->next_vpid를 재지정하고, 오른쪽 페이지를 사망 표시(node_level = -1)하고,btree_set_vpid_previous_vpid로 다음 노드의prev_vpid를 재연결한다. - 루트 축소가 유일한 높이 수축이다.
btree_merge_root는 두 자식의 레코드를 변경되지 않은 루트로 쏟아 넣고node_level을 감소시킨다.node_level > 2사전 점검과assert_release(node_level > 1)이 트리를 단일 페이지로, 루트를 리프로 만들지 않도록 막는다. - 반납된 페이지는 병합 내에서 표시만 되고 해제되지 않는다.
node_level = -1(RVBT_MARK_DEALLOC_PAGE로 undo 로그)은 동시 재고정이 죽은 페이지를 감지하게 해 준다.file_dealloc은 드라이버에서, 여전히 시스템 op 내에서 수행된다. - 병합은 자기 undo된다.
log_sysop_start/commit괄호가 각 병합을 감싸므로 중간 오류 시 정확한 병합 전 레이아웃(재분할)으로 abort되는 반면, 커밋된 병합은 포함된 삭제가 롤백되더라도 유지된다 — Chapter 6의 분할 로깅을 미러링한다.
Chapter 10: 특수 경로와 엣지 경로
섹션 제목: “Chapter 10: 특수 경로와 엣지 경로”3~9장에서 다룬 단일 키 삽입/삭제 생애주기 밖에서 트리를 구축하거나 유지하는
경로들을 정리한다. 인덱스 일괄 적재 두 가지, 리프 레코드 보조 기능(펜스 키,
FK 탐색), 유일 통계 집계, 중복 키 모드가 그 대상이다. 일괄 적재가 N번의 삽입보다
빠른 이유, 펜스 키의 역할에 대한 이론적 배경은 cubrid-btree.md의 “Bulk Loading”
과 “Prefix Compression and Fence Keys” 절을 참고하라.
10.1 오프라인 일괄 적재: xbtree_load_index
섹션 제목: “10.1 오프라인 일괄 적재: xbtree_load_index”xbtree_load_index는 동시 쓰기 트랜잭션이 없는 상황에서 CREATE INDEX 시
선택되는 경로다. 최상위 시스템 op 하나로 트리를 비가시 상태로 완전히
구축한 뒤 원자적으로 커밋한다. 커밋 전까지 다른 트랜잭션이 파일에 접근하지 않으므로
래칭을 생략한다 — 페이지는 버퍼 관리자가 요구하는 PGBUF_LATCH_WRITE만
획득하며, 트랜잭션 간 조율을 위한 래치는 사용하지 않는다. Figure 10-1은 모든
분기를 추적한다. 적재는 전부-아니면-전무(goto error마다 시스템 op 중단 후 NULL
반환)이며, 키가 없는 경우에는 일반 xbtree_add_index로 빈 인덱스를 재생성
한다.
flowchart TD
A["xbtree_load_index"] --> B["init SORT_ARGS + LOAD_ARGS\nset deduplicate_key_idx"]
B --> C["btree_index_sort -> btree_construct_leafs callback"]
C --> D{"any keys loaded?\nleaf.pgptr != NULL"}
D -->|yes| E["btree_save_last_leafrec"]
E --> F{"has_fk?"}
F -->|yes| G["btree_load_check_fk"]
F -->|no| H["btree_build_nleafs"]
G --> H
D -->|no| I["log_sysop_abort\nxbtree_add_index empty"]
H --> J{"is_sysop_started?"}
I --> J
J -->|yes| K["attach to outer\nif unique: undo RVBT_REMOVE_UNIQUE_STATS"]
J -->|no| L["nothing to log"]
K --> M["return btid"]
L --> M
Figure 10-1. xbtree_load_index 제어 흐름 — 빈 인덱스 분기와 FK 분기 포함.
10.2 리프를 왼쪽에서 오른쪽으로 채우기: btree_construct_leafs와 btree_first_oid
섹션 제목: “10.2 리프를 왼쪽에서 오른쪽으로 채우기: btree_construct_leafs와 btree_first_oid”btree_construct_leafs는 정렬 콜백이다. 정렬된 (key, oid) 쌍을 하나씩 받아
현재 리프에 접어 넣는다. 주석으로 다섯 가지 분기가 명시되어 있다.
// btree_construct_leafs -- src/storage/btree_load.c next = *(char **) recdes->data; /* <- save forward link before mutating recdes */ if (VPID_ISNULL (&(load_args->leaf.vpid))) /* <- A: very first record */ bt_load_get_first_leaf_page_and_init_args (...); else { int c = btree_compare_key (&sparam.this_key, &load_args->current_key, ...); if (c == DB_GT) bt_load_make_new_record_on_leaf_page (...); /* <- B: new key */ else if (c == DB_EQ) bt_load_add_same_key_to_record (...); /* <- C: append OID */ else { assert_release (false); goto error; } /* <- D: out-of-order = corruption */ } if (!next) break; /* <- E: chain exhausted */불변식 — 정렬 입력 단조성.
btree_compare_key(this_key, current_key)는 절대DB_LT를 반환하지 않는다. 정렬 단계가 키의 비감소 순서를 보장하므로else분기는assert_release(false)다. 비교자에 버그가 있으면 정렬이 깨진 리프 체인이 생성되어 스캔(Ch 7)과 검색(Ch 3)이 오작동하는데, assert가 잠재적 오염을 즉시 실패로 전환한다.
bt_load_make_new_record_on_leaf_page는 채움 인수(fill factor)를 적용한다.
(cur_maxspace - leaf_nleaf_recdes.length) < LOAD_FIXED_EMPTY_FOR_LEAF 이고
spage_number_of_records > 1이면 btree_proceed_leaf를 호출해 새 리프를 시작
(prev/next 체인 연결)한 뒤 spage_insert와 btree_first_oid를 호출한다.
LOAD_FIXED_EMPTY_FOR_LEAF는 이후 인플레이스 삽입을 위한 여유 공간을 예약하고,
> 1 가드는 오버플로 백업을 받는 초대형 키가 임계값을 초과해 무한 루프에 빠지는
것을 막는다. btree_first_oid는 키 크기(key_len < BTREE_MAX_KEYLEN_INPAGE이면
BTREE_NORMAL_KEY, 아니면 BTREE_OVERFLOW_KEY이며 ovfid가 null이면
btree_create_overflow_key_file로 오버플로 키 파일을 생성)와 첫 객체의 삭제 플래그
(MVCC_IS_HEADER_DELID_VALID이면 curr_non_del_obj_count = 0 — 객체가 이미
MVCC 삭제됨, 키가 살아있지 않음; 아니면 = 1이고 ++n_keys)에 따라 분기한다.
유일한 객체가 이미 삭제된 키는 고유 키 통계에 계산되지 않는다 — §10.9 런타임
관리의 일괄 적재 버전이다.
10.3 내부 레벨 상향 구축과 btree_load_new_page
섹션 제목: “10.3 내부 레벨 상향 구축과 btree_load_new_page”btree_build_nleafs는 두 작업 목록(push_list/pop_list)을 핑퐁하며 내부 레벨을
한 번에 하나씩 쌓는다(Figure 10-2). 모든 페이지는 btree_load_new_page를 거치며,
node_level 인수로 종류를 구별한다.
// btree_load_new_page -- src/storage/btree_load.c assert (log_check_system_op_is_started (thread_p)); /* <- caller must hold a sysop */ log_sysop_start (thread_p); /* <- nested sysop just for this alloc */ file_alloc (thread_p, &btid->vfid, btree_initialize_new_page, NULL, vpid_new, page_new); if (header) /* <- leaf/non-leaf (node_level >= 1) */ { header->node_level = node_level; header->common_prefix = 0; btree_init_node_header (...); } else /* <- overflow page (node_level == -1) */ { VPID_SET_NULL (&ovf_header_info.next_vpid); btree_init_overflow_header (...); }end: /* <- error: log_sysop_abort; success: log_sysop_commit (committed independently) */ if (error_code != NO_ERROR) log_sysop_abort (thread_p); else log_sysop_commit (thread_p);불변식 — 페이지별 할당 내구성. 각 할당은 독립적으로 커밋하는 중첩 시스템 오퍼레이션으로 감싼다. 소스 주석: “we need to commit page allocations. if loading index is aborted, the entire file is destroyed.” 전체 파일이 외부 중단 시 삭제되므로 개별 페이지 해제는 생략하고 최상위의
file_destroy에 의존한다.assert는 보호 최상위 오퍼레이션 없이 적재 페이지를 할당하는 경우를 방지한다.
btree_proceed_leaf(§10.2)는 이 함수를 node_level == 1로 호출해 새 리프를
할당하고 prev_vpid/next_vpid를 연결한다 — 7장의 범위 스캔 체인이 여기서 올바르게
구성된다.
flowchart TD
A["btree_build_nleafs"] --> B["pop children level k"]
B --> C["btree_connect_page -> push parent level k+1"]
C --> D{"pop_list length > 0?"}
D -->|yes| B
D -->|no| E["swap push_list / pop_list\nlevel := k+1"]
E --> F{"more than one page\nat this level?"}
F -->|yes| B
F -->|no| G["write root header\nSET_DECOMPRESS_IDX_HEADER"]
Figure 10-2. btree_build_nleafs의 내부 레벨 상향 구축.
10.4 온라인(동시) 인덱스 구축: xbtree_load_online_index
섹션 제목: “10.4 온라인(동시) 인덱스 구축: xbtree_load_online_index”온라인 경로는 인덱스를 구축하는 동안 DML을 허용한다. §10.1과 세 가지 점에서
다르다. MVCC builder_snapshot으로 일관된 테이블 버전을 확보한다. 스키마 락을
클래스별로 SCH_M_LOCK에서 IX_LOCK으로 강등(lock_demote_class_lock)해 쓰기
트랜잭션을 차단하지 않고, 종료 시 다시 승격한다. 일괄 삽입은 online_index_builder
가 워커 풀로 분배한다. tdes->has_deadlock_priority = true를 설정해 빌더가
DML과의 데드락에서 승리하도록 한다. 재승격 루프는 가장 안전-핵심적인 엣지 경로다.
락이 커밋/롤백 전에 반드시 SCH_M_LOCK으로 복귀해야 한다 — 그렇지 않으면 카탈로그
변경이 동시 읽기 트랜잭션과 경쟁한다.
// xbtree_load_online_index -- src/storage/btree_load.c xlogtb_reset_wait_msecs (thread_p, LK_INFINITE_WAIT); /* <- never give up */ while (true) { lock_ret = lock_object (..., SCH_M_LOCK, LK_UNCOND_LOCK); if (lock_ret == LK_GRANTED) break; else if (lock_ret == LK_NOTGRANTED_DUE_ERROR && er_errid () == ER_INTERRUPTED) { er_clear (); logtb_set_tran_index_interrupt (...); continue; } /* <- swallow interrupt, retry */ else if (lock_ret == LK_NOTGRANTED_DUE_TIMEOUT && css_is_shutdowning_server ()) { er_clear (); continue; } /* <- shutdown: still must promote */ assert (0); /* <- any other outcome unacceptable */ }락이 복귀한 뒤에야 빌더의 반환 코드(if (ret != NO_ERROR) goto error;)를 처리한다.
유니크 인덱스의 경우 btree_online_index_check_unique_constraint로 모든 클래스에 걸쳐
유일성을 재검증한다 — 스냅샷 빌더가 볼 수 없었던 동시 삽입이 충돌을 만들었을 수
있기 때문이다. 스냅샷은 양쪽 종료 경로에서 무효화된다.
10.5 삽입 목록 분배자: online_index_builder
섹션 제목: “10.5 삽입 목록 분배자: online_index_builder”online_index_builder는 생산자다. heap_next 루프는 스캔 결과에 따라 분기한다.
S_END이면 중단(힙 소진), S_ERROR이면 오류를 설정하고 중단, 그 외 비-S_SUCCESS
는 assert(false)를 발동한다. filter_pred(부분 인덱스)가 있으면 V_ERROR는
중단하고 비-V_TRUE는 continue로 행을 건너뛴다. 그 외의 경우 키를 생성하고
(heap_attrinfo_generate_key; NULL → ER_FAILED), index_builder_loader_task를
지연 할당하며, add_key(§10.6)를 호출한다. BATCH_FULL이 반환되면 태스크를
ib_workpool에 밀어 넣고 tasks_started를 증가시킨다. load_context.m_has_error가
설정되어 있으면 ER_IB_ERROR_ABORT로 중단한다. 루프 종료 후 최종 부분 태스크를
밀어 넣고 m_tasks_executed == tasks_started가 될 때까지 스핀하며, 유니크 인덱스의
경우 logtb_tran_update_btid_unique_stats를 (num_keys, num_oids, num_nulls)로
호출한다(§10.9).
각 태스크의 execute는 배치를 정렬(prepare_list)한 뒤 키마다
btree_online_index_list_dispatcher를 purpose BTREE_OP_ONLINE_INDEX_IB_INSERT로
한 번씩 호출한다. 오류 발생 시 m_load_context.m_has_error를 원자 교환으로 설정하고
중단한다. BTREE_OP_ONLINE_INDEX_* enum(btree.h)은 일반 삽입/삭제 기계가 읽는
라우팅 키다. 일곱 값은 빌더의 _IB_INSERT/_IB_DELETE, 동시 DML의
_TRAN_INSERT, _TRAN_INSERT_DF(UPDATE가 삭제할 행을 삽입할 때 객체를 미리
DELETE 플래그로 표시해 두 번째 하강 없이 vacuum이 회수할 수 있게 함),
_TRAN_DELETE, 그리고 롤백용 _UNDO_TRAN_INSERT/_UNDO_TRAN_DELETE로 나뉜다.
10.6 add_key: NULL 처리와 배치 경계
섹션 제목: “10.6 add_key: NULL 처리와 배치 경계”index_builder_loader_task::add_key는 NULL을 물리 트리에서 걸러내되 통계를 위해
계산은 한다.
// index_builder_loader_task::add_key -- src/storage/btree_load.c if (DB_IS_NULL (key) || btree_multicol_key_is_null (const_cast<DB_VALUE *>(key))) { if (BTREE_IS_UNIQUE (m_unique_pk)) ++m_insert_list.m_ignored_nulls_cnt; /* <- count, don't store */ return BATCH_CONTINUE; } m_memsize += m_insert_list.add_key (key, oid); return (m_memsize > prm_get_bigint_value (PRM_ID_IB_TASK_MEMSIZE)) ? BATCH_FULL : BATCH_CONTINUE;불변식 — NULL은 추적하되 저장하지 않는다. NULL(또는 전체가 NULL인 다중 컬럼) 키는 트리에 삽입되지 않지만, 유니크 인덱스에서는
m_ignored_nulls_cnt를 증가시키고execute가 이를m_num_oids와m_num_nulls모두에 반영한다. 이는rows == keys + nulls(§10.9)를 보존한다. NULL은 행 하나와 null 하나를 추가하되 키는 추가 하지 않는다 — SQL 표준이 요구하는 바대로(여러 NULL은 유일성을 위반하지 않는다). NULL을 저장하거나 계산하지 않으면 COUNT 쿼리와 유일성 검사가 불일치한다.
10.7 펜스 키: BTREE_LEAF_RECORD_FENCE
섹션 제목: “10.7 펜스 키: BTREE_LEAF_RECORD_FENCE”펜스 키는 실제 객체 없이 리프의 경계만 기록하는 센티널 리프 레코드다
(BTREE_LEAF_RECORD_FENCE 플래그로 표시). CUBRID는 펜스를 접두사 압축의 기준점으로
사용한다. 각 키의 공통 접두사는 리프의 펜스 키와 비교해 계산하며, 레코드는 접미사
만 저장한다. 읽기 경로는 펜스를 건너뛰어야 한다. btree_read_record의 압축 해제
분기는 양의 접두사를 가진 페이지에서 비-펜스 레코드에 대해서만 전체 키를 복원한다.
// btree_read_record (decompress branch) -- src/storage/btree.c if (node_type == BTREE_LEAF_NODE && !btree_leaf_is_flaged (rec, BTREE_LEAF_RECORD_OVERFLOW_KEY) && !btree_leaf_is_flaged (rec, BTREE_LEAF_RECORD_FENCE)) { /* <- never decompress a fence */ int n_prefix = btree_node_get_common_prefix (thread_p, btid, pgptr); if (n_prefix > 0) { spage_get_record (thread_p, pgptr, 1, &peek_rec, PEEK); /* <- slot 1 = fence */ assert (btree_leaf_is_flaged (&peek_rec, BTREE_LEAF_RECORD_FENCE)); pr_midxkey_add_prefix (&result, &lf_key, key, n_prefix); /* <- fence prefix + record suffix */ } }불변식 — 슬롯 1이 접두사 기준점이다. 리프에 양의
common_prefix가 있으면 슬롯 1이 펜스 키를 보유하며, 압축 해제기는 이를assert한다. 슬롯 1에 비-펜스 레코드가 있으면pr_midxkey_add_prefix가 잘못된 접두사를 붙여 키가 오염된다.
스캔 건너뛰기는 이의 거울상이다. btree_search_leaf_page의 이진 탐색에서 펜스와
의 DB_EQ 일치는 명중으로 처리하지 않는다.
// btree_search_leaf_page -- src/storage/btree.c if (c == DB_EQ) { if (btree_leaf_is_flaged (&rec, BTREE_LEAF_RECORD_FENCE)) { assert (middle == 1 || middle == key_cnt); if (middle == 1) c = DB_GT; /* <- left fence: fall through to next key */ else if (middle == key_cnt) { search_key->result = BTREE_KEY_BIGGER; search_key->slotid = key_cnt; return NO_ERROR; } } else { search_key->result = BTREE_KEY_FOUND; search_key->slotid = middle; return NO_ERROR; } }이는 첫 키 빠른 확인 btree_leaf_is_key_between_min_max와는 다르다. 후자는
슬롯 1의 펜스를 만나면 result = BTREE_KEY_BETWEEN, slotid = -1을 설정해 전체
탐색을 강제한다. 어느 경로로 들어오든 범위 스캔(Ch 7)은 펜스 객체를 반환하지
않는다.
10.8 외래 키 탐색: btree_find_foreign_key와 btree_check_foreign_key
섹션 제목: “10.8 외래 키 탐색: btree_find_foreign_key와 btree_check_foreign_key”두 FK 헬퍼 모두 범위 스캔을 재사용하며, 방향이 다르다.
btree_check_foreign_key는 참조하는 쪽에서 실행된다 — 자식 키가 부모의 PK
인덱스에 존재하는지 검증한다. 주석이 모든 분기를 명시한다.
// btree_check_foreign_key -- src/storage/btree.c if (has_null == true) return NO_ERROR; /* <- 1: NULL FK column trivially satisfied */ classrepr = heap_classrepr_get (...); if (classrepr == NULL) goto exit_on_error; /* <- 2: no repr -> cleanup + normalize errid */ if (classrepr->has_partition_info > 0) { /* <- 3: partitioned parent -> prune to local btid */ partition_init_pruning_context (&pcontext); partition_load_pruning_context (...); } BTID_COPY (&local_btid, pk_btid); if (classrepr->has_partition_info > 0 && pcontext.partitions != NULL) partition_prune_unique_btid (&pcontext, keyval, &part_oid, &class_hfid, &local_btid); ret_search = xbtree_find_unique (thread_p, &local_btid, S_SELECT_WITH_LOCK, keyval, ...); if (ret_search == BTREE_KEY_NOTFOUND) /* <- 4a: no parent row -> FK violation */ { er_set (...ER_FK_INVALID...); ret = ER_FK_INVALID; goto exit_on_error; } else if (ret_search == BTREE_ERROR_OCCURRED) { ASSERT_ERROR_AND_SET (ret); goto exit_on_error; } /* <- 4b */ assert (ret_search == BTREE_KEY_FOUND); /* <- 4c: parent exists -> ok */ // ... success and exit_on_error share cleanup: clear pcontext + heap_classrepr_free_and_init ...성공과 exit_on_error 모두 클린업을 공유한다. exit_on_error만 비-NO_ERROR
반환을 강제하며, (ret == NO_ERROR && er_errid() == NO_ERROR) ? ER_FAILED : ret
로 결정된다.
btree_find_foreign_key는 참조받는 쪽에서 실행된다 — 부모 PK를 삭제/갱신하기
전에 해당 키를 참조하는 자식 행이 여전히 존재하는지 확인하고, 검사가 안정적으로
유지되도록 잠근다. btree_remake_foreign_key_with_PK로 is_newly를 결정하고,
!is_newly이면 키를 양쪽 범위 경계에 pr_share_value로 공유하고 range = GE_LE,
find_fk_object.lock_mode = S_LOCK을 설정하고 btree_scan.is_fk_remake = is_newly
를 스레딩한 뒤 btree_range_scan_find_fk_any_object 비지터로 btree_range_scan을
구동한다. 오류 또는 불일치(OID_ISNULL (&find_fk_object.found_oid))이면
locked_object가 설정된 경우 획득한 락을 해제한다(lock_unlock_object_donot_move_to_non2pl).
is_newly 플래그는 중복 키 모드를 연결한다. btree_remake_foreign_key_with_PK가
탐색 키를 재구성해 범위 경계가 디스크의 dedup-보강 키와 일치하게 할 수 있으며,
그 경우 원본 키는 공유되지 않고 재구성된 복사본이 종료 시 해제된다(§10.10).
10.9 유일 통계: btree_unique_stats와 multi_index_unique_stats
섹션 제목: “10.9 유일 통계: btree_unique_stats와 multi_index_unique_stats”유니크 인덱스는 세 개의 카운터를 관리해 COUNT(*), COUNT(DISTINCT key), 유일성
검사를 스캔 없이 답한다. btree_unique_stats는 인덱스별 삼중 값(btree_unique.hpp의
stat_type m_rows, m_keys, m_nulls;)이다.
| 필드 | 역할 | 존재 이유 |
|---|---|---|
m_rows | NULL 키 포함 전체 객체(OID) 수 | COUNT(*)에 답함; 유일성 항등식의 좌변 |
m_keys | 실 키를 가진 비-NULL 객체 수 (행당 하나) | “충분히 고유한” 카운트; is_unique 계산에 참여 |
m_nulls | 인덱스 키가 NULL인 객체 수 | NULL은 유일성 면제 대상이므로 별도 집계 |
변경자는 두 카운터를 함께 이동한다. 하나만 건드리는 설정자는 없다.
// btree_unique_stats mutators -- src/storage/btree_unique.cpp void insert_key_and_row () { ++m_keys; ++m_rows; } void insert_null_and_row () { ++m_nulls; ++m_rows; } void add_row () { ++m_rows; } /* <- dup OID of existing key */ void delete_key_and_row () { --m_keys; --m_rows; } void delete_null_and_row () { --m_nulls; --m_rows; } void delete_row () { --m_rows; } bool is_unique () const { return m_rows == m_keys + m_nulls; }불변식 —
m_rows == m_keys + m_nulls. 구조적으로 강제된다.m_keys또는m_nulls를 변경하는 변경자는 같은 양만큼m_rows도 변경하고,add_row/delete_row(기존 키의 중복 OID)는m_rows만 이동한다. 유니크 인덱스는is_unique()가 참일 때 정합 상태다.m_rows > m_keys + m_nulls이면 어떤 키에 두 개의 살아있는 OID가 있어 위반이다.+=/-=연산자는 세 카운터를 컴포넌트별로 이동하므로 워커 스레드(§10.5)의 부분 통계를 집계해도 항등식은 깨지지 않는다.
multi_index_unique_stats는 한 구문이 건드린 여러 인덱스(다중 컬럼, 파티션,
온라인 빌더의 클래스별 루프)를 집계한다. btid_comparator는
(root_pageid, vfid.volid) 순으로 정렬한다.
| 필드 | 역할 | 존재 이유 |
|---|---|---|
m_stats_map | std::map<BTID, btree_unique_stats, btid_comparator> | 건드린 인덱스별 삼중 값; 맵 구조상 인덱스가 최대 한 번만 나타남 |
변경자는 operator+=에 위임한다. add_index_stats(index, us)는
m_stats_map[index] += us(non-null BTID 단언 포함)를 수행하고, operator+=(other)는
같은 방식으로 모든 항목을 접는다. construct/destruct는 객체가 외부 관리 메모리를
쓰는 트랜잭션 디스크립터 안에 있을 수 있으므로 배치 new/delete를 사용한다.
GLOBAL_UNIQUE_STATS_TABLE에 공급. 트랜잭션별 통계는 커밋 전까지 로컬이며,
커밋 시 서버 전역 GLOBAL_UNIQUE_STATS_TABLE(BTID 키의 LF_HASH_TABLE)로
플러시된다. 두 개의 복구 훅이 기록된 플러시를 재실행한다.
btree_rv_redo_global_unique_stats_commit은 (btid, num_nulls, num_oids, num_keys)를
언팩하고 logtb_rv_update_global_unique_stats_by_abs를 호출한다 — 절댓값 합계를
재실행(멱등). btree_rv_undo_global_unique_stats_commit은 같은 필드를 언팩하지만
logtb_update_global_unique_stats_by_delta를 부호 반전 카운트로 호출해 중단된
트랜잭션의 기여를 되돌린다. LOG_RECOVERY_UNDO_PHASE 중에는 먼저
disk_is_page_sector_reserved를 확인하고 인덱스 파일이 없으면 NO_ERROR를
반환한다 — 끊어진 참조를 허용하는 규칙을 지킨다. §10.1이 일괄 적재 undo를 위해
연결하는 RVBT_REMOVE_UNIQUE_STATS 생애주기다.
10.10 중복 키 모드: BTID_INT.deduplicate_key_idx
섹션 제목: “10.10 중복 키 모드: BTID_INT.deduplicate_key_idx”SUPPORT_DEDUPLICATE_KEY_MODE는 비유일 인덱스가 숨겨진 판별자 컬럼을 추가해
중복 키를 효율적으로 저장할 수 있게 한다. 위치는 적재 시 한 번
dk_get_deduplicate_key_position으로 계산해 btid_int.deduplicate_key_idx에 저장하고,
SET_DECOMPRESS_IDX_HEADER(root_header, load_args->btid->deduplicate_key_idx)로
루트 헤더에 영속화하며, 이후 열 때마다 GET_DECOMPRESS_IDX_HEADER로 읽는다.
세 곳이 일반 경로에서 벗어난다. (1) 범위 스캔 / 접두사 길이는
env->same_prefix_len = ...deduplicate_key_idx로 설정해 dedup 컬럼 경계가 사용자
키 접두사와 판별자 접미사를 구분하게 한다. (2) 외래 키 재구성(§10.8)은
is_newly/is_fk_remake 플래그로 dedup 컬럼을 포함하거나 제거하며 탐색
키를 재구성한다. (3) 같은 헤더 필드가 압축 인덱스 마커로도 사용된다
(SET/GET_DECOMPRESS_IDX_HEADER), 새 필드를 소비하지 않고 슬롯을 재사용한다.
일반 인덱스에서 deduplicate_key_idx는 dedup 컬럼이 없을 때 dk_get_deduplicate_key_position이
반환하는 sentinel이며, 세 분기 모두 3, 7, 8장의 일반 동작으로 귀결된다.
10.11 챕터 요약 — 핵심 정리
섹션 제목: “10.11 챕터 요약 — 핵심 정리”- 오프라인 일괄 적재는 비가시 상태로 구축하고 원자적으로 커밋한다.
xbtree_load_index는 힙을 정렬하고btree_construct_leafs콜백으로 리프를 왼쪽에서 오른쪽으로 채우고, 내부 레벨을 상향 구축하며, 런타임 래칭을 생략한다. 오류나 빈 결과는 파일 전체를 중단한다. - 정렬 입력은 적재 시 불변식이다.
btree_construct_leafs는btree_compare_key가 절대DB_LT를 반환하지 않음을 신뢰한다.assert_release는 비교자 버그를 묵음 오염이 아닌 즉각 실패로 전환한다. - 모든 적재 페이지 할당은 독립 커밋되는 sysop으로 감싼다.
btree_load_new_page는file_alloc을 독립 커밋되는 중첩 시스템 op으로 감싸고 클린업은 전체 파일 삭제에 의존한다.node_level이 리프/비리프와 오버플로를 구분한다. - 온라인 구축은 정렬 대신 스냅샷, 락 강등, 워커 풀을 사용한다.
xbtree_load_online_index는SCH_M_LOCK을IX_LOCK으로 강등하고online_index_builder로 배치BTREE_OP_ONLINE_INDEX_IB_INSERT태스크를 분배한 뒤, 중단 불가 루프에서 스키마 락을 반드시 재승격하고, 그 사이 끼어든 동시 변경분의 유일성을 재검사한다. - NULL 키는 추적하되 저장하지 않는다.
add_key는 NULL을 트리에서 건너뛰지만 유일 통계를 위해 계산해rows == keys + nulls를 보존한다. - 펜스 키는 접두사 압축의 기준점이며 읽기 경로가 건너뛴다.
슬롯 1의
BTREE_LEAF_RECORD_FENCE가 공통 접두사를 제공하고, 압축 해제기는 펜스를 압축 해제하지 않으며,btree_search_leaf_page는 equal-but-fence 일치에서 슬롯 1이면c = DB_GT로 통과하고 슬롯key_cnt이면BTREE_KEY_BIGGER를 반환해BTREE_KEY_FOUND대신 처리한다. - 유일 통계는 구조적으로 강제된 삼중 값으로 전역 해시에 공급된다.
btree_unique_stats는m_keys/m_nulls를m_rows와 연동해 이동하고,multi_index_unique_stats는BTID별로 집계하며, 커밋 시 절댓값으로 redo하고 델타 부호 반전으로 undo하는 방식으로GLOBAL_UNIQUE_STATS_TABLE에 플러시된다 (이미 삭제된 인덱스 허용). 빌드 시 루트 헤더에 찍히는deduplicate_key_idx는 범위 스캔, FK 재구성, 압축 경로를 dedup 키 모드로 전환한다.
이 리비전 시점의 위치 힌트
섹션 제목: “이 리비전 시점의 위치 힌트”다음 줄 번호는 2026-06-08 기준으로 관측한 값이다. 심벌이 정본 앵커이고, 줄 번호는 시간이 지나면서 어긋나는 힌트일 뿐이다.
| Symbol | File | Line |
|---|---|---|
OR_OID_SIZE | src/base/object_representation_constants.h | 67 |
OR_OID_PAGEID | src/base/object_representation_constants.h | 68 |
OR_OID_SLOTID | src/base/object_representation_constants.h | 69 |
OR_OID_VOLID | src/base/object_representation_constants.h | 70 |
BTREE_SPLIT_LOWER_BOUND | src/storage/btree.c | 79 |
BTREE_SPLIT_MIN_PIVOT | src/storage/btree.c | 82 |
BTREE_SPLIT_DEFAULT_PIVOT | src/storage/btree.c | 85 |
BTREE_NODE_MAX_SPLIT_SIZE | src/storage/btree.c | 88 |
CAN_MERGE_WHEN_EMPTY | src/storage/btree.c | 101 |
FORCE_MERGE_WHEN_EMPTY | src/storage/btree.c | 104 |
BTREE_LEAF_RECORD_FENCE | src/storage/btree.c | 113 |
BTREE_LEAF_RECORD_OVERFLOW_OIDS | src/storage/btree.c | 115 |
BTREE_LEAF_RECORD_OVERFLOW_KEY | src/storage/btree.c | 117 |
BTREE_LEAF_RECORD_CLASS_OID | src/storage/btree.c | 119 |
BTREE_LEAF_RECORD_MASK | src/storage/btree.c | 121 |
BTREE_OID_HAS_MVCC_INSID | src/storage/btree.c | 124 |
BTREE_OID_HAS_MVCC_DELID | src/storage/btree.c | 126 |
BTREE_RECORD_OR_BUF_INIT | src/storage/btree.c | 260 |
BTREE_GET_MVCC_INFO_SIZE_FROM_FLAGS | src/storage/btree.c | 278 |
BTREE_MERGE_NO | src/storage/btree.c | 311 |
BTREE_MERGE_TRY | src/storage/btree.c | 312 |
BTREE_MERGE_FORCE | src/storage/btree.c | 313 |
btree_search_key_helper | src/storage/btree.c | 358 |
BTREE_FIND_UNIQUE_HELPER | src/storage/btree.c | 387 |
BTS_IS_SOFT_CAPACITY_ENOUGH | src/storage/btree.c | 568 |
BTS_IS_HARD_CAPACITY_ENOUGH | src/storage/btree.c | 582 |
BTS_SAVE_OID_IN_BUFFER | src/storage/btree.c | 592 |
btree_insert_helper | src/storage/btree.c | 662 |
BTREE_INSERT_HELPER | src/storage/btree.c | 662 |
btree_delete_helper | src/storage/btree.c | 766 |
BTREE_DELETE_HELPER | src/storage/btree.c | 766 |
btree_or_get_object | src/storage/btree.c | 1458 |
btree_fix_root_with_info | src/storage/btree.c | 1850 |
btree_create_overflow_key_file | src/storage/btree.c | 1975 |
btree_leaf_record_change_overflow_link | src/storage/btree.c | 2318 |
btree_leaf_get_first_object | src/storage/btree.c | 2451 |
btree_record_get_num_oids | src/storage/btree.c | 2757 |
btree_leaf_change_first_object | src/storage/btree.c | 2822 |
btree_leaf_set_flag | src/storage/btree.c | 3461 |
btree_record_object_set_mvcc_flags | src/storage/btree.c | 3480 |
btree_append_oid | src/storage/btree.c | 3637 |
btree_record_append_object | src/storage/btree.c | 3798 |
btree_start_overflow_page | src/storage/btree.c | 3973 |
btree_get_disk_size_of_key | src/storage/btree.c | 4065 |
btree_write_record | src/storage/btree.c | 4100 |
btree_read_record | src/storage/btree.c | 4257 |
btree_read_record_without_decompression | src/storage/btree.c | 4515 |
btree_search_nonleaf_page | src/storage/btree.c | 5189 |
btree_leaf_is_key_between_min_max | src/storage/btree.c | 5369 |
btree_search_leaf_page | src/storage/btree.c | 5537 |
btree_find_foreign_key | src/storage/btree.c | 6361 |
btree_delete_key_from_leaf | src/storage/btree.c | 9653 |
btree_replace_first_oid_with_ovfl_oid | src/storage/btree.c | 9781 |
btree_modify_leaf_ovfl_vpid | src/storage/btree.c | 9978 |
btree_modify_overflow_link | src/storage/btree.c | 10054 |
btree_delete_meta_record | src/storage/btree.c | 10148 |
btree_write_default_split_info | src/storage/btree.c | 10247 |
btree_merge_root | src/storage/btree.c | 10284 |
btree_merge_node | src/storage/btree.c | 10557 |
btree_node_size_uncompressed | src/storage/btree.c | 11097 |
btree_node_mergeable | src/storage/btree.c | 11172 |
btree_key_append_object_as_new_overflow | src/storage/btree.c | 11330 |
btree_find_oid_and_its_page | src/storage/btree.c | 11646 |
btree_find_oid_does_mvcc_info_match | src/storage/btree.c | 11794 |
btree_find_oid_from_leaf | src/storage/btree.c | 11946 |
btree_find_oid_from_ovfl | src/storage/btree.c | 12037 |
btree_find_split_point | src/storage/btree.c | 12419 |
btree_split_find_pivot | src/storage/btree.c | 12849 |
btree_split_next_pivot | src/storage/btree.c | 12874 |
btree_recompress_record | src/storage/btree.c | 13118 |
btree_split_node | src/storage/btree.c | 13324 |
btree_split_root | src/storage/btree.c | 14184 |
btree_locate_key | src/storage/btree.c | 14882 |
btree_keyval_search | src/storage/btree.c | 15458 |
btree_scan_update_range | src/storage/btree.c | 16055 |
btree_rv_util_save_page_records | src/storage/btree.c | 17242 |
btree_get_next_page | src/storage/btree.c | 19384 |
btree_set_vpid_previous_vpid | src/storage/btree.c | 19435 |
btree_compare_key | src/storage/btree.c | 19460 |
btree_check_foreign_key | src/storage/btree.c | 22655 |
btree_rv_undo_global_unique_stats_commit | src/storage/btree.c | 23050 |
btree_rv_redo_global_unique_stats_commit | src/storage/btree.c | 23120 |
btree_search_key_and_apply_functions | src/storage/btree.c | 23186 |
btree_get_root_with_key | src/storage/btree.c | 23390 |
btree_advance_and_find_key | src/storage/btree.c | 23455 |
btree_key_find_and_lock_unique | src/storage/btree.c | 23645 |
btree_key_find_and_lock_unique_of_unique | src/storage/btree.c | 23673 |
btree_key_find_and_lock_unique_of_non_unique | src/storage/btree.c | 23903 |
btree_range_scan_start | src/storage/btree.c | 24926 |
btree_range_scan_resume | src/storage/btree.c | 25024 |
btree_range_scan_read_record | src/storage/btree.c | 25185 |
btree_range_scan_advance_over_filtered_keys | src/storage/btree.c | 25230 |
btree_range_scan_descending_fix_prev_leaf | src/storage/btree.c | 25451 |
btree_range_scan | src/storage/btree.c | 25794 |
btree_range_scan_select_visible_oids | src/storage/btree.c | 26008 |
btree_select_visible_object_for_range_scan | src/storage/btree.c | 26330 |
btree_range_scan_find_fk_any_object | src/storage/btree.c | 26545 |
btree_insert_internal | src/storage/btree.c | 26976 |
btree_fix_root_for_insert | src/storage/btree.c | 27154 |
btree_get_max_new_data_size | src/storage/btree.c | 27429 |
btree_split_node_and_advance | src/storage/btree.c | 27495 |
btree_key_insert_new_object | src/storage/btree.c | 28120 |
btree_key_insert_new_key | src/storage/btree.c | 28302 |
btree_key_lock_and_append_object_unique | src/storage/btree.c | 28572 |
btree_key_append_object_non_unique | src/storage/btree.c | 28927 |
btree_key_append_object_unique | src/storage/btree.c | 29042 |
btree_key_relocate_last_into_ovf | src/storage/btree.c | 29156 |
btree_key_append_object_into_ovf | src/storage/btree.c | 29309 |
btree_delete_internal | src/storage/btree.c | 30525 |
btree_fix_root_for_delete | src/storage/btree.c | 30687 |
btree_merge_node_and_advance | src/storage/btree.c | 30895 |
btree_key_delete_remove_object | src/storage/btree.c | 31493 |
btree_key_remove_object_and_keep_visible_first | src/storage/btree.c | 31753 |
btree_leaf_record_replace_first_with_last | src/storage/btree.c | 32046 |
btree_record_remove_object | src/storage/btree.c | 32137 |
btree_record_remove_object_internal | src/storage/btree.c | 32230 |
btree_key_remove_object | src/storage/btree.c | 32298 |
btree_overflow_remove_object | src/storage/btree.c | 32344 |
btree_leaf_remove_object | src/storage/btree.c | 32517 |
btree_key_remove_insert_mvccid | src/storage/btree.c | 32637 |
btree_key_remove_delete_mvccid | src/storage/btree.c | 32818 |
btree_key_remove_delete_mvccid_unique | src/storage/btree.c | 32986 |
btree_remove_delete_mvccid_unique_internal | src/storage/btree.c | 33113 |
btree_key_remove_delete_mvccid_non_unique | src/storage/btree.c | 33257 |
btree_record_remove_insid | src/storage/btree.c | 33419 |
btree_record_remove_delid | src/storage/btree.c | 33479 |
btree_online_index_list_dispatcher | src/storage/btree.c | 34209 |
btree_rv_log_delete_object | src/storage/btree.c | 35823 |
btree_rv_log_insert_object | src/storage/btree.c | 35885 |
BTREE_NEED_UNIQUE_CHECK | src/storage/btree.h | 63 |
BTREE_NODE_TYPE | src/storage/btree.h | 81 |
non_leaf_rec | src/storage/btree.h | 103 |
leaf_rec | src/storage/btree.h | 111 |
btid_int | src/storage/btree.h | 119 |
BTID_INT.deduplicate_key_idx | src/storage/btree.h | 133 |
btree_keyrange | src/storage/btree.h | 139 |
bts_key_status | src/storage/btree.h | 151 |
btree_scan | src/storage/btree.h | 198 |
BTREE_INIT_SCAN | src/storage/btree.h | 297 |
BTREE_END_OF_SCAN | src/storage/btree.h | 368 |
btree_op_purpose | src/storage/btree.h | 535 |
BTREE_OP_DELETE_OBJECT_PHYSICAL | src/storage/btree.h | 549 |
BTREE_OP_ONLINE_INDEX_IB_INSERT | src/storage/btree.h | 565 |
btree_mvcc_info | src/storage/btree.h | 581 |
btree_object_info | src/storage/btree.h | 594 |
BTREE_RANGE_SCAN_PROCESS_KEY_FUNC | src/storage/btree.h | 710 |
btree_node_header_undo_log | src/storage/btree_load.c | 410 |
btree_node_header_redo_log | src/storage/btree_load.c | 432 |
xbtree_load_index | src/storage/btree_load.c | 856 |
dk_get_deduplicate_key_position | src/storage/btree_load.c | 925 |
btree_save_last_leafrec | src/storage/btree_load.c | 1275 |
SET_DECOMPRESS_IDX_HEADER | src/storage/btree_load.c | 1929 |
btree_load_new_page | src/storage/btree_load.c | 2030 |
btree_proceed_leaf | src/storage/btree_load.c | 2122 |
btree_first_oid | src/storage/btree_load.c | 2199 |
bt_load_make_new_record_on_leaf_page | src/storage/btree_load.c | 2582 |
LOAD_FIXED_EMPTY_FOR_LEAF | src/storage/btree_load.c | 2591 |
btree_construct_leafs | src/storage/btree_load.c | 3011 |
xbtree_load_online_index | src/storage/btree_load.c | 4992 |
online_index_builder | src/storage/btree_load.c | 5287 |
index_builder_loader_task::add_key | src/storage/btree_load.c | 5526 |
index_builder_loader_task::execute | src/storage/btree_load.c | 5562 |
LEAF_FENCE_MAX_SIZE | src/storage/btree_load.h | 89 |
NON_LEAF_ENTRY_MAX_SIZE | src/storage/btree_load.h | 96 |
BTREE_OBJECT_FIXED_SIZE | src/storage/btree_load.h | 130 |
BTREE_MAX_KEYLEN_INPAGE | src/storage/btree_load.h | 147 |
BTREE_MAX_OIDCOUNT_IN_LEAF_RECORD | src/storage/btree_load.h | 155 |
BTREE_GET_KEY_LEN_IN_PAGE | src/storage/btree_load.h | 167 |
btree_node_header | src/storage/btree_load.h | 206 |
btree_root_header | src/storage/btree_load.h | 218 |
btree_unique_stats::insert_key_and_row | src/storage/btree_unique.cpp | 72 |
btree_unique_stats::is_unique | src/storage/btree_unique.cpp | 118 |
multi_index_unique_stats::add_index_stats | src/storage/btree_unique.cpp | 168 |
btree_unique_stats | src/storage/btree_unique.hpp | 34 |
multi_index_unique_stats | src/storage/btree_unique.hpp | 69 |
spage_find_empty_slot_at | src/storage/slotted_page.c | 1674 |
spage_insert_at | src/storage/slotted_page.c | 1902 |
spage_header | src/storage/slotted_page.h | 64 |
spage_slot | src/storage/slotted_page.h | 88 |
NON_LEAF_RECORD_SIZE | src/storage/storage_common.h | 134 |
LEAF_RECORD_SIZE | src/storage/storage_common.h | 136 |
btree_node_split_info | src/storage/storage_common.h | 140 |
BTREE_SEARCH | src/storage/storage_common.h | 399 |
BTREE_CONSTRAINT_UNIQUE | src/storage/storage_common.h | 616 |
BTREE_CONSTRAINT_PRIMARY_KEY | src/storage/storage_common.h | 617 |
Sources
섹션 제목: “Sources”cubrid-btree.md— 상위 동반 문서 (설계 의도, 이론).raw/code-analysis/cubrid/storage/index/아래의 원시 분석.- 코드 —
src/storage/btree.{c,h},btree_load.{c,h},btree_unique.{cpp,hpp}; slotted page 레이아웃은src/storage/slotted_page.h에. - 방법론 —
knowledge/methodology/code-analysis-detail-doc.md.