(KO) PostgreSQL 시스템 카탈로그 — 스키마 정의, 부트스트랩 메커니즘, 카탈로그 쓰기 경로
목차
- 학술적 배경
- DBMS 공통 설계 패턴
- PostgreSQL의 구현
- 소스 코드 가이드
- 소스 검증 (2026-06-05 기준)
- PostgreSQL 너머 — 비교 설계와 연구 프론티어
- 출처
학술적 배경
섹션 제목: “학술적 배경”모든 관계형 데이터베이스 엔진은 데이터 딕셔너리 (data dictionary) 를 갖는다. 이는 테이블, 컬럼, 타입, 함수, 제약조건, 그리고 시스템 내의 모든 명명된 객체를 기술하는 테이블의 집합이다. PostgreSQL에서는 이를 시스템 카탈로그 (system catalog) 라고 부른다. pg_ 접두어를 가진 약 60개의 힙 릴레이션으로 구성되며, 그 내용은 다른 모든 릴레이션의 스키마를 기술한다. 심지어 카탈로그 자신도 서로를 기술한다. Database System Concepts (Silberschatz, ch. 25 §“Catalog Management”)는 데이터 딕셔너리를 모든 파싱, 플래닝, 실행 단계에서 엔진이 의존하는 중앙 메타데이터 저장소로 정의한다. Database Internals (Petrov, ch. 6 §“Catalog and Schema”)는 카탈로그 접근이 모든 쿼리의 핵심 경로에 있다는 점을 지적한다. 플래너는 pg_statistic에서 컬럼 통계를 가져오고, 실행기는 pg_operator에서 연산자 OID를 함수 포인터로 변환하며, 파서는 pg_class에서 릴레이션 존재 여부를 확인한다. 카탈로그의 구조와 그것을 캐시하는 방식이 성능에 큰 영향을 미친다는 점이다.
데이터 딕셔너리 시스템의 성격을 결정하는 설계 축은 두 가지다.
-
자기 기술 (self-describing) 대 외부 메타데이터. 엔진이 자신의 스키마를 일반 SQL로 조회 가능한 테이블에 저장하는가, 아니면 별도의 특권적 저장소에 저장하는가 하는 문제다. PostgreSQL은 버클리 POSTGRES 기원부터 자기 기술 모델을 선택했다.
pg_class자신도pg_class에 한 행으로 존재하고,pg_class의 컬럼 하나하나는pg_attribute에 한 행으로 기술된다. 이 순환성이 PostgreSQL 카탈로그의 본질적 특성이다. 모든 카탈로그를 원칙적으로SELECT로 조회할 수 있고, 모든 DDL 연산은 결국 사용자 릴레이션을 기술하는 바로 그 테이블에 대한 쓰기 연산이 된다는 점이다. -
부트스트랩 (bootstrapping). 카탈로그가 자기 기술적이므로 가장 초기의 카탈로그 행은 일반 DDL 경로로는 만들 수 없다. 카탈로그가 아직 없을 때 카탈로그를 읽을 수 없기 때문이다. 모든 자기 기술 시스템은 이 닭이 먼저냐 달걀이 먼저냐의 문제를 부트스트랩 단계로 해결한다. 이 단계에서는 최소한의 카탈로그 릴레이션과 초기 행이 실행기를 우회하는 특수 코드로 만들어진다. 이 부트스트랩 단계의 형태가 어떤 행이 사전 할당된 OID를 갖는지, 어떤 릴레이션이 핀 고정(삭제 불가)되는지, 그리고 어떤 릴레이션이
pg_class에 매핑 정보를 기록하는 대신 별도의 파일-OID 매핑 테이블이 필요한지를 결정한다.
이 두 축, 즉 자기 기술성과 부트스트랩 의존성이 PostgreSQL 카탈로그 코드 구조의 비직관적인 부분 대부분을 설명한다.
DBMS 공통 설계 패턴
섹션 제목: “DBMS 공통 설계 패턴”관계형 메타데이터 저장소를 가진 거의 모든 상용 DBMS는 엔진별 선택이 개입하기 전에 이미 설계 공간을 좁혀 주는 공통 패턴을 공유한다.
가장 안쪽 카탈로그의 고정 스키마
섹션 제목: “가장 안쪽 카탈로그의 고정 스키마”카탈로그 자신을 기술하는 가장 안쪽 카탈로그 테이블은 컴파일 시점에 알려진 스키마를 가져야 한다. 어떤 행도 읽기 전에 이 테이블을 열고 스캔해야 하기 때문이다. 실제로는 물리 튜플 형식이 그대로 C 구조체로 표현된다. pg_class의 모든 컬럼은 C 구조체의 필드로 존재하고, pg_attribute의 모든 컬럼도 마찬가지다. 이 구조체 레이아웃이 디스크상의 형식과 코드 사이의 계약이 된다.
부트스트랩 객체에 대한 사전 할당 OID
섹션 제목: “부트스트랩 객체에 대한 사전 할당 OID”부트스트랩 단계는 일반 OID 할당 경로가 동작하기 전에 이미 찾을 수 있어야 하는 고정된 카탈로그 행들을 만든다. 이 행들은 소스 코드에 하드코딩된 사전 할당 OID 를 갖는다. 덕분에 헤더를 기반으로 컴파일된 코드가 카탈로그 조회 없이 pg_class를 RelationRelationId (1259)라는 상수로 참조할 수 있다.
표준적인 접근법은 OID 공간을 부트스트랩 객체용 컴파일러 할당 범위(어떤 임계값 아래)와 일반 객체용 런타임 할당 범위(그 위)로 나누는 것이다. 이 임계값은 핀 고정 객체 경계로도 사용된다. OID가 임계값 아래인 객체는 핀 고정 상태로, 컴파일된 심볼 참조가 그 존재에 의존하기 때문에 의존성 시스템이 삭제할 수 없다.
공유 카탈로그 대 퍼-데이터베이스 카탈로그
섹션 제목: “공유 카탈로그 대 퍼-데이터베이스 카탈로그”하나의 postmaster를 공유하는 데이터베이스 클러스터는 카탈로그를 두 계층으로 나눈다. 일부 메타데이터는 클러스터 전체에 속하며(사용자, 테이블스페이스, 복제 원점) 어느 데이터베이스에서도 읽을 수 있어야 한다. 반면 다른 메타데이터(테이블, 컬럼, 함수)는 단일 데이터베이스에 범위가 한정되어 서로 격리되어야 한다. 설계 관례는 두 물리 계층으로 나뉜다.
- 공유 카탈로그 — 클러스터 당 한 벌. 글로벌 테이블스페이스(
pg_global디렉터리)에 저장된다. 어느 데이터베이스의 연결에서도 같은 행을 볼 수 있다. 예:pg_authid,pg_database,pg_tablespace. - 퍼-데이터베이스 카탈로그 — 데이터베이스당 한 벌. 해당 데이터베이스의 테이블스페이스에 저장된다. 데이터베이스 A의 연결에서 데이터베이스 B의 행은 볼 수 없다. 예:
pg_class,pg_attribute,pg_type.
카탈로그 쓰기 경로는 힙 쓰기를 감싼다
섹션 제목: “카탈로그 쓰기 경로는 힙 쓰기를 감싼다”일반적인 카탈로그 쓰기 경로는 힙 튜플을 삽입, 수정, 또는 삭제함과 동시에 모든 보조 인덱스를 원자적으로 유지한다. 힙 행을 삽입한 뒤 그 카탈로그의 모든 보조 인덱스에 해당하는 인덱스 항목을 삽입하는 것이다. 이 과정이 단일 호출로 이루어지기 때문에 호출자가 인덱스를 빠뜨려 불일치가 생기는 일이 없다. 동일한 래퍼 함수가 카탈로그 수준 트리거(무효화 메커니즘)도 선택적으로 발동시켜, 쓰기가 커밋된 후 구 상태의 캐시 뷰가 플러시되도록 한다.
이론 ↔ PostgreSQL 매핑
섹션 제목: “이론 ↔ PostgreSQL 매핑”| 개념 | PostgreSQL 명칭 |
|---|---|
| 데이터 딕셔너리 | 시스템 카탈로그 (pg_class, pg_attribute, …) |
| 고정 스키마 내부 카탈로그 | FormData_pg_class, FormData_pg_attribute C 구조체 |
| 사전 할당 부트스트랩 OID | 하드코딩된 RelationRelationId, TypeRelationId 등 |
| 핀 고정 객체 OID 임계값 | FirstUnpinnedObjectId (12000) |
| 일반 객체 OID 하한 | FirstNormalObjectId (16384) |
| 공유 카탈로그 | BKI_SHARED_RELATION 카탈로그 (11개 릴레이션) |
| 퍼-데이터베이스 카탈로그 | pg_catalog 네임스페이스의 나머지 카탈로그 |
| 부트스트랩 단계 스크립트 | postgres.bki (genbki.pl이 생성) |
| 카탈로그 쓰기 래퍼 | CatalogTupleInsert / CatalogTupleUpdate / CatalogTupleDelete |
| 릴레이션 생성 진입점 | heap_create_with_catalog |
PostgreSQL의 구현
섹션 제목: “PostgreSQL의 구현”CATALOG() 매크로와 헤더 중심 스키마
섹션 제목: “CATALOG() 매크로와 헤더 중심 스키마”모든 PostgreSQL 시스템 카탈로그는 src/include/catalog/ 아래의 단일 C 헤더 파일에 정의된다. 대표적인 예는 src/include/catalog/pg_class.h다.
// FormData_pg_class — src/include/catalog/pg_class.hCATALOG(pg_class,1259,RelationRelationId) BKI_BOOTSTRAP BKI_ROWTYPE_OID(83,RelationRelation_Rowtype_Id) BKI_SCHEMA_MACRO{ Oid oid; NameData relname; Oid relnamespace BKI_DEFAULT(pg_catalog) BKI_LOOKUP(pg_namespace); Oid reltype BKI_LOOKUP_OPT(pg_type); Oid reloftype BKI_DEFAULT(0) BKI_LOOKUP_OPT(pg_type); Oid relowner BKI_DEFAULT(POSTGRES) BKI_LOOKUP(pg_authid); Oid relam BKI_DEFAULT(heap) BKI_LOOKUP_OPT(pg_am); Oid relfilenode BKI_DEFAULT(0); /* 0 = mapped relation */ Oid reltablespace BKI_DEFAULT(0) BKI_LOOKUP_OPT(pg_tablespace); int32 relpages BKI_DEFAULT(0); float4 reltuples BKI_DEFAULT(-1); /* ... */ bool relhasindex BKI_DEFAULT(f); bool relisshared BKI_DEFAULT(f); char relkind BKI_DEFAULT(r); /* ... */ TransactionId relfrozenxid BKI_DEFAULT(3); TransactionId relminmxid BKI_DEFAULT(1); /* variable-length fields follow */} FormData_pg_class;typedef FormData_pg_class *Form_pg_class;CATALOG(name, oid, oidmacro) 매크로는 C 컴파일러에서 typedef struct FormData_<name>으로 전개된다(genbki.h 경유). 따라서 이 구조체는 코드 전반에서 사용되는 평범한 C typedef다. 동시에 genbki.pl은 동일한 헤더를 Perl 데이터 구조 명세로 읽어 postgres.bki와 pg_*_d.h 심볼 파일을 생성한다.
주석 매크로(BKI_BOOTSTRAP, BKI_SHARED_RELATION, BKI_DEFAULT(value), BKI_LOOKUP(catalog))는 C 컴파일러에서는 아무 동작도 하지 않는다. 모두 빈 값으로 전개된다. 반면 genbki.pl은 이 매크로들을 지시자로 인식한다.
BKI_BOOTSTRAP— 이 카탈로그는 부트스트랩 단계(일반 실행기가 준비되기 전)에 생성되어야 한다.BKI_SHARED_RELATION— 이 카탈로그는 글로벌 테이블스페이스에 저장되며 클러스터의 모든 데이터베이스가 공유한다.BKI_DEFAULT(value)—pg_*.dat의 초기 데이터 행에서 해당 컬럼이 없을 때 사용하는 기본값이다.BKI_LOOKUP(catalog)— 이 OID 컬럼은 지정된 카탈로그를 참조한다.genbki.pl이.dat파일의 심볼 이름을 OID로 변환할 때 쓴다.
BKI_BOOTSTRAP을 가진 카탈로그는 네 개다. pg_class (1259), pg_attribute (1249), pg_proc (1255), pg_type (1247)이다. 이 네 개가 가장 안쪽의 자기 참조 루프를 형성한다. pg_class가 pg_attribute를 기술하고, pg_attribute가 pg_class를 기술하는 방식이다. 이 네 개는 실행기가 시작되기 전에 bootstrap/bootstrap.c의 하드코딩된 C 코드로 가장 먼저 생성된다.
그림 1 — 카탈로그 헤더가 genbki.pl, C 컴파일러, 런타임으로 흘러가는 방식.
flowchart LR
H["pg_class.h\n(CATALOG 매크로 + 필드)"]
D["pg_class.dat\n(초기 행 값)"]
G["genbki.pl"]
BKI["postgres.bki\n(부트스트랩 스크립트)"]
SYM["pg_class_d.h\n(OID #define 심볼)"]
CC["C 컴파일러"]
FD["FormData_pg_class\n(C 구조체)"]
RT["런타임\n(relcache, catcache)"]
H --> G
D --> G
G --> BKI
G --> SYM
H --> CC
CC --> FD
BKI --> RT
SYM --> RT
FD --> RT
그림 1 — pg_class.h는 두 소비자에게 읽힌다. genbki.pl은 이를 데이터 구조 명세로 읽어 pg_class.dat의 초기 행과 결합해 postgres.bki(initdb가 실행하는 부트스트랩 스크립트)와 pg_class_d.h(각 OID 및 컬럼 번호 #define 매크로)를 생성한다. C 컴파일러는 같은 헤더를 구조체 정의로 읽어 실행기, relcache, catcache 전반에서 쓰이는 FormData_pg_class를 만든다. 두 출력이 모두 런타임을 공급한다.
pg_attribute 행 레이아웃
섹션 제목: “pg_attribute 행 레이아웃”pg_attribute는 컬럼 카탈로그다. 카탈로그 릴레이션 자신도 포함하여, 모든 릴레이션의 모든 컬럼마다 한 행씩 존재한다. 이 구조체에는 힙 접근 코드가 다른 카탈로그를 조회하지 않고도 모든 튜플을 디코딩할 수 있도록 타입 인코딩 필드가 밀집해 있다.
// FormData_pg_attribute — src/include/catalog/pg_attribute.hCATALOG(pg_attribute,1249,AttributeRelationId) BKI_BOOTSTRAP ...{ Oid attrelid BKI_LOOKUP(pg_class); /* 소유 릴레이션 OID */ NameData attname; /* 컬럼 이름 */ Oid atttypid BKI_LOOKUP_OPT(pg_type); int16 attlen; /* pg_type.typlen 복사본 — 타입 조회 불필요 */ int16 attnum; /* 1 기반 컬럼 번호; 음수 = 시스템 컬럼 */ char attalign; /* pg_type.typalign 복사본 */ char attstorage;/* varlena 인라인 vs. TOAST 전략 */ bool attnotnull; bool atthasdef; /* pg_attrdef에 DEFAULT 식이 있는가? */ char attgenerated; /* 생성 컬럼 종류, 또는 '\0' */ bool attisdropped; /* 논리 삭제 — 이후 컬럼 슬롯 재사용 가능 */ /* ... */} FormData_pg_attribute;핵심 설계 선택은 attlen, attalign, attstorage가 pg_type의 해당 필드 복사본이라는 점이다. 덕분에 힙 AM은 릴레이션의 pg_attribute 행만으로 튜플을 배치하고 디코딩할 수 있다. 테이블 스캔 중에 pg_type을 열 필요가 없다. attisdropped 필드는 PostgreSQL이 힙을 물리적으로 재작성하지 않고 ALTER TABLE DROP COLUMN을 처리하는 방식이다. 컬럼을 삭제 표시만 하고 슬롯을 유지하여 기존 행의 튜플 레이아웃이 유지된다. attnum도 고정되어 OID 기반 참조가 계속 유효하다.
OID 범위와 핀 고정 객체 경계
섹션 제목: “OID 범위와 핀 고정 객체 경계”PostgreSQL의 모든 객체는 OID를 가진다. OID 공간은 서로 다른 의미를 가진 세 범위로 나뉜다.
| 범위 | 상수 | 의미 |
|---|---|---|
[1, 11999] | FirstUnpinnedObjectId (12000) 미만 | 핀 고정 부트스트랩 객체 — genbki.pl이 사전 할당, 삭제 불가 |
[12000, 16383] | [FirstUnpinnedObjectId, FirstNormalObjectId) | 핀 고정되지 않은 부트스트랩 객체 (public 네임스페이스, 템플릿 데이터베이스) |
[16384, …) | FirstNormalObjectId (16384) 이상 | 일반 사용자 생성 객체 |
IsCatalogRelationOid는 릴레이션이 시스템 카탈로그인지를 FirstUnpinnedObjectId와의 단순 비교로 판단한다.
// IsCatalogRelationOid — src/backend/catalog/catalog.cboolIsCatalogRelationOid(Oid relid){ /* * We consider a relation to be a system catalog if it has a pinned OID. * This includes all the defined catalogs, their indexes, and their * TOAST tables and indexes. */ return (relid < (Oid) FirstUnpinnedObjectId);}IsPinnedObject는 모든 객체 클래스에 동일한 테스트를 적용하되, 핀 고정 OID를 가지지만 정책상 핀 고정되지 않은 세 가지 예외를 둔다.
// IsPinnedObject — src/backend/catalog/catalog.cboolIsPinnedObject(Oid classId, Oid objectId){ if (objectId >= FirstUnpinnedObjectId) return false; if (classId == LargeObjectRelationId) return false; /* public 네임스페이스와 데이터베이스는 정책상 핀 고정 제외 */ if (classId == NamespaceRelationId && objectId == PG_PUBLIC_NAMESPACE) return false; if (classId == DatabaseRelationId) return false; return true;}이 OID 범위 테스트는 과거에 모든 사전 로드 객체를 대상으로 명시적으로 유지하던 대규모 pg_depend 행 집합을 대체한다. 정수 비교 한 번 vs. 수천 개의 의존성 행 유지라는 트레이드오프다.
공유 카탈로그와 퍼-데이터베이스 카탈로그 분리
섹션 제목: “공유 카탈로그와 퍼-데이터베이스 카탈로그 분리”11개의 공유 카탈로그는 헤더의 BKI_SHARED_RELATION으로 컴파일 시점에 식별된다. IsSharedRelation은 권위 있는 하드코딩 목록을 유지한다.
// IsSharedRelation — src/backend/catalog/catalog.cboolIsSharedRelation(Oid relationId){ /* These are the shared catalogs (look for BKI_SHARED_RELATION) */ if (relationId == AuthIdRelationId || relationId == AuthMemRelationId || relationId == DatabaseRelationId || relationId == DbRoleSettingRelationId || relationId == ParameterAclRelationId || relationId == ReplicationOriginRelationId || relationId == SharedDependRelationId || relationId == SharedDescriptionRelationId || relationId == SharedSecLabelRelationId || relationId == SubscriptionRelationId || relationId == TableSpaceRelationId) return true; /* ... 인덱스와 toast 테이블 목록이 이어진다 ... */ return false;}왜 pg_class.relisshared를 읽지 않고 하드코딩하는지는 코드 주석에 기록되어 있다. 릴레이션을 잠그기 전에 공유 여부를 알기 위해 pg_class를 스캔하면 부트스트랩 경쟁 조건이 생긴다. 정적 목록이 그 순환을 끊는 방법이다.
매핑 관계 (mapped relation) 메커니즘
섹션 제목: “매핑 관계 (mapped relation) 메커니즘”네 개의 부트스트랩 카탈로그(pg_class, pg_attribute, pg_proc, pg_type)와 몇 개의 다른 릴레이션은 pg_class 행의 relfilenode가 0이다. 이 0값은 물리 파일 매핑이 pg_class가 아니라 relmapper.c가 읽는 별도 파일 pg_filenode.map에 있다는 신호다. 이를 매핑 관계 (mapped relation) 라고 부른다.
매핑 관계가 존재하는 이유는 부트스트랩 순환이다. pg_class 자신의 relfilenode를 기록하려면 pg_class에 행을 써야 한다. 그런데 그 쓰기 자체가 pg_class가 이미 존재해야 한다. pg_filenode.map이 이 순환을 끊는다. 이 파일은 카탈로그 쓰기 경로를 거치지 않고 relmapper.c가 직접 갱신하는 소형 바이너리 파일이다. 공유 카탈로그용과 퍼-데이터베이스 카탈로그용으로 두 벌이 존재한다.
// RelMapFile — src/backend/utils/cache/relmapper.c#define RELMAPPER_FILENAME "pg_filenode.map"#define RELMAPPER_FILEMAGIC 0x592717 /* version ID */
typedef struct RelMapFile{ int32 magic; /* always RELMAPPER_FILEMAGIC */ /* ... (relid -> relfilenumber) 매핑 배열 ... */} RelMapFile;일반 사용자 릴레이션은 모두 pg_class에 0이 아닌 relfilenode를 가지며 매퍼를 사용하지 않는다. 매핑 관계 메커니즘은 일반 SQL에서는 보이지 않는 내부 부트스트랩 구현 세부사항이다.
그림 2 — 매핑 관계와 일반 관계의 파일 조회 경로 분기.
flowchart TD
Q["OID로 릴레이션 열기"]
R["pg_class 행 읽기\n(relcache 경유)"]
Z{"relfilenode == 0?"}
MAP["relmapper.c\npg_filenode.map 읽기\nRelFileNumber 반환"]
DIRECT["pg_class의 relfilenode\n직접 사용"]
SMGR["smgr_open(RelFileLocator)"]
Q --> R
R --> Z
Z -- 예 매핑 관계 --> MAP
Z -- 아니오 일반 --> DIRECT
MAP --> SMGR
DIRECT --> SMGR
그림 2 — relcache가 릴레이션을 열 때 pg_class.relfilenode가 0인지 확인한다. 0이면 relmapper.c가 퍼-데이터베이스(또는 글로벌) pg_filenode.map 파일에서 물리 파일 번호를 읽어 반환한다. 0이 아니면 직접 사용한다. 이후 smgr_open이 물리 세그먼트 파일을 찾는다.
카탈로그 쓰기 경로
섹션 제목: “카탈로그 쓰기 경로”새 스키마 객체를 만들거나 삭제하는 모든 DDL 문은 결국 공통 카탈로그 쓰기 경로를 통과한다. 새 테이블 생성의 진입점은 catalog/heap.c의 heap_create_with_catalog다. 이 함수는 여덟 단계를 조율한다.
// heap_create_with_catalog — src/backend/catalog/heap.c// (heap.c 410행의 주석 블록에서 발췌)//// 1. CheckAttributeNamesTypes — 컬럼 이름과 타입 OID 유효성 검사// 2. get_relname_relid — 중복 이름 존재 여부 확인// 3. heap_create — relcache 항목 + 물리 스토리지 파일 생성// 4. TypeCreate — 내재적 복합 행 타입 생성// 5. AddNewRelationTuple — pg_class 행 삽입// 6. AddNewAttributeTuples — 컬럼마다 pg_attribute 행 삽입// 7. StoreConstraints — CHECK / NOT NULL 제약 기록// 8. 새 OID 반환AddNewRelationTuple은 CatalogTupleInsert를 사용해 pg_class에 단일 행을 삽입한다. AddNewAttributeTuples는 컬럼 목록을 순회하며 컬럼마다 CatalogTupleInsertWithInfo를 호출한다. 인덱스 열기 비용을 CatalogIndexState를 유지하여 분할 상환한다.
CatalogTupleInsert는 힙과 인덱스 쓰기를 동기화 상태로 유지하는 얇은 래퍼다.
// CatalogTupleInsert — src/backend/catalog/indexing.cvoidCatalogTupleInsert(Relation heapRel, HeapTuple tup){ CatalogIndexState indstate;
CatalogTupleCheckConstraints(heapRel, tup);
indstate = CatalogOpenIndexes(heapRel);
simple_heap_insert(heapRel, tup);
CatalogIndexInsert(indstate, tup, TU_All); CatalogCloseIndexes(indstate);}CatalogOpenIndexes는 카탈로그의 모든 보조 인덱스를 열고, CatalogIndexInsert는 새 힙 튜플의 키 필드를 각 인덱스에 전파하며, CatalogCloseIndexes는 인덱스를 닫는다. CatalogTupleUpdate와 CatalogTupleDelete에도 동일한 래퍼 논리가 적용된다. 이 래퍼가 없으면 동시 DDL 아래에서 인덱스가 힙과 무음으로 벌어질 수 있다.
새 객체의 OID 할당
섹션 제목: “새 객체의 OID 할당”DDL 문이 새 릴레이션의 신선한 OID를 필요로 할 때 GetNewOidWithIndex(또는 그 래퍼 GetNewRelFileNumber)를 호출한다.
// GetNewOidWithIndex — src/backend/catalog/catalog.cOidGetNewOidWithIndex(Relation relation, Oid indexId, AttrNumber oidcolumn){ Oid newOid; SysScanDesc scan; ScanKeyData key; bool collides;
Assert(IsSystemRelation(relation));
/* 부트스트랩 모드에서는 인덱스가 없으므로 순차 카운터를 사용 */ if (IsBootstrapProcessingMode()) return GetNewObjectId();
do { newOid = GetNewObjectId(); /* 클러스터 전체 OID 카운터 증가 */
ScanKeyInit(&key, oidcolumn, BTEqualStrategyNumber, F_OIDEQ, ObjectIdGetDatum(newOid));
/* SnapshotAny: 미커밋 행도 보아 일시적 충돌 방지 */ scan = systable_beginscan(relation, indexId, true, SnapshotAny, 1, &key); collides = HeapTupleIsValid(systable_getnext(scan)); systable_endscan(scan); } while (collides);
return newOid;}재시도 루프는 OID 카운터 순환(wrap-around) 충돌의 이론적 경우를 처리한다. systable_beginscan은 표준 카탈로그 스캔 기본 연산이다. indexOK가 true이고 인덱스가 있으면 인덱스를 사용하고, 그렇지 않으면 순차 힙 스캔으로 폴백하며, 제공된 Snapshot으로 MVCC 가시성을 제어한다.
systable_beginscan / systable_getnext API
섹션 제목: “systable_beginscan / systable_getnext API”시스템 카탈로그를 읽는 모든 코드, OID 할당뿐 아니라 모든 DDL 조회도 src/include/access/genam.h에 선언된 systable_* 패밀리를 사용한다.
// systable_beginscan — src/include/access/genam.hextern SysScanDesc systable_beginscan( Relation heapRelation, Oid indexId, /* InvalidOid = 순차 스캔 */ bool indexOK, /* 인덱스 사용 가능? */ Snapshot snapshot, /* MVCC 가시성 */ int nkeys, ScanKey key);extern HeapTuple systable_getnext(SysScanDesc sysscan);extern void systable_endscan(SysScanDesc sysscan);indexOK 플래그는 인덱스가 아직 유효하지 않을 수 있을 때(예: 인덱스가 빌드되기 전 부트스트랩 단계) 호출자가 힙 스캔으로 폴백할 수 있게 한다. SnapshotAny를 넘기면 트랜잭션 상태와 무관하게 모든 튜플 버전이 보이고, SnapshotSelf(호출자의 현재 스냅숏)를 넘기면 일반 MVCC 가시성이 적용된다.
인플레이스 업데이트 예외
섹션 제목: “인플레이스 업데이트 예외”대부분의 카탈로그 수정은 일반 힙 업데이트다. 구 튜플이 죽고 새 튜플로 교체되며, WAL에 기록되고, 커밋 후 새 스냅숏에 보인다. 하지만 두 개의 카탈로그, pg_class와 pg_database는 인플레이스 (inplace) 업데이트 경로도 지원한다. 이는 relpages, reltuples 같은 통계 필드와 전통적인 트랜잭션 의미론이 필요 없는 일부 필드를 위해 쓰인다.
// IsInplaceUpdateOid — src/backend/catalog/catalog.cboolIsInplaceUpdateOid(Oid relid){ return (relid == RelationRelationId || relid == DatabaseRelationId);}인플레이스 업데이트는 새 튜플 버전을 만들지 않고 그 자리에서 필드를 덮어쓴다. 일반적인 의미의 WAL 기록이 없고(MVCC 언두 체인이 만들어지지 않음), 커밋을 기다리지 않아도 동시 트랜잭션이 볼 수 있다. 이 메커니즘은 특히 autovacuum의 pg_class 통계 쓰기를 위해 설계되었다. pg_class의 퍼-테이블 통계 행에 전통적 트랜잭션 업데이트(죽은 튜플, vacuum, 인덱스 업데이트)를 매번 수행하면 자기 모순적 오버헤드가 발생하기 때문이다. genam.h의 systable_inplace_update_begin / systable_inplace_update_finish API가 이 경로에 대한 잠금 규율을 제공한다.
그림 3 — CREATE TABLE의 전체 카탈로그 쓰기 경로.
flowchart TD
DDL["CREATE TABLE 문\n(utility.c → tablecmds.c)"]
HCC["heap_create_with_catalog\n(catalog/heap.c)"]
HC["heap_create\nrelcache 항목 빌드\n물리 파일 생성"]
TC["TypeCreate\n내재적 행 타입용\npg_type 행 삽입"]
ART["AddNewRelationTuple\npg_class 행 삽입"]
AAT["AddNewAttributeTuples\npg_attribute 행 삽입\n(컬럼마다 한 행)"]
SC["StoreConstraints\npg_constraint 행 삽입"]
CTI["CatalogTupleInsert\n(catalog/indexing.c)"]
SHI["simple_heap_insert\n(힙 튜플)"]
CII["CatalogIndexInsert\n(모든 보조 인덱스)"]
DDL --> HCC
HCC --> HC
HCC --> TC
HCC --> ART
HCC --> AAT
HCC --> SC
ART --> CTI
AAT --> CTI
CTI --> SHI
CTI --> CII
그림 3 — CREATE TABLE의 DDL 흐름. heap_create_with_catalog가 여덟 단계를 조율한다. 모든 카탈로그 행 삽입은 CatalogTupleInsert를 거쳐 힙 튜플과 해당 카탈로그의 모든 보조 인덱스 항목이 원자적으로 기록된다.
소스 코드 가이드
섹션 제목: “소스 코드 가이드”catalog/catalog.c — 분류 및 OID 유틸리티
섹션 제목: “catalog/catalog.c — 분류 및 OID 유틸리티”IsSystemRelation—IsSystemClass에 위임; 카탈로그이거나 toast 테이블인지 테스트IsCatalogRelation/IsCatalogRelationOid— OID <FirstUnpinnedObjectIdIsSharedRelation— 11개 공유 카탈로그 OID 및 그 인덱스/toast의 하드코딩 목록IsCatalogNamespace/IsToastNamespace— 네임스페이스 기반 분류IsReservedName—pg_로 시작하는 이름은 예약됨IsInplaceUpdateRelation/IsInplaceUpdateOid—pg_class와pg_database만 해당IsPinnedObject— OID 범위 테스트, 세 가지 정책 예외 포함GetNewOidWithIndex—systable_beginscan+SnapshotAny를 사용한 재시도 루프 OID 할당기GetNewRelFileNumber— 새 relfilenode를 위해pg_class에서GetNewOidWithIndex호출
catalog/heap.c — 릴레이션 생성 및 삭제
섹션 제목: “catalog/heap.c — 릴레이션 생성 및 삭제”heap_create— relcache 항목 + 물리 스토리지 파일 빌드;pg_class쓰기 없음heap_create_with_catalog— 8단계 릴레이션 생성 조율자AddNewRelationTuple—pg_class행 구성 및 삽입AddNewAttributeTuples— 분할 상환 인덱스 상태로pg_attribute행 반복 삽입heap_drop_with_catalog—pg_class,pg_attribute, 의존 테이블의 행 삭제
catalog/indexing.c — 카탈로그 쓰기 래퍼
섹션 제목: “catalog/indexing.c — 카탈로그 쓰기 래퍼”CatalogOpenIndexes— 카탈로그 릴레이션의 모든 보조 인덱스 열기CatalogTupleInsert—simple_heap_insert+CatalogIndexInsert(모든 인덱스)CatalogTupleInsertWithInfo— 호출자가 제공한CatalogIndexState로 동일 작업CatalogTupleUpdate—heap_update+ 인덱스 유지CatalogTupleDelete—heap_delete+ (현재 인덱스 수준 삭제 훅 불필요)
include/catalog/genbki.h — 매크로 계약
섹션 제목: “include/catalog/genbki.h — 매크로 계약”CATALOG(name,oid,oidmacro)— C에서는 구조체 typedef로 전개;genbki.pl이 스키마로 파싱BKI_BOOTSTRAP,BKI_SHARED_RELATION— C에서 빈 값인 카탈로그 수준 주석BKI_DEFAULT(value),BKI_LOOKUP(catalog),BKI_FORCE_NULL— 필드 수준 주석
include/catalog/pg_class.h — 릴레이션 카탈로그
섹션 제목: “include/catalog/pg_class.h — 릴레이션 카탈로그”FormData_pg_class— C 구조체; 필드:oid,relname,relnamespace,reltype,relam,relfilenode(0=매핑),reltablespace,relpages,reltuples,relhasindex,relisshared,relkind,relfrozenxid,relminmxid등MAKE_SYSCACHE(RELOID, …)/MAKE_SYSCACHE(RELNAMENSP, …)— syscache 항목 두 개RELKIND_RELATION'r',RELKIND_INDEX'i',RELKIND_VIEW'v'등
include/catalog/pg_attribute.h — 컬럼 카탈로그
섹션 제목: “include/catalog/pg_attribute.h — 컬럼 카탈로그”FormData_pg_attribute—attrelid,attname,atttypid,attlen,attnum,attalign,attstorage,attnotnull,atthasdef,attgenerated,attisdropped
utils/cache/relmapper.c — 부트스트랩 파일 번호 맵
섹션 제목: “utils/cache/relmapper.c — 부트스트랩 파일 번호 맵”RELMAPPER_FILENAME—"pg_filenode.map"(데이터베이스마다 한 벌 + 글로벌 한 벌)RelMapFile— 바이너리 구조체: 매직 워드 +(relid → relfilenumber)쌍 배열RelationMapOidToFilenumber— relcache가 사용하는 조회 함수
위치 힌트 (커밋 273fe94, 2026-06-05)
섹션 제목: “위치 힌트 (커밋 273fe94, 2026-06-05)”| 심볼 | 파일 | 행 |
|---|---|---|
IsSystemRelation | src/backend/catalog/catalog.c | 74 |
IsCatalogRelation | src/backend/catalog/catalog.c | 104 |
IsCatalogRelationOid | src/backend/catalog/catalog.c | 121 |
IsInplaceUpdateOid | src/backend/catalog/catalog.c | 193 |
IsToastRelation | src/backend/catalog/catalog.c | 206 |
IsCatalogNamespace | src/backend/catalog/catalog.c | 243 |
IsToastNamespace | src/backend/catalog/catalog.c | 261 |
IsReservedName | src/backend/catalog/catalog.c | 278 |
IsSharedRelation | src/backend/catalog/catalog.c | 304 |
IsPinnedObject | src/backend/catalog/catalog.c | 370 |
GetNewOidWithIndex | src/backend/catalog/catalog.c | 448 |
GetNewRelFileNumber | src/backend/catalog/catalog.c | 557 |
heap_create | src/backend/catalog/heap.c | 285 |
CheckAttributeNamesTypes | src/backend/catalog/heap.c | 452 |
AddNewAttributeTuples | src/backend/catalog/heap.c | 848 |
AddNewRelationTuple | src/backend/catalog/heap.c | 1001 |
heap_create_with_catalog | src/backend/catalog/heap.c | 1139 |
heap_drop_with_catalog | src/backend/catalog/heap.c | 1801 |
CatalogOpenIndexes | src/backend/catalog/indexing.c | 43 |
CatalogTupleInsert | src/backend/catalog/indexing.c | 233 |
CatalogTupleInsertWithInfo | src/backend/catalog/indexing.c | 256 |
CatalogTupleUpdate | src/backend/catalog/indexing.c | 313 |
CatalogTupleDelete | src/backend/catalog/indexing.c | 365 |
소스 검증 (2026-06-05 기준)
섹션 제목: “소스 검증 (2026-06-05 기준)”검증된 사실
섹션 제목: “검증된 사실”-
IsCatalogRelationOid는 테이블 스캔이나 인메모리 집합이 아닌FirstUnpinnedObjectId(12000)와의 단순 OID 비교를 사용한다.catalog.c:121–136에서 검증. 주석에 이유가 명시되어 있다. OID 순환(wrap-around)이 핀 고정 범위를 건너뛰므로 사용자 정의 객체가 절대로 이 임계값 아래로 내려갈 수 없다.information_schema의 릴레이션들은 12000 이상의 OID를 가지므로 올바르게 제외된다. -
IsSharedRelation은relisshared읽기가 아닌 OID 상수의 하드코딩 목록이다.catalog.c:304–361에서 검증. 288행의 주석이 이유를 기록하고 있다. 릴레이션의pg_class.relisshared를 읽어 공유 여부를 파악하면 부트스트랩 경쟁 조건이 생긴다. 공유 카탈로그 테이블 11개와 그 인덱스 및 TOAST 구조가 열거되어 있다. -
BKI_BOOTSTRAP카탈로그는 정확히 네 개다:pg_class,pg_attribute,pg_proc,pg_type.src/include/catalog/의 모든 헤더에서BKI_BOOTSTRAP을 그렙하여 검증. 이 네 개가CATALOG()라인에BKI_BOOTSTRAP을 가진다. 다른 모든 카탈로그는 실행기가 시작된 후postgres.bki의 BKI 명령으로 생성된다. -
pg_class의relfilenode = 0은 매핑 관계의 신호다.RELMAPPER_FILENAME은"pg_filenode.map"이다.pg_class.h56행 주석과relmapper.c70행에서 검증. 매퍼는 두 벌을 유지한다. 공유 카탈로그용은 글로벌 테이블스페이스에, 퍼-데이터베이스 카탈로그용은 각 데이터베이스 디렉터리에 있다. -
CatalogTupleInsert는simple_heap_insert를 호출한 뒤TU_All로CatalogIndexInsert를 호출한다. 명시적 WAL 플러시는 이 함수에서 수행하지 않는다. WAL 규칙은 페이지 플러시 시점에 버퍼 매니저가 강제한다.indexing.c:233–250에서 검증. 인덱스 업데이트는 지연되지 않고 함수가 반환하기 전 같은 호출 내에서 이루어진다. -
GetNewOidWithIndex는 충돌 검사에SnapshotDirty나 호출자의 MVCC 스냅숏이 아닌SnapshotAny를 사용한다.catalog.c:448–515에서 검증. 주석에 설명이 있다.SnapshotDirty는 최근 삭제된 행을 놓쳐 일시적 OID 재사용 위험을 만든다.SnapshotAny는 모든 버전을 본다. -
IsInplaceUpdateOid는 정확히 두 카탈로그에만 제한된다:pg_class(RelationRelationId1259)와pg_database(DatabaseRelationId1262).catalog.c:193–197에서 검증. 인플레이스 경로는 autovacuum의pg_class통계 쓰기와pg_database의datfrozenxid업데이트 오버헤드를 줄이기 위해 존재한다.
미해결 질문
섹션 제목: “미해결 질문”-
인플레이스 업데이트 잠금 프로토콜.
genam.h:278–285에 선언된systable_inplace_update_begin/_finish는 이전heap_inplace_updateAPI를 대체하기 위해 최근 사이클에 추가되었다.README.tuplock의 §“Locking to write inplace-updated tables” 섹션은 이 테이블들에 대한heap_update가 동시 인플레이스 쓰기자와 호환되는 잠금을 유지해야 한다고 언급한다. 같은pg_class행에 대한 autovacuum의 동시 VACUUM 유발 인플레이스 쓰기와의 정확한 상호작용은 이 문서에서 끝까지 추적되지 않았다. 조사 경로:README.tuplock을 읽고,heap_inplace_update_and_unlock(heapam.c:6495)과check_lock_if_inplace_updateable_rel호출을 추적한다. -
부트스트랩 단계와 initdb 경계. 네 개의
BKI_BOOTSTRAP카탈로그는 실행기가 시작되기 전에bootstrap/bootstrap.c의 하드코딩된 C 코드로 생성된다. 이후 어떤 카탈로그 행들이postgres.bki의 BKI 명령으로 만들어지고(여전히 실행기 없이), 어떤 것들이BootstrapModeExit()호출 이후의 SQL 스크립트로 만들어지는지는 이 문서에서 완전히 매핑되지 않았다. 조사 경로:src/bin/initdb/initdb.c와postgres.bki대system_functions.sql실행 순서를 읽는다.
PostgreSQL 너머 — 비교 설계와 연구 프론티어
섹션 제목: “PostgreSQL 너머 — 비교 설계와 연구 프론티어”-
System R의 데이터 딕셔너리 — System R (IBM, 1976; Astrahan et al.)은 카탈로그 정보를 사용자 데이터와 동일한 관계형 엔진으로 조회 가능한 일반 기반 릴레이션(
SYSRELATIONS,SYSCOLUMNS)에 저장했다. PostgreSQL은 이 자기 기술 철학을 직접 계승한다. CATALOG 매크로와 헤더 중심 접근법은 같은 아이디어의 현대적 구체화다.knowledge/research/dbms-papers/systemr.md가 System R 설계를 다룬다. -
Oracle의 X$ 고정 테이블 — Oracle의 가장 안쪽 카탈로그 계층은 디스크상의 힙 페이지가 아니라 가상 릴레이션으로 노출된 인메모리 C 구조체인 “X$” 테이블로 구성된다.
V$뷰와DBA_*뷰가 그 위에 쌓인다. PostgreSQL의 접근법, 즉 하드코딩된 구조체 레이아웃을 가진 물리 힙 페이지는 System R에 더 가깝고, Oracle이 가장 안쪽 계층에서 사용하는 2단계 가상 릴레이션 간접성을 피한다. -
SQL Server의
sysobjects/sys.*패밀리 — SQL Server는 사용자 가시 카탈로그 뷰(sys.tables,sys.columns)를 비관계적 내부 구조를 포함하는 엔진 물리 저장소와 분리한다. PostgreSQL은 원시 카탈로그 테이블을pg_class,pg_attribute등으로 직접 노출하고information_schema뷰를 그 위에 빌드한다. 따라서 DBA에게 물리 레이아웃이 보인다. -
pg_filenode.map부트스트랩 순환 — 매핑 관계 메커니즘은 깨끗한 부트스트랩에서 살아남아야 하는 자기 기술 DBMS 모두가 공유하는 패턴이다. MySQL의.frm파일(8.0 이전)은 스키마를 엔진의 자체 스토리지 밖에 두어 같은 문제를 해결했다. PostgreSQL 8.4가 현재의relmapper.c접근법을 도입하여 이전의 하드코딩된 relfilenode 가정을 대체했다. 진화 문서(postgres-evolution-system-catalog.md, 계획 중)가 부트스트랩과 카탈로그 스토리지가 버클리 POSTGRES부터 PG 7.x를 거쳐 현재의 헤더 중심 genbki 접근법으로 변화한 과정을 추적해야 한다. -
동적 카탈로그와 컬럼형 카탈로그 저장소 — Babelfish, Citus 분산 카탈로그 등 여러 연구 시스템이 PostgreSQL 카탈로그를 확장하거나 재분산한다. 주요 제약은
pg_class와pg_attribute가 컴파일 시점 구조체 지식만으로 힙 AM이 읽을 수 있어야 한다는 점이다. 이 두 릴레이션의 물리 레이아웃을 바꾸는 확장은 부트스트랩 체인을 끊는다.
소스 파일 (커밋 273fe94, REL_18_STABLE)
섹션 제목: “소스 파일 (커밋 273fe94, REL_18_STABLE)”src/backend/catalog/catalog.c— OID 분류, IsPinnedObject, GetNewOidWithIndexsrc/backend/catalog/heap.c— heap_create_with_catalog, AddNewRelationTuple, heap_drop_with_catalogsrc/backend/catalog/indexing.c— CatalogTupleInsert/Update/Delete 래퍼src/include/catalog/catalog.h— catalog.c 함수 프로토타입src/include/catalog/genbki.h— CATALOG(), BKI_* 매크로 정의src/include/catalog/pg_class.h— FormData_pg_class, RELKIND_* 상수src/include/catalog/pg_attribute.h— FormData_pg_attributesrc/include/catalog/pg_proc.h— BKI_BOOTSTRAP 부트스트랩 카탈로그src/include/catalog/pg_type.h— BKI_BOOTSTRAP 부트스트랩 카탈로그src/include/catalog/pg_authid.h— BKI_SHARED_RELATION 예시src/include/catalog/pg_database.h— BKI_SHARED_RELATION 예시src/include/access/transam.h— FirstUnpinnedObjectId, FirstNormalObjectIdsrc/include/access/genam.h— systable_beginscan/getnext/endscan 선언src/backend/utils/cache/relmapper.c— RelMapFile, RELMAPPER_FILENAME
교재 참고문헌
섹션 제목: “교재 참고문헌”- Database System Concepts, Silberschatz et al., ch. 25 §“Catalog Management” — 데이터 딕셔너리 설계
- Database Internals, Petrov, ch. 6 §“Catalog and Schema” — 쿼리 핵심 경로상의 메타데이터 접근
이 KB의 관련 문서
섹션 제목: “이 KB의 관련 문서”postgres-relcache.md—pg_class와pg_attribute행이 RelationData로 조립되는 방법postgres-catcache-syscache.md— 개별 카탈로그 튜플에 대한 백엔드별 캐시postgres-cache-invalidation.md— DDL 후 relcache/catcache를 플러시하는 sinval 큐postgres-ddl-execution.md—heap_create_with_catalog를 호출하는utility.c디스패치postgres-dependency-tracking.md—pg_depend/pg_shdepend와IsPinnedObjectpostgres-namespace-search-path.md— 카탈로그 스캔에 앞서 이루어지는namespace.c와search_path분석