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_pathOIDs 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_classand theRELKIND_*macros, plusRELKIND_HAS_STORAGE()),pg_database.h,pg_namespace.h. TheCATALOG(...)macro and markers such asBKI_SHARED_RELATIONare read bygenbki.plto buildpostgres.bkifor initdb. catalog.c:IsSharedRelation()(the 11 shared catalogs and their indexes),GetNewOidWithIndex()(OID allocation that skips values already in the catalog index) andGetNewRelFileNumber().namespace.c:RangeVarGetRelidExtended()(name to OID, with lock, and the "cross-database references are not implemented" error),recomputeNamespacePath()andfetch_search_path().dbcommands.c:createdb(), withCreateDatabaseUsingWalLog()andCreateDatabaseUsingFileCopy()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 DDLLookups 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.sqlanddatabase.sqlcover search_path and CREATE DATABASE;src/test/recovery/t/034_create_database.plcovers 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;sepgsqluses it.ProcessUtility_hooksees 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