Skip to content

PostgreSQL System Catalogs — Schema Definition, Bootstrap Mechanics, and the Catalog Write Path

Contents:

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:

  1. 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_class is itself described by a row in pg_class, and a column in pg_class is described by a row in pg_attribute. This circularity is the defining property of the PostgreSQL catalog — it means every catalog can, in principle, be read with a plain SELECT, and every DDL operation is ultimately a write to the same tables that describe user relations.

  2. 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_class itself.

These two axes — self-description and bootstrap dependency — define everything unusual about how PostgreSQL’s catalog code is structured.

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.

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.

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.

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_global directory). 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.

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.

ConceptPostgreSQL name
Data dictionarySystem catalogs (pg_class, pg_attribute, …)
Fixed-schema inner catalogFormData_pg_class, FormData_pg_attribute C structs
Pre-assigned bootstrap OIDHard-coded RelationRelationId, TypeRelationId, etc.
Pinned-object OID thresholdFirstUnpinnedObjectId (12000)
Normal-object OID floorFirstNormalObjectId (16384)
Shared catalogBKI_SHARED_RELATION catalogs (11 relations)
Per-database catalogAll other catalogs in pg_catalog namespace
Bootstrap phase scriptpostgres.bki (generated by genbki.pl)
Catalog write wrapperCatalogTupleInsert / CatalogTupleUpdate / CatalogTupleDelete
Relation-creation entry pointheap_create_with_catalog

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.h
CATALOG(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 in pg_*.dat use this value when the column is absent.
  • BKI_LOOKUP(catalog) — this OID column references the named catalog; genbki.pl resolves symbolic names to OIDs in the .dat files.

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.

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.h
CATALOG(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.

Every object in PostgreSQL carries an OID. The OID space is partitioned into three ranges that carry different semantics:

RangeConstantMeaning
[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.c
bool
IsCatalogRelationOid(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.c
bool
IsPinnedObject(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.

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.c
bool
IsSharedRelation(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 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.

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 OID

AddNewRelationTuple 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.c
void
CatalogTupleInsert(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.

When a DDL statement needs a fresh OID for a new relation, it calls GetNewOidWithIndex (or its wrapper GetNewRelFileNumber):

// GetNewOidWithIndex — src/backend/catalog/catalog.c
Oid
GetNewOidWithIndex(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.h
extern 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.

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.c
bool
IsInplaceUpdateOid(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.

catalog/catalog.c — classification and OID utilities

Section titled “catalog/catalog.c — classification and OID utilities”
  • IsSystemRelation — delegates to IsSystemClass; tests catalog-or-toast
  • IsCatalogRelation / IsCatalogRelationOid — OID < FirstUnpinnedObjectId
  • IsSharedRelation — hard-coded list of 11 shared catalog OIDs and their indexes/toast
  • IsCatalogNamespace / IsToastNamespace — namespace-based classification
  • IsReservedName — names starting with pg_ are reserved
  • IsInplaceUpdateRelation / IsInplaceUpdateOidpg_class and pg_database only
  • IsPinnedObject — OID-range test with three policy exceptions
  • GetNewOidWithIndex — retry-loop OID allocator using systable_beginscan + SnapshotAny
  • GetNewRelFileNumber — calls GetNewOidWithIndex on pg_class for 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_class
  • heap_create_with_catalog — full 8-step relation-creation orchestrator
  • AddNewRelationTuple — constructs and inserts the pg_class row
  • AddNewAttributeTuples — loops and inserts pg_attribute rows, with amortized index state
  • heap_drop_with_catalog — deletes rows from pg_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 relation
  • CatalogTupleInsertsimple_heap_insert + CatalogIndexInsert (all indexes)
  • CatalogTupleInsertWithInfo — same, with caller-supplied CatalogIndexState
  • CatalogTupleUpdateheap_update + index maintenance
  • CatalogTupleDeleteheap_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 by genbki.pl
  • BKI_BOOTSTRAP, BKI_SHARED_RELATION — catalog-level annotation no-ops in C
  • BKI_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 more
  • MAKE_SYSCACHE(RELOID, …) / MAKE_SYSCACHE(RELNAMENSP, …) — two syscache entries
  • RELKIND_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_attributeattrelid, 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) pairs
  • RelationMapOidToFilenumber — lookup function used by the relcache

Position hints (commit 273fe94, 2026-06-05)

Section titled “Position hints (commit 273fe94, 2026-06-05)”
SymbolFileLine
IsSystemRelationsrc/backend/catalog/catalog.c74
IsCatalogRelationsrc/backend/catalog/catalog.c104
IsCatalogRelationOidsrc/backend/catalog/catalog.c121
IsInplaceUpdateOidsrc/backend/catalog/catalog.c193
IsToastRelationsrc/backend/catalog/catalog.c206
IsCatalogNamespacesrc/backend/catalog/catalog.c243
IsToastNamespacesrc/backend/catalog/catalog.c261
IsReservedNamesrc/backend/catalog/catalog.c278
IsSharedRelationsrc/backend/catalog/catalog.c304
IsPinnedObjectsrc/backend/catalog/catalog.c370
GetNewOidWithIndexsrc/backend/catalog/catalog.c448
GetNewRelFileNumbersrc/backend/catalog/catalog.c557
heap_createsrc/backend/catalog/heap.c285
CheckAttributeNamesTypessrc/backend/catalog/heap.c452
AddNewAttributeTuplessrc/backend/catalog/heap.c848
AddNewRelationTuplesrc/backend/catalog/heap.c1001
heap_create_with_catalogsrc/backend/catalog/heap.c1139
heap_drop_with_catalogsrc/backend/catalog/heap.c1801
CatalogOpenIndexessrc/backend/catalog/indexing.c43
CatalogTupleInsertsrc/backend/catalog/indexing.c233
CatalogTupleInsertWithInfosrc/backend/catalog/indexing.c256
CatalogTupleUpdatesrc/backend/catalog/indexing.c313
CatalogTupleDeletesrc/backend/catalog/indexing.c365
  • IsCatalogRelationOid uses a single OID comparison against FirstUnpinnedObjectId (12000), not a table scan or in-memory set. Verified at catalog.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. The information_schema relations have OIDs above 12000 and are therefore correctly excluded.

  • IsSharedRelation is a hard-coded list of OID constants, not a relisshared read. Verified at catalog.c:304–361. The comment at line 288 records why: acquiring the lock on a relation before reading its pg_class.relisshared would create a bootstrapping race condition. There are 11 shared catalog tables enumerated, plus their indexes and TOAST structures.

  • There are exactly four BKI_BOOTSTRAP catalogs: pg_class, pg_attribute, pg_proc, pg_type. Verified by grepping BKI_BOOTSTRAP across all headers in src/include/catalog/. These four carry BKI_BOOTSTRAP in their CATALOG() line. All other catalogs are created after the executor comes up, by executing BKI commands in postgres.bki.

  • relfilenode = 0 in pg_class is the signal for a mapped relation; RELMAPPER_FILENAME is "pg_filenode.map". Verified in pg_class.h comment at line 56 and relmapper.c line 70. The mapper maintains two copies: one in the global tablespace (shared catalogs) and one per database directory (per-database catalogs).

  • CatalogTupleInsert calls simple_heap_insert followed by CatalogIndexInsert with TU_All — no explicit WAL flush is done here; the WAL rule is enforced by the buffer manager at page flush time. Verified at indexing.c:233–250. The index update is not deferred; it happens in the same call before the function returns.

  • GetNewOidWithIndex uses SnapshotAny (not SnapshotDirty or the caller’s MVCC snapshot) to scan for collisions. Verified at catalog.c:448–515. The comment explains that SnapshotDirty would miss recently-deleted rows, creating a risk of transient OID reuse; and that SnapshotToast would hold dead rows indefinitely, amplifying the risk for TOAST OID allocation. SnapshotAny sees all versions.

  • IsInplaceUpdateOid is restricted to exactly two catalogs: pg_class (RelationRelationId 1259) and pg_database (DatabaseRelationId 1262). Verified at catalog.c:193–197. The inplace path exists for autovacuum’s statistics writes to pg_class and to avoid overhead when updating datfrozenxid in pg_database.

  1. Inplace-update locking protocol. systable_inplace_update_begin / _finish (declared in genam.h:278–285) was added in a recent cycle to replace the older heap_inplace_update API. The locking protocol documented in README.tuplock §“Locking to write inplace-updated tables” mentions that heap_update on these tables must hold a lock compatible with concurrent inplace writers. The exact interaction with autovacuum’s concurrent VACUUM-triggered inplace writes to the same pg_class row has not been traced end-to-end in this document. Investigation path: read README.tuplock, trace heap_inplace_update_and_unlock (heapam.c:6495) and the check_lock_if_inplace_updateable_rel call.

  2. Bootstrap vs. initdb boundary. The four BKI_BOOTSTRAP catalogs are created in bootstrap/bootstrap.c by hard-wired C code before the executor starts. Exactly which subsequent catalog rows are created by BKI commands (still in postgres.bki, still before the executor) vs. by the SQL scripts executed after BootstrapModeExit() is called has not been mapped in this document. Investigation path: read src/bin/initdb/initdb.c and the order of postgres.bki vs. system_functions.sql execution.

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.md covers 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 and DBA_* 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 as pg_class, pg_attribute, etc., and builds information_schema views on top of them, so the physical layout is visible to the DBA.

  • The pg_filenode.map bootstrap circularity — the mapped-relation mechanism is a pattern shared by any self-describing DBMS that must survive a clean-room bootstrap. MySQL’s .frm files (pre-8.0) solved the same problem by keeping schema outside the engine’s own storage. PostgreSQL 8.4 introduced the current relmapper.c approach, 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_class and pg_attribute must 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.

Source files (commit 273fe94, REL_18_STABLE)

Section titled “Source files (commit 273fe94, REL_18_STABLE)”
  • src/backend/catalog/catalog.c — OID classification, IsPinnedObject, GetNewOidWithIndex
  • src/backend/catalog/heap.c — heap_create_with_catalog, AddNewRelationTuple, heap_drop_with_catalog
  • src/backend/catalog/indexing.c — CatalogTupleInsert/Update/Delete wrappers
  • src/include/catalog/catalog.h — catalog.c function prototypes
  • src/include/catalog/genbki.h — CATALOG(), BKI_* macro definitions
  • src/include/catalog/pg_class.h — FormData_pg_class, RELKIND_* constants
  • src/include/catalog/pg_attribute.h — FormData_pg_attribute
  • src/include/catalog/pg_proc.h — BKI_BOOTSTRAP bootstrap catalog
  • src/include/catalog/pg_type.h — BKI_BOOTSTRAP bootstrap catalog
  • src/include/catalog/pg_authid.h — BKI_SHARED_RELATION example
  • src/include/catalog/pg_database.h — BKI_SHARED_RELATION example
  • src/include/access/transam.h — FirstUnpinnedObjectId, FirstNormalObjectId
  • src/include/access/genam.h — systable_beginscan/getnext/endscan declarations
  • src/backend/utils/cache/relmapper.c — RelMapFile, RELMAPPER_FILENAME
  • 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
  • postgres-relcache.md — how pg_class and pg_attribute rows are assembled into RelationData
  • postgres-catcache-syscache.md — per-backend cache for individual catalog tuples
  • postgres-cache-invalidation.md — sinval queue that flushes relcache/catcache after DDL
  • postgres-ddl-execution.mdutility.c dispatch that calls heap_create_with_catalog
  • postgres-dependency-tracking.mdpg_depend / pg_shdepend and IsPinnedObject
  • postgres-namespace-search-path.mdnamespace.c and the search_path resolution that precedes catalog scans