(KO) PostgreSQL 테이블 접근 방법 — TableAmRoutine을 통한 플러그형 스토리지 디스패치
목차
- 학술적 배경
- DBMS 공통 설계 패턴
- PostgreSQL의 구현
- 소스 코드 가이드
- 소스 검증 (2026-06-05 기준)
- PostgreSQL 너머 — 비교 설계와 연구 프론티어
- 출처
학술적 배경
섹션 제목: “학술적 배경”데이터베이스 엔진의 스토리지 계층은 논리 모델(릴레이션, 튜플, 스냅샷)과 물리 매체 사이에 위치한다. 모든 릴레이션은 구체적인 스토리지 구현이 필요하다. 이 구현이 답해야 할 질문은 네 가지다. 튜플이 메모리 어디에 고정(pin)되는가, 디스크 위에 어떻게 배치되는가, 스캔은 어떻게 진행되는가, 그리고 삽입·갱신·삭제 시 무슨 일이 일어나는가. 이 설계에서 핵심 선택은 그 구현이 고정되는지 교체 가능한지에 있다.
고정 모델에서는 익스큐터 코드가 스토리지 함수를 직접 호출한다. 단순하고 빠르지만 두 관심사가 뒤섞인다. 익스큐터가 필요로 하는 것(가시적 튜플 위의 커서, 행을 삽입하는 방법)과 그 필요를 충족하는 방법(MVCC 튜플이 담긴 슬롯 페이지 힙)이 분리되지 않는다. 이 둘을 분리하려면 간접 계층이 필요하다. 익스큐터는 안정적인 인터페이스를 호출하고, 스토리지 구현은 릴레이션 오픈 시점에 선택되어 디스패치 테이블에 연결된다. 이 구조는 객체지향 설계에서 말하는 전형적인 vtable 또는 전략 객체(strategy object) 패턴이다(Database Internals, Petrov, 3장; Database System Concepts, Silberschatz 외, 7판, 13장).
플러그형 스토리지의 실질적 동기는 워크로드 다양성이다. Stonebraker의 버클리 POSTGRES 계보에서 이어진 덮어쓰지 않는 MVCC 힙은 혼합 읽기-쓰기 OLTP에 탁월하지만, 분석 워크로드나 추가 전용 워크로드에서는 컬럼형이나 인메모리 방식이 제거할 수 있는 비용(튜플 팽창, vacuum 부담, 기본 컬럼 압축 없음)을 수반한다. 스토리지 모델이 익스큐터에 하드코딩되어 있다면 교체는 엔진 전체를 포크해야 함을 의미한다. 간접 계층이 있으면 각 스토리지 구현을 플러그인으로 만들어 쿼리 파이프라인을 공유할 수 있다.
플러그형 스토리지에 대한 고전적 논의는 Stonebraker & Rowe 1986 (The Design of POSTGRES)에서 찾을 수 있다. 그 논문은 쿼리 처리와 스토리지 관리를 분리하는 “추상 데이터 관리자(abstract data manager)” 인터페이스를 제안했다. PostgreSQL 12의 TableAmRoutine은 그 비전의 프로덕션 실현에 해당한다. 유사한 형태의 인덱스 접근 방법 인터페이스(amapi.h의 IndexAmRoutine)가 먼저 있었고, 테이블 AM 인터페이스는 그와 같은 모양을 본떴다.
DBMS 공통 설계 패턴
섹션 제목: “DBMS 공통 설계 패턴”플러그형 스토리지 인터페이스에서 여러 시스템에 걸쳐 반복되는 설계 관행이 있다.
디스패치 메커니즘으로서의 함수 포인터 테이블
섹션 제목: “디스패치 메커니즘으로서의 함수 포인터 테이블”vtable의 C 관용 구현은 함수 포인터 구조체다. 구조체의 각 슬롯은 추상화에 대한 하나의 연산에 대응하고, 구체적 구현이 자신의 함수로 포인터를 채운다. 릴레이션(메모리 내 테이블 객체)은 해당 vtable에 대한 포인터를 가지므로, 디스패처 코드는 relation->am_routine->some_op(...) 형태로 어떤 AM이 사용 중인지 알 필요 없이 호출한다.
이 방식의 장점은 세 가지다. 첫째, 호출 측은 어떤 AM이 동작 중인지 알 필요가 없다. 둘째, 새로운 AM을 추가할 때 코어 코드를 수정하지 않아도 된다. 셋째, vtable이 static const 구조체로 세션당 한 번만 할당되므로 역참조 비용은 포인터 추적 두 번뿐이다(릴레이션 → vtable → 함수).
필수 콜백과 선택적 콜백
섹션 제목: “필수 콜백과 선택적 콜백”모든 AM이 모든 연산에 의미를 부여할 수 있는 것은 아니다. 인메모리 AM은 vacuum 개념 자체가 없을 수 있고, 컬럼형 AM은 행 수준 잠금을 지원하지 않을 수 있다. 이에 대한 관행은 핵심 콜백 집합을 필수(루틴 검증자가 NULL이 아님을 단언)로 표시하고, 나머지는 NULL을 허용하는 선택적 항목으로 두는 것이다. 검증자는 등록 시 한 번 실행되며, 필수 슬롯이 누락되면 런타임 null 포인터 충돌 대신 명확한 오류를 즉시 발생시킨다.
공유 DML 결과 어휘
섹션 제목: “공유 DML 결과 어휘”DML 연산(갱신, 삭제, 잠금)이 다른 트랜잭션도 건드리는 튜플을 수정하려 할 때, 가능한 결과는 여럿이다. 연산 성공, 같은 명령에서 이미 자신이 수정, 커밋된 다른 트랜잭션에 의해 갱신, 진행 중인 동시 트랜잭션이 수정 중, 또는 대기 없이 실행하도록 요청받았는데 블록될 상황 등이다. 잘 설계된 인터페이스는 이 결과 공간을 열거형(enum)으로 포착하고 오류를 raise하는 대신 호출 측에 반환한다. 이렇게 하면 호출 측이 재시도·오류 처리·건너뛰기 중 정책을 직접 선택할 수 있다.
스캔 생명주기 — begin → getnextslot → rescan/end
섹션 제목: “스캔 생명주기 — begin → getnextslot → rescan/end”순차 스캔은 거의 모든 스토리지 인터페이스에서 세 단계 생명주기를 따른다. begin은 스캔 디스크립터를 할당하고 릴레이션을 핀한다. getnextslot 반복 호출이 커서를 진행시키며 한 번에 튜플 하나를 호출자 제공 슬롯에 담아 반환한다. end는 자원을 해제한다. rescan은 디스크립터를 해제하지 않고 커서를 재시작해, 같은 스캔을 반복해야 할 때(예: 중첩 루프 조인 내부 측) begin/end 오버헤드를 상각한다.
이론 ↔ PostgreSQL 테이블 AM 대응표
섹션 제목: “이론 ↔ PostgreSQL 테이블 AM 대응표”| 설계 개념 | PostgreSQL 테이블 AM 이름 |
|---|---|
| 함수 포인터 vtable | TableAmRoutine (tableam.h) |
| 릴레이션에 등록된 vtable | Relation.rd_tableam (relcache 필드) |
| DML 결과 어휘 | TM_Result 열거형 |
| DML 실패 세부 정보 | TM_FailureData 구조체 |
| 스캔 디스크립터 | TableScanDescData / TableScanDesc |
| 인덱스 페치 상태 | IndexFetchTableData |
| 필수 콜백 검증자 | GetTableAmRoutine (tableamapi.c) |
| REL_18 기준 유일한 인-트리 AM | heapam_methods (heapam_handler.c) |
PostgreSQL의 구현
섹션 제목: “PostgreSQL의 구현”AM이 릴레이션에 연결되는 방식
섹션 제목: “AM이 릴레이션에 연결되는 방식”백엔드가 릴레이션을 열면(table_open → heap_open → RelationIdGetRelation), relcache 코드는 해당 릴레이션의 pg_class 행에서 pg_am.amhandler를 읽어 GetTableAmRoutine(amhandler)를 호출하고, 반환된 const TableAmRoutine *을 rel->rd_tableam에 저장한다. 이후 그 릴레이션에서 발생하는 모든 table_* 호출은 이 포인터로 디스패치된다. 내장 힙 AM의 핸들러 OID는 heap_tableam_handler로 해석되며, 이 함수는 단순히 &heapam_methods를 반환한다.
flowchart TB REL["Relation (relcache)\nrd_tableam → &heapam_methods"] EXEC["익스큐터\n(nodeSeqscan, nodeIndexscan,\nexecModifyTable, vacuum.c)"] WRAP["table_* 인라인 래퍼\n(tableam.h)"] RT["const TableAmRoutine *\n= rel->rd_tableam"] HM["heapam_methods\n(static const TableAmRoutine)"] IMPL["heapam_handler.c / heapam.c\n(힙 구현)"] EXEC --> WRAP WRAP --> RT RT --> HM HM --> IMPL REL --> RT
그림 1 — 디스패치 체인. 익스큐터는 tableam.h의 table_* 인라인 래퍼를 호출하고, 래퍼는 rel->rd_tableam을 읽어 함수 포인터 슬롯으로 디스패치한다. REL_18에서 모든 일반 테이블은 heapam_methods에 바인딩되며, 이는 heapam_handler.c를 거쳐 최종적으로 heapam.c에 위임한다.
TableAmRoutine 구조체 — 영역별 콜백 목록
섹션 제목: “TableAmRoutine 구조체 — 영역별 콜백 목록”TableAmRoutine은 src/include/access/tableam.h 288번 줄부터 선언된다. 약 40개의 콜백 슬롯이 여섯 기능 영역으로 구성된다.
flowchart LR
subgraph TAR["TableAmRoutine (tableam.h:288)"]
direction TB
S["슬롯\nslot_callbacks"]
SC["순차 스캔\nscan_begin\nscan_end\nscan_rescan\nscan_getnextslot\nscan_set_tidrange\nscan_getnextslot_tidrange\nparallelscan_*"]
IF["인덱스 페치\nindex_fetch_begin\nindex_fetch_reset\nindex_fetch_end\nindex_fetch_tuple"]
TV["튜플 가시성\ntuple_fetch_row_version\ntuple_tid_valid\ntuple_get_latest_tid\ntuple_satisfies_snapshot\nindex_delete_tuples"]
DML["DML\ntuple_insert\ntuple_insert_speculative\ntuple_complete_speculative\nmulti_insert\ntuple_delete\ntuple_update\ntuple_lock\nfinish_bulk_insert"]
DDL["DDL / vacuum / analyze\nrelation_set_new_filelocator\nrelation_nontransactional_truncate\nrelation_copy_data\nrelation_copy_for_cluster\nrelation_vacuum\nscan_analyze_next_block\nscan_analyze_next_tuple\nindex_build_range_scan\nindex_validate_scan"]
MISC["기타 / 플래너\nrelation_size\nrelation_needs_toast_table\nrelation_toast_am\nrelation_fetch_toast_slice\nrelation_estimate_size\nscan_bitmap_next_tuple\nscan_sample_next_block\nscan_sample_next_tuple"]
end
그림 2 — TableAmRoutine 콜백 목록. 약 40개 슬롯 전체를 영역별로 묶었다. DML과 스캔 그룹은 필수(GetTableAmRoutine이 단언)다. finish_bulk_insert, relation_toast_am, relation_fetch_toast_slice 콜백은 선택적(NULL 허용)이다.
TM_Result — 공유 DML 결과 열거형
섹션 제목: “TM_Result — 공유 DML 결과 열거형”TM_Result는 tuple_delete, tuple_update, tuple_lock의 반환 타입이다. 이 열거형은 모든 MVCC 엔진이 처리해야 하는 결과 공간을 포착한다.
// TM_Result — src/include/access/tableam.htypedef enum TM_Result{ TM_Ok, /* 연산 성공 */ TM_Invisible, /* 해당 스냅샷에서 튜플 미가시 */ TM_SelfModified, /* 이 백엔드가 이미 수정 */ TM_Updated, /* 커밋된 다른 트랜잭션이 갱신 */ TM_Deleted, /* 커밋된 다른 트랜잭션이 삭제 */ TM_BeingModified, /* 동시 진행 중 수정 (nowait 전용) */ TM_WouldBlock, /* 잠금 불가, nowait, 건너뜀 (lock_tuple 전용) */} TM_Result;DML 호출이 TM_Ok 이외의 값을 반환하면, AM이 채운 TM_FailureData 구조체가 현재 ctid(체인 끝), 갱신한 xmax, 그리고 TM_SelfModified의 경우 충돌 명령의 cmax를 담는다. 익스큐터는 이를 토대로 재시도, 오류 발생, 또는 (READ COMMITTED 격리 수준에서) 최신 버전을 다시 가져와 자격 조건을 재평가할지 결정한다.
flowchart TD CALL["table_tuple_delete / update / lock"] --> AM["AM 콜백\ntuple_delete / update / lock"] AM --> OK["TM_Ok\n→ 완료"] AM --> INV["TM_Invisible\n→ 튜플 없음"] AM --> SELF["TM_SelfModified\n→ 이 명령에서 이미 처리"] AM --> UPD["TM_Updated\n→ 익스큐터가 최신 ctid 재페치\n(READ COMMITTED)"] AM --> DEL["TM_Deleted\n→ 튜플 삭제됨"] AM --> BM["TM_BeingModified\n→ 대기 또는 건너뜀 (nowait)"] AM --> WB["TM_WouldBlock\n→ lock_tuple nowait 전용"]
그림 3 — TM_Result 결정 트리. 익스큐터 호출 측은 반환 코드를 검사하고, TM_Updated / TM_Deleted의 경우 TM_FailureData.ctid를 사용해 후계 튜플을 찾아 재시도한다. 힙 AM은 튜플의 t_ctid와 t_xmax에서 TM_FailureData를 채운다. tableam.c의 simple_table_tuple_delete와 simple_table_tuple_update가 가장 단순한 호출 측 예시다.
스캔 생명주기와 ScanOptions
섹션 제목: “스캔 생명주기와 ScanOptions”scan_begin은 두 가지를 전달하는 ScanOptions 플래그 비트마스크를 받는다. 하나는 스캔 유형(정확히 하나: SO_TYPE_SEQSCAN, SO_TYPE_BITMAPSCAN, SO_TYPE_SAMPLESCAN, SO_TYPE_TIDSCAN, SO_TYPE_TIDRANGESCAN, SO_TYPE_ANALYZE)이고, 다른 하나는 동작 힌트(0개 이상: SO_ALLOW_STRAT, SO_ALLOW_SYNC, SO_ALLOW_PAGEMODE)다. AM은 지원하지 않는 힌트를 무시해도 된다.
생명주기 상태 기계는 다음과 같다.
stateDiagram-v2 [*] --> 스캔중 : table_beginscan\nScanOptions 플래그 설정 스캔중 --> 스캔중 : table_scan_getnextslot\ntrue 반환 스캔중 --> 완료 : table_scan_getnextslot\nfalse 반환 스캔중 --> 스캔중 : table_rescan\n커서 재시작 완료 --> [*] : table_endscan\n자원 해제 스캔중 --> [*] : table_endscan\n조기 종료
그림 4 — 스캔 생명주기 상태 기계. 익스큐터는 table_beginscan을 호출하고, table_scan_getnextslot 루프를 돌다가 table_endscan으로 마친다. table_rescan은 디스크립터를 해제하지 않고 커서를 재시작한다. 이는 중첩 루프 조인 재실행과 ExecReScanSeqScan에서 사용된다.
table_beginscan 인라인 래퍼는 표준 순차 스캔 플래그를 설정한다.
// table_beginscan — src/include/access/tableam.hstatic inline TableScanDesctable_beginscan(Relation rel, Snapshot snapshot, int nkeys, struct ScanKeyData *key){ uint32 flags = SO_TYPE_SEQSCAN | SO_ALLOW_STRAT | SO_ALLOW_SYNC | SO_ALLOW_PAGEMODE;
return rel->rd_tableam->scan_begin(rel, snapshot, nkeys, key, NULL, flags);}table_scan_getnextslot은 슬롯의 tts_tableOid를 찍고 디스패치한다.
// table_scan_getnextslot — src/include/access/tableam.hstatic inline booltable_scan_getnextslot(TableScanDesc sscan, ScanDirection direction, TupleTableSlot *slot){ slot->tts_tableOid = RelationGetRelid(sscan->rs_rd); /* ... 논리 디코딩을 위한 CheckXidAlive 가드 ... */ return sscan->rs_rd->rd_tableam->scan_getnextslot(sscan, direction, slot);}익스큐터 호출 지점 — nodeSeqscan.c
섹션 제목: “익스큐터 호출 지점 — nodeSeqscan.c”SeqNext는 ExecSeqScan 내부에서 튜플 단위로 작동하는 워크호스(workhorse)다. 이 함수는 정확히 두 개의 테이블 AM 함수를 호출한다. 첫 번째 호출에서 table_beginscan으로 지연 초기화하고, 이후 매 호출마다 table_scan_getnextslot을 사용한다.
// SeqNext — src/backend/executor/nodeSeqscan.cstatic TupleTableSlot *SeqNext(SeqScanState *node){ TableScanDesc scandesc = node->ss.ss_currentScanDesc; TupleTableSlot *slot = node->ss.ss_ScanTupleSlot;
if (scandesc == NULL) { scandesc = table_beginscan(node->ss.ss_currentRelation, estate->es_snapshot, 0, NULL); node->ss.ss_currentScanDesc = scandesc; }
if (table_scan_getnextslot(scandesc, direction, slot)) return slot; return NULL;}ExecInitSeqScan은 초기화 시 table_slot_callbacks를 호출해 적절한 TupleTableSlotOps 구현을 선택한다. 힙의 경우 TTSOpsBufferHeapTuple이 반환된다. 이는 버퍼에 핀된 힙 튜플을 복사 없이 담을 수 있는 슬롯 타입으로, 일반적인 읽기 경로에서 모든 튜플을 복사하는 비용을 피하는 방식이다.
ExecEndSeqScan은 단순히 table_endscan을 호출한다. ExecReScanSeqScan은 table_rescan(scan, NULL)을 호출해 디스크립터를 해제하지 않고 스캔 위치를 재설정한다.
인덱스 페치 호출 지점 — table_index_fetch_tuple
섹션 제목: “인덱스 페치 호출 지점 — table_index_fetch_tuple”인덱스 스캔은 인덱스 순회와 힙 페치를 분리한다. AM은 별도의 IndexFetchTableData 상태 객체를 제공한다(table_index_fetch_begin으로 시작, table_index_fetch_end로 해제). 인덱스가 내어 주는 각 TID마다 익스큐터는 다음을 호출한다.
// table_index_fetch_tuple — src/include/access/tableam.hstatic inline booltable_index_fetch_tuple(struct IndexFetchTableData *scan, ItemPointer tid, Snapshot snapshot, TupleTableSlot *slot, bool *call_again, bool *all_dead){ /* ... CheckXidAlive 가드 ... */ return scan->rel->rd_tableam->index_fetch_tuple(scan, tid, snapshot, slot, call_again, all_dead);}call_again 출력 파라미터는 HOT을 위한 연결 고리다. 단일 인덱스 항목은 HOT 체인을 거슬러 여러 힙 버전에 닿을 수 있으므로, 같은 TID에서 반환할 버전이 하나 더 있을 때 AM은 *call_again = true를 설정한다. all_dead 출력은 어떤 백엔드도 이 튜플을 볼 수 없음을 AM이 인덱스 AM에 알리는 수단으로, 인덱스 항목을 dead로 표시해 이후 스캔에서 건너뛸 수 있게 한다.
GetTableAmRoutine — 등록과 검증
섹션 제목: “GetTableAmRoutine — 등록과 검증”GetTableAmRoutine(tableamapi.c)은 릴레이션 오픈 시 호출되는 팩토리 함수다. 핸들러 OID 함수를 실행해 TableAmRoutine *을 받고, 모든 필수 콜백이 NULL이 아님을 단언한다.
// GetTableAmRoutine — src/backend/access/table/tableamapi.c (요약)const TableAmRoutine *GetTableAmRoutine(Oid amhandler){ Datum datum = OidFunctionCall0(amhandler); const TableAmRoutine *routine = (TableAmRoutine *) DatumGetPointer(datum);
if (routine == NULL || !IsA(routine, TableAmRoutine)) elog(ERROR, "table access method handler %u did not return " "a TableAmRoutine struct", amhandler);
Assert(routine->scan_begin != NULL); Assert(routine->scan_end != NULL); Assert(routine->scan_getnextslot != NULL); Assert(routine->index_fetch_begin != NULL); Assert(routine->index_fetch_tuple != NULL); Assert(routine->tuple_insert != NULL); Assert(routine->tuple_delete != NULL); Assert(routine->tuple_update != NULL); Assert(routine->relation_vacuum != NULL); /* ... 약 30개의 필수 단언 추가 ... */
return routine;}heapam_methods — 레퍼런스 구현
섹션 제목: “heapam_methods — 레퍼런스 구현”힙 AM의 vtable은 heapam_handler.c의 static const 구조체다. 모든 필수 슬롯이 채워져 있고, 일부 선택적 항목(finish_bulk_insert)도 제공된다. 주요 바인딩은 다음과 같다.
// heapam_methods — src/backend/access/heap/heapam_handler.c (요약)static const TableAmRoutine heapam_methods = { .type = T_TableAmRoutine,
.slot_callbacks = heapam_slot_callbacks,
.scan_begin = heap_beginscan, .scan_end = heap_endscan, .scan_rescan = heap_rescan, .scan_getnextslot = heap_getnextslot,
.index_fetch_begin = heapam_index_fetch_begin, .index_fetch_reset = heapam_index_fetch_reset, .index_fetch_end = heapam_index_fetch_end, .index_fetch_tuple = heapam_index_fetch_tuple,
.tuple_insert = heapam_tuple_insert, .multi_insert = heap_multi_insert, .tuple_delete = heapam_tuple_delete, .tuple_update = heapam_tuple_update, .tuple_lock = heapam_tuple_lock,
.tuple_satisfies_snapshot = heapam_tuple_satisfies_snapshot, .index_delete_tuples = heap_index_delete_tuples,
.relation_vacuum = heap_vacuum_rel, .relation_size = table_block_relation_size, .relation_estimate_size = heapam_estimate_rel_size,
/* ... 나머지 모든 슬롯 ... */};SQL에서 참조 가능한 amhandler 함수인 heap_tableam_handler는 단순히 &heapam_methods를 반환한다. 힙임을 전제로 한 내부 코드에서 사용하는 GetHeapamTableAmRoutine도 동일한 포인터를 직접 반환한다.
table_tuple_insert — DML 호출 종단 간 추적
섹션 제목: “table_tuple_insert — DML 호출 종단 간 추적”삽입 래퍼는 세 줄이다. AM으로 디스패치하는 것이 전부다.
// table_tuple_insert — src/include/access/tableam.hstatic inline voidtable_tuple_insert(Relation rel, TupleTableSlot *slot, CommandId cid, int options, struct BulkInsertStateData *bistate){ rel->rd_tableam->tuple_insert(rel, slot, cid, options, bistate);}힙 구현(heapam_handler.c의 heapam_tuple_insert)은 슬롯에서 HeapTuple을 꺼내 heapam.c의 heap_insert를 호출한다. 슬롯은 익스큐터의 컬럼형 행 표현을 담고 있으며, AM이 그것을 자신의 온디스크 형식으로 직렬화할 책임을 진다. 힙의 경우 그것은 23바이트 HeapTupleHeaderData 접두사와 데이터다. 전체 레이아웃은 postgres-heap-am.md에서 다룬다.
Vacuum 디스패치
섹션 제목: “Vacuum 디스패치”table_relation_vacuum은 VACUUM을 AM으로 라우팅하는 래퍼다.
// table_relation_vacuum — src/include/access/tableam.hstatic inline voidtable_relation_vacuum(Relation rel, struct VacuumParams *params, BufferAccessStrategy bstrategy){ rel->rd_tableam->relation_vacuum(rel, params, bstrategy);}vacuum.c는 ShareUpdateExclusiveLock을 획득한 뒤 이를 호출한다. 힙 AM의 경우 heapam_methods 슬롯이 heap_vacuum_rel로 연결되어 있다. dead 튜플이 쌓이지 않는 인메모리 AM은 이 슬롯을 no-op 또는 최소한의 구현으로 연결해도 된다.
소스 코드 가이드
섹션 제목: “소스 코드 가이드”심볼 이름을 기준으로 삼고, 줄 번호에 의존하지 않는다. 아래 위치 힌트 표의 줄 번호는 커밋
273fe94(REL_18_STABLE, 2026-06-05) 기준 빠른 참고용이다. 현재 위치는git grep -n '<심볼>'로 확인한다.
핵심 인터페이스 (src/include/access/tableam.h)
섹션 제목: “핵심 인터페이스 (src/include/access/tableam.h)”typedef enum TM_Result— DML 결과 코드(TM_Ok,TM_Updated,TM_Deleted,TM_SelfModified,TM_BeingModified,TM_WouldBlock,TM_Invisible).typedef struct TM_FailureData— 실패 세부 구조체:ctid,xmax,cmax,traversed.typedef enum TU_UpdateIndexes—tuple_update가 반환하는 인덱스 갱신 힌트:TU_None(HOT, 인덱스 갱신 불필요),TU_All,TU_Summarizing.typedef enum ScanOptions—scan_begin비트마스크:SO_TYPE_*(스캔 유형, 정확히 하나)와SO_ALLOW_*(동작 힌트, 0개 이상).typedef struct TableAmRoutine— 약 40개 슬롯의 함수 포인터 vtable.table_beginscan/table_endscan/table_rescan/table_scan_getnextslot— 순차 스캔 생명주기 인라인 래퍼.table_index_fetch_begin/table_index_fetch_end/table_index_fetch_tuple— 인덱스 페치 생명주기 인라인 래퍼.call_again과all_dead출력 파라미터 포함.table_tuple_insert/table_tuple_delete/table_tuple_update/table_tuple_lock—void또는TM_Result를 반환하는 DML 인라인 래퍼.table_relation_vacuum— vacuum 디스패치 래퍼.DEFAULT_TABLE_ACCESS_METHOD— 컴파일 타임 상수"heap".default_table_access_methodGUC의 기본값이기도 하다.
등록과 검증 (src/backend/access/table/tableamapi.c)
섹션 제목: “등록과 검증 (src/backend/access/table/tableamapi.c)”GetTableAmRoutine(Oid amhandler)— 핸들러 함수를 호출하고, 모든 필수 콜백이 NULL이 아님을 검증한 뒤const TableAmRoutine *을 반환한다.check_default_table_access_method— GUC 체크 훅. 지명된 AM이pg_am에 존재하는지 검증한다.
스캔과 병렬 헬퍼 (src/backend/access/table/tableam.c)
섹션 제목: “스캔과 병렬 헬퍼 (src/backend/access/table/tableam.c)”table_beginscan_catalog— 카탈로그 스캔용 변형. 카탈로그 스냅샷을 자동으로 등록한다.simple_table_tuple_insert/simple_table_tuple_delete/simple_table_tuple_update— 동시 갱신 사례를 처리하지 않는 호출 측을 위한 래퍼.TM_Ok이외의 결과에서 오류를 발생시킨다.table_block_parallelscan_estimate/…_initialize/…_reinitialize/…_startblock_init/…_nextpage— 병렬 순차 스캔을 구현하는 블록 지향 AM을 위한 공유 헬퍼. vtable에는 없으며, AM이 자신의parallelscan_*콜백에서 이를 호출한다.
스캔 디스크립터 (src/include/access/relscan.h)
섹션 제목: “스캔 디스크립터 (src/include/access/relscan.h)”typedef struct TableScanDescData— 각 AM이 내장(또는 확장)하는 기본 스캔 디스크립터.rs_rd(Relation),rs_snapshot,rs_nkeys,rs_key,rs_flags(ScanOptions비트마스크),rs_parallel(병렬 스캔 상태 또는 NULL)을 포함한다.typedef struct IndexFetchTableData— 인덱스 페치 상태의 최소 기반 구조체. AM은 이를 더 큰 AM 전용 구조체에 내장한다.
힙 AM 바인딩 (src/backend/access/heap/heapam_handler.c)
섹션 제목: “힙 AM 바인딩 (src/backend/access/heap/heapam_handler.c)”heapam_methods—static const TableAmRoutine. 약 40개 슬롯 모두 채워져 있다.GetHeapamTableAmRoutine()—&heapam_methods를 반환. 힙임을 전제로 한 코드에서 사용한다.heap_tableam_handler(PG_FUNCTION_ARGS)— SQL 가시amhandler함수.PG_RETURN_POINTER로&heapam_methods를 반환한다.heapam_tuple_insert/heapam_tuple_delete/heapam_tuple_update/heapam_tuple_lock— 얇은 래퍼.TupleTableSlot을 풀어heapam.c의heap_{insert,delete,update}를 호출하고, 결과 TID를 슬롯에 다시 복사한다.heapam_index_fetch_begin/heapam_index_fetch_tuple— TID 기반 조회를 위한HeapScanDescData할당 및 구동.heapam_index_fetch_tuple은heap_hot_search_buffer를 호출한다(postgres-heap-am.md참조).heapam_slot_callbacks—&TTSOpsBufferHeapTuple을 반환. 버퍼에 핀된 힙 튜플을 복사 없이 담는 슬롯 타입이다.
익스큐터 호출 지점
섹션 제목: “익스큐터 호출 지점”SeqNext(nodeSeqscan.c) —table_beginscan(지연)을 호출하고table_scan_getnextslot루프를 돈다.ExecSeqScan의 AM 접면이다.ExecInitSeqScan(nodeSeqscan.c) — 초기화 시table_slot_callbacks를 호출해 올바른 슬롯 타입을 선택한다.ExecEndSeqScan/ExecReScanSeqScan(nodeSeqscan.c) — 각각table_endscan/table_rescan을 호출한다.- 인덱스 스캔 TID 해석(
nodeIndexscan.c) —table_index_fetch_begin을 호출하고,call_again과 함께table_index_fetch_tuple루프를 돌다가table_index_fetch_end로 마친다.
위치 힌트 (2026-06-05 기준, REL_18 273fe94)
섹션 제목: “위치 힌트 (2026-06-05 기준, REL_18 273fe94)”| 심볼 | 파일 | 줄 |
|---|---|---|
typedef enum TM_Result | access/tableam.h | 71 |
typedef struct TM_FailureData | access/tableam.h | 146 |
typedef struct TableAmRoutine | access/tableam.h | 288 |
} TableAmRoutine | access/tableam.h | 843 |
table_beginscan (인라인) | access/tableam.h | 875 |
table_endscan (인라인) | access/tableam.h | 984 |
table_scan_getnextslot (인라인) | access/tableam.h | 1020 |
table_index_fetch_begin (인라인) | access/tableam.h | 1157 |
table_index_fetch_tuple (인라인) | access/tableam.h | 1206 |
table_tuple_insert (인라인) | access/tableam.h | 1367 |
table_tuple_delete (인라인) | access/tableam.h | 1456 |
table_tuple_update (인라인) | access/tableam.h | 1500 |
table_relation_vacuum (인라인) | access/tableam.h | 1674 |
typedef struct TableScanDescData | access/relscan.h | 33 |
typedef struct IndexFetchTableData | access/relscan.h | 121 |
GetTableAmRoutine | table/tableamapi.c | 28 |
table_beginscan_catalog | table/tableam.c | 113 |
simple_table_tuple_insert | table/tableam.c | 277 |
simple_table_tuple_delete | table/tableam.c | 291 |
simple_table_tuple_update | table/tableam.c | 336 |
static const TableAmRoutine heapam_methods | heap/heapam_handler.c | 2616 |
GetHeapamTableAmRoutine | heap/heapam_handler.c | 2676 |
heap_tableam_handler | heap/heapam_handler.c | 2682 |
SeqNext | executor/nodeSeqscan.c | 51 |
ExecSeqScan | executor/nodeSeqscan.c | 110 |
ExecInitSeqScan | executor/nodeSeqscan.c | 207 |
ExecEndSeqScan | executor/nodeSeqscan.c | 289 |
소스 검증 (2026-06-05 기준)
섹션 제목: “소스 검증 (2026-06-05 기준)”각 항목은 커밋
273fe94(REL_18_STABLE) 기준 현재 소스에 대한 사실이다. 확인 방법을 함께 기록한다.
검증된 사실
섹션 제목: “검증된 사실”-
TableAmRoutine은tableam.h288번 줄에typedef struct로 선언되고, 닫는 중괄호는 843번 줄에 있다. 파일을 직접 읽어 확인했다. 구조체 본문이 약 555줄에 달하는 이유는 각 콜백 슬롯마다 여러 줄의 주석이 달려 있기 때문이다. -
GetTableAmRoutine은 약 30개의 필수 콜백이 NULL이 아님을 단언한다.tableamapi.c를 읽어 확인했다.finish_bulk_insert,scan_bitmap_next_tuple,relation_toast_am,relation_fetch_toast_slice는 단언 대상이 아닌 선택적 콜백이다.scan_set_tidrange와scan_getnextslot_tidrange도 단언 대상이 아니다(둘 다 제공하거나 둘 다 제공하지 않아야 한다). -
REL_18 기준
GetTableAmRoutine에 등록되는 인-트리 AM은 heap뿐이다.src/backend와src/include에서git grep -r 'GetTableAmRoutine\|amhandler.*heap_tableam'으로 확인했다.heapam_handler.c와tableamapi.c만 이 함수를 참조한다. -
DEFAULT_TABLE_ACCESS_METHOD는 문자열"heap"이며,default_table_access_methodGUC의 초기값이다.tableam.h29번 줄(#define DEFAULT_TABLE_ACCESS_METHOD "heap")과tableam.c49번 줄(char *default_table_access_method = DEFAULT_TABLE_ACCESS_METHOD)에서 확인했다. -
table_beginscan은SO_TYPE_SEQSCAN | SO_ALLOW_STRAT | SO_ALLOW_SYNC | SO_ALLOW_PAGEMODE를 설정한다.tableam.h의table_beginscan인라인(875–881번 줄)에서 확인했다.table_beginscan_bm은SO_TYPE_BITMAPSCAN으로 대체하고SO_ALLOW_SYNC를 제거한다.table_beginscan_analyze는SO_TYPE_ANALYZE만 사용하고 다른 플래그는 없다. -
SeqNext는table_beginscan을 지연 호출(scandesc == NULL인 경우에만)하고,table_scan_getnextslot을 루프로 돈다.nodeSeqscan.c51–84번 줄에서 위에 인용한 내용 그대로 확인했다. -
table_index_fetch_tuple에는 힙 AM이 HOT 체인에 사용하는call_again출력 파라미터가 있다.tableam.h의 인라인 래퍼(1206번 줄)와index_fetch_tuple주석 블록(436–459번 줄)에서 확인했다. 주석에는 “If there potentially is another tuple matching the tid, *call_again needs to be set to true”라고 명시되어 있다. -
heapam_methods는heapam_handler.c2616번 줄에static const TableAmRoutine으로 정의된다. 파일을 직접 읽어 확인했다. 2682번 줄의heap_tableam_handler는PG_RETURN_POINTER로&heapam_methods를 반환한다.src/backend/에 다른TableAmRoutine구조체는 없다. -
TU_UpdateIndexes는TM_Result와 별개의 열거형이다.tableam.h109–119번 줄에서 확인했다.tuple_update는TM_Result를 반환하고 출력 포인터(update_indexes)에TU_UpdateIndexes를 채운다. 익스큐터는 HOT 갱신에서 인덱스 갱신을 건너뛰기 위해TU_None을, 요약 인덱스(BRIN)만 갱신할 때TU_Summarizing을 사용한다.
미해결 질문
섹션 제목: “미해결 질문”-
세션 시작 시 커스텀 AM 등록 경로.
CREATE ACCESS METHOD ... TYPE TABLE로 설치된 커스텀 AM은pg_am에 핸들러를 등록한다. 다음 세션에서 해당 AM을 사용하는 릴레이션을 열면GetTableAmRoutine이 호출된다. 세션 수준 캐싱(즉,TableAmRoutine *이 릴레이션 오픈 때마다 다시 페치되는지, 고정되는지)은 relcache 무효화 메커니즘이 관장한다. 동시ALTER TABLE SET ACCESS METHOD하에서의 정확한 캐싱 동작은 이 문서에서 추적하지 않는다. -
논리 디코딩과의 상호작용.
table_scan_getnextslot과table_index_fetch_tuple의CheckXidAlive가드는 논리 디코딩 중 호출을 거부한다.table_scan_getnextslot을 거치지 않고 자체적으로 튜플을 재구성하는 커스텀 AM이 논리 디코딩에 올바르게 참여하는지는 인터페이스 계약에 명시되어 있지 않다.
PostgreSQL 너머 — 비교 설계와 연구 프론티어
섹션 제목: “PostgreSQL 너머 — 비교 설계와 연구 프론티어”분석이 아닌 출발점. 각 항목은 후속 문서를 위한 시작 고리다.
-
인덱스 AM 인터페이스(
amapi.h의IndexAmRoutine)는 형제 계약이다. 둘 다 PostgreSQL 12 이전부터 존재했다. 인덱스 AM API가 더 오래됐고TableAmRoutine은 나중에 추가되어 플러그형 스토리지 그림을 완성했다. 두 인터페이스를 함께 이해하면 “확장 가능한 데이터베이스 엔진으로서의 PostgreSQL” 전체 모습이 보인다. nbtree 문서(postgres-nbtree.md)는 B-트리 측에서IndexAmRoutine을 다룬다. -
zheap — 취소된 제자리 덮어쓰기 AM. zheap 프로젝트(Percona / EnterpriseDB, 2017–2020년경)는 힙 팽창과 vacuum 부담을 없애기 위해
TableAmRoutine을 확장 지점으로 사용해 undo를 갖춘 제자리 스토리지 AM을 만들려는 시도였다. 그 설계 문서들은 AM 저자 관점에서 AM 계약의 가장 어려운 부분(가시성, 프리징, TOAST, WAL)을 설명하며, 인터페이스가 아직 표현하기에 충분히 유연하지 않았던 부분도 드러낸다. 프로젝트는 중단됐지만TableAmRoutine이 아키텍처적 의도로 설계됐음을 보여주는 가장 명확한 사례로 남아 있다. -
컬럼형/추가 전용 AM. 외부 프로젝트들(Citus columnar, Hydra, ParadeDB)이 컬럼형 스토리지를 위해
TableAmRoutine을 구현한다. 이들은 진정으로 AM-중립적인 콜백(스캔 생명주기, DML 결과 코드)과 블록 지향 가정을 내포한 콜백(table_block_relation_size,scan_bitmap_next_tuple)이 무엇인지 드러낸다.tableam.c의 블록 지향 헬퍼(table_block_parallelscan_*)가 제공되는 이유가 바로 많은 AM이 이 가정을 공유하기 때문이다. -
Oracle의 플러그형 스토리지(In-Memory Column Store). Oracle 12c는 인메모리 컬럼 형식을 이중 형식 옵션으로 도입했다. 온디스크 행 스토어는 그대로 두고, 백그라운드 프로세스가 추가 표현을 채운다. 쿼리는 둘 중 하나를 사용할 수 있다. 이 방식은 테이블이 정확히 하나의 AM을 갖는 PostgreSQL 모델과 대비된다. PostgreSQL에서 유사한 것을 구현하려면 두 형식을 내부적으로 유지하는 커스텀 AM이나 외부 테이블 오버레이가 필요하며, 둘 다 현재로서는 깔끔하지 않다.
-
Stonebraker 1986의 비전. The Design of POSTGRES(Stonebraker & Rowe, 1986)에는 쿼리 처리와 스토리지 관리를 분리하는 “추상 데이터 관리자”에 대한 절이 있었다.
TableAmRoutine은 그 비전이 PostgreSQL에서 26년 만에 실현된 결과물에 가장 가깝다. 1986년 논문과 현재tableam.h를 나란히 읽으면 원래 비전에서 인터페이스가 얼마나 좁아졌는지 볼 수 있어 흥미롭다.
인-트리 문서
섹션 제목: “인-트리 문서”src/include/access/tableam.h— 1차 출처. 각 슬롯 정의 위에 모든 콜백이 인라인으로 문서화되어 있다. 헤더 주석은 더 높은 수준의 문서로tableam.sgml을 참조한다.
교과서 챕터 (knowledge/research/dbms-general/ 아래)
섹션 제목: “교과서 챕터 (knowledge/research/dbms-general/ 아래)”- Database Internals (Petrov), 3장 “File Formats” — 플러그형 스토리지 계층 개념과 vtable 디스패치 패턴.
- Database System Concepts (Silberschatz 외, 7판), 13장 “Data Storage Structures” — 스토리지 관리자 추상화와 접근 방법 인터페이스.
PostgreSQL 소스 (/data/hgryoo/references/postgres/, REL_18 273fe94)
섹션 제목: “PostgreSQL 소스 (/data/hgryoo/references/postgres/, REL_18 273fe94)”src/include/access/tableam.hsrc/backend/access/table/tableam.csrc/backend/access/table/tableamapi.csrc/backend/access/heap/heapam_handler.csrc/backend/executor/nodeSeqscan.csrc/include/access/relscan.h
교차 참조 (형제 문서)
섹션 제목: “교차 참조 (형제 문서)”postgres-heap-am.md— 레퍼런스 구현으로서의 힙 AM:HeapTupleHeaderData, HOT, 정리(pruning), 가시성,heap_insert/heap_update/heap_delete.postgres-executor.md— 테이블 AM API를 소비하는 익스큐터 노드 트리와 슬롯 타입 전체.postgres-mvcc-snapshots.md— 스냅샷 생성.table_beginscan과table_index_fetch_tuple에 전달되는 스냅샷.postgres-vacuum.md—table_relation_vacuum호출 측, autovacuum 스케줄링.postgres-nbtree.md— 형제 인터페이스인 인덱스 AM(IndexAmRoutine).postgres-page-layout.md— 블록 지향 AM이 전제하는 페이지 기하 구조.