(KO) PostgreSQL Relation Cache — RelationData, 부트스트랩 네일링, Sinval 기반 재빌드
목차
이론적 배경
섹션 제목: “이론적 배경”익스큐터가 실행하는 모든 쿼리는 접근하는 릴레이션의 메타데이터가 필요하다. 컬럼 이름과 타입, 물리 파일 위치, 적용 가능한 인덱스, 재작성 규칙, 트리거, 행 보안 정책이 모두 여기에 해당한다. 이 메타데이터를 매 플랜 노드 호출마다 카탈로그에서 가져온다면 모든 튜플 접근이 카탈로그 스캔 뒤에 직렬화된다. 이를 해결하는 표준 방법이 릴레이션 디스크립터 캐시다. 릴레이션 OID를 키로 하는 백엔드 전용 인메모리 저장소를 두고, OID를 완전히 조립된 메타데이터 레코드에 매핑한다. 카탈로그 스캔 비용은 세션 내 릴레이션당 최대 한 번만 발생한다.
Database System Concepts (Silberschatz, 11장 §“System Catalog”)는 핵심 긴장 관계를 지적한다. 카탈로그 자체가 릴레이션들의 집합이므로, 카탈로그로부터 캐시를 만들려면 캐시가 카탈로그 릴레이션들의 항목을 이미 갖고 있어야 한다. 모든 DBMS는 이 부트스트랩 순환 참조를 어떤 방식으로든 끊어야 한다. 주요 접근 방식은 세 가지다.
- 하드와이어드 스텁. 어떤 카탈로그 행이든 읽으려면 반드시 필요한 소수의 카탈로그가 있다. 엔진은 그 디스크립터를 컴파일 타임 상수(빌드 시 스키마로부터 생성된 배열 초기화값)로 제공하고, 일반 카탈로그 스캔 경로를 우회해 직접 설치한다.
- 초기화 파일. 첫 완전한 빌드 이후 조립된 디스크립터를 디스크에 직렬화해 두고 이후 시동에서 다시 읽어옴으로써, 백엔드 시작마다 카탈로그 스캔 비용을 반복 지불하지 않는다.
- 무효화 신호. DDL이 릴레이션 스키마를 변경하면 캐시된 메타데이터가 낡아질 수 있다. 캐시는 카탈로그 무효화 메커니즘에서 오는 신호를 수신하고 해당 항목을 버리거나 재빌드해야 한다.
두 가지 설계 선택이 모든 구현의 형태를 결정한다.
- 캐시 항목의 단위. 항목 하나가 카탈로그 행 하나(
pg_class)에 대응하는가, 아니면 릴레이션을 기술하는 모든 하위 카탈로그(pg_attribute,pg_index,pg_rewrite,pg_trigger등)를 통합한 집합체에 대응하는가. 완전히 조립된 항목은 빌드 비용이 높지만 쿼리 실행 시 필드별 카탈로그 조회를 없앤다. - 열린 항목의 재빌드 전략. 백엔드가 현재 사용 중인 릴레이션에 무효화가 도착했을 때, 항목을 제자리에서 재빌드할 수 있는가, 아니면 백엔드가 기다려야 하는가. 제자리 재빌드는 호출자가 하위 구조(튜플 디스크립터, 규칙 트리)에 대한 원시 C 포인터를 보유할 수 있으므로 재빌드 전후로 포인터 안정성이 유지돼야 한다.
PostgreSQL은 완전히 조립된 집합체 항목을 선택하고 제자리 스왑-앤-리빌드 규칙을 채택한다. 그 이유는 §“PostgreSQL의 접근 방식”에서 살펴본다.
공통 DBMS 설계
섹션 제목: “공통 DBMS 설계”교과서는 모형을 제시하고, 이 절은 거의 모든 멀티유저 RDBMS가 채택하는 엔지니어링 관례를 정리한다.
OID를 키로 한 백엔드 전용 해시
섹션 제목: “OID를 키로 한 백엔드 전용 해시”디스크립터 캐시는 릴레이션 OID를 키로 하는 프로세스 로컬 해시 테이블이다. OID는 데이터베이스 클러스터 생애 동안 안정적이다. 기존 객체의 OID는 재활용되지 않으므로 캐시된 항목 아래서 키가 바뀌는 일이 없다. 테이블이 프로세스 로컬인 이유는 카탈로그 메타데이터가 일반 MVCC 스냅샷으로 읽히기 때문이다. 서로 다른 트랜잭션 격리 수준에 있는 두 백엔드는 릴레이션 스키마의 다른 버전을 정당하게 볼 수 있다. 공유 캐시는 스냅샷 인식 축출 로직이 필요해 프로세스별 테이블보다 비용이 높다.
완전히 조립된 집합체 디스크립터
섹션 제목: “완전히 조립된 집합체 디스크립터”캐시는 단일 시스템 카탈로그 행을 캐시하는 대신, 릴레이션당 하나의 인메모리 레코드를 조립한다. 이 레코드에는 다음이 포함된다.
pg_class행 (릴레이션 이름, OID, 타입 OID, 지속성 등)- 튜플 디스크립터 (컬럼당
FormData_pg_attribute하나) - 규칙 락 (
pg_rewrite에서 파싱된 재작성 규칙) - 트리거 디스크립터 (
pg_trigger에서) - RLS 디스크립터 (
pg_policy에서) - 인덱스 목록 (
pg_index에서) - 인덱스 AM 루틴 포인터 (인덱스 릴레이션용)
- 물리 파일 위치
레코드 하나를 조립하는 비용은 높지만, 쿼리 실행은 컬럼별 메타데이터나 규칙·트리거 적용 여부를 알기 위해 시스템 카탈로그를 읽지 않고 캐시 항목을 읽는다.
네일된 항목 대 일반 항목
섹션 제목: “네일된 항목 대 일반 항목”임의의 카탈로그를 읽으려면 반드시 필요한 소수의 카탈로그(릴레이션 카탈로그, 어트리뷰트 카탈로그, 그리고 그 핵심 인덱스들)가 있다. 이것들은 카탈로그 스캔으로 캐시 항목을 만드는 코드가 실행되기 전에 이미 존재해야 하므로 캐시가 자기 자신으로부터 부트스트랩돼야 한다. 관례는 이 항목들을 네일하는 것이다. 절대 축출되지 않도록 표시하고 하드와이어드 컴파일 타임 데이터로 미리 설치한다. 다른 모든 항목은 요청 시 빌드되고 무효화 시 축출(플러시)될 수 있다.
리소스 오너 안전망과 참조 카운팅
섹션 제목: “리소스 오너 안전망과 참조 카운팅”캐시 항목은 백엔드가 사용하는 동안 해제되면 안 되지만, 캐시가 항목을 영원히 살려두는 것도 바람직하지 않다. 표준 패턴은 참조 카운트다. 열기(핀)하면 증가하고, 닫기(언핀)하면 감소한다. 참조 카운트가 0인 항목은 축출될 수 있다. 리소스 오너 메커니즘은 열린 참조를 트랜잭션 범위에 연결해, 호출자가 명시적 닫기 전에 트랜잭션이 중단되면 캐시 참조가 자동 해제된다.
무효화 기반 재빌드
섹션 제목: “무효화 기반 재빌드”DDL이 릴레이션을 변경하면 시스템은 모든 백엔드에 공유 무효화(sinval, shared-invalidation) 메시지를 보낸다. 수신 시, 해당 OID가 캐시에 있는 백엔드는 항목을 버리거나(참조 카운트가 0이면) 무효 표시 후 다음 접근 시 재빌드해야 한다. 현재 열려 있는(참조 카운트 > 0인) 항목을 재빌드할 때는, 호출자가 하위 구조에 대한 직접 C 포인터를 보유할 수 있으므로 기존 RelationData 구조체나 하위 포인터를 이동시켜서는 안 된다.
초기화 파일 프리로딩
섹션 제목: “초기화 파일 프리로딩”시동마다 네일된 항목 전체를 처음부터 조립하는 것은 느리다. 첫 번째로 프로세스를 완성한 백엔드는 네일된 항목을 바이너리 파일(pg_internal.init)에 직렬화한다. 이후 백엔드는 카탈로그를 스캔하지 않고 이 파일을 로드해 부트스트랩 스캔 전체를 건너뛴다. 파일은 네일된 항목을 변경할 수 있는 sinval 이벤트가 발생할 때마다 원자적으로 무효화된다(rename으로 교체).
이론 ↔ PostgreSQL 매핑
섹션 제목: “이론 ↔ PostgreSQL 매핑”| 이론 / 관례 | PostgreSQL 이름 |
|---|---|
| 백엔드 전용 OID 키 캐시 | RelationIdCache — relcache.c의 HTAB * |
| 항목별 집합체 디스크립터 | RelationData (rel.h에서 Relation으로 typedef) |
pg_class 행 하위 필드 | RelationData.rd_rel (Form_pg_class) |
| 튜플 디스크립터 하위 필드 | RelationData.rd_att (TupleDesc) |
| 규칙 락 하위 필드 | RelationData.rd_rules (RuleLock *) |
| 물리 파일 주소 | RelationData.rd_locator (RelFileLocator) |
| 네일된 항목 | rd_isnailed = true, rd_refcnt 초기값 1 |
| 하드와이어드 스텁 빌더 | formrdesc() |
| 초기화 파일 로드/쓰기 | load_relcache_init_file() / write_relcache_init_file() |
| 참조 카운트 | rd_refcnt, RelationIncrementReferenceCount / RelationDecrementReferenceCount가 관리 |
| 리소스 오너 연동 | ResourceOwnerRememberRelationRef / relref_resowner_desc |
| sinval 기반 플러시/재빌드 | RelationCacheInvalidateEntry → RelationFlushRelation → RelationRebuildRelation |
| 무효화됐지만 열린 항목 표시 | rd_isvalid = false |
| 빌드 진행 중 감시 | relcache.c의 in_progress_list[] |
PostgreSQL의 접근 방식
섹션 제목: “PostgreSQL의 접근 방식”PostgreSQL의 relcache는 릴레이션 OID를 키로 하는 백엔드 전용 HTAB이다. 각 항목은 CacheMemoryContext에 할당된 RelationData 구조체(typedef Relation)로, 릴레이션 하나의 완전히 조립된 메타데이터를 담는다. 핵심 설계 특성은 세 가지다. 첫째, 컴파일 타임 하드와이어드 스텁과 바이너리 초기화 파일을 활용해 부트스트랩 순환 참조를 끊는 3단계 시동 시퀀스다. 둘째, 사용 중인 항목이 해제되는 일을 막는 참조 카운트 + 리소스 오너 연동이다. 셋째, 참조 카운트가 0인 항목은 삭제하고 열린 항목은 포인터 안정성을 보존하는 스왑-인-플레이스 재빌드를 수행하는 무효화 경로다.
RelationData: 집합체 디스크립터
섹션 제목: “RelationData: 집합체 디스크립터”RelationData(src/include/utils/rel.h 정의)는 중심 자료 구조다. 필드는 다섯 그룹으로 나뉜다.
// struct RelationData — src/include/utils/rel.h (condensed)typedef struct RelationData{ RelFileLocator rd_locator; /* physical identity: spc/db/relNumber */ SMgrRelation rd_smgr; /* cached smgr file handle, or NULL */ int rd_refcnt; /* reference count */ ProcNumber rd_backend; /* owning backend for temp rels */ bool rd_islocaltemp; /* this session's temp rel */ bool rd_isnailed; /* nailed in cache (never evicted) */ bool rd_isvalid; /* entry is valid (not stale) */
/* subtransaction-tracking fields for new/modified rels */ SubTransactionId rd_createSubid; SubTransactionId rd_newRelfilelocatorSubid; SubTransactionId rd_firstRelfilelocatorSubid; SubTransactionId rd_droppedSubid;
/* catalog-derived sub-structures */ Form_pg_class rd_rel; /* pg_class row (fixed-width part) */ TupleDesc rd_att; /* tuple descriptor (rd_rel->relnatts cols) */ Oid rd_id; /* relation OID */ LockInfoData rd_lockInfo; /* lock manager identity */ RuleLock *rd_rules; /* parsed rewrite rules, or NULL */ MemoryContext rd_rulescxt; /* private context for rd_rules */ TriggerDesc *trigdesc; /* trigger info, or NULL */ struct RowSecurityDesc *rd_rsdesc; /* RLS policies, or NULL */
/* on-demand lazy fields (NULL until first request) */ List *rd_fkeylist; /* FK cache */ PartitionKey rd_partkey; /* partition key */ PartitionDesc rd_partdesc; /* partition descriptor */ List *rd_indexlist; /* OID list of indexes */ Oid rd_pkindex; /* primary key index OID */ Bitmapset *rd_keyattr; /* FK-usable cols */ Bitmapset *rd_hotblockingattr; /* HOT-blocking cols */
/* index-relation-only fields */ MemoryContext rd_indexcxt; /* private context for index info */ struct IndexAmRoutine *rd_indam;/* index AM API struct */ Oid *rd_opfamily; /* opfamily per index column */ Oid *rd_opcintype; /* opclass input type per column */ RegProcedure *rd_support; /* support procedure OIDs */
/* ... additional fields omitted ... */} RelationData;즉시 로드 필드와 지연 로드 필드 사이의 분리는 의도적이다. rd_rel, rd_att, rd_rules, trigdesc, rd_rsdesc는 대부분의 호출자가 필요로 하므로 RelationBuildDesc가 즉시 로드한다. rd_fkeylist, rd_indexlist, rd_partkey, rd_partdesc, 그리고 어트리뷰트 비트맵 필드들은 첫 요청 시 로드되며 각자의 유효 플래그(rd_fkeyvalid, rd_indexvalid, rd_partkey != NULL, rd_attrsvalid)로 보호된다. 재귀를 피하기 위해서다. RelationGetIndexList는 추가 카탈로그 읽기를 촉발하고, 그 카탈로그 읽기는 다시 relcache 항목이 필요하다. 이것이 즉시 빌드의 일부라면 재귀가 더 깊어지고 끊기도 어려워진다.
flowchart TD OID["relation OID"] --> HASH["RelationIdCache (HTAB)\n keyed on rd_id"] HASH -- "hit + rd_isvalid" --> RET["return Relation*\n(refcount++)"] HASH -- "hit + !rd_isvalid" --> REBUILD["RelationRebuildRelation\n(swap-in-place)"] REBUILD --> RET HASH -- "miss" --> BUILD["RelationBuildDesc\n(catalog scans)"] BUILD --> INSERT["RelationCacheInsert\nrd_isvalid = true"] INSERT --> RET
그림 1 — RelationIdGetRelation 조회 경로. 유효한 항목의 캐시 히트는 참조 카운트를 증가시킨 후 즉시 반환한다. 무효 항목 히트는 제자리 재빌드를 촉발한다. 캐시 미스는 RelationBuildDesc를 호출해 시스템 카탈로그로부터 항목을 조립한 뒤 삽입한다.
RelationBuildDesc: 카탈로그로부터 항목 조립
섹션 제목: “RelationBuildDesc: 카탈로그로부터 항목 조립”RelationBuildDesc는 캐시 미스 시 호출된다. 일련의 카탈로그 읽기를 수행하는데, 각 읽기는 그 자체로 relcache 항목을 필요로 할 수 있어 잠재적 재귀가 발생한다. 이 재귀는 in_progress_list 메커니즘으로 끊는다. 항목 R을 빌드하는 도중 R에 대한 무효화가 도착하면, 오래된 부분 항목을 반환하는 대신 처음부터 재시작한다(goto retry 루프).
// RelationBuildDesc — utils/cache/relcache.c (condensed)in_progress_list[in_progress_offset].reloid = targetRelId;retry:in_progress_list[in_progress_offset].invalidated = false;
pg_class_tuple = ScanPgRelation(targetRelId, true, false);if (!HeapTupleIsValid(pg_class_tuple)) { /* deleted */ return NULL; }
relation = AllocateRelationDesc(relp); /* alloc + copy pg_class fixed part */RelationBuildTupleDesc(relation); /* scans pg_attribute */
/* lazy fields are NIL/NULL; loaded on demand */relation->rd_fkeylist = NIL;relation->rd_partkey = NULL;
/* access method info — index or table AM */if (relkind == RELKIND_INDEX || ...) RelationInitIndexAccessInfo(relation);else if (RELKIND_HAS_TABLE_AM(relkind)) RelationInitTableAccessMethod(relation);
RelationParseRelOptions(relation, pg_class_tuple); /* rd_options */
if (relation->rd_rel->relhasrules) RelationBuildRuleLock(relation); /* scans pg_rewrite */if (relation->rd_rel->relhastriggers) RelationBuildTriggers(relation); /* scans pg_trigger */if (relation->rd_rel->relrowsecurity) RelationBuildRowSecurity(relation); /* scans pg_policy */
RelationInitLockInfo(relation);RelationInitPhysicalAddr(relation); /* rd_locator from relfilenode or mapper */
if (in_progress_list[in_progress_offset].invalidated){ RelationDestroyRelation(relation, false); goto retry; /* restart if inval arrived mid-build */}
if (insertIt) RelationCacheInsert(relation, true);relation->rd_isvalid = true;return relation;위 스케치는 실제 제어 흐름을 축약한 것이다. 실제 함수는 두 가지를 더 보여준다. 첫째, 즉시 로드와 지연 로드의 경계가 AllocateRelationDesc가 pg_class 고정 부분을 복사한 직후에 필드별로 설정되며, 지연 필드마다 명시적으로 0으로 초기화된다. 둘째, 액세스 메서드 초기화는 relkind 디스패치다. 인덱스 릴레이션은 RelationInitIndexAccessInfo를, 테이블 AM 릴레이션은 RelationInitTableAccessMethod를, 파티셔닝된 테이블은 아무것도 받지 않는다. 파티션이 AM을 상속하기 때문이다.
// RelationBuildDesc — utils/cache/relcache.c (condensed, real)relation = AllocateRelationDesc(relp);RelationGetRelid(relation) = relid;
relation->rd_refcnt = 0;relation->rd_isnailed = false; /* ordinary rels are evictable */relation->rd_createSubid = InvalidSubTransactionId;/* ... three more SubTransactionId fields zeroed ... */
RelationBuildTupleDesc(relation); /* scans pg_attribute → rd_att */
/* foreign key data is not loaded till asked for */relation->rd_fkeylist = NIL;relation->rd_fkeyvalid = false;/* partitioning data is not loaded till asked for */relation->rd_partkey = NULL;relation->rd_partdesc = NULL;relation->rd_partcheckvalid = false;
/* initialize access method information */if (relation->rd_rel->relkind == RELKIND_INDEX || relation->rd_rel->relkind == RELKIND_PARTITIONED_INDEX) RelationInitIndexAccessInfo(relation);else if (RELKIND_HAS_TABLE_AM(relation->rd_rel->relkind) || relation->rd_rel->relkind == RELKIND_SEQUENCE) RelationInitTableAccessMethod(relation);else if (relation->rd_rel->relkind == RELKIND_PARTITIONED_TABLE) /* Do nothing: partitions inherit the AM. */ ;else Assert(relation->rd_rel->relam == InvalidOid);
RelationParseRelOptions(relation, pg_class_tuple); /* rd_options */
if (relation->rd_rel->relhasrules) RelationBuildRuleLock(relation); /* scans pg_rewrite */else { relation->rd_rules = NULL; relation->rd_rulescxt = NULL; }/* ... triggers (pg_trigger) and row security (pg_policy) likewise ... */
RelationInitLockInfo(relation); /* see lmgr.c */RelationInitPhysicalAddr(relation); /* rd_locator */relation->rd_smgr = NULL; /* no open file yet */heap_freetuple(pg_class_tuple);rd_refcnt = 0 / rd_isnailed = false 기본값에 주목한다. 새로 빌드된 일반 항목은 핀이 없는 상태로 시작하고, 호출자(RelationIdGetRelation)가 이후에 핀을 설정한다. 네일된 항목은 반대 경로를 밟는다. 일반 캐시 미스 경로가 아닌 formrdesc / load_critical_index로 빌드되고, rd_refcnt = 1로 시작한다.
RelationInitPhysicalAddr는 두 경로로 나뉜다. 대부분의 릴레이션은 pg_class.relfilenode에서 직접 물리 파일 번호를 읽는다. 매핑된(mapped) 릴레이션, 즉 부트스트랩 중에 파일 번호를 pg_class 자체에 저장할 수 없는 소수의 릴레이션은 relmapper.c를 참조한다. relmapper.c는 시동 시 로드되는 별도의 작은 파일(pg_filenode.map)을 관리한다. 시스템 카탈로그(pg_class, pg_attribute 등)가 매핑된 릴레이션에 해당한다.
3단계 부트스트랩 시퀀스
섹션 제목: “3단계 부트스트랩 시퀀스”PostgreSQL은 pg_class에 대한 relcache 항목 없이는 pg_class를 스캔할 수 없고, 그 항목을 만들려면 pg_class를 스캔해야 하는 순환 구조다. 이를 세 단계로 끊는다.
1단계 — RelationCacheInitialize: 빈 RelationIdCache 해시 테이블과 in_progress_list 배열을 생성한다. 카탈로그 접근은 없다.
2단계 — RelationCacheInitializePhase2: 공유 카탈로그(pg_database, pg_authid, pg_auth_members, pg_shseclabel, pg_subscription)에 대한 접근을 준비한다. global/pg_internal.init 로드를 시도하고, 실패하면 formrdesc를 다섯 번 호출해 하드와이어드 스텁을 설치한다.
// RelationCacheInitializePhase2 — utils/cache/relcache.c (condensed)if (!load_relcache_init_file(true)){ formrdesc("pg_database", DatabaseRelation_Rowtype_Id, true, ...); formrdesc("pg_authid", AuthIdRelation_Rowtype_Id, true, ...); formrdesc("pg_auth_members", AuthMemRelation_Rowtype_Id, true, ...); formrdesc("pg_shseclabel", SharedSecLabelRelation_Rowtype_Id, true, ...); formrdesc("pg_subscription", SubscriptionRelation_Rowtype_Id, true, ...);#define NUM_CRITICAL_SHARED_RELS 5}3단계 — RelationCacheInitializePhase3: 핵심 단계다. $PGDATA/base/<dboid>/pg_internal.init 로드를 시도하고, 실패하면 로컬 네일 카탈로그 4개(pg_class, pg_attribute, pg_proc, pg_type)에 formrdesc를 네 번 호출한 뒤 핵심 인덱스 7개를 하나씩 로드한다.
// RelationCacheInitializePhase3 — utils/cache/relcache.c (condensed)if (!load_relcache_init_file(false)){ formrdesc("pg_class", RelationRelation_Rowtype_Id, false, ...); formrdesc("pg_attribute", AttributeRelation_Rowtype_Id, false, ...); formrdesc("pg_proc", ProcedureRelation_Rowtype_Id, false, ...); formrdesc("pg_type", TypeRelation_Rowtype_Id, false, ...);#define NUM_CRITICAL_LOCAL_RELS 4}// ...if (!criticalRelcachesBuilt){ load_critical_index(ClassOidIndexId, RelationRelationId); load_critical_index(AttributeRelidNumIndexId, AttributeRelationId); load_critical_index(IndexRelidIndexId, IndexRelationId); load_critical_index(OpclassOidIndexId, OperatorClassRelationId); load_critical_index(AccessMethodProcedureIndexId, AccessMethodProcedureRelationId); load_critical_index(RewriteRelRulenameIndexId, RewriteRelationId); load_critical_index(TriggerRelidNameIndexId, TriggerRelationId);#define NUM_CRITICAL_LOCAL_INDEXES 7 criticalRelcachesBuilt = true;}criticalRelcachesBuilt = true 설정이 핵심 전환점이다. 이 시점 이후에는 ScanPgRelation과 RelationBuildTupleDesc가 순차 힙 스캔 대신 인덱스 스캔을 사용할 수 있다. 이전에는 모든 카탈로그 읽기가 무조건 힙 스캔으로 폴백한다. 3단계는 캐시 전체를 스캔하고, formrdesc 스텁(rd_rel->relowner == InvalidOid로 식별)을 SearchSysCache1로 읽은 실제 pg_class 행으로 교체하며 마무리된다.
formrdesc 자체는 genbki.pl이 생성한 컴파일 타임 어트리뷰트 배열(Desc_pg_class, Desc_pg_attribute 등)로부터 최소한의 RelationData를 빌드하고 항목을 네일한다(rd_isnailed = true, rd_refcnt = 1). 항목은 의도적으로 rd_isvalid = false다. 3단계가 이를 수정하기 때문이다. 3단계 완료 전에 접근이 발생하면 재빌드로 떨어진다.
flowchart LR P1["Phase 1\nRelationCacheInitialize\n(empty hash table)"] P2["Phase 2\nRelationCacheInitializePhase2\nshared stubs or init file"] P3["Phase 3\nRelationCacheInitializePhase3\nlocal stubs + critical indexes\ncriticalRelcachesBuilt = true\nfake → real pg_class rows"] NORMAL["Normal operation\nRelationIdGetRelation on demand"] P1 --> P2 --> P3 --> NORMAL
그림 2 — 3단계 부트스트랩. 1단계는 빈 해시를 만든다. 2단계는 공유 카탈로그용 스텁을 설치한다. 3단계는 로컬 스텁을 설치하고 핵심 인덱스 7개를 로드하며 criticalRelcachesBuilt를 설정한 뒤, 스텁을 실제 pg_class 데이터로 교체한다. 첫 번째 사용자 쿼리가 실행되기 전에 세 단계 모두 완료된다.
초기화 파일: pg_internal.init
섹션 제목: “초기화 파일: pg_internal.init”write_relcache_init_file(bool shared)는 시동 완료 시 네일된(핵심) relcache 항목 전체를 바이너리 파일로 직렬화한다. 파일은 임시 이름으로 쓰인 뒤 원자적으로 이름이 변경되므로 동시 읽기 중인 프로세스는 불완전한 파일을 보지 않는다. 파일의 첫 번째 필드는 매직 번호(RELCACHE_INIT_FILEMAGIC = 0x573266)로 버전 식별자 역할을 한다. 불일치 시 파일은 무시되고 처음부터 재빌드한다.
load_relcache_init_file은 파일을 다시 읽어 각 RelationData를 CacheMemoryContext에 재구성하고, 락 정보와 물리 주소를 재계산한다(CREATE DATABASE로 파일이 다른 데이터베이스에서 복사됐을 경우를 대비해). 네일된 항목 수를 NUM_CRITICAL_LOCAL_RELS / NUM_CRITICAL_LOCAL_INDEXES와 비교해 온전성도 검사한다.
쓰기 측은 길이 접두사가 붙은 바이너리 덤프다. 매직 번호 뒤에 RelationIdCache를 순회하며, 올바른 그룹(relisshared == shared)에 속하고 파일에 포함돼야 할 항목마다 RelationData 구조체, pg_class form, 각 어트리뷰트의 고정 부분, reloptions 블롭을 쓴다. RELKIND_INDEX이면 pg_index 튜플과 opfamily/opcintype/support 벡터도 쓴다. 전체는 시동 이후 sinval 이벤트를 받지 않았다는 조건 하에서만 실행되고, 마지막 단계는 RelCacheInitLock 아래서 temp 파일을 원자적으로 rename한다.
// write_relcache_init_file — utils/cache/relcache.c (condensed, real)if (relcacheInvalsReceived != 0L) return; /* already stale; don't bother */
/* ... snprintf temp + final names, AllocateFile(tempfilename) ... */magic = RELCACHE_INIT_FILEMAGIC;fwrite(&magic, 1, sizeof(magic), fp);
hash_seq_init(&status, RelationIdCache);while ((idhentry = hash_seq_search(&status)) != NULL){ Relation rel = idhentry->reldesc; Form_pg_class relform = rel->rd_rel;
if (relform->relisshared != shared) /* wrong group */ continue; if (!shared && !RelationIdIsInInitFile(RelationGetRelid(rel))) { Assert(!rel->rd_isnailed); /* nailed must be stored */ continue; } write_item(rel, sizeof(RelationData), fp); write_item(relform, CLASS_TUPLE_SIZE, fp); for (i = 0; i < relform->relnatts; i++) write_item(TupleDescAttr(rel->rd_att, i), ATTRIBUTE_FIXED_PART_SIZE, fp); write_item(rel->rd_options, rel->rd_options ? VARSIZE(rel->rd_options) : 0, fp); if (rel->rd_rel->relkind == RELKIND_INDEX) { /* pg_index + opfamily/support */ }}
/* serialize against the unlink-and-SI path */LWLockAcquire(RelCacheInitLock, LW_EXCLUSIVE);AcceptInvalidationMessages();if (relcacheInvalsReceived == 0L){ if (rename(tempfilename, finalfilename) < 0) /* atomic publish */ unlink(tempfilename);}else unlink(tempfilename); /* obsolete; drop it */LWLockRelease(RelCacheInitLock);읽기 측은 덤프를 역방향으로 재구성하지만 맹목적으로 신뢰하지 않는다. 구조적 검사 두 가지(magic != RELCACHE_INIT_FILEMAGIC, 각 디스크립터의 len != sizeof(RelationData))가 read_failed로 점프해 레이아웃 변경 시 깨끗한 재빌드를 강제한다. 각 항목을 재구성한 뒤에는 락 및 물리 주소를 재계산한다. 온디스크의 rd_locator / rd_lockInfo가 CREATE DATABASE로 복사된 파일에서 왔다면 다른 데이터베이스 것일 수 있기 때문이다. 마지막으로 무언가를 삽입하기 전에 네일된 항목 수를 컴파일 타임 상수와 교차 검사한다.
// load_relcache_init_file — utils/cache/relcache.c (condensed, real)if (fread(&magic, 1, sizeof(magic), fp) != sizeof(magic)) goto read_failed;if (magic != RELCACHE_INIT_FILEMAGIC) goto read_failed;
for (relno = 0;; relno++){ if ((nread = fread(&len, 1, sizeof(len), fp)) != sizeof(len)) { if (nread == 0) break; /* clean EOF */ goto read_failed; } if (len != sizeof(RelationData)) goto read_failed; /* layout drift */ rel = rels[num_rels++] = (Relation) palloc(len); fread(rel, 1, len, fp); /* ... read pg_class form, rebuild rd_att, AM info, nailed counts ... */
rel->rd_smgr = NULL; rel->rd_refcnt = rel->rd_isnailed ? 1 : 0; rel->rd_indexvalid = false; rel->rd_indexlist = NIL; /* recompute — file may have been copied by CREATE DATABASE */ RelationInitLockInfo(rel); RelationInitPhysicalAddr(rel);}
if (!shared && (nailed_rels != NUM_CRITICAL_LOCAL_RELS || nailed_indexes != NUM_CRITICAL_LOCAL_INDEXES)) goto read_failed; /* critical set changed → rebuild */
for (relno = 0; relno < num_rels; relno++) RelationCacheInsert(rels[relno], false);criticalRelcachesBuilt = true; /* (criticalSharedRelcachesBuilt if shared) */return true;
read_failed: /* leak the half-built rels (not in cache) and fall back to formrdesc */ return false;load_relcache_init_file이 성공하면 criticalRelcachesBuilt(공유라면 criticalSharedRelcachesBuilt)를 설정한다. 3단계가 load_critical_index 이후 설정하는 것과 동일한 플래그다. 어느 경로든 부트스트랩 후 상태에 도달한다. 초기화 파일은 단지 빠른 경로일 뿐이다.
flowchart TD
START["backend startup\nPhase 2 / Phase 3"] --> TRY["load_relcache_init_file(shared)"]
TRY --> OPEN{"AllocateFile ok?"}
OPEN -- "no" --> SLOW["formrdesc stubs +\nload_critical_index\n(slow bootstrap)"]
OPEN -- "yes" --> MAGIC{"magic + len checks?"}
MAGIC -- "fail" --> RF["read_failed\nreturn false"]
RF --> SLOW
MAGIC -- "ok" --> RECON["reconstruct entries\nRelationInitLockInfo\nRelationInitPhysicalAddr"]
RECON --> COUNT{"nailed counts ==\nNUM_CRITICAL_*?"}
COUNT -- "no" --> RF
COUNT -- "yes" --> INS["RelationCacheInsert all\ncriticalRelcachesBuilt = true"]
SLOW --> RUN["normal operation"]
INS --> RUN
RUN --> WRITE["write_relcache_init_file\nat end of startup\nif relcacheInvalsReceived == 0"]
WRITE --> RENAME["rename temp → pg_internal.init\nunder RelCacheInitLock"]
그림 4 — 초기화 파일 빠른 경로 대 느린 부트스트랩. load_relcache_init_file은 파일이 열리고 매직/길이 구조 검사를 통과하며 네일된 항목 수가 일치할 때 formrdesc 경로를 단락한다. 실패하면 read_failed에서 느린 부트스트랩으로 돌아온다. 시동을 마칠 때까지 sinval 이벤트를 보지 않은 첫 번째 백엔드가 파일을 쓰고 원자적으로 이름 변경한다.
파일 무효화는 RelationCacheInitFilePreInvalidate / RelationCacheInitFilePostInvalidate가 담당한다. inval.c의 sinval 시스템이 이 함수들을 호출한다. 사전 무효화는 살아있는 파일의 이름을 임시 이름으로 변경하고, 사후 무효화는 그 임시 파일을 삭제한다. 사전과 사후 사이에 파일을 로드한 백엔드는 일관성이 없는 뷰를 갖지만, sinval 처리가 낡은 데이터가 사용되기 전에 수정한다.
참조 카운팅과 리소스 오너십
섹션 제목: “참조 카운팅과 리소스 오너십”RelationIdGetRelation의 모든 호출자는 rd_refcnt가 증가된 항목을 받는다. 호출자는 완료 시 RelationClose를 호출할 책임이 있다. RelationClose는 RelationDecrementReferenceCount를 호출한다. 실제로는 table_open / table_close(또는 index_open / index_close)가 진입점이며, 이것들이 각각 RelationIdGetRelation과 RelationClose를 호출한다.
오류 경로에서의 누수를 막기 위해 각 열린 참조는 ResourceOwnerRememberRelationRef로 CurrentResourceOwner에 등록된다.
// RelationIncrementReferenceCount — utils/cache/relcache.c (condensed)voidRelationIncrementReferenceCount(Relation rel){ ResourceOwnerEnlarge(CurrentResourceOwner); rel->rd_refcnt += 1; if (!IsBootstrapProcessingMode()) ResourceOwnerRememberRelationRef(CurrentResourceOwner, rel);}relref_resowner_desc 디스크립터는 ResOwnerReleaseRelation을 정리 콜백으로 등록한다. 호출자가 잊어버린 경우 트랜잭션 종료 시 RelationDecrementReferenceCount를 호출해 준다. 수명이 긴 프로세스(autovacuum 워커 등)가 트랜잭션 경계에서 누수된 relcache 핀을 누적하지 않도록 보장한다.
무효화: RelationFlushRelation과 스왑-인-플레이스 재빌드
섹션 제목: “무효화: RelationFlushRelation과 스왑-인-플레이스 재빌드”릴레이션 OID에 대한 sinval 메시지가 도착하면 RelationCacheInvalidateEntry는 relcacheInvalsReceived를 증가시키고 RelationFlushRelation을 호출한다. 플러시 결정 트리는 다음과 같다.
flowchart TD
F["RelationFlushRelation(rel)"]
F --> NEW{"rd_createSubid != 0\nor rd_firstRelfilelocatorSubid != 0?"}
NEW -- "yes (new-in-txn rel)" --> TXNSTATE{"IsTransactionState\nand not dropped?"}
TXNSTATE -- "yes" --> REBUILDNEW["bump refcnt\nRelationRebuildRelation\ndecrement refcnt"]
TXNSTATE -- "no" --> INVAL["RelationInvalidateRelation\nrd_isvalid = false"]
NEW -- "no (pre-existing rel)" --> REFZERO{"refcnt == 0?"}
REFZERO -- "yes" --> CLEAR["RelationClearRelation\n(destroy entry)"]
REFZERO -- "no + !IsTransactionState" --> INVAL
REFZERO -- "no + nailed + refcnt==1" --> INVAL
REFZERO -- "no + open" --> REBUILD["RelationRebuildRelation\n(swap-in-place)"]
그림 3 — RelationFlushRelation 결정 트리. 참조 카운트가 0인 기존 항목은 단순히 제거된다. 열린 항목은 새 RelationData를 빌드하고, 필드를 하나씩 스왑하는 방식으로 제자리 재빌드된다. rd_refcnt, rd_smgr, 트랜잭션 서브ID, 그리고 구조적으로 동일하다면 rd_att / rd_rules / rd_rsdesc도 보존된다.
RelationRebuildRelation의 스왑-인-플레이스 로직은 relcache에서 가장 복잡한 부분이다. 구조체 전체를 memcpy로 스왑한 뒤, 불변식을 보존하기 위해 수십 개의 필드를 다시 스왑한다.
// RelationRebuildRelation — utils/cache/relcache.c (condensed)newrel = RelationBuildDesc(save_relid, false); /* build into temp entry */
keep_tupdesc = equalTupleDescs(relation->rd_att, newrel->rd_att);keep_rules = equalRuleLocks(relation->rd_rules, newrel->rd_rules);keep_policies = equalRSDesc(relation->rd_rsdesc, newrel->rd_rsdesc);keep_partkey = (relation->rd_partkey != NULL); /* immutable once set */
/* swap all fields at once */{ RelationData tmp; memcpy(&tmp, newrel, ...); memcpy(newrel, relation, ...); memcpy(relation, &tmp, ...); }
/* then swap back fields that must be preserved */SWAPFIELD(SMgrRelation, rd_smgr); /* back-links from smgr level */SWAPFIELD(int, rd_refcnt); /* callers hold this count */SWAPFIELD(SubTransactionId, rd_createSubid);/* ... other SubTransactionId fields ... */SWAPFIELD(Form_pg_class, rd_rel);memcpy(relation->rd_rel, newrel->rd_rel, CLASS_TUPLE_SIZE); /* update content */
if (keep_tupdesc) SWAPFIELD(TupleDesc, rd_att); /* preserve pointer */if (keep_rules) { SWAPFIELD(RuleLock*, rd_rules); SWAPFIELD(MemoryContext, rd_rulescxt); }if (keep_policies) SWAPFIELD(RowSecurityDesc*, rd_rsdesc);if (keep_partkey) SWAPFIELD(PartitionKey, rd_partkey);/* ... partition desc context handling ... */
RelationDestroyRelation(newrel, !keep_tupdesc);튜플 디스크립터가 구조적으로 변경되지 않았을 때 rd_att를 보존하는 것은 중요하다. catcache 항목이 TupleDesc 컬럼에 대한 포인터를 내장할 수 있으므로, 디스크립터를 이동하면 해당 포인터들이 무효화된다. equalTupleDescs는 보존이 안전한지 결정하기 위해 구조적 비교를 수행한다.
재빌드 중 RelationBuildDesc가 NULL을 반환하는 경우, 히스토릭 디코딩 스냅샷에서는 보이지만 현재 스냅샷에서는 보이지 않는 릴레이션에서 발생할 수 있다. 히스토릭 스냅샷이 활성 상태라면 함수는 재빌드 없이 반환한다. 항목은 무효 상태로 남는다. 그렇지 않으면 열린 상태로 릴레이션이 삭제돼서는 안 되므로 오류를 발생시킨다.
지연 계산 필드: 요청 시 하위 구조 로딩
섹션 제목: “지연 계산 필드: 요청 시 하위 구조 로딩”여러 RelationData 필드는 첫 번째 요청 시에만 채워진다. RelationGetIndexList는 pg_index를 스캔하고 결과를 rd_indexlist에 캐시한다. RelationGetFKeyList는 pg_constraint를 스캔한다. 두 함수 모두 CacheMemoryContext에 저장하기 전에 각자의 rd_*valid 플래그를 설정한다.
RelationGetIndexAttrBitmap에서는 미묘한 동시성 문제가 발생한다. 각 인덱스를 열어 어트리뷰트 비트맵을 수집하는 루프 도중, relcache 플러시가 도착해 rd_indexlist를 리셋할 수 있다. 이 함수는 재시작 루프로 처리한다. 비트맵 전체를 수집한 후 RelationGetIndexList를 한 번 더 호출해 이전 스냅샷과 비교하고, 다르면 부분적으로 빌드된 비트맵을 해제하고 처음부터 재시작한다.
서브트랜잭션 추적 SubID
섹션 제목: “서브트랜잭션 추적 SubID”rd_createSubid, rd_newRelfilelocatorSubid, rd_firstRelfilelocatorSubid, rd_droppedSubid는 어떤 서브트랜잭션이 릴레이션의 파일 아이덴티티를 마지막으로 변경했는지 추적한다. RelationNeedsWAL()에 중요한 필드들이다. 이 함수는 현재 최상위 트랜잭션에서 저장소가 생성된 릴레이션이면 false를 반환한다. 해당 저장소에 대한 WAL 레코드가 불필요하기 때문이다. 트랜잭션 종료 시 AtEOXact_RelationCache가 모든 서브ID를 0으로 리셋하고, 같은 트랜잭션에서 생성됐다가 삭제된 릴레이션이라면 항목을 지운다.
소스 워크스루
섹션 제목: “소스 워크스루”심볼 이름으로 앵커한다. 줄 번호로 재탐색하려면
git grep -n '<symbol>' src/backend/utils/cache/relcache.c를 사용한다. 아래 위치 힌트 테이블의 줄 번호는 커밋273fe94기준이다.
핵심 자료 구조
섹션 제목: “핵심 자료 구조”struct RelationData(rel.h) — 집합체 디스크립터,Relation으로 typedef. 하위 필드:rd_locator,rd_rel,rd_att,rd_rules,trigdesc,rd_rsdesc,rd_indexlist,rd_indam.struct relidcacheent/RelationIdCache(relcache.c) — OID를 키로 하는 백엔드 전용HTAB *.struct InProgressEnt/in_progress_list(relcache.c) —RelationBuildDesc의 재귀 감시 스택.eoxact_list[]/eoxact_list_overflowed(relcache.c) — 트랜잭션 종료 정리를 위한 빠른 경로.criticalRelcachesBuilt/criticalSharedRelcachesBuilt(relcache.c) —ScanPgRelation을 힙 스캔에서 인덱스 스캔으로 전환하는 플래그.
초기화
섹션 제목: “초기화”RelationCacheInitialize— 1단계: 빈 해시 +in_progress_list.RelationCacheInitializePhase2— 2단계: 공유 카탈로그 스텁 또는 초기화 파일.RelationCacheInitializePhase3— 3단계: 로컬 스텁, 핵심 인덱스,criticalRelcachesBuilt, 스텁을 실제 행으로 교체.formrdesc— 컴파일 타임 어트리뷰트로부터 하드와이어드 네일 항목 빌드.load_critical_index— 핵심 인덱스 하나씩RelationBuildDesc를 호출하고 네일.
항목 빌드
섹션 제목: “항목 빌드”ScanPgRelation— OID 하나에 대한pg_class힙 또는 인덱스 스캔.AllocateRelationDesc—CacheMemoryContext에RelationData할당.RelationBuildTupleDesc—pg_attribute,pg_attrdef,pg_constraint를 스캔해rd_att채움.RelationBuildDesc— 최상위 빌더: 카탈로그 스캔 조율,in_progress_list재시작 처리,RelationCacheInsert호출.RelationInitPhysicalAddr—pg_class.relfilenode또는 매핑된 릴레이션의 경우relmapper.c에서rd_locator채움.RelationBuildRuleLock—pg_rewrite스캔,rd_rules빌드.RelationInitIndexAccessInfo— 인덱스 릴레이션용:rd_indam,rd_opfamily,rd_support등 채움.RelationBuildLocalRelation—pg_class행 없이 새로 생성되는 릴레이션에 대한 항목 빌드.
조회 인터페이스
섹션 제목: “조회 인터페이스”RelationIdGetRelation— 공개 진입점: 해시 조회 → 낡으면 재빌드 → 미스 시RelationBuildDesc. 참조 카운트 증가.RelationIncrementReferenceCount/RelationDecrementReferenceCount— 참조 카운트 ±1, 리소스 오너 등록 포함.RelationClose— 참조 카운트 감소,RelationCloseCleanup호출.
무효화
섹션 제목: “무효화”RelationCacheInvalidateEntry— sinval 디스패치: OID 조회,RelationFlushRelation호출;in_progress_list[].invalidated설정.RelationCacheInvalidate— SI 버퍼 오버플로 시 캐시 전체 플러시.RelationFlushRelation— 디스패치: 제거(참조 카운트=0) 또는 재빌드(열린 항목).RelationRebuildRelation— 열린 항목의 스왑-인-플레이스 재빌드.RelationInvalidateRelation— 재빌드 없이rd_isvalid = false표시.RelationForgetRelation— 호출자 보고 삭제: 삭제됨 표시 또는 제거.
초기화 파일
섹션 제목: “초기화 파일”load_relcache_init_file—pg_internal.init을 역직렬화해 캐시에 삽입.write_relcache_init_file— 네일된 항목을 임시 파일에 직렬화한 후 이름 변경.RelationCacheInitFilePreInvalidate/RelationCacheInitFilePostInvalidate— sinval 처리 전후 초기화 파일의 원자적 무효화.RelationCacheInitFileRemove— 초기화 파일 제거 (initdb,pg_upgrade등에서 사용).RelationIdIsInInitFile— 이 OID가 로컬 초기화 파일에 포함돼야 하는지 판정.
요청 시 하위 구조 로더
섹션 제목: “요청 시 하위 구조 로더”RelationGetFKeyList— FK 제약 조건 목록.RelationGetIndexList— 인덱스 OID 목록 +rd_pkindex/rd_replidindex.RelationGetIndexExpressions/RelationGetIndexPredicate— 파싱된 인덱스 표현식 / 술어 트리.RelationGetIndexAttrBitmap— HOT/FK/PK/복제 아이덴티티용 컬럼 비트맵; 인덱스 목록 동시 변경에 대한 재시작 루프 포함.
위치 힌트 (2026-06-05, REL_18 273fe94 기준)
섹션 제목: “위치 힌트 (2026-06-05, REL_18 273fe94 기준)”| 심볼 | 파일 | 줄 |
|---|---|---|
struct RelationData | utils/rel.h | 55 |
RelationIdCache | utils/cache/relcache.c | 134 |
criticalRelcachesBuilt | utils/cache/relcache.c | 140 |
in_progress_list | utils/cache/relcache.c | 170 |
eoxact_list | utils/cache/relcache.c | 185 |
ScanPgRelation | utils/cache/relcache.c | 340 |
AllocateRelationDesc | utils/cache/relcache.c | 413 |
RelationParseRelOptions | utils/cache/relcache.c | 468 |
RelationBuildTupleDesc | utils/cache/relcache.c | 525 |
RelationBuildRuleLock | utils/cache/relcache.c | 752 |
RelationBuildDesc | utils/cache/relcache.c | 1059 |
RelationInitPhysicalAddr | utils/cache/relcache.c | 1339 |
RelationInitIndexAccessInfo | utils/cache/relcache.c | 1445 |
RelationInitTableAccessMethod | utils/cache/relcache.c | 1829 |
RelationDestroyRelation | utils/cache/relcache.c | 2439 |
formrdesc | utils/cache/relcache.c | 1894 |
RelationIdGetRelation | utils/cache/relcache.c | 2099 |
RelationIncrementReferenceCount | utils/cache/relcache.c | 2187 |
RelationDecrementReferenceCount | utils/cache/relcache.c | 2200 |
RelationClose | utils/cache/relcache.c | 2220 |
RelationRebuildRelation | utils/cache/relcache.c | 2585 |
RelationFlushRelation | utils/cache/relcache.c | 2827 |
RelationCacheInvalidateEntry | utils/cache/relcache.c | 2938 |
RelationCacheInvalidate | utils/cache/relcache.c | 2994 |
AtEOXact_RelationCache | utils/cache/relcache.c | 3226 |
RelationBuildLocalRelation | utils/cache/relcache.c | 3515 |
RelationCacheInitialize | utils/cache/relcache.c | 4002 |
RelationCacheInitializePhase2 | utils/cache/relcache.c | 4048 |
RelationCacheInitializePhase3 | utils/cache/relcache.c | 4107 |
RelationGetFKeyList | utils/cache/relcache.c | 4731 |
RelationGetIndexList | utils/cache/relcache.c | 4836 |
load_relcache_init_file | utils/cache/relcache.c | 6167 |
write_relcache_init_file | utils/cache/relcache.c | 6585 |
write_item | utils/cache/relcache.c | 6797 |
RelationIdIsInInitFile | utils/cache/relcache.c | 6820 |
RelationCacheInitFilePreInvalidate | utils/cache/relcache.c | 6860 |
RelationCacheInitFilePostInvalidate | utils/cache/relcache.c | 6885 |
RelationCacheInitFileRemove | utils/cache/relcache.c | 6900 |
struct relidcacheent | utils/cache/relcache.c | 128 |
소스 검증 (2026-06-05 기준)
섹션 제목: “소스 검증 (2026-06-05 기준)”커밋
273fe94현재 소스에 대한 검증 사실. 열린 질문은 큐레이터의 기록된 미비 사항이다.
검증된 사실
섹션 제목: “검증된 사실”-
RelationIdCache는Oid를 키로 하고entrysize = sizeof(RelIdCacheEnt)인 백엔드 전용HTAB *이다.RelationCacheInitialize(relcache.c)에서 검증:hash_create("Relcache by OID", INITRELCACHESIZE, &ctl, HASH_ELEM | HASH_BLOBS). -
NUM_CRITICAL_LOCAL_RELS = 4와NUM_CRITICAL_LOCAL_INDEXES = 7은 컴파일 타임 상수로 하드코딩된 네일 로컬 카탈로그/인덱스 수를 나타낸다.RelationCacheInitializePhase3에서 검증. 네일된 집합을 바꾸려면 이 상수와 그에 대응하는formrdesc/load_critical_index호출 목록을 함께 수정해야 한다. -
criticalRelcachesBuilt는ScanPgRelation을 힙 스캔에서 인덱스 스캔으로 전환한다. 검증:ScanPgRelation은indexOK && criticalRelcachesBuilt를systable_beginscan의 indexOK 인수로 전달한다. 3단계 완료 전에는 모든 카탈로그 읽기가 무조건 힙 스캔이다. -
in_progress_list재시작(goto retry)은 빌드 중인 OID에 대한 무효화 메시지가 도착할 때 동작한다.RelationBuildDesc에서 검증: 전체 빌드 시퀀스 완료 후if (in_progress_list[offset].invalidated) { RelationDestroyRelation(...); goto retry; }.RelationCacheInvalidateEntry는 릴레이션이 아직 해시 테이블에 없을 때 일치하는 OID에 대해in_progress_list[i].invalidated = true를 설정한다. -
RelationRebuildRelation의 스왑-인-플레이스 재빌드는equalTupleDescs가 true를 반환할 때rd_att를 보존한다. catcache 항목 포인터 무효화를 피하기 위해서다. 검증:keep_tupdesc = equalTupleDescs(relation->rd_att, newrel->rd_att)및 조건부SWAPFIELD(TupleDesc, rd_att)경로. -
write_relcache_init_file은 시동 이후 sinval을 수신했다면relcacheInvalsReceived != 0검사로 조기 중단한다.write_relcache_init_file최상단에서 검증. 이미 낡은 파일이 쓰이는 것을 방지한다. -
RELCACHE_INIT_FILEMAGIC = 0x573266은pg_internal.init의 버전 확인에 사용되는 컴파일 타임 상수다.relcache.c93번째 줄에서 정의됨. 매직 불일치 시load_relcache_init_file은read_failed로 점프하고 false를 반환한다. -
rd_createSubid계열 필드는AtEOXact_RelationCache에 의해 트랜잭션 종료 시InvalidSubTransactionId로 리셋된다.AtEOXact_cleanup에서 검증: 잠재적RelationClearRelation호출 전에 네 개의 서브ID 필드가 모두 0으로 초기화된다. -
RelationGetIndexAttrBitmap에는 명시적인 플러시-시 재시작 루프가 있다. 검증: 메인foreach루프 이후RelationGetIndexList를 두 번째 호출해 이전 스냅샷과 비교한다. 다르면 비트맵을 해제하고restart:레이블로 점프한다.
열린 질문
섹션 제목: “열린 질문”-
debug_discard_caches와RelationBuildDesc의 상호작용.debug_discard_caches > 0이 설정되면RelationBuildDesc는 임시 메모리 컨텍스트를 사용하고, 각 쿼리 후RelationCacheInvalidate가 호출돼 모든 항목을 폐기한다. 동시debug_discard_caches스트레스 하에서in_progress_list가 관리되는 정확한 시퀀스는 여기서 완전히 추적하지 않는다. 조사 경로:MAYBE_RECOVER_RELATION_BUILD_MEMORY주변RelationBuildDesc주석 블록을 읽고debug_discard_caches = 1로 계측한다. -
relmapper.c업데이트 원자성. 매핑된 릴레이션의 파일 번호는relmapper.c가 관리하는pg_filenode.map에 저장된다. 매핑된 릴레이션의 저장소가 재작성될 때(예: 카탈로그에VACUUM FULL), 매퍼 파일은 자체 2단계 rename으로 업데이트된다.RelationRebuildRelation의RelationInitPhysicalAddr가 동시 매퍼 업데이트와 어떻게 상호작용하는지는 여기서 추적하지 않는다. 조사 경로:relmapper.c와RelationInitPhysicalAddr의RelFileLocatorSkippingWAL경로를 읽는다. -
CREATE DATABASE하의 초기화 파일 정확성.load_relcache_init_file의 코드 주석은 “pg_internal.init 파일이CREATE DATABASE로 다른 데이터베이스에서 복사됐을 경우” 락 정보와 물리 주소를 재계산해야 한다고 설명한다. 복사된 초기화 파일과 새 데이터베이스의 온디스크 상태 사이에서 달라질 수 있는 정확한 필드 목록은 여기서 완전히 열거되지 않는다. 조사 경로:commands/dbcommands.c의CREATE DATABASE테이블스페이스 재작성 경로를 읽는다.
PostgreSQL 너머 — 비교 설계 및 연구 방향
섹션 제목: “PostgreSQL 너머 — 비교 설계 및 연구 방향”후속 문서를 위한 초기 목록이다. 분석이 아닌 씨앗이다.
-
catcache의 동반 계층. PostgreSQL의 catcache(
utils/cache/catcache.c)는 시스템 카탈로그에서 개별 튜플을 캐시한다(카탈로그 행당 항목 하나). relcache는 집합된 디스크립터를 캐시한다. 두 레이어는 sinval 무효화 채널을 공유하지만 다른 소비자를 지원한다. 축출 정책(catcache는 syscache당 클록 기반 LRU, relcache 항목은 sinval까지 생존)을 병렬 비교하면 아키텍처 분리가 명확해진다.postgres-catcache-syscache.md참조. -
sinval 무효화 결합.
RelationCacheInvalidateEntry는inval.c에 등록된 여러 핸들러 중 하나다.RelationCacheInitFilePreInvalidate를 통한 초기화 파일 무효화도 마찬가지다. sinval 전체 파이프라인(공유 메모리 링 버퍼, 재연결 시 따라잡기, catcache 리셋)이 relcache를 DDL에 연결하는 메커니즘이다.postgres-cache-invalidation.md참조. -
CUBRID의 relcache 유사물. CUBRID는 스레드별
schema_manager를 유지하며, 물리적 스키마(OR_CLASSREP)와 논리적 객체-관계형 스키마(SM_CLASS)를 별도로 캐시한다. 이 분리는 PostgreSQL의rd_att(물리)와rd_rel(카탈로그 행) 분리를 반영하지만, CUBRID의 카탈로그가 객체-관계형이라 매핑이 더 복잡하다. 부트스트랩 전략을 비교하면(두 시스템 모두 하드와이어드 디스크립터가 필요한 핵심 테이블 집합이 있다) 범용적인 것과 스키마 모델 특정적인 것이 구분된다. -
디스크립터 안정성과 포인터 앨리어싱. PostgreSQL의 스왑-인-플레이스 재빌드(
SWAPFIELD)는 포인터 안정성을 위한 임시방편적 접근이다. 더 새로운 데이터베이스 연구(Andy Pavlo 그룹의 OLAP 시스템 라이브 스키마 변경 등)는 모든 독자가 에폭을 떠날 때까지 오래된 디스크립터가 살아남는 에폭 기반 접근을 탐색한다. 이런 접근이keep_tupdesc/ catcache 포인터 결합을 단순화할 수 있는지는postgres-relcache-evolution.md를 위한 열린 설계 질문이다. -
일반 부트스트랩 기법으로서
formrdesc/ 초기화 파일 패턴. 자기 자신을 초기화하는 데 자기 자신이 필요한 구성 요소 문제는 시스템 초기화의 순환 의존성 문제의 특수 사례다. Architecture of a DB System (Hellerstein et al., 2007 —dbms-papers/fntdb07-architecture.md) §“Catalog Manager”는 하드와이어드 스텁이 범용적 해결책임을 간략히 언급한다. MySQL/InnoDB, Oracle, SQL Server 각각이 동일한 순환 참조를 어떻게 끊는지 조사하면 유용한 비교 노트가 될 것이다.
PostgreSQL 소스 (/data/hgryoo/references/postgres, REL_18 273fe94)
섹션 제목: “PostgreSQL 소스 (/data/hgryoo/references/postgres, REL_18 273fe94)”src/backend/utils/cache/relcache.c— 부트스트랩, 빌드, 조회, 무효화, 초기화 파일 로직 전체.src/include/utils/rel.h—RelationData구조체 정의와 모든 접근 매크로.src/include/utils/relcache.h— 공개 API 선언,RELCACHE_INIT_FILENAME.src/include/utils/relmapper.h— 매핑된 릴레이션 파일 번호 인터페이스.
교과서 장 (knowledge/research/dbms-general/ 아래)
섹션 제목: “교과서 장 (knowledge/research/dbms-general/ 아래)”- Database System Concepts (Silberschatz), 11장 §“System Catalog” —
RelationBuildDesc가 읽는 카탈로그 구조. - Database Internals (Petrov), 2장 §“Memory-Mapped Files and Direct I/O” — 캐싱 및 디스크립터 수명 맥락.
논문 (knowledge/research/dbms-papers/ 아래)
섹션 제목: “논문 (knowledge/research/dbms-papers/ 아래)”- Architecture of a DB System (Hellerstein et al., 2007) —
fntdb07-architecture.md. §“Catalog Manager”는formrdesc가 해결하는 카탈로그-릴레이션-부트스트랩 문제를 다룬다.
상호 참조 (형제 모듈 문서)
섹션 제목: “상호 참조 (형제 모듈 문서)”postgres-catcache-syscache.md— relcache 빌드에 입력되는 catcache(개별 카탈로그 행 캐시).postgres-cache-invalidation.md—RelationCacheInvalidateEntry를 구동하는 sinval 파이프라인.postgres-system-catalogs.md—RelationBuildDesc가 읽는pg_class,pg_attribute등 테이블.postgres-memory-contexts.md— 모든 relcache 항목을 호스팅하는CacheMemoryContext.postgres-architecture-overview.md— Axis 6 (카탈로그 + 캐시 레이어)와 sinval 루프.