DB Internals AtlasPostgreSQL 18.6 Home About

Architecture

Logical structure

One PostgreSQL server (a cluster) holds many databases. Each database holds schemas, and schemas hold tables, indexes, views and sequences. Roles and tablespaces belong to the whole cluster.

In plain words

A PostgreSQL cluster is one running server with one data directory. Despite the name it is not several machines: it is a single instance that can hold many databases.

Databases are kept apart: a connection is always attached to exactly one database, and PostgreSQL does not support direct cross-database table references. Access to another database requires a separate connection or an extension such as postgres_fdw or dblink. Inside a database, schemas work like folders, so sales.orders and public.orders can both exist. When you write just orders, PostgreSQL searches search_path and uses the first match.

Inside the schemas live the things you actually use: tables, indexes, views, sequences and more. Each one has a number, its OID, that PostgreSQL uses internally.

A few things belong to the whole cluster instead of one database: users and roles (the same login works in every database) and tablespaces (places on disk where data can be stored).

For DBAs and operators

Shared versus per-database catalogs

Most system catalogs (pg_class, pg_attribute, pg_namespace, pg_proc …) exist separately in each database. A handful are shared across the cluster and stored in global/: pg_database, pg_authid, pg_auth_members, pg_tablespace, pg_shdepend, pg_db_role_setting, pg_shdescription, pg_shseclabel, pg_parameter_acl, pg_subscription and pg_replication_origin. That is why roles are visible from every database while privileges on tables are recorded per database.

Templates

CREATE DATABASE copies template1 by default (since PostgreSQL 15 using WAL-logged block copies, STRATEGY = wal_log). template0 is a pristine copy you use when you need a different encoding or locale, or to restore a dump. Objects you add to template1 appear in every new database.

Crossing databases

There is no cross-database query. Trying it gives:

ERROR: cross-database references are not implemented: "postgres.public.t"

Use postgres_fdw or dblink, or put the data in different schemas of the same database.

Schemas and search_path

Unqualified names are resolved through search_path (default "$user", public). pg_catalog is always searched first unless listed explicitly, and the session's temporary schema (pg_temp_N) comes before everything for tables. Since PostgreSQL 15, ordinary users can no longer create objects in public by default (it is owned by pg_database_owner). Functions with SECURITY DEFINER should set their own search_path.

Relations and relkind

Tables, indexes, sequences, views, materialized views, composite types, TOAST tables, foreign tables and partitioned tables are all rows in pg_class, distinguished by relkind (r i S v m c t f p I). Each relation has an oid as its catalog identity. Storage-bearing relations such as ordinary tables, indexes, sequences, materialized views and TOAST tables normally have a relfilenode that identifies their current physical storage. Views, foreign tables and partitioned parents do not have their own local relation file, and mapped system catalogs use PostgreSQL's relation-mapping mechanism instead of storing the physical file number directly in pg_class.relfilenode. A rewrite can change the physical file number while the relation OID remains the same.

SELECT c.oid, n.nspname, c.relname, c.relkind, c.relfilenode,
       coalesce(t.spcname, 'pg_default') AS tablespace
  FROM pg_class c
  JOIN pg_namespace n ON n.oid = c.relnamespace
  LEFT JOIN pg_tablespace t ON t.oid = c.reltablespace
 WHERE n.nspname NOT IN ('pg_catalog', 'information_schema')
   AND n.nspname NOT LIKE 'pg_toast%'
 ORDER BY 2, 3;

SELECT oid, datname, pg_size_pretty(pg_database_size(oid)) FROM pg_database;
SELECT 'orders'::regclass::oid;   -- resolve a name through search_path

OIDs are 32-bit and wrap around, but uniqueness is enforced per catalog, so when the OID counter wraps around, OIDs already in use in that catalog are skipped instead of causing errors. User tables no longer get a hidden oid column (removed in PostgreSQL 12).

For developers: inside the source

Where it lives

  • Catalog definitions: one header per catalog in src/include/catalog/, e.g. pg_class.h (FormData_pg_class and the RELKIND_* macros, plus RELKIND_HAS_STORAGE()), pg_database.h, pg_namespace.h. The CATALOG(...) macro and markers such as BKI_SHARED_RELATION are read by genbki.pl to build postgres.bki for initdb.
  • catalog.c: IsSharedRelation() (the 11 shared catalogs and their indexes), GetNewOidWithIndex() (OID allocation that skips values already in the catalog index) and GetNewRelFileNumber().
  • namespace.c: RangeVarGetRelidExtended() (name to OID, with lock, and the "cross-database references are not implemented" error), recomputeNamespacePath() and fetch_search_path().
  • dbcommands.c: createdb(), with CreateDatabaseUsingWalLog() and CreateDatabaseUsingFileCopy() for the two strategies.
  • relmapper.c: mapped catalogs (relfilenode = 0), RelationMapOidToFilenumber().

OID ranges

src/include/access/transam.h splits the OID space: hand-assigned OIDs in the catalog .dat files are below FirstGenbkiObjectId (10000), OIDs assigned by genbki and initdb sit below FirstUnpinnedObjectId (12000) and FirstNormalObjectId (16384), and GetNewObjectId() in varsup.c hands out everything after that. That is why the first user database or table in a fresh cluster gets OID 16384 or close to it.

Name resolution, in calls

RangeVarGetRelidExtended(rv, lockmode, …)
  rv->catalogname set and not current db  → ERROR cross-database
  rv->schemaname set → LookupExplicitNamespace() + get_relname_relid()
  else               → RelnameGetRelid(): walks activeSearchPath
  LockRelationOid(), then re-check for concurrent DDL

Lookups go through the catalog caches: SearchSysCache1(RELOID, …) in syscache.c for single rows, and the relcache (RelationIdGetRelation() → RelationBuildDesc() in relcache.c) for the full RelationData a backend works with. Both are per backend and are kept correct by shared invalidation messages (inval.c).

Experiment and extend

  • src/test/regress/sql/namespace.sql and database.sql cover search_path and CREATE DATABASE; src/test/recovery/t/034_create_database.pl covers WAL-logged database creation.
  • oid2name (contrib) maps OIDs and file nodes to names.
  • object_access_hook (objectaccess.h) is called on create, alter, drop and namespace search; sepgsql uses it. ProcessUtility_hook sees every DDL statement.

Parameters that shape this

search_path default_tablespace temp_tablespaces max_connections

Related topics

Physical layout on disk Roles and privileges Process model TOAST Index types