PostgreSQL System Catalogs — Schema Definition, Bootstrap Mechanics, and the Catalog Write Path
Contents:
- Theoretical Background
- Common DBMS Design
- PostgreSQL’s Approach
- Source Walkthrough
- Source verification (as of 2026-06-05)
- Beyond PostgreSQL — Comparative Designs & Research Frontiers
- Sources
Theoretical Background
Section titled “Theoretical Background”Every relational database engine carries a data dictionary — the set
of tables that describe tables, columns, types, functions, constraints,
and every other named object in the system. In PostgreSQL these are the
system catalogs: a collection of roughly 60 heap relations prefixed
pg_ whose rows describe the schema of all other relations, including
themselves. Database System Concepts (Silberschatz, ch. 25 §“Catalog
Management”) names the data dictionary as the central metadata store
whose consistent state the engine relies on for every parse, plan, and
execute phase. Database Internals (Petrov, ch. 6 §“Catalog and Schema”)
observes that catalog access sits on the critical path of every query —
the planner fetches column statistics from pg_statistic, the executor
resolves operator OIDs from pg_operator, and the parser checks relation
existence against pg_class — so both the structure of the catalog and
the mechanism for caching it have outsized performance impact.
Two design axes determine the character of any data-dictionary system:
-
Self-description vs. external metadata. Does the engine store its schema in ordinary tables that can be queried with ordinary SQL (self-describing), or in a separate, privileged store with its own access path? PostgreSQL chose the self-describing model from its Berkeley POSTGRES origins:
pg_classis itself described by a row inpg_class, and a column inpg_classis described by a row inpg_attribute. This circularity is the defining property of the PostgreSQL catalog — it means every catalog can, in principle, be read with a plainSELECT, and every DDL operation is ultimately a write to the same tables that describe user relations. -
Bootstrapping. Because the catalog is self-describing, its earliest rows cannot themselves be built by the normal DDL path — there is no catalog to read when no catalog exists yet. Every self-describing system must solve this chicken-and-egg problem through a bootstrap phase in which a minimal set of catalog relations and their initial rows are created by special-cased code, bypassing the catalog write path that would normally be used. The shape of that bootstrap phase determines which rows carry pre-assigned OIDs, which relations are “pinned” (undeletable), and which relations require their own file-to-OID mapping table rather than encoding the mapping in
pg_classitself.
These two axes — self-description and bootstrap dependency — define everything unusual about how PostgreSQL’s catalog code is structured.
Common DBMS Design
Section titled “Common DBMS Design”Nearly every production DBMS with a relational metadata store shares a set of engineering conventions that narrow the design space before engine-specific choices enter.
Fixed schema for the innermost catalogs
Section titled “Fixed schema for the innermost catalogs”The innermost catalog tables (the ones that describe the catalog itself)
must have a schema that is known at compile time — the engine has to
open and scan them before any row can be read. In practice this means
a C struct whose layout is the physical tuple format for that catalog’s
rows. Every column in pg_class is a field in a C struct; every column
in pg_attribute is a field in a different C struct. The struct layout
is the contract between the on-disk format and the code that reads it.
Pre-assigned OIDs for bootstrap objects
Section titled “Pre-assigned OIDs for bootstrap objects”The bootstrap phase creates a fixed set of catalog rows that must be
findable before the normal OID-assignment path works. These rows carry
pre-assigned OIDs — hard-coded in the source — so that code compiled
against the headers can refer to pg_class as RelationRelationId
(1259) without a catalog lookup.
The standard approach is to partition the OID space into a compiler-assigned range for bootstrap objects (always below some threshold) and a runtime-assigned range for normal objects (above it). The threshold also serves as the pinned-object boundary: an object whose OID is below the threshold is “pinned” — meaning the dependency system cannot drop it — because the engine’s compiled-in symbol references depend on its continued existence.
Shared vs. per-database catalogs
Section titled “Shared vs. per-database catalogs”A database cluster (multiple logical databases sharing one postmaster) faces a catalog split: some metadata belongs to the whole cluster (users, tablespaces, replication origins) and must be read from any database, while other metadata (tables, columns, functions) is scoped to a single database and must be isolated across them. The design convention is two physical tiers:
- Shared catalogs — one copy per cluster, stored in the global
tablespace (the
pg_globaldirectory). Any connection from any database can see the same rows. Examples:pg_authid,pg_database,pg_tablespace. - Per-database catalogs — one copy per database, stored in that
database’s tablespace. A connection to database A cannot see the rows
for database B. Examples:
pg_class,pg_attribute,pg_type.
Catalog write path wraps the heap write
Section titled “Catalog write path wraps the heap write”The normal catalog write path inserts, updates, or deletes a heap tuple and maintains all covering indexes atomically: insert the heap row, then for each secondary index on the catalog, insert the corresponding index entry. The function that does this is a single call, not a two-step, so callers cannot accidentally leave indexes out of sync. The same wrapper also optionally fires any catalog-level trigger (the invalidation machinery), ensuring that cached views of the old state are flushed after the write commits.
Theory ↔ PostgreSQL mapping
Section titled “Theory ↔ PostgreSQL mapping”| Concept | PostgreSQL name |
|---|---|
| Data dictionary | System catalogs (pg_class, pg_attribute, …) |
| Fixed-schema inner catalog | FormData_pg_class, FormData_pg_attribute C structs |
| Pre-assigned bootstrap OID | Hard-coded RelationRelationId, TypeRelationId, etc. |
| Pinned-object OID threshold | FirstUnpinnedObjectId (12000) |
| Normal-object OID floor | FirstNormalObjectId (16384) |
| Shared catalog | BKI_SHARED_RELATION catalogs (11 relations) |
| Per-database catalog | All other catalogs in pg_catalog namespace |
| Bootstrap phase script | postgres.bki (generated by genbki.pl) |
| Catalog write wrapper | CatalogTupleInsert / CatalogTupleUpdate / CatalogTupleDelete |
| Relation-creation entry point | heap_create_with_catalog |
PostgreSQL’s Approach
Section titled “PostgreSQL’s Approach”The CATALOG() macro and header-driven schema
Section titled “The CATALOG() macro and header-driven schema”Every PostgreSQL system catalog is defined in a single C header file
under src/include/catalog/. The canonical example is
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;The CATALOG(name, oid, oidmacro) macro expands to typedef struct FormData_<name> when compiled by the C compiler (via genbki.h), so
the struct is a plain C typedef used throughout the code. The same
header is read by genbki.pl as a Perl data-structure description to
generate postgres.bki and pg_*_d.h symbol files.
The annotation macros (BKI_BOOTSTRAP, BKI_SHARED_RELATION,
BKI_DEFAULT(value), BKI_LOOKUP(catalog)) are no-ops in the C
compiler — they expand to nothing — but genbki.pl recognizes them
as directives:
BKI_BOOTSTRAP— this catalog must be created during the bootstrap phase (before the normal executor is available).BKI_SHARED_RELATION— this catalog lives in the global tablespace and is shared across all databases in the cluster.BKI_DEFAULT(value)— the initial data rows inpg_*.datuse this value when the column is absent.BKI_LOOKUP(catalog)— this OID column references the named catalog;genbki.plresolves symbolic names to OIDs in the.datfiles.
Four catalogs carry BKI_BOOTSTRAP: pg_class (1259), pg_attribute
(1249), pg_proc (1255), and pg_type (1247). These four are the
innermost self-referential loop: pg_class describes pg_attribute
which describes pg_class, and so on. They are created first in the
bootstrap phase using hard-wired C code in bootstrap/bootstrap.c
before the executor comes up.
Figure 1 — How a catalog header feeds genbki.pl, the C compiler, and the runtime.
flowchart LR
H["pg_class.h\n(CATALOG macro + fields)"]
D["pg_class.dat\n(initial row values)"]
G["genbki.pl"]
BKI["postgres.bki\n(bootstrap script)"]
SYM["pg_class_d.h\n(OID #defines)"]
CC["C compiler"]
FD["FormData_pg_class\n(C struct)"]
RT["Runtime\n(relcache, catcache)"]
H --> G
D --> G
G --> BKI
G --> SYM
H --> CC
CC --> FD
BKI --> RT
SYM --> RT
FD --> RT
Figure 1 — The catalog header file pg_class.h is read by two consumers. genbki.pl reads it as a data-structure spec, combining it with pg_class.dat initial rows to emit postgres.bki (the bootstrap script executed by initdb) and pg_class_d.h (C #define macros for every OID and column number). The C compiler reads the same header as a struct definition, producing FormData_pg_class used throughout the executor, relcache, and catcache. Both outputs feed the runtime.
The pg_attribute row layout
Section titled “The pg_attribute row layout”pg_attribute is the column catalog — one row per column per relation,
including catalog relations themselves. Its struct carries a dense set
of type-encoding fields that allow the heap access code to decode any
tuple without going through another catalog lookup:
// FormData_pg_attribute — src/include/catalog/pg_attribute.hCATALOG(pg_attribute,1249,AttributeRelationId) BKI_BOOTSTRAP ...{ Oid attrelid BKI_LOOKUP(pg_class); /* owning relation OID */ NameData attname; /* column name */ Oid atttypid BKI_LOOKUP_OPT(pg_type); int16 attlen; /* copy of pg_type.typlen — no type lookup needed */ int16 attnum; /* 1-based column number; negative = system col */ char attalign; /* copy of pg_type.typalign */ char attstorage;/* varlena inline vs. TOAST strategy */ bool attnotnull; bool atthasdef; /* has DEFAULT expression in pg_attrdef? */ char attgenerated; /* generated column kind, or '\0' */ bool attisdropped; /* logical deletion — column slot reused later */ /* ... */} FormData_pg_attribute;The critical design choice is that attlen, attalign, and
attstorage are copies of the corresponding fields from pg_type.
This means the heap AM can lay out and decode a tuple using only the
pg_attribute rows for that relation — it never needs to open pg_type
during a table scan. The field attisdropped is how PostgreSQL handles
ALTER TABLE DROP COLUMN without physically rewriting the heap: the
column is marked dropped, its slot is kept to preserve the tuple layout
for existing rows, and attnum stays frozen so OID-indexed references
remain valid.
OID ranges and the pinned-object boundary
Section titled “OID ranges and the pinned-object boundary”Every object in PostgreSQL carries an OID. The OID space is partitioned into three ranges that carry different semantics:
| Range | Constant | Meaning |
|---|---|---|
[1, 11999] | below FirstUnpinnedObjectId (12000) | Pinned bootstrap objects — pre-assigned by genbki.pl, cannot be dropped |
[12000, 16383] | [FirstUnpinnedObjectId, FirstNormalObjectId) | Non-pinned bootstrap objects (public namespace, template databases) |
[16384, …) | above FirstNormalObjectId (16384) | Normal user-created objects |
IsCatalogRelationOid tests whether a relation is a system catalog by
comparing its OID to 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 applies the same test to any object class, with three
carved-out exceptions that have pinned OIDs but are intentionally
non-pinned by policy:
// IsPinnedObject — src/backend/catalog/catalog.cboolIsPinnedObject(Oid classId, Oid objectId){ if (objectId >= FirstUnpinnedObjectId) return false; if (classId == LargeObjectRelationId) return false; /* public namespace and databases are not pinned by policy */ if (classId == NamespaceRelationId && objectId == PG_PUBLIC_NAMESPACE) return false; if (classId == DatabaseRelationId) return false; return true;}This OID-range test replaces what was formerly a large set of explicit
pg_depend rows for every pre-loaded object. The trade-off: a single
integer comparison vs. maintaining thousands of dependency rows whose
only purpose was marking the object as non-droppable.
Shared vs. per-database catalog split
Section titled “Shared vs. per-database catalog split”The 11 shared catalogs are identified at compile time by
BKI_SHARED_RELATION in their headers. IsSharedRelation maintains
the authoritative hard-coded list:
// 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; /* ... their indexes and toast tables follow ... */ return false;}The comment explains why this is hard-coded rather than read from
pg_class.relisshared: reading pg_class to discover whether a
relation is shared would create a bootstrapping dependency — you need to
lock the relation before opening its catalog entry, but to know which
lock to acquire you need the entry. The static list breaks that cycle.
The mapped-relation mechanism
Section titled “The mapped-relation mechanism”The four bootstrap catalogs (pg_class, pg_attribute, pg_proc,
pg_type) and a handful of others carry relfilenode = 0 in their
pg_class rows. A zero relfilenode is the signal that the
physical-file mapping is not stored in pg_class but in a separate
file, pg_filenode.map, read by relmapper.c. This is called a
mapped relation.
The reason mapped relations exist is the bootstrap circle: writing the
pg_class row for pg_class itself requires knowing its relfilenode,
but recording the relfilenode requires writing a row to pg_class,
which does not yet exist. pg_filenode.map breaks the cycle — it is a
small binary file updated by relmapper.c (not through the catalog
write path), and RelationMapFilename locates the shared vs. per-database
variant. There are two map files: one in the global tablespace for shared
catalogs, and one in each database directory for per-database catalogs.
// 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 */ /* ... array of (relid -> relfilenumber) mappings ... */} RelMapFile;All non-bootstrap user relations have a non-zero relfilenode in
pg_class and do not use the mapper. The mapped-relation mechanism is
therefore an internal bootstrapping detail invisible to normal SQL.
Figure 2 — Relation-to-file resolution: mapped vs. normal path.
flowchart TD
Q["Open relation by OID"]
R["Read pg_class row\n(via relcache)"]
Z{"relfilenode == 0?"}
MAP["relmapper.c\nReads pg_filenode.map\nReturns RelFileNumber"]
DIRECT["Use relfilenode\ndirectly from pg_class"]
SMGR["smgr_open(RelFileLocator)"]
Q --> R
R --> Z
Z -- yes mapped --> MAP
Z -- no normal --> DIRECT
MAP --> SMGR
DIRECT --> SMGR
Figure 2 — When the relcache opens a relation, it checks whether pg_class.relfilenode is zero. If it is, the physical file number is resolved via relmapper.c which reads the per-database (or global) pg_filenode.map file. Non-zero values are used directly. The smgr_open call then locates the physical segment file.
The catalog write path
Section titled “The catalog write path”Every DDL statement that creates or drops a schema object ultimately
threads through a common catalog write path. The entry point for
creating a new table is heap_create_with_catalog in catalog/heap.c.
The function orchestrates eight distinct steps:
// heap_create_with_catalog — src/backend/catalog/heap.c// (steps distilled from the comment block at line 410)//// 1. CheckAttributeNamesTypes — validate column names and type OIDs// 2. get_relname_relid — check no duplicate name exists// 3. heap_create — build relcache entry + physical storage file// 4. TypeCreate — create the implicit composite row type// 5. AddNewRelationTuple — insert the pg_class row// 6. AddNewAttributeTuples — insert one pg_attribute row per column// 7. StoreConstraints — record CHECK / NOT NULL constraints// 8. return new OIDAddNewRelationTuple inserts a single row into pg_class using
CatalogTupleInsert. AddNewAttributeTuples loops over the column
list and calls CatalogTupleInsertWithInfo for each column, amortizing
the index-opening cost with a held CatalogIndexState.
CatalogTupleInsert is the thin wrapper that ensures heap and index
writes stay in sync:
// 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 opens all secondary indexes on the catalog;
CatalogIndexInsert propagates the new heap tuple’s key fields into
each one; CatalogCloseIndexes releases them. The same wrapper logic
applies to CatalogTupleUpdate and CatalogTupleDelete. Without this
wrapper, indexes could silently diverge from the heap under concurrent
DDL.
OID allocation for new objects
Section titled “OID allocation for new objects”When a DDL statement needs a fresh OID for a new relation, it calls
GetNewOidWithIndex (or its wrapper GetNewRelFileNumber):
// GetNewOidWithIndex — src/backend/catalog/catalog.cOidGetNewOidWithIndex(Relation relation, Oid indexId, AttrNumber oidcolumn){ Oid newOid; SysScanDesc scan; ScanKeyData key; bool collides;
Assert(IsSystemRelation(relation));
/* In bootstrap mode, no indexes exist yet; use sequential counter */ if (IsBootstrapProcessingMode()) return GetNewObjectId();
do { newOid = GetNewObjectId(); /* increment cluster-wide OID counter */
ScanKeyInit(&key, oidcolumn, BTEqualStrategyNumber, F_OIDEQ, ObjectIdGetDatum(newOid));
/* SnapshotAny: see uncommitted rows to avoid transient collisions */ scan = systable_beginscan(relation, indexId, true, SnapshotAny, 1, &key); collides = HeapTupleIsValid(systable_getnext(scan)); systable_endscan(scan); } while (collides);
return newOid;}The retry loop handles the theoretical case of OID wrap-around
collision. systable_beginscan is the standard catalog-scan primitive
— it uses an index if available and indexOK is true, otherwise falls
back to a sequential heap scan, and it uses the provided Snapshot so
the caller controls MVCC visibility.
The systable_beginscan / systable_getnext API
Section titled “The systable_beginscan / systable_getnext API”All code that reads a system catalog — not just OID allocation, but
every DDL lookup — uses the systable_* family declared in
src/include/access/genam.h:
// systable_beginscan — src/include/access/genam.hextern SysScanDesc systable_beginscan( Relation heapRelation, Oid indexId, /* InvalidOid = sequential scan */ bool indexOK, /* may use index? */ Snapshot snapshot, /* MVCC visibility */ int nkeys, ScanKey key);extern HeapTuple systable_getnext(SysScanDesc sysscan);extern void systable_endscan(SysScanDesc sysscan);The indexOK flag allows callers to fall back to a heap scan when the
index may not yet be valid — for instance, during the bootstrap phase
before indexes have been built. Passing SnapshotAny makes the scan
see all tuple versions regardless of transaction status; passing
SnapshotSelf (the caller’s current snapshot) gives normal MVCC
visibility.
The inplace-update exception
Section titled “The inplace-update exception”Most catalog modifications are normal heap updates — the old tuple is
dead-and-replaced, WAL-logged, and visible to new snapshots after
commit. Two catalogs — pg_class and pg_database — also support an
inplace update path used for statistics fields (relpages,
reltuples) and a few other fields that are changed frequently without
needing full transactional semantics:
// IsInplaceUpdateOid — src/backend/catalog/catalog.cboolIsInplaceUpdateOid(Oid relid){ return (relid == RelationRelationId || relid == DatabaseRelationId);}An inplace update overwrites the tuple’s fields in place without
creating a new tuple version. It is not WAL-logged in the usual sense
(no MVCC undo chain is built), and it can be seen by concurrent
transactions without waiting for commit. The mechanism is designed
specifically for autovacuum’s pg_class statistics writes, where the
cost of a full transactional update (dead tuple, vacuum, index update)
on the per-table statistics row would be self-defeating. The
systable_inplace_update_begin / systable_inplace_update_finish API
in genam.h provides the locking discipline for this path.
Figure 3 — The full catalog write path for CREATE TABLE.
flowchart TD
DDL["CREATE TABLE statement\n(utility.c → tablecmds.c)"]
HCC["heap_create_with_catalog\n(catalog/heap.c)"]
HC["heap_create\nBuild relcache entry\nCreate physical file"]
TC["TypeCreate\nInsert pg_type row\nfor implicit row type"]
ART["AddNewRelationTuple\nInsert pg_class row"]
AAT["AddNewAttributeTuples\nInsert pg_attribute rows\n(one per column)"]
SC["StoreConstraints\nInsert pg_constraint rows"]
CTI["CatalogTupleInsert\n(catalog/indexing.c)"]
SHI["simple_heap_insert\n(heap tuple)"]
CII["CatalogIndexInsert\n(all secondary indexes)"]
DDL --> HCC
HCC --> HC
HCC --> TC
HCC --> ART
HCC --> AAT
HCC --> SC
ART --> CTI
AAT --> CTI
CTI --> SHI
CTI --> CII
Figure 3 — DDL flow for CREATE TABLE. heap_create_with_catalog coordinates the eight steps; every catalog row insertion passes through CatalogTupleInsert, which atomically writes both the heap tuple and all secondary index entries for that catalog row.
Source Walkthrough
Section titled “Source Walkthrough”catalog/catalog.c — classification and OID utilities
Section titled “catalog/catalog.c — classification and OID utilities”IsSystemRelation— delegates toIsSystemClass; tests catalog-or-toastIsCatalogRelation/IsCatalogRelationOid— OID <FirstUnpinnedObjectIdIsSharedRelation— hard-coded list of 11 shared catalog OIDs and their indexes/toastIsCatalogNamespace/IsToastNamespace— namespace-based classificationIsReservedName— names starting withpg_are reservedIsInplaceUpdateRelation/IsInplaceUpdateOid—pg_classandpg_databaseonlyIsPinnedObject— OID-range test with three policy exceptionsGetNewOidWithIndex— retry-loop OID allocator usingsystable_beginscan+SnapshotAnyGetNewRelFileNumber— callsGetNewOidWithIndexonpg_classfor new relfilenode
catalog/heap.c — relation creation and deletion
Section titled “catalog/heap.c — relation creation and deletion”heap_create— builds relcache entry + physical storage file; does NOT write pg_classheap_create_with_catalog— full 8-step relation-creation orchestratorAddNewRelationTuple— constructs and inserts thepg_classrowAddNewAttributeTuples— loops and insertspg_attributerows, with amortized index stateheap_drop_with_catalog— deletes rows frompg_class,pg_attribute, and dependents
catalog/indexing.c — catalog write wrappers
Section titled “catalog/indexing.c — catalog write wrappers”CatalogOpenIndexes— opens all secondary indexes on a catalog relationCatalogTupleInsert—simple_heap_insert+CatalogIndexInsert(all indexes)CatalogTupleInsertWithInfo— same, with caller-suppliedCatalogIndexStateCatalogTupleUpdate—heap_update+ index maintenanceCatalogTupleDelete—heap_delete+ (currently no index-level delete hook needed)
include/catalog/genbki.h — macro contract
Section titled “include/catalog/genbki.h — macro contract”CATALOG(name,oid,oidmacro)— expands to struct typedef in C; parsed as schema bygenbki.plBKI_BOOTSTRAP,BKI_SHARED_RELATION— catalog-level annotation no-ops in CBKI_DEFAULT(value),BKI_LOOKUP(catalog),BKI_FORCE_NULL— field-level annotations
include/catalog/pg_class.h — the relation catalog
Section titled “include/catalog/pg_class.h — the relation catalog”FormData_pg_class— C struct; fields:oid,relname,relnamespace,reltype,relam,relfilenode(0 = mapped),reltablespace,relpages,reltuples,relhasindex,relisshared,relkind,relfrozenxid,relminmxid, and moreMAKE_SYSCACHE(RELOID, …)/MAKE_SYSCACHE(RELNAMENSP, …)— two syscache entriesRELKIND_RELATION'r',RELKIND_INDEX'i',RELKIND_VIEW'v', etc.
include/catalog/pg_attribute.h — the column catalog
Section titled “include/catalog/pg_attribute.h — the column catalog”FormData_pg_attribute—attrelid,attname,atttypid,attlen,attnum,attalign,attstorage,attnotnull,atthasdef,attgenerated,attisdropped
utils/cache/relmapper.c — bootstrap file-number map
Section titled “utils/cache/relmapper.c — bootstrap file-number map”RELMAPPER_FILENAME—"pg_filenode.map"(one per database + one global)RelMapFile— binary struct: magic word + array of(relid → relfilenumber)pairsRelationMapOidToFilenumber— lookup function used by the relcache
Position hints (commit 273fe94, 2026-06-05)
Section titled “Position hints (commit 273fe94, 2026-06-05)”| Symbol | File | Line |
|---|---|---|
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 |
Source verification (as of 2026-06-05)
Section titled “Source verification (as of 2026-06-05)”Verified facts
Section titled “Verified facts”-
IsCatalogRelationOiduses a single OID comparison againstFirstUnpinnedObjectId(12000), not a table scan or in-memory set. Verified atcatalog.c:121–136. The comment explicitly states the rationale: OID wrap-around skips the pinned range, so user-defined objects can never accidentally fall below the threshold. Theinformation_schemarelations have OIDs above 12000 and are therefore correctly excluded. -
IsSharedRelationis a hard-coded list of OID constants, not arelissharedread. Verified atcatalog.c:304–361. The comment at line 288 records why: acquiring the lock on a relation before reading itspg_class.relissharedwould create a bootstrapping race condition. There are 11 shared catalog tables enumerated, plus their indexes and TOAST structures. -
There are exactly four
BKI_BOOTSTRAPcatalogs:pg_class,pg_attribute,pg_proc,pg_type. Verified by greppingBKI_BOOTSTRAPacross all headers insrc/include/catalog/. These four carryBKI_BOOTSTRAPin theirCATALOG()line. All other catalogs are created after the executor comes up, by executing BKI commands inpostgres.bki. -
relfilenode = 0inpg_classis the signal for a mapped relation;RELMAPPER_FILENAMEis"pg_filenode.map". Verified inpg_class.hcomment at line 56 andrelmapper.cline 70. The mapper maintains two copies: one in the global tablespace (shared catalogs) and one per database directory (per-database catalogs). -
CatalogTupleInsertcallssimple_heap_insertfollowed byCatalogIndexInsertwithTU_All— no explicit WAL flush is done here; the WAL rule is enforced by the buffer manager at page flush time. Verified atindexing.c:233–250. The index update is not deferred; it happens in the same call before the function returns. -
GetNewOidWithIndexusesSnapshotAny(notSnapshotDirtyor the caller’s MVCC snapshot) to scan for collisions. Verified atcatalog.c:448–515. The comment explains thatSnapshotDirtywould miss recently-deleted rows, creating a risk of transient OID reuse; and thatSnapshotToastwould hold dead rows indefinitely, amplifying the risk for TOAST OID allocation.SnapshotAnysees all versions. -
IsInplaceUpdateOidis restricted to exactly two catalogs:pg_class(RelationRelationId1259) andpg_database(DatabaseRelationId1262). Verified atcatalog.c:193–197. The inplace path exists for autovacuum’s statistics writes topg_classand to avoid overhead when updatingdatfrozenxidinpg_database.
Open questions
Section titled “Open questions”-
Inplace-update locking protocol.
systable_inplace_update_begin/_finish(declared ingenam.h:278–285) was added in a recent cycle to replace the olderheap_inplace_updateAPI. The locking protocol documented inREADME.tuplock§“Locking to write inplace-updated tables” mentions thatheap_updateon these tables must hold a lock compatible with concurrent inplace writers. The exact interaction with autovacuum’s concurrent VACUUM-triggered inplace writes to the samepg_classrow has not been traced end-to-end in this document. Investigation path: readREADME.tuplock, traceheap_inplace_update_and_unlock(heapam.c:6495) and thecheck_lock_if_inplace_updateable_relcall. -
Bootstrap vs. initdb boundary. The four
BKI_BOOTSTRAPcatalogs are created inbootstrap/bootstrap.cby hard-wired C code before the executor starts. Exactly which subsequent catalog rows are created by BKI commands (still inpostgres.bki, still before the executor) vs. by the SQL scripts executed afterBootstrapModeExit()is called has not been mapped in this document. Investigation path: readsrc/bin/initdb/initdb.cand the order ofpostgres.bkivs.system_functions.sqlexecution.
Beyond PostgreSQL — Comparative Designs & Research Frontiers
Section titled “Beyond PostgreSQL — Comparative Designs & Research Frontiers”-
System R’s data dictionary — System R (IBM, 1976; Astrahan et al.) stored catalog information in ordinary base relations (
SYSRELATIONS,SYSCOLUMNS) queryable by the same relational engine used for user data. PostgreSQL inherits this self-describing philosophy directly; the CATALOG macro and header-driven approach is a modern elaboration of the same idea.knowledge/research/dbms-papers/systemr.mdcovers the System R design. -
Oracle’s X$ fixed tables — Oracle’s innermost catalog layer consists of “X$” tables that are in-memory C structs exposed as virtual relations, not heap pages on disk. The
V$views andDBA_*views layer on top. PostgreSQL’s approach — physical heap pages with hard-coded struct layouts — is closer to System R and avoids the two- level virtual-relation indirection Oracle uses for its innermost layer. -
SQL Server’s
sysobjects/sys.*family — SQL Server separates user-visible catalog views (sys.tables,sys.columns) from the physical storage used by the engine, which includes non-relational internal structures. PostgreSQL exposes the raw catalog tables directly aspg_class,pg_attribute, etc., and buildsinformation_schemaviews on top of them, so the physical layout is visible to the DBA. -
The
pg_filenode.mapbootstrap circularity — the mapped-relation mechanism is a pattern shared by any self-describing DBMS that must survive a clean-room bootstrap. MySQL’s.frmfiles (pre-8.0) solved the same problem by keeping schema outside the engine’s own storage. PostgreSQL 8.4 introduced the currentrelmapper.capproach, replacing earlier hard-coded relfilenode assumptions. An evolution doc (postgres-evolution-system-catalog.md, planned) should trace how the bootstrap and catalog storage changed from Berkeley POSTGRES through PG 7.x to the current header-driven genbki approach. -
Dynamic catalogs and columnar catalog stores — several research systems (e.g., Babelfish, Citus distributed catalog) extend or redistribute the PostgreSQL catalog. The primary constraint is that
pg_classandpg_attributemust remain readable by the heap AM using only compile-time struct knowledge; any extension that changes the physical layout of these two relations breaks the bootstrap chain.
Sources
Section titled “Sources”Source files (commit 273fe94, REL_18_STABLE)
Section titled “Source files (commit 273fe94, REL_18_STABLE)”src/backend/catalog/catalog.c— OID classification, IsPinnedObject, GetNewOidWithIndexsrc/backend/catalog/heap.c— heap_create_with_catalog, AddNewRelationTuple, heap_drop_with_catalogsrc/backend/catalog/indexing.c— CatalogTupleInsert/Update/Delete wrapperssrc/include/catalog/catalog.h— catalog.c function prototypessrc/include/catalog/genbki.h— CATALOG(), BKI_* macro definitionssrc/include/catalog/pg_class.h— FormData_pg_class, RELKIND_* constantssrc/include/catalog/pg_attribute.h— FormData_pg_attributesrc/include/catalog/pg_proc.h— BKI_BOOTSTRAP bootstrap catalogsrc/include/catalog/pg_type.h— BKI_BOOTSTRAP bootstrap catalogsrc/include/catalog/pg_authid.h— BKI_SHARED_RELATION examplesrc/include/catalog/pg_database.h— BKI_SHARED_RELATION examplesrc/include/access/transam.h— FirstUnpinnedObjectId, FirstNormalObjectIdsrc/include/access/genam.h— systable_beginscan/getnext/endscan declarationssrc/backend/utils/cache/relmapper.c— RelMapFile, RELMAPPER_FILENAME
Textbook references
Section titled “Textbook references”- Database System Concepts, Silberschatz et al., ch. 25 §“Catalog Management” — data dictionary design
- Database Internals, Petrov, ch. 6 §“Catalog and Schema” — metadata access on the query critical path
Related docs in this KB
Section titled “Related docs in this KB”postgres-relcache.md— howpg_classandpg_attributerows are assembled into RelationDatapostgres-catcache-syscache.md— per-backend cache for individual catalog tuplespostgres-cache-invalidation.md— sinval queue that flushes relcache/catcache after DDLpostgres-ddl-execution.md—utility.cdispatch that callsheap_create_with_catalogpostgres-dependency-tracking.md—pg_depend/pg_shdependandIsPinnedObjectpostgres-namespace-search-path.md—namespace.cand thesearch_pathresolution that precedes catalog scans