Skip to content

PostgreSQL DDL & Schema — Section Overview

Contents:

This subcategory is the schema-change machinery: the code that runs when a statement defines or alters database objects rather than reading or writing rows. The organizing fact is the dispatch boundary. A query that the planner can optimize (SELECT, INSERT, UPDATE, DELETE, MERGE) goes down the query pipeline; everything else — CREATE TABLE, ALTER TABLE, CREATE INDEX, CREATE TRIGGER, CREATE POLICY, COPY, REFRESH MATERIALIZED VIEW, and the rest of the utility-statement set — is handed to ProcessUtility (tcop/utility.c), which is where this section begins.

Concretely, the docs here cover:

  • DDL execution and dispatchProcessUtilitystandard_ProcessUtilityProcessUtilitySlow, the table-creation path (DefineRelationheap_create_with_catalog), and the define.c helpers. This is the router doc for the whole section.
  • ALTER TABLE — the multi-pass rewrite engine in tablecmds.c (AlterTable → the AT* phase walk), the single largest command file in the tree.
  • Index creationDefineIndex (indexcmds.c) and the catalog writer index_create (catalog/index.c), including the CONCURRENTLY multi-transaction build.
  • Constraints — how CHECK, NOT NULL, primary-key, unique, foreign-key, and exclusion constraints are stored (pg_constraint, heap.c’s StoreConstraints / AddRelationNewConstraints) and enforced.
  • Triggers and event triggers — row/statement triggers (commands/trigger.c, fired from the executor) and the separate command-level event-trigger mechanism (commands/event_trigger.c).
  • Declarative partitioning — partition bounds and descriptors (partitioning/, catalog/partition.c) plus the executor-side tuple routing and pruning seams.
  • COPY — utility-dispatched bulk load/unload (copyfrom.c / copyto.c).
  • Materialized viewsCREATE/REFRESH MATERIALIZED VIEW (matview.c, createas.c).
  • Row-level securityCREATE POLICY storage (commands/policy.c) and the query-rewrite-time enforcement (rewrite/rowsecurity.c).

The sharp boundaries — what is not here and who owns it:

  • The catalog as data structure belongs to system-catalog: pg_class/pg_attribute/pg_index layout, the relcache/catcache, and the sinval invalidation a committed DDL emits all live in postgres-overview-system-catalog.md. This section writes catalog rows; that section explains what they are and how every backend stays coherent.
  • The rewriter and planner belong to query-processing. RLS appears in both: this section owns CREATE POLICY and the policy semantics, but the rewrite-phase injection point (fireRIRrulesget_row_security_policies) lives at the rewriter seam documented in postgres-overview-query-processing.md.
  • Trigger and partition-routing execution is shared with query-processing: this section defines triggers and partitions; the executor fires triggers (AfterTriggerBeginQuery) and routes tuples (ExecFindPartition) at DML time.
  • VACUUM / CLUSTER / autovacuum are maintenance, not schema change — they live in txn-recovery, even though they share the commands/ directory.
  • CREATE EXTENSION, foreign tables, and PLs are extensibility; publications/subscriptions are replication-ha. They are utility statements dispatched by the same ProcessUtility, but their mechanism belongs to those sections — this section only notes the shared front door.

The pipeline: ProcessUtility and its two escapees

Section titled “The pipeline: ProcessUtility and its two escapees”

