콘텐츠로 이동

(KO) PostgreSQL 스토리지 — 고정 배선 힙에서 플러그형 테이블 접근 메서드 API까지

목차

이 서브시스템이 진화해야 했던 이유 (원래의 한계)

섹션 제목: “이 서브시스템이 진화해야 했던 이유 (원래의 한계)”

PostgreSQL은 버클리 POSTGRES로부터 단일하고 견고한 테이블 스토리지 모델을 물려받았다. **비덮어쓰기 힙(no-overwrite heap)**이다. 테이블은 8 KB 블록의 시퀀스이고, 각 블록은 슬롯형 페이지이며, 각 행은 MVCC 가시성을 구동하는 xmin/xmax 트랜잭션 스탬프를 담은 23바이트 헤더의 HeapTuple이다. 업데이트는 절대 제자리를 덮어쓰지 않는다 — 새 투플 버전을 기록하고 오래된 것은 VACUUM이 회수하도록 남겨둔다. 이 모델의 메커니즘은 postgres-heap-am.md에 문서화되어 있으므로 이 진화 문서에서 재도출하지 않는다.

수십 년 동안 이 모델은 하나의 접근 메서드가 아니었다 — 유일한 스토리지 계층이었으며, 백엔드의 나머지 부분이 직접 호출했다. 실행기의 순차 스캔 노드는 heap_getnext를 불렀다. 인덱스 기계는 TID를 행으로 해석하고자 heap_fetch / heap_hot_search_buffer를 불렀다. COPY, INSERT, UPDATE, DELETEheap_insert, heap_update, heap_delete에 도달했다. VACUUM은 힙 페이지 위의 lazy_vacuum_rel이었다. 간접 참조가 없었다 — heapam.c의 함수 이름들이 모든 호출 지점에 하드코딩된 스토리지 인터페이스 자체였다. “heap”이라는 단어는 사실상 “테이블”의 동의어였다.

이 구조는 단순하고 빠르지만, 분리 가능한 두 관심사를 혼용했다.

  • 실행기가 필요한 것 — 가시 투플에 대한 커서, 행을 삽입하는 방법, 동시 트랜잭션이 같은 투플을 건드렸을 때의 결과 코드.
  • 그 필요를 충족하는 방법 — MVCC 버전 체인이 있는 슬롯형 페이지 힙과 죽은 버전을 회수하는 vacuum 프로세스.

이 혼용의 비용은 워크로드 경직성이다. 비덮어쓰기 힙은 혼합 읽기-쓰기 OLTP에 탁월하지만, 일부 워크로드가 지불하고 싶지 않은 구조적 비용을 수반한다.

  • 투플 부풀음과 vacuum 부담. 모든 업데이트가 죽은 버전을 남기고, 이를 회수하려면 VACUUM, autovacuum 스케줄링, XID 래핑을 막기 위한 freezing이 필요하다. 추가 전용이나 분석 워크로드는 이 비용을 아무 이득 없이 지불한다.
  • 네이티브 컬럼형 레이아웃 없음. 행 우선 힙은 투플의 모든 컬럼을 함께 저장한다. 50개 컬럼 중 3개만 접근하는 OLAP 스캔도 여전히 버퍼 매니저에서 전체 행을 끌어온다 — 스토리지 수준의 컬럼 투영도, 컬럼 단위 압축도 없다.
  • 대안 동시성 모델 없음. MVCC 버전 체인이 힙에 고정 배선되어 있다. 별도의 undo 로그를 사용하는 인플레이스 업데이트 모델(부풀음 제거 목적)은 heapam.c를 포크하지 않고는 표현할 수 없었다.

스토리지가 고정 배선인 한, 다른 모델을 제공하는 유일한 방법은 엔진 전체를 포크하는 것이었다. 해결책은 객체 지향 설계의 고전적인 전략 객체(strategy object) / vtable 패턴이다 — 실행기가 호출하는 안정적인 인터페이스를 정의하고, 릴레이션을 열 때 구체적인 구현을 선택하며, 디스패치 테이블로 연결한다. PostgreSQL은 인덱스 측에서 이미 정확히 이것을 갖추고 있었다 — 인덱스 접근 메서드 API(IndexAmRoutine, postgres-index-am.md 참조)가 수년간 btree, hash, GiST, GIN, SP-GiST, BRIN을 하나의 인터페이스 뒤에 공존시켰다. 테이블 측이 빠진 절반이었다. 이 비전은 Stonebraker와 Rowe의 1986년 논문 Design of POSTGRES로 거슬러 올라가는데, 그 논문은 쿼리 처리와 스토리지를 분리하는 “추상 데이터 관리자(abstract data manager)“를 기술했다. PostgreSQL 12가 마침내 테이블 측 이음매를 구축했고, 기존의 인덱스 측을 본보기로 삼았다.

