PostgreSQL Mastery
PostgreSQL Mastery lessons / Module 27: System catalogs

System catalogs: how PostgreSQL describes itself

Level 5: Internals25 min

Every table, column, index, constraint, function and role is a row in a system catalog. psql meta-commands, ORMs, migration tools and monitoring agents all read these tables. Knowing a handful of them lets you answer almost any "what exists and how is it defined" question with SQL.

This lesson is interactive. Run every example on a real PostgreSQL 18 server in your browser, read EXPLAIN ANALYZE plans, and take the quiz.
Open the interactive lesson
You will learn to
  • Navigate pg_class, pg_namespace, pg_attribute, pg_index and pg_constraint
  • Use regclass and regtype casts and the pg_get_*def functions
  • Choose between pg_catalog and information_schema

conceptThe concept

The catalogs live in the pg_catalog schema. The ones you will use most:

CatalogOne row per
pg_namespaceschema
pg_classrelation: table (relkind r), partitioned table (p), index (i), sequence (S), view (v), materialized view (m), TOAST table (t)
pg_attributecolumn of a relation (attnum > 0 for user columns)
pg_indexindex, with its table (indrelid) and column numbers (indkey)
pg_constraintprimary key, foreign key, unique, check or exclusion constraint
pg_proc, pg_type, pg_roles, pg_dependfunction, type, role, dependency between objects

Rows are identified by OIDs. Casting a name to regclass ('orders'::regclass) resolves it through the search_path to an OID, and casting an OID back prints a readable name. The pg_get_indexdef, pg_get_constraintdef, pg_get_viewdef and pg_get_functiondef functions turn catalog rows back into DDL.

information_schema is the SQL-standard view layer over the catalogs. It is portable but slower, hides PostgreSQL-specific details (index types, partitioning, storage) and only shows objects you have privileges on.

whyWhy it matters

Catalog queries are how you audit a database you did not design: tables without primary keys, foreign keys without indexes, duplicate indexes, oversized columns, objects owned by the wrong role. They are also the basis of every schema-diff and migration tool.

Use caseReal-world scenario

Before a migration, a DBA generates a report of every table with its row estimate, size, primary key and index count straight from the catalogs, then compares it with the same report after the migration.

Try itExamples

Run and edit these in the interactive lesson.

Tables in a schema with sizes and estimates
SELECT c.relname, c.relkind, c.reltuples::bigint AS est_rows, c.relpages,
       pg_size_pretty(pg_total_relation_size(c.oid)) AS total_size
FROM pg_class c
JOIN pg_namespace n ON n.oid = c.relnamespace
WHERE n.nspname = 'public' AND c.relkind IN ('r', 'p')
ORDER BY pg_total_relation_size(c.oid) DESC;
Columns of a table from pg_attribute
SELECT a.attnum, a.attname, format_type(a.atttypid, a.atttypmod) AS type, a.attnotnull
FROM pg_attribute a
WHERE a.attrelid = 'orders'::regclass AND a.attnum > 0 AND NOT a.attisdropped
ORDER BY a.attnum;
attnum below zero are system columns such as ctid and xmin; attisdropped hides columns removed with ALTER TABLE DROP COLUMN, which stay in the catalog.
Indexes and constraints rebuilt as DDL
SELECT indexrelid::regclass AS index_name, pg_get_indexdef(indexrelid) AS definition
FROM pg_index WHERE indrelid = 'order_items'::regclass;
SELECT conname, contype, pg_get_constraintdef(oid) AS definition
FROM pg_constraint WHERE conrelid = 'order_items'::regclass ORDER BY conname;
regclass, regtype and regproc casts
SELECT 'orders'::regclass::oid AS orders_oid,
       'orders'::regclass AS back_to_name,
       'numeric(12,2)'::regtype AS type,
       'pgm.plan'::regproc AS function;
The same question through information_schema
SELECT table_name, column_name, data_type, is_nullable
FROM information_schema.columns
WHERE table_schema = 'public' AND table_name = 'customers'
ORDER BY ordinal_position;

internalsUnder the hood

The catalogs are ordinary heap tables with MVCC, so DDL is transactional: a CREATE TABLE inside a rolled-back transaction leaves no catalog rows behind. Each backend caches catalog rows (the relcache and syscache); DDL sends invalidation messages so other sessions reload them. Statistics such as reltuples and relpages in pg_class are estimates updated by VACUUM and ANALYZE, which is why they are cheap to read.

Avoid / preferDo this, not that

Avoid
SELECT * FROM pg_class WHERE relname = 'orders';
Prefer
SELECT * FROM pg_class WHERE oid = 'orders'::regclass;

Names are only unique per schema, so a relname filter can return rows from several schemas. regclass resolves through the search_path exactly as a query would, and errors if the table does not exist.

Avoid
SELECT column_name FROM information_schema.columns
WHERE table_name = 'orders';
Prefer
SELECT attname FROM pg_attribute
WHERE attrelid = 'public.orders'::regclass
  AND attnum > 0 AND NOT attisdropped;

information_schema is portable, but on databases with thousands of tables it is much slower and it silently hides objects you lack privileges on. For PostgreSQL-only tooling, query pg_catalog.

PitfallsCommon mistakes

  • Forgetting NOT attisdropped and listing ghost columns from earlier ALTER TABLE DROP COLUMN.
  • Treating reltuples as an exact count. It is the estimate from the last VACUUM or ANALYZE, and is -1 for a table never analyzed.
  • Writing catalog queries against relkind r only and missing partitioned tables (p).

LimitsWhen not to use it

Do not modify catalogs directly with UPDATE or DELETE. It bypasses every consistency check and is a classic way to corrupt a cluster. Use DDL, and for statistics use ANALYZE.

SecuritySecurity notes

Most catalogs are readable by every role, so object names, column names and function source are visible to all users of a database even without table privileges. Do not put secrets in function bodies, comments or default values. pg_authid (password hashes) is superuser-only; pg_roles is the safe view.

ChallengeTry it yourself

List every table in public with its number of columns, number of indexes and whether it has a primary key, ordered by table name.

Show one solution
One solution
SELECT c.relname,
       (SELECT count(*) FROM pg_attribute a WHERE a.attrelid = c.oid AND a.attnum > 0 AND NOT a.attisdropped) AS columns,
       (SELECT count(*) FROM pg_index i WHERE i.indrelid = c.oid) AS indexes,
       EXISTS (SELECT 1 FROM pg_constraint k WHERE k.conrelid = c.oid AND k.contype = 'p') AS has_pk
FROM pg_class c
WHERE c.relnamespace = 'public'::regnamespace AND c.relkind IN ('r', 'p')
ORDER BY c.relname;

InterviewQuestions you should be able to answer

What is the difference between pg_catalog and information_schema?
pg_catalog holds the real system catalogs, PostgreSQL-specific and complete: index types, partitioning, storage parameters, OIDs. information_schema is the SQL-standard set of views over them: portable across databases but slower, missing PostgreSQL-specific details, and filtered to objects the current role has privileges on. Tools that only target PostgreSQL usually query pg_catalog.
Why is DDL transactional in PostgreSQL?
Because the catalogs are ordinary MVCC tables. CREATE, ALTER and DROP insert, update and delete catalog rows inside the current transaction, so a ROLLBACK undoes them like any other change. That makes migrations safe to wrap in a transaction: either the whole migration applies or none of it does. A few commands cannot run inside a transaction block, such as CREATE INDEX CONCURRENTLY and VACUUM.

QuizCheck your understanding

4 questions with instant feedback in the interactive lesson; a wrong answer goes into your review deck.

Go deeperOfficial documentation