PostgreSQL Schema Visualizer
1. Open your database client (psql, pgAdmin, DBeaver) and run this query:
WITH excluded AS (
SELECT unnest(ARRAY['information_schema']) AS s
),
cols AS (
SELECT
n.nspname AS schema,
c.relname AS tbl,
a.attname AS col,
pg_catalog.format_type(a.atttypid, a.atttypmod) AS typ,
a.attnotnull AS notnull,
pg_get_expr(d.adbin, d.adrelid) AS dflt,
a.attnum AS pos,
a.attidentity AS identity_type,
a.attgenerated AS gen,
col_description(c.oid, a.attnum) AS comment
FROM pg_class c
JOIN pg_namespace n ON n.oid = c.relnamespace
JOIN pg_attribute a ON a.attrelid = c.oid
AND a.attnum > 0
AND NOT a.attisdropped
LEFT JOIN pg_attrdef d ON d.adrelid = c.oid AND d.adnum = a.attnum
WHERE c.relkind = 'r'
AND n.nspname NOT LIKE 'pg_%'
AND n.nspname NOT IN (SELECT s FROM excluded)
),
pks AS (
SELECT
n.nspname AS schema,
c.relname AS tbl,
array_agg(a.attname ORDER BY k.pos) AS cols
FROM pg_index ix
JOIN pg_class c ON c.oid = ix.indrelid
JOIN pg_namespace n ON n.oid = c.relnamespace
JOIN LATERAL unnest(ix.indkey::smallint[])
WITH ORDINALITY AS k(attnum, pos) ON k.attnum > 0
JOIN pg_attribute a ON a.attrelid = c.oid
AND a.attnum = k.attnum::int2
WHERE ix.indisprimary
AND n.nspname NOT LIKE 'pg_%'
AND n.nspname NOT IN (SELECT s FROM excluded)
GROUP BY n.nspname, c.relname
),
uqs AS (
SELECT
n.nspname AS schema,
c.relname AS tbl,
array_agg(a.attname ORDER BY k.pos) AS cols
FROM pg_index ix
JOIN pg_class c ON c.oid = ix.indrelid
JOIN pg_namespace n ON n.oid = c.relnamespace
JOIN LATERAL unnest(ix.indkey::smallint[])
WITH ORDINALITY AS k(attnum, pos) ON k.attnum > 0
JOIN pg_attribute a ON a.attrelid = c.oid
AND a.attnum = k.attnum::int2
WHERE ix.indisunique AND NOT ix.indisprimary
AND ix.indpred IS NULL
AND ix.indexprs IS NULL
AND n.nspname NOT LIKE 'pg_%'
AND n.nspname NOT IN (SELECT s FROM excluded)
GROUP BY n.nspname, c.relname, ix.indexrelid
),
fks AS (
SELECT
n.nspname AS schema,
c.relname AS tbl,
array_agg(a.attname ORDER BY k.pos) AS cols,
fn.nspname AS ref_schema,
fc.relname AS ref_tbl,
array_agg(fa.attname ORDER BY k.pos) AS ref_cols,
con.confdeltype AS on_delete,
con.confupdtype AS on_update
FROM pg_constraint con
JOIN pg_class c ON c.oid = con.conrelid
JOIN pg_namespace n ON n.oid = c.relnamespace
JOIN pg_class fc ON fc.oid = con.confrelid
JOIN pg_namespace fn ON fn.oid = fc.relnamespace
JOIN LATERAL unnest(con.conkey)
WITH ORDINALITY AS k(attnum, pos) ON TRUE
JOIN LATERAL unnest(con.confkey)
WITH ORDINALITY AS fk(attnum, pos) ON fk.pos = k.pos
JOIN pg_attribute a ON a.attrelid = c.oid AND a.attnum = k.attnum
JOIN pg_attribute fa ON fa.attrelid = fc.oid AND fa.attnum = fk.attnum
WHERE con.contype = 'f'
AND n.nspname NOT LIKE 'pg_%'
AND n.nspname NOT IN (SELECT s FROM excluded)
AND fn.nspname NOT LIKE 'pg_%'
AND fn.nspname NOT IN (SELECT s FROM excluded)
GROUP BY n.nspname, c.relname, fn.nspname, fc.relname,
con.confdeltype, con.confupdtype, con.oid
),
enums AS (
SELECT
n.nspname AS schema,
t.typname AS name,
array_agg(e.enumlabel ORDER BY e.enumsortorder) AS vals
FROM pg_type t
JOIN pg_namespace n ON n.oid = t.typnamespace
JOIN pg_enum e ON e.enumtypid = t.oid
WHERE t.typtype = 'e'
AND n.nspname NOT LIKE 'pg_%'
AND n.nspname NOT IN (SELECT s FROM excluded)
GROUP BY n.nspname, t.typname
),
chks AS (
SELECT
n.nspname AS schema,
c.relname AS tbl,
a.attname AS col,
string_agg(pg_get_expr(con.conbin, con.conrelid), ' AND ') AS expr
FROM pg_constraint con
JOIN pg_class c ON c.oid = con.conrelid
JOIN pg_namespace n ON n.oid = c.relnamespace
JOIN LATERAL unnest(con.conkey) AS k(attnum) ON TRUE
JOIN pg_attribute a ON a.attrelid = c.oid AND a.attnum = k.attnum
WHERE con.contype = 'c'
AND array_length(con.conkey, 1) = 1
AND n.nspname NOT LIKE 'pg_%'
AND n.nspname NOT IN (SELECT s FROM excluded)
GROUP BY n.nspname, c.relname, a.attname
),
tbls AS (
SELECT DISTINCT schema, tbl FROM cols ORDER BY schema, tbl
)
SELECT json_build_object(
'tables', (
SELECT json_agg(
json_build_object(
'schema', t.schema,
'name', t.tbl,
'columns', (
SELECT json_agg(json_build_object(
'name', c.col,
'type', c.typ,
'not_null', c.notnull,
'default', c.dflt,
'identity_type', c.identity_type,
'generated', c.gen,
'comment', c.comment,
'check', (SELECT ch.expr FROM chks ch
WHERE ch.schema = t.schema
AND ch.tbl = t.tbl
AND ch.col = c.col)
) ORDER BY c.pos)
FROM cols c
WHERE c.schema = t.schema AND c.tbl = t.tbl
),
'primary_key', (SELECT p.cols FROM pks p
WHERE p.schema = t.schema AND p.tbl = t.tbl),
'unique_constraints', (SELECT json_agg(u.cols)
FROM uqs u
WHERE u.schema = t.schema AND u.tbl = t.tbl),
'foreign_keys', (SELECT json_agg(json_build_object(
'columns', f.cols,
'ref_schema', f.ref_schema,
'ref_table', f.ref_tbl,
'ref_columns', f.ref_cols,
'on_delete', f.on_delete,
'on_update', f.on_update
))
FROM fks f
WHERE f.schema = t.schema AND f.tbl = t.tbl)
)
)
FROM tbls t
),
'enums', (
SELECT json_agg(json_build_object(
'schema', e.schema,
'name', e.name,
'values', e.vals
))
FROM enums e
)
) AS result;- Copy the result (right-click the single JSON cell → Copy), paste it below, and click Visualize Schema.
For a cleaner result from a live database, use the "Query your DB" tab.
What you get
Your full database renders as an interactive ER diagram — tables with column names and types, primary key indicators, foreign key arrows with crow-foot notation, and enum nodes. Drag tables to rearrange, zoom, export as PNG or SVG, or continue editing the schema with AI.
Privacy
The query runs entirely inside your own database client. VibeSchema never sees your credentials or your data — only the JSON schema description you paste.
Free, no sign-up
VibeSchema is free to use. No account required.
Open Schema Editor
Design a schema from scratch with AI. Free, no sign-up.