timeline
  title PostgreSQL 스토리지 — 고정 배선 힙에서 플러그형 테이블 AM까지
  section 고정 배선 시대
    12 이전 : 힙이 스토리지 계층 : 실행기가 heap_getnext heap_insert를 직접 호출 : 테이블 수준 이음매 없음 : 인덱스 AM(IndexAmRoutine)은 이미 플러그형
  section 이음매 절단 (PG 12)
    PG 12 테이블 AM API : rd_tableam 위의 TableAmRoutine vtable : 힙이 heapam_methods로, 가능한 여러 AM 중 하나가 됨 : GetTableAmRoutine이 필수 콜백 검증
    PG 12 슬롯 일반화 : TupleTableSlotOps 추상화 : TTSOpsBufferHeapTuple / Virtual / HeapTuple / MinimalTuple : 실행기가 어떤 AM 인메모리 투플 형태도 운반
    PG 12 SQL 표면 : CREATE ACCESS METHOD ... TYPE TABLE : CREATE TABLE ... USING : default_table_access_method GUC
  section 테이블 AM 변경
    PG 15 SET ACCESS METHOD : ALTER TABLE ... SET ACCESS METHOD가 테이블을 재작성 : AT_SetAccessMethod
    PG 17 matview + GUC : ALTER MATERIALIZED VIEW ... SET ACCESS METHOD : default_table_access_method가 matview를 지배 : SET ACCESS METHOD DEFAULT, pg_dump가 이를 출력
  section 생태계
    2017-2020 zheap : undo 기반 인플레이스 AM : 중단됐지만 API 의도를 증명
    현재 진행 컬럼형 AM : Citus columnar, Hydra, ParadeDB : 동일 쿼리 파이프라인 위의 OLAP 스토리지
  section 현재
    REL_18 : 힙이 여전히 유일한 코어 트리 AM : tableam.c, heapam_handler.c, tableam.h : API가 안정적인 확장점

그림 1 — 진화 흐름. 결정적 전환은 PostgreSQL 12에서 일어났다. AM vtable, 슬롯 일반화, SQL 표면이라는 세 가지 조율된 변경이 함께 “힙”을 스토리지 계층에서 하나의 접근 메서드로 바꿨다. PG15와 PG17이 이어서 테이블의 AM을 변경 가능하게 만들었다. 힙은 REL_18에서도 유일한 코어 트리 AM으로 남아 있고, 대안들은 익스텐션으로 존재한다.

시대 0 — 고정 배선 힙 (12 이전): 단일 스토리지 모델, 이음매 없음

섹션 제목: “시대 0 — 고정 배선 힙 (12 이전): 단일 스토리지 모델, 이음매 없음”

설계가 어떤 모습이었나. PostgreSQL 12 이전에는 TableAmRoutine도, relcache 항목의 rd_tableam 필드도, table_* 래퍼 계층도 없었다. 실행기, 인덱스 계층, COPY, VACUUM이 모두 heap 함수를 이름으로 직접 불렀다. 콜 그래프는 각 소비자에서 heapam.c로 향하는 직접 에지였다.

flowchart TB
  subgraph PRE["12 이전 — 고정 배선 힙 (이음매 없음)"]
    direction TB
    SEQ["SeqNext / ExecSeqScan"] --> HGN["heap_getnext"]
    IDX["인덱스 스캔 TID 해석"] --> HF["heap_fetch / heap_hot_search_buffer"]
    DML["INSERT / UPDATE / DELETE / COPY"] --> HINS["heap_insert / heap_update / heap_delete"]
    VAC["VACUUM"] --> LVR["lazy_vacuum_rel"]
    HGN --> HEAP["heapam.c — 스토리지 계층"]
    HF --> HEAP
    HINS --> HEAP
    LVR --> HEAP
  end

그림 2 — 12 이전 구조. 모든 소비자가 heap 함수를 이름으로 직접 호출한다. “heap”은 릴레이션의 설정 가능한 속성이 아니라 존재하는 유일한 테이블 스토리지다. 릴레이션이 가진 디스패치 포인터도, 교체 가능한 인터페이스 경계도 없다.

