Docs & Code Guide

Postgres schema diagram: pg_dump the schema, walk the FK graph, regenerate when migrations land

A Postgres schema changes the way your team ships: through migrations, in reviewable files, on merge. That is exactly the hook a living diagram needs. Export the schema with pg_dump, read the foreign key graph from pg_catalog, and let the same CI event that applies a migration trigger the redraw. If you are here for a first diagram rather than the loop, the PostgreSQL diagram generator is the shorter path; this page is about never redrawing by hand again.

7 min readFor backend teams whose schema ships through migrations and whose ER diagram never kept up

See it as a diagram

Everything below, as a diagram you can edit. Describe yours and see it in seconds.

181/20003 credits left
Try:

No account needed · Editable canvas, not a picture

pg_dump --schema-only is the canonical export

One command produces the complete structure, ordered for reload: tables, constraints, indexes, sequences, comments. Adding --no-owner and --no-privileges strips the role-specific noise that differs between environments, which makes the output stable and diffable. That diffability is the quiet win: the schema diff between two releases is precisely the diagram diff, so the export doubles as the change log your redraw responds to.

pg_dump --schema-only --no-owner --no-privileges \
  "$DATABASE_URL" > schema.sql

information_schema or pg_catalog: the honest tradeoff

information_schema is the SQL standard's window and travels across databases, but it has two costs in Postgres: it shows only objects your role has privileges on, and it flattens away Postgres-specific structure, partial indexes, exclusion constraints, table inheritance. pg_catalog, the system tables underneath, is complete, fast, and stable enough that tooling has depended on it for decades; it is just Postgres-only.

For a diagram, the choice is easy: you are already committed to Postgres, so read pg_catalog and get the whole truth. The foreign key edges in particular are one join away in pg_constraint, no view indirection.

The FK graph in one query

Every declared foreign key is a row in pg_constraint with contype f, pointing from the constrained table to the referenced one. Joining through pg_class and pg_namespace gives you qualified names, and the result set is literally the edge list of your ER diagram.

SELECT con.conname          AS constraint_name,
       src_ns.nspname       AS from_schema,
       src.relname          AS from_table,
       tgt_ns.nspname       AS to_schema,
       tgt.relname          AS to_table
FROM pg_constraint con
JOIN pg_class src        ON src.oid = con.conrelid
JOIN pg_class tgt        ON tgt.oid = con.confrelid
JOIN pg_namespace src_ns ON src_ns.oid = src.relnamespace
JOIN pg_namespace tgt_ns ON tgt_ns.oid = tgt.relnamespace
WHERE con.contype = 'f'
  AND NOT src.relispartition;

Schemas as zones, migrations as the trigger

Postgres schemas are namespaces, and teams that use them, public for the app, billing for invoicing, audit for the trail, have already drawn the zones: map each schema to a zone and the diagram inherits the boundaries your grants enforce.

The regeneration trigger is the migrations directory. A CI job filtered on migrations/** runs when and only when the schema changes: apply the migrations to a scratch database or read the post-deploy dump from staging, then hand the result to an agent connected to Datadef's MCP server, registry name io.datadef/mcp, which updates the existing diagram in place. Datadef does not watch the repository or the database; the migration merge is the detection, and the redraw is one CI command. The agent authenticates with an API key created in settings, on paid plans.

Why the loop beats regenerating from scratch

Updating the existing diagram preserves the layout and annotations your team added by hand. A from-scratch regeneration is a new drawing every time; an MCP update is an edit.

Where pg_dump falls short

The FK query returns the edges you declared, and only those. An application that enforces relationships in the ORM without database constraints, common in older Rails codebases, produces a graph with missing arrows. An agent reading the model layer can infer those edges, and the diagram should mark them as inferred rather than pass them off as constraints.

Partitioned tables are the other trap: each partition is a pg_class row, and a naive walk turns one logical table into hundreds of nodes. The relispartition filter in the query above keeps partitions collapsed into their parent. Finally, the standing surface limits: a live-embedded diagram requires the project shared public, and the loop maintains the diagram, not the prose documentation around it.

FAQ

What is the best way to visualize a Postgres schema?

Export the structure with pg_dump --schema-only and generate a diagram from it, with schemas as zones and foreign keys as edges. For a one-off, paste the dump into a generator such as Datadef's PostgreSQL diagram generator; for a diagram that stays correct, wire the regeneration into CI so it reruns when migrations merge.

How do I extract the foreign key graph from Postgres?

Query pg_constraint for rows with contype f and join pg_class and pg_namespace for the table and schema names on both sides. That result is the edge list of the ER diagram. Filter out rows where the source table has relispartition true, or every partition shows up as its own node.

Should I use information_schema or pg_catalog for schema automation?

pg_catalog, unless you genuinely need portability to other databases. information_schema in Postgres shows only objects your role has privileges on and omits Postgres-specific details like partial indexes and exclusion constraints, while pg_catalog is complete and one join away from the FK graph.

How does the diagram update when a migration lands?

Through a CI job, not through watching: a paths filter on the migrations directory triggers a job that applies the migrations to a scratch database, reads the resulting schema, and asks an MCP-connected agent to update the diagram. The diagram edit lands minutes after the merge, and every live embed follows.

What if our app enforces relationships without foreign key constraints?

The database then has no edge to report, and a constraint-only diagram will be missing arrows. Have the agent read the ORM models as well and add the implied relationships, visually marked as inferred, so readers can tell a declared constraint from an application convention.