The structural through-line is one dispatch function with two members that deliberately do not go through it. standard_ProcessUtility handles the self-contained statements inline (a big switch) and forwards the catalog-heavy ones to ProcessUtilitySlow, which calls the per-command commands/*.c modules; those in turn call the catalog writers under catalog/ and the bound machinery under partitioning/. Row-level security enforcement and partition tuple routing are the two escapees — they fire in the rewriter and the executor respectively, not at utility dispatch.

flowchart TB
  PORTAL["portal / PortalRunUtility<br/>(query-processing)"] --> PU["ProcessUtility / standard_ProcessUtility<br/>postgres-ddl-execution.md"]

  PU -->|simple, inline| SIMPLE["DROP, RENAME, COPY dispatch,<br/>REFRESH dispatch, ..."]
  PU -->|catalog-heavy| SLOW["ProcessUtilitySlow"]

  SLOW --> CRT["DefineRelation (CREATE TABLE)<br/>postgres-ddl-execution.md"]
  SLOW --> ALT["AlterTable / AT-phase walk<br/>postgres-alter-table.md"]
  SLOW --> IDX["DefineIndex<br/>postgres-index-creation.md"]
  SLOW --> CON["constraint definition<br/>postgres-constraints.md"]
  SLOW --> TRG["CreateTrigger<br/>postgres-triggers.md"]
  SLOW --> EVT["event triggers<br/>postgres-event-triggers.md"]
  SLOW --> POL["CREATE POLICY storage<br/>postgres-row-level-security.md"]
  SLOW --> MV["CREATE MATERIALIZED VIEW<br/>postgres-matview.md"]

  SIMPLE --> CP["DoCopy<br/>postgres-copy.md"]
  SIMPLE --> MVR["REFRESH MATERIALIZED VIEW<br/>postgres-matview.md"]

  CRT --> CAT["catalog writers<br/>heap.c / index.c / partition.c"]
  ALT --> CAT
  IDX --> CAT
  CON --> CAT
  POL --> CAT
  CAT --> PART["partition bounds / descriptors<br/>postgres-partitioning.md"]

  subgraph ESCAPE["the two escapees — NOT via ProcessUtility"]
    direction TB
    RLS["RLS enforcement<br/>get_row_security_policies @ rewrite time<br/>postgres-row-level-security.md"]
    ROUTE["partition tuple routing + pruning<br/>ExecFindPartition @ execute time<br/>postgres-partitioning.md"]
  end

  REWRITE["rewriter (query-processing)"] --> RLS
  EXECDML["executor on INSERT/UPDATE (query-processing)"] --> ROUTE
  EXECDML --> TRGFIRE["trigger firing<br/>postgres-triggers.md"]

  CAT -. "emits sinval on commit" .-> SYSCAT["system-catalog section"]

Three things to read off the diagram:

  1. ProcessUtilitySlow is the real DDL workhorse. The split exists because the catalog-mutating commands need a ParseState, table-open ordering, and event-trigger fences that the trivial statements do not. postgres-ddl-execution.md owns that split.
  2. The catalog writers are shared infrastructure. heap.c (heap_create_with_catalog, StoreConstraints), index.c (index_create), and partition.c are called by many of the command modules. The module docs name the entry they call; the row layouts those writers produce are explained in the system-catalog section.
  3. The escapees are the section’s headline subtlety. A reader who assumes “all schema features run at DDL time” will be wrong twice: RLS policies are defined by DDL but applied by the rewriter on every query against the table, and partition membership is declared by DDL but resolved per-row by the executor. Both module docs lead with that.

Cross-referenced-first — read the dispatch spine before the leaves:

  1. postgres-ddl-execution.md — the ProcessUtility dispatch model and CREATE TABLE. Every other doc in the section assumes this entry point.
  2. postgres-alter-table.md — the AT-phase rewrite engine; the most intricate command and the one that exercises the most catalog surface.
  3. postgres-index-creation.md and postgres-constraints.md — read as a pair; constraints are frequently backed by indexes, and both write into the same catalog path.
  4. postgres-triggers.md then postgres-event-triggers.md — the row/statement model first, then the command-level model that reuses the name but not the mechanism.
  5. postgres-partitioning.md — pulls together catalog (bounds), planner (pruning), and executor (routing); easier once triggers and constraints are understood, since partitions interact with both.
  6. postgres-row-level-security.md — read after the query-processing rewriter overview, since RLS is a rewrite-time mechanism.
  7. postgres-copy.md and postgres-matview.md — standalone leaves; read last, in any order.

Forward references; these module docs may not exist yet.

DocOne-line scope
postgres-ddl-execution.mdThe ProcessUtilitystandard_ProcessUtilityProcessUtilitySlow dispatch, the simple/slow split, and the CREATE TABLE path (DefineRelationheap_create_with_catalog); the router for the whole section.
postgres-alter-table.mdThe multi-phase ALTER TABLE engine in tablecmds.c — the AlterTable prep/exec/rewrite passes (AT* subcommands), table rewrites, and recursion to children.
postgres-index-creation.mdDefineIndex and index_create, opclass/collation resolution, the CONCURRENTLY multi-transaction build, and REINDEX.
postgres-constraints.mdHow CHECK, NOT NULL, primary-key, unique, foreign-key, and exclusion constraints are stored in pg_constraint and enforced (triggers for FKs, indexes for PK/unique).
postgres-triggers.mdRow- and statement-level trigger definition (CreateTrigger) and the executor-time firing machinery (AfterTrigger* queue, transition tables).
postgres-event-triggers.mdCommand-level event triggers (ddl_command_start/end, sql_drop, table_rewrite) — a distinct mechanism from row triggers that fences around ProcessUtility.
postgres-partitioning.mdDeclarative partitioning end-to-end: bounds (partbounds.c), descriptors (partdesc.c), planner pruning (partprune.c), and executor tuple routing (ExecFindPartition).
postgres-row-level-security.mdCREATE POLICY storage (commands/policy.c) and the query-rewrite-time enforcement (get_row_security_policies, called from rewriteHandler.c) — a rewriter mechanism, not a DDL-time one.
postgres-copy.mdCOPY bulk load/unload: DoCopy dispatch, the copyfrom.c/copyto.c paths, multi-insert batching, and partition routing during COPY ... FROM.
postgres-matview.mdMaterialized views: CREATE MATERIALIZED VIEW (shared with createas.c) and REFRESH MATERIALIZED VIEW (ExecRefreshMatView), including the concurrent-refresh diff.
  • postgres-overview-system-catalog.md — the closest neighbor. This section writes catalog rows; that section explains the pg_* layout the writers target, the relcache/catcache that read them, and the sinval invalidation a committed DDL emits. Read them as a write-side / read-side pair.
  • postgres-overview-query-processing.md — owns the rewriter seam where RLS injects (get_row_security_policies), the planner pruning that partitioning relies on, and the executor that fires triggers and routes partition tuples. Three of this section’s docs hand mechanism across this border.
  • postgres-overview-txn-recovery.mdVACUUM/CLUSTER/autovacuum share the commands/ directory but are maintenance, not schema change, and live there; also the owner of the transactional guarantees every DDL statement runs under (DDL in PostgreSQL is transactional and WAL-logged).
  • postgres-overview-extensibility.md and postgres-overview-replication-ha.md — also reached through ProcessUtility (CREATE EXTENSION, foreign tables, PLs; publications/ subscriptions), but their mechanism lives there. This section only owns the shared dispatch front door.