왜 그렇게 만들었나. 하나의 스토리지 모델을 중심으로 성장한 엔진의 자연스러운 상태였다. 비덮어쓰기 MVCC의 슬롯형 페이지 힙은 버클리 POSTGRES 설계였고, 단일 스토리지 시스템에서 간접 참조는 순수한 오버헤드다 — 컴파일러가 절대 인라인할 수 없는 함수 포인터 추적이자 아무런 이득 없는 유지보수 부담이다. 그 비용이 용납 불가능해진 것은 동일 서버에서 하나 이상의 스토리지 모델을 원하게 됐을 때다.

이미 플러그형이었던 것, 그리고 왜 중요했나. 인덱스 측은 IndexAmRoutine(amapi.h) 덕분에 수년째 플러그형이었다. 실행기와 플래너는 rel->rd_indam으로 디스패치하는 안정적인 index_* 래퍼 계층으로 인덱스와 대화했고, 구조적으로 상이한 여섯 가지 인덱스 타입(btree, hash, GiST, GIN, SP-GiST, BRIN)이 그 뒤에 공존했다. 이것이 존재 증명이자 템플릿이었다 — 테이블 AM 저자들이 의도적으로 TableAmRoutineIndexAmRoutine의 형태를 본 따 만들었다. 릴레이션 위의 vtable 패턴, 필수/선택 콜백 분리, pg_am 경유 핸들러 함수 등록이 같다. pg_am 카탈로그는 이미 amtype 컬럼을 갖추고 있었다 — 12 이전에는 AMTYPE_INDEX(‘i’)만 담았다. 교차 참조: postgres-index-am.md가 이쪽을 형성한 인덱스 측 인터페이스를 문서화한다.

시대 1 — PostgreSQL 12: 테이블 접근 메서드 API (TableAmRoutine)

섹션 제목: “시대 1 — PostgreSQL 12: 테이블 접근 메서드 API (TableAmRoutine)”

무엇이 바뀌었나. PostgreSQL 12가 TableAmRoutine을 도입했다. src/include/access/tableam.h에 선언된 약 40개 함수 포인터로 이뤄진 struct로, 백엔드의 나머지가 테이블 스토리지에서 필요한 모든 것을 담는다 — 슬롯 콜백, 순차 스캔 생명주기(scan_begin / scan_getnextslot / scan_end / scan_rescan), 인덱스 패치(index_fetch_begin / index_fetch_tuple), DML(tuple_insert / tuple_update / tuple_delete / tuple_lock / multi_insert), DDL/vacuum/analyze(relation_vacuum, scan_analyze_next_block, index_build_range_scan, …). 이 vtable을 가리키는 포인터는 새 rd_tableam 필드로 모든 릴레이션의 relcache 항목에 저장된다. vtable이 table_open 시점에 어떻게 해석되는지, GetTableAmRoutine이 필수 슬롯을 어떻게 검증하는지, 각 table_* 인라인 래퍼가 어떻게 디스패치하는지는 postgres-table-am.md에 전부 문서화되어 있다 — 이 문서에서 중복 기술하지 않는다.

구조적 전환 (이전 → 이후). 그림 2의 모든 고정 배선 에지가 두 단계 간접 참조로 재경유됐다: 소비자 → table_* 래퍼 → vtable 슬롯 → AM 구현. 힙이 사라진 것이 아니라 강등됐다 — heapam_handler.cstatic const TableAmRoutine인 단일 채워진 vtable heapam_methods로, 그 슬롯이 직접 호출되던 바로 그 heap_* 함수들을 가리킨다.

flowchart TB
  subgraph POST["PG 12+ — 플러그형 테이블 AM (이음매 절단)"]
    direction TB
    SEQ["SeqNext / ExecSeqScan"] --> W1["table_scan_getnextslot\n(tableam.h 인라인)"]
    IDX["인덱스 스캔 TID 해석"] --> W2["table_index_fetch_tuple\n(tableam.h 인라인)"]
    DML["INSERT / UPDATE / DELETE / COPY"] --> W3["table_tuple_insert / update / delete\n(tableam.h 인라인)"]
    VAC["VACUUM"] --> W4["table_relation_vacuum\n(tableam.h 인라인)"]
    W1 --> RT["rel->rd_tableam\n(const TableAmRoutine *)"]
    W2 --> RT
    W3 --> RT
    W4 --> RT
    RT --> HM["heapam_methods\n(static const TableAmRoutine)"]
    RT -.-> OTHER["some_other_methods\n(columnar / zheap / ...)"]
    HM --> HEAP["heapam.c의 heap_* 함수들"]
    OTHER -.-> OIMPL["대안 스토리지 구현"]
  end

그림 3 — PG12 구조 (그림 2의 이전 → 이후). 동일한 네 소비자가 이제 안정적인 table_* 래퍼를 호출한다. 각 래퍼는 rel->rd_tableam을 읽어 함수 포인터 슬롯으로 디스패치한다. 힙은 하나의 채워진 vtable(heapam_methods)이고, 점선 에지는 대안 AM이 꽂히는 이음매를 나타낸다. 힙 구현 함수(heap_getnext, heap_insert, …)는 변경되지 않는다 — 그것들로 향하는 호출 경로만 vtable 뒤로 옮겨졌을 뿐이다.

왜 했나. PG12 커밋과 릴리스 자료에 명시된 동기는 zheap 인플레이스 엔진 같은 대안 스토리지 엔진들이 PostgreSQL의 실행기, 플래너, WAL 인프라, SQL 표면을 포크 없이 공유하게 하는 것이었다. 힙을 하나의 AM으로 재편하는 것이 인터페이스가 완전하다는 증명이었다 — 기존 힙이 남아 있는 고정 배선 호출 없이 vtable만으로 완전히 표현된다면, 두 번째 AM도 표현될 수 있다.

감수한 비용. 스토리지 연산마다 두 번의 포인터 추적(릴레이션 → vtable → 함수)이 발생하며 컴파일러가 간접 참조를 가로질러 인라인할 수 없다. 핫 순차 스캔 경로에서는 측정 가능하지만 작은 수준이고, PG12는 배치로 완화했다 — scan_getnextslot은 한 번에 한 투플을 반환하지만 힙 구현은 하나의 버퍼 핀 아래 전체 페이지를 읽으며, 슬롯 추상화(시대 2)가 실행기의 투플 복사를 피하게 해준다. 교차 참조: postgres-table-am.md가 이 시대의 모든 것에 대한 현재 상태 메커니즘 문서다 — TableAmRoutine, GetTableAmRoutine, TM_Result, ScanOptions, 디스패치 체인.

시대 2 — PostgreSQL 12: 슬롯 추상화 일반화 (TupleTableSlotOps)

섹션 제목: “시대 2 — PostgreSQL 12: 슬롯 추상화 일반화 (TupleTableSlotOps)”

무엇이 바뀌었나. 플러그형 스토리지 계층에는 플러그형 인메모리 투플 표현이 필요하다 — 모든 AM이 행을 HeapTuple로 저장하지는 않기 때문이다. PostgreSQL 12가 실행기의 TupleTableSlot을 힙과 미니멀 투플을 아는 구조체에서 자체 vtable인 TupleTableSlotOps(src/include/executor/tuptable.h에 선언)로 구동되는 것으로 일반화했다. 슬롯은 이제 const TupleTableSlotOps *tts_ops 포인터를 가지며, “이 애트리뷰트를 달라”, “복사본으로 구체화하라”, “비워라” 같은 연산이 그 포인터로 디스패치된다.

릴리스에서 네 가지 슬롯 ops 구현이 각각 const TupleTableSlotOps로 제공됐다.

  • TTSOpsVirtualDatum/isnull 배열에 컬럼을 보유하며 백킹 투플이 없다. 표현식 평가와 투영이 만들어내는 형태다.
  • TTSOpsHeapTuple — palloc된 독립적인 HeapTuple.
  • TTSOpsMinimalTuple — tuplestore, 정렬, 해시 조인 스풀에서 쓰는 헤더 제거 형태.
  • TTSOpsBufferHeapTuple — 공유 버퍼 페이지에 여전히 핀된 HeapTuple. 힙 스캔 경로가 반환하는 제로 복사 형태다.

구조적 전환 (이전 → 이후). PG12 이전에는 슬롯이 사실상 힙 형태였다 — 하나의 일체형 구조체 안의 몇 가지 boolean 플래그로 힙 투플 또는 미니멀 투플을 보유했다. PG12 이후 슬롯은 얇은 컨테이너와 행동 vtable의 조합이며, AM이 스캔이 어떤 ops를 생성할지 선택한다. 테이블 AM의 slot_callbacks 슬롯이 정확히 이 질문에 답한다 — 힙의 경우 &TTSOpsBufferHeapTuple을 반환해 순차 스캔이 복사본을 구체화하지 않고 버퍼 페이지에 살아 있는 투플을 실행기에 전달한다. 컬럼형 AM은 &TTSOpsVirtual(또는 커스텀 ops)을 반환해 조작된 행 대신 투영된 컬럼 집합을 돌려줄 수 있다.

flowchart LR
  subgraph BEFORE["12 이전 슬롯"]
    direction TB
    S0["TupleTableSlot\n(일체형;\n힙-또는-미니멀,\n플래그로 결정)"]
  end
  subgraph AFTER["PG 12+ 슬롯"]
    direction TB
    S1["TupleTableSlot\n+ tts_ops 포인터"]
    S1 --> V["TTSOpsVirtual"]
    S1 --> H["TTSOpsHeapTuple"]
    S1 --> M["TTSOpsMinimalTuple"]
    S1 --> B["TTSOpsBufferHeapTuple"]
    AM["AM slot_callbacks\nops 선택"] --> S1
  end
  BEFORE -.PG12 일반화.-> AFTER

그림 4 — 슬롯 일반화. 12 이전 슬롯은 힙/미니멀 선택을 하나의 구조체에 고정했다. PG12는 행동을 tts_ops가 선택하는 TupleTableSlotOps vtable로 분리했고, 테이블 AM의 slot_callbacks가 스캔이 어떤 ops를 생성할지 결정한다. 이것이 힙이 아닌 AM이 실행기가 알거나 신경 쓰지 않고 자체 인메모리 형태로 투플을 반환할 수 있게 하는 메커니즘이다.

왜 했나. 이 일반화 없이는 테이블 AM API가 힙 표현을 누설했을 것이다 — 어떤 AM이든 실행기를 만족시키려면 HeapTuple을 만들어야 했을 것이고, 컬럼형이나 인플레이스 스토리지의 의미가 무색해진다. 슬롯 일반화는 PG12 작업 중 테이블 AM API를 읽기 경로에서 진정으로 스토리지 중립적으로 만든 절반이다. TM_Result쓰기 경로를 스토리지 중립적으로 만든 것과 같다. 교차 참조: postgres-table-am.mdtable_slot_callbacksExecInitSeqScan이 슬롯 타입을 선택하는 방법을 문서화한다; postgres-heap-am.md가 버퍼 핀된 힙 투플 형태를 문서화한다.

시대 3 — PostgreSQL 12: SQL 표면에 이음매 드러내기 (CREATE ACCESS METHOD, USING, GUC)

섹션 제목: “시대 3 — PostgreSQL 12: SQL 표면에 이음매 드러내기 (CREATE ACCESS METHOD, USING, GUC)”

무엇이 바뀌었나. 내부 vtable은 사용자에게 보이지 않는다. PG12는 AM을 실제로 설치하고 선택할 수 있도록 이음매를 SQL과 카탈로그 수준에서도 노출했다.

  • pg_am.amtype't' 값 추가 (AMTYPE_TABLE, src/include/catalog/pg_am.h). 오래된 'i'(AMTYPE_INDEX)와 나란히. 내장 heap 행은 이제 amhandlerheap_tableam_handler인 테이블 타입 AM이다.
  • CREATE ACCESS METHOD <name> TYPE TABLE HANDLER <fn> — 익스텐션이 새 테이블 AM을 등록한다. 핸들러 함수가 TableAmRoutine *을 반환하고, GetTableAmRoutine이 첫 사용 시 이를 검증한다.
  • CREATE TABLE ... USING <am> — 생성 시 릴레이션의 AM을 선택한다.
  • default_table_access_method GUCUSING이 생략됐을 때 사용할 AM을 설정한다. 컴파일 타임 기본값은 문자열 "heap"이다(tableam.hDEFAULT_TABLE_ACCESS_METHOD).

구조적 전환 (이전 → 이후). 12 이전에는 테이블의 스토리지가 아무도 이름 붙일 수 없는 속성이었다 — 가상의 USING 뒤에 무언가를 놓을 수 없었다. 12 이후 “어떤 접근 메서드인가”가 릴레이션의 일급 카탈로그 기록 속성(pg_class.relam)이 됐고, table_open 시점에 vtable로 해석된다. 릴레이션이 자신의 AM을 기억하는 방식이 테이블스페이스를 기억하는 방식과 같아졌다.

왜 했나. vtable 이음매(시대 1)는 기본값이 아닌 AM을 요청할 방법이 없으면 쓸모없다. 이 시대가 PG12 작업의 사용자 대면 완성이다 — 등록(CREATE ACCESS METHOD), 테이블별 선택(USING), 서버 전체 기본값(GUC). 교차 참조: postgres-table-am.mdheap_tableam_handler, GetTableAmRoutine, DEFAULT_TABLE_ACCESS_METHOD 상수를 다룬다; pg_class.relam을 기록하는 DDL 실행 경로는 postgres-ddl-execution.md에 있다.

시대 4 — PostgreSQL 15: ALTER TABLE … SET ACCESS METHOD

섹션 제목: “시대 4 — PostgreSQL 15: ALTER TABLE … SET ACCESS METHOD”

무엇이 바뀌었나. PG12는 선택한 AM으로 테이블을 생성할 수 있게 했지만, 기존 테이블의 AM을 제자리에서 변경하는 지원 방법이 없었다. PostgreSQL 15가 ALTER TABLE ... SET ACCESS METHOD <am>을 추가했다 — src/include/nodes/parsenodes.h의 파스 트리 서브커맨드 AT_SetAccessMethod. AM을 바꾸는 것은 메타데이터만의 전환이 아니다 — 행이 새 AM의 온디스크 형식으로 물리적으로 재인코딩돼야 하므로, 이 명령은 전체 테이블 재작성을 수행한다(새 relfilenode, 모든 투플을 대상 AM의 tuple_insert 경로로 재삽입). 이미 온디스크 레이아웃을 변경하는 타입 변경에 ALTER TABLE이 사용하던 재작성 기계와 동일하다.

구조적 전환 (이전 → 이후). PG15 이전에는 테이블의 AM이 생성 후 사실상 불변이었다 — 다른 AM으로 데이터를 이동하려면 대상 AM을 USING한 새 테이블을 만들고 행을 직접 복사(INSERT ... SELECT)한 뒤 이름을 변경해야 했다. PG15 이후 마이그레이션이 단일 DDL 문이 됐다. 실행기가 쓰는 것과 동일한 table_* insert 경로로 재작성을 구동하고, 의존성·인덱스 재구축·제약 재검사 부기가 ALTER TABLE 단계 기계에서 처리된다.

왜 했나. 플러그형 스토리지는 새 테이블만이 아니라 기존 데이터에도 대안 AM을 적용할 수 있을 때 훨씬 유용하다. 수년 전 힙으로 생성된 대형 추가 전용 테이블을 가진 사용자가 수작업 복사 없이 컬럼형 AM으로 변환할 수 있어야 한다. SET ACCESS METHOD가 이 변환을 일급, 원자적, 의존성 인식 연산으로 만든다. 교차 참조: 재작성/단계 기계는 postgres-alter-table.md에 문서화되어 있고, 그것이 구동하는 행별 insert 경로는 postgres-table-am.mdtable_tuple_insert다.

시대 5 — PostgreSQL 17: matview의 SET ACCESS METHOD와 GUC 기본값

섹션 제목: “시대 5 — PostgreSQL 17: matview의 SET ACCESS METHOD와 GUC 기본값”

무엇이 바뀌었나. PostgreSQL 17이 SET ACCESS METHOD 스토리를 세 가지 방향으로 완성했다.

  • ALTER MATERIALIZED VIEW ... SET ACCESS METHOD — matview도 힙 기반 릴레이션이며, PG17이 일반 테이블과 동일한 방식으로 다른 AM으로 이동할 수 있게 했다. (교차 참조: postgres-matview.md.)
  • default_table_access_method가 matview와 CREATE TABLE AS / SELECT INTO를 더 일관되게 지배 — 서버 전체 기본 AM 선택이 이 명령들이 구체화하는 릴레이션에도 적용된다. 평범한 CREATE TABLE에만 적용되던 것에서 확장됐다.
  • SET ACCESS METHOD DEFAULT 표기법 추가와 pg_dump/pg_restoreSET ACCESS METHOD를 출력하도록 개선 — 이것 없이는 논리적 덤프가 모든 테이블을 서버 기본 AM으로 조용히 재구체화해 컬럼형(또는 다른) 선택을 잃어버렸다.

구조적 전환 (이전 → 이후). PG17 이전에는 AM을 제어할 수 있는 릴레이션 종류가 더 좁았고(일반 테이블만), 논리적 덤프가 AM을 충실하게 전달하지 않았다. PG17 이후 AM은 물리 스토리지를 가진 릴레이션 종류 전반에서 완전히 일급이고 덤프 보존되는 속성이 됐으며, 기본 AM GUC가 균일하게 적용된다. 새 메커니즘이라기보다 플러그형 스토리지 기능의 클로저다 — 이음매가 이제 테이블형 릴레이션이 생성·변경·덤프되는 모든 곳에서 일관되게 작동한다.

왜 했나. 기능 완성과 운영 안전성이다. pg_dump로 보존할 수 없는 AM은 프로덕션에서 신뢰할 수 없고, 테이블에는 설정할 수 있지만 matview에는 설정할 수 없는 AM은 놀라운 격차다. PG17이 두 거친 모서리를 모두 제거했다. 교차 참조: postgres-pg-dump-restore.md에서 덤프 측 출력을 다루고, postgres-table-am.md에서 GUC를 다룬다.

시대 6 — API 위에서 자란 대안 AM 생태계

섹션 제목: “시대 6 — API 위에서 자란 대안 AM 생태계”

힙이 유일한 코어 트리 AM임에도 API가 가능하게 한 것. 이음매의 전체 목적은 스토리지 모델이 쿼리 파이프라인을 공유하면서 코어 트리 밖에 살 수 있게 하는 것이었다. 세 범주의 외부 AM이 인터페이스를 활용한다.

  • zheap (Percona / EnterpriseDB, ~2017–2020). 별도의 undo 로그를 가진 인플레이스 업데이트 엔진으로, 힙 부풀음과 vacuum/freeze 부담을 제거하려 했다. zheap이 TableAmRoutine의 원래 동기가 된 소비자였다 — PG12 인터페이스 형태의 상당 부분이 “zheap이 필요로 하는 것은?”으로 정당화됐다. 프로젝트는 중단됐지만(undo 서브시스템이 올바르게 만들기 매우 어려움이 증명됐다), AM 계약의 가장 어려운 부분들 — 가시성, freezing, TOAST 통합, WAL — 의 카탈로그로서 여전히 API가 실제 두 번째 AM을 염두에 두고 설계됐다는 가장 명확한 증명이다.
  • 컬럼형 / OLAP AM (Citus columnar, Hydra, ParadeDB). 분석 스캔이 스토리지 수준에서 투영과 압축을 수행하면서도 PostgreSQL의 플래너와 실행기 위에서 실행되도록 컬럼 우선, 압축, 추가 최적화 스토리지에 TableAmRoutine을 구현한다. 조작된 힙 행 대신 투영된 컬럼 집합을 반환하기 위해 슬롯 일반화(시대 2)를 활용한다.
  • 블록 지향 보조 함수들 — 공유 기반. 많은 AM이 여전히 블록-버퍼 기반이므로, tableam.c가 AM이 자체 콜백에서 호출할 수 있는 공유 보조 함수(table_block_relation_size, table_block_parallelscan_* 계열)를 제공한다. 이것들은 계약의 어느 부분이 진정으로 AM 중립적인지(스캔 생명주기, TM_Result 결과 코드)와 어느 부분이 블록 구조 릴레이션을 가정하는지를 드러낸다.

왜 이것이 전체 흐름의 성과인가. 시대 0의 고정 배선 힙은 이것들 하나하나를 엔진 포크 프로젝트로 만들었다. 시대 1–5가 이것들을 안정적인 헤더로 컴파일하고, CREATE ACCESS METHOD로 등록하며, 실행기·플래너·WAL·복제·SQL 표면을 무료로 상속하는 익스텐션으로 바꿨다. 생태계가 이음매가 실재한다는 증거다. 교차 참조: postgres-table-am.md의 “Beyond PostgreSQL” 절에 이 프로젝트들이 더 자세히 열거되어 있고, postgres-heap-am.md가 AM 저자가 먼저 읽는 참조 구현이다.

REL_18(커밋 273fe94, PG 18.x)에서 플러그형 스토리지 아키텍처는 성숙하고 안정적이며, 힙은 여전히 코어 트리의 유일한 테이블 AM이다. 현재 설계는 PG15/PG17이 다듬은 PG12 형태 그대로다.

  • src/include/access/tableam.hTableAmRoutine(약 40슬롯 vtable), table_* 인라인 디스패치 래퍼, TM_Result, ScanOptions, DEFAULT_TABLE_ACCESS_METHOD "heap" 상수를 선언한다.
  • src/backend/access/table/tableam.c(와 tableamapi.c)가 GetTableAmRoutine(핸들러 해석 + 필수 콜백 검증), simple_table_tuple_* 편의 래퍼, 공유 블록 지향 병렬 스캔 보조 함수를 제공한다.
  • src/backend/access/heap/heapam_handler.cheapam_methods(참조 static const TableAmRoutine)와 heap_tableam_handler(SQL에서 보이는 amhandler 함수로 &heapam_methods를 반환)를 정의한다.
  • 슬롯 측(src/include/executor/tuptable.h)이 네 가지 TupleTableSlotOps 구현을 가지며, TTSOpsBufferHeapTuple이 힙의 제로 복사 스캔 형태다.
  • CREATE ACCESS METHOD ... TYPE TABLE, CREATE TABLE ... USING, default_table_access_method, ALTER TABLE / MATERIALIZED VIEW ... SET ACCESS METHOD가 모두 활성이고, pg_dump가 기본값이 아닌 AM을 보존한다.

이 모든 메커니즘은 현재 상태 모듈 문서에 문서화되어 있다 — 여기서 재도출하지 않는다. postgres-table-am.md(디스패치 계층과 TableAmRoutine 인벤토리), postgres-heap-am.md(참조 AM으로서의 힙: HeapTupleHeaderData, HOT, pruning, 가시성, heap_insert/update/delete), postgres-index-am.md(테이블 측 API를 형성한 형제 IndexAmRoutine 인터페이스).

PG19 다음 단계. REL_18 기준으로 코어 트리 AM 집합은 PG12 이후 변경이 없다. 앞으로의 방향은 두 번째 내장 AM을 추가하기보다 인터페이스에 남아 있는 블록 지향 가정들을 제거해 진정으로 비블록 AM(컬럼형, 인메모리)이 더 적은 우회책으로 작동할 수 있게 하는 점진적 방향이다. 여기서 PG19 시대의 개선은 최신 전향 노트로만 취급한다 — 현재 REL_18 동작이 아니다.

PostgreSQL 릴리스 노트 (기능 귀속)

섹션 제목: “PostgreSQL 릴리스 노트 (기능 귀속)”
  • PostgreSQL 12 릴리스 노트 — “Allow table access methods” / 테이블 접근 메서드 API; CREATE ACCESS METHOD ... TYPE TABLE, CREATE TABLE ... USING, default_table_access_method. 슬롯 추상화(TupleTableSlotOps)는 플러그형 스토리지를 가능하게 하는 실행기 작업의 일환으로 같은 릴리스에 포함됐다.
  • PostgreSQL 15 릴리스 노트ALTER TABLE ... SET ACCESS METHOD.
  • PostgreSQL 17 릴리스 노트ALTER MATERIALIZED VIEW ... SET ACCESS METHOD, matview / CREATE TABLE AS에 적용되는 default_table_access_method, SET ACCESS METHOD DEFAULT, pg_dumpSET ACCESS METHOD 출력.

현재 상태 모듈 문서 (메커니즘 — 여기서 재도출하지 않음)

섹션 제목: “현재 상태 모듈 문서 (메커니즘 — 여기서 재도출하지 않음)”
  • postgres-table-am.md — 디스패치 계층, TableAmRoutine, GetTableAmRoutine, TM_Result, ScanOptions, heapam_methods 바인딩, 실행기 호출 지점.
  • postgres-heap-am.md — 참조 구현으로서의 힙.
  • postgres-index-am.md — 테이블 AM 설계의 모델이 된 형제 인덱스 AM(IndexAmRoutine) 인터페이스.

관련 모듈 문서 (위 시대에서 다뤄진 것들)

섹션 제목: “관련 모듈 문서 (위 시대에서 다뤄진 것들)”
  • postgres-alter-table.mdSET ACCESS METHOD 뒤의 재작성/단계 기계.
  • postgres-matview.md — AM 기반 릴레이션으로서의 구체화 뷰 (PG17).
  • postgres-pg-dump-restore.mdSET ACCESS METHOD의 덤프 측 출력.
  • postgres-ddl-execution.md — 생성 시 pg_class.relam 기록.

PostgreSQL 소스 (/data/hgryoo/references/postgres/, REL_18 273fe94)

섹션 제목: “PostgreSQL 소스 (/data/hgryoo/references/postgres/, REL_18 273fe94)”
  • src/include/access/tableam.hTableAmRoutine, table_* 래퍼, TM_Result, ScanOptions, DEFAULT_TABLE_ACCESS_METHOD.
  • src/backend/access/table/tableam.c, tableamapi.c — 디스패치 보조 함수, GetTableAmRoutine.
  • src/backend/access/heap/heapam_handler.cheapam_methods, heap_tableam_handler.
  • src/include/executor/tuptable.hTupleTableSlotOps와 네 가지 TTSOps* 슬롯 구현.
  • src/include/catalog/pg_am.hamtype, AMTYPE_TABLE(‘t’).
  • src/include/nodes/parsenodes.hAT_SetAccessMethod.
  • Stonebraker & Rowe, The Design of POSTGRES (1986) — 쿼리 처리와 스토리지를 분리하는 “추상 데이터 관리자” 비전.
  • Database Internals (Petrov), 3장 — 플러그형 스토리지와 vtable 디스패치.
  • Database System Concepts (Silberschatz et al., 7e), 13장 — 스토리지 매니저 추상화와 접근 메서드 인터페이스.