Skip to content
Discord Get Started

System Catalog

DB9 implements PostgreSQL-compatible system catalog views for introspecting database objects.

ViewDescription
pg_tablesUser tables
pg_viewsUser views
pg_classTables, indexes, sequences, views
pg_attributeTable columns
pg_attrdefColumn default values
pg_namespaceSchemas
pg_typeData types
pg_indexIndex metadata
pg_indexesIndex definitions (indexdef)
pg_constraintConstraints (PK, FK, CHECK, UNIQUE, NOT NULL)
pg_procFunctions and procedures
pg_triggerTriggers. tgtype carries PostgreSQL’s timing/level/event bitmask, tgnargs and tgargs the trigger arguments; information_schema.triggers (action_timing, action_orientation) gives the same facts by name, and pg_get_triggerdef() reconstructs the whole statement (see below). tgenabled is live state, written by ALTER TABLE ... DISABLE TRIGGER
pg_enumEnum type values
pg_sequenceSequence metadata
pg_sequencesOne row per sequence, with PostgreSQL 17’s columns. last_value is NULL until the first nextval(), and with CACHE above 1 it can run ahead of the values handed out so far, as in PostgreSQL
pg_extensionInstalled extensions
pg_collationCollations — built-ins only; collations you create work but never appear
pg_roles / pg_userUser and role definitions
pg_authidThe same roles as pg_roles, with rolpassword always NULL
pg_auth_membersRole membership: built-in rows making pg_monitor a member of pg_read_all_settings, pg_read_all_stats and pg_stat_scan_tables, plus any GRANT <role> TO <role> you issue. REVOKE removes the row again
pg_db_role_settingPer-role parameter defaults written by ALTER ROLE … [IN DATABASE …] SET (also shown in pg_roles.rolconfig); applied to new sessions for that role
pg_databaseDatabases
pg_descriptionObject descriptions/comments
pg_dependDependency tracking
pg_amAccess methods
pg_stat_user_tablesOne row per user table. relid, schemaname and relname are populated; every counter is NULL
pg_opclassOperator classes
pg_policyRow-level security policies
pg_policiesReadable RLS policy view (qual, with_check)
pg_settingsRuntime parameters, including DB9’s db9.* tuning settings
pg_prepared_statementsThe current session’s prepared statements — SQL PREPARE (from_sql = true) and named wire-protocol statements (from_sql = false); DEALLOCATE removes the row. Over the HTTP SQL API each request is its own session, so it lists only statements prepared earlier in the same request
pg_timezone_namesTime zone names, abbreviations and UTC offsets (name, abbrev, utc_offset, is_dst)
pg_rewriteRewrite rules — populated with the _RETURN rule backing each view
pg_aggregateAggregate functions. Exposes only aggfnoid; the transition-function columns are absent
pg_publicationPublications, written by CREATE PUBLICATION. A logical replication slot captures changes only for tables in a publication, apart from the rows a CREATE TABLE ... AS or SELECT ... INTO loads — see pg_replication_slots below
pg_publication_relPublication table membership, written by ALTER PUBLICATION ... ADD/DROP/SET TABLE. Membership at write time decides which changes a slot captures, apart from the rows a CREATE TABLE ... AS or SELECT ... INTO loads. A materialized view cannot be added (42809)
pg_publication_tablesThe readable view over the same membership — pubname, schemaname, tablename, attnames, rowfilter. Populated whenever pg_publication_rel is
pg_languageFunction languages — internal, sql and plpgsql. Joins to pg_proc.prolang
pg_tablespacepg_default and pg_global. Present for tools that join to it; DB9 has no user tablespaces and SET TABLESPACE is rejected
pg_default_aclDefault privilege rules. One system row is present from database creation (a read grant to _db9_sys_readonly on public); ALTER DEFAULT PRIVILEGES adds your own, see Roles & Privileges
pg_stat_activityOne row per session on the server node your connection landed on — 17 columns with live values, including state, query, backend_start, query_start, wait_event and client_addr. It is not a database-wide list: concurrent sessions served by another node do not appear, so it cannot stand in for a connection count. See Observability
pg_stat_replicationNode-local, like pg_stat_activity: a client streaming from a replication slot appears only to sessions on the server node it is connected to, so the view is usually empty — read a slot’s state from pg_replication_slots.active instead
pg_stat_databaseOne row per database. For the database you are connected to, numbackends, xact_commit, xact_rollback and the tup_* counters carry values; the block, timing and session counters are NULL. The counters are kept per server node, like pg_stat_activity — each node has its own totals and its own stats_reset, so two connections can read different numbers
pg_stat_all_tablesThe same columns and rows as pg_stat_user_tables — user tables only, every counter NULL
pg_stat_all_indexes / pg_statio_all_tables / pg_statio_all_indexesOne row per user index or table with the identifying columns populated; every counter is NULL
pg_stat_gssapiOne row per session on the same node
pg_stat_sslOne row per session on the same node, with that connection’s real TLS state in ssl, version, cipher and bits — true, TLSv1.3, TLS_AES_256_GCM_SHA384 and 256 for a TLS session, false and NULLs for a plaintext one. A query sent over the HTTP SQL API sees its own session as ssl = false. See Authentication & Roles
pg_statsPer-column statistics written by ANALYZE — null_frac, avg_width, n_distinct, most_common_vals and histogram_bounds. n_distinct is a count of distinct values, never PostgreSQL’s negative fraction, and correlation is NULL. A table that has never been analyzed has no rows here
pg_event_triggerEvent triggers, written by CREATE EVENT TRIGGER — see Advanced SQL — Event triggers
pg_inheritsOne row per partition of a partitioned table. CREATE TABLE ... INHERITS is rejected (42601), so partitions are the only parent/child pairs
pg_partitioned_tableEmpty until the database has a partitioned table; from then on any query against it fails with 0A000 (cannot export persisted RANGE partitions through pg_partitioned_table: …). Find partitioned tables with pg_class.relkind = 'p' instead. Partitioning itself covers PARTITION BY RANGE on a timestamp/timestamptz key only — see the compatibility matrix
pg_replication_slotsLogical replication slots, 19 columns including conflicting and the PostgreSQL 17 additions failover/synced. Create a slot with SELECT * FROM pg_create_logical_replication_slot('<slot>', 'wal2json') — the function resolves in the FROM clause only (as SELECT pg_create_logical_replication_slot(...) it reports 42883), and through SQL only wal2json is accepted (pgoutput and test_decoding give 22023; pgoutput slots are for the replication protocol). Read changes with SELECT data FROM pg_logical_slot_peek_changes('<slot>', NULL, NULL, 'format-version', '2') (or pg_logical_slot_get_changes, which consumes them): wal2json v2 JSON rows, only for tables that were in a publication when the change was written (plus the rows a CREATE TABLE ... AS or SELECT ... INTO loads into its new table), a few seconds after the write. format-version 2 must be passed explicitly (22023). Drop a slot with SELECT pg_drop_replication_slot('<slot>'). CREATE SUBSCRIPTION is not parsed, so DB9 cannot subscribe to another server. Details, including the replication protocol: Logical decoding (CDC). Creating a slot turns on Realtime capture for the database permanently — it stays on after the slot is dropped, and ADD PRIMARY KEY on a populated table, dropping a populated table’s primary key, and ALTER TYPE ... RENAME VALUE on a label that stored rows use then fail with 55000; see DDL

The following relations resolve but are always empty — they exist so that catalog-introspecting tools and ORMs do not error, not because the feature behind them is implemented:

RelationNote
pg_castNo user-defined casts
pg_shdescriptionNo shared-object comments
pg_conversionNo encoding conversions
pg_operator / pg_opfamilyOperators and operator families are not catalog-registered, the way many built-in functions are not
pg_amop / pg_amprocThe strategy operators and support functions behind an operator class. pg_opclass has its built-in rows, but nothing describes their members, and CREATE OPERATOR CLASS is not parsed
pg_rangeCREATE TYPE ... AS RANGE is not parsed, and the built-in int4range is not registered here either — read pg_type where typtype = 'r' instead
pg_foreign_data_wrapper / pg_foreign_server / pg_foreign_tableCREATE FOREIGN DATA WRAPPER, CREATE SERVER and CREATE FOREIGN TABLE are all rejected at parse time (42601)
pg_transformCREATE TRANSFORM is not parsed
pg_seclabel / pg_seclabelsSECURITY LABEL is not parsed — the same reason pg_shseclabel below stays empty
pg_parameter_aclGRANT ... ON PARAMETER is not parsed (42601)
pg_prepared_xactsPREPARE TRANSACTION is not parsed (42601)
pg_init_privsInitial-privilege snapshots are not recorded; CREATE EXTENSION does not write here
pg_ts_config / pg_ts_dict / pg_ts_parser / pg_ts_templateFull-text search works through built-in configurations that are not catalogued — see Full-Text Search
pg_largeobject_metadataLarge objects are not implemented
pg_shseclabelSecurity labels are not implemented
pg_subscriptionAn empty stub — CREATE SUBSCRIPTION is not parsed, so nothing can populate it
pg_locksLock introspection is not exposed
pg_publication_namespaceALTER PUBLICATION ... ADD TABLES IN SCHEMA is rejected, so this is never populated
pg_statistic_extNo extended statistics objects

Absent entirely — these fail with relation "..." does not exist (42P01) rather than returning zero rows: pg_largeobject, pg_subscription_rel, pg_statistic_ext_data, pg_rules (use pg_rewrite) and pg_matviews (use pg_class filtered on relkind = 'm'). Also absent, and often reached for: pg_statistic, pg_shdepend, pg_ts_config_map, pg_cursors, pg_available_extensions, pg_file_settings, pg_hba_file_rules, pg_config, pg_user_mapping and pg_user_mappings.

Most of the pg_stat* family is absent as well. The views listed above — pg_stat_activity, pg_stat_database, pg_stat_all_tables, pg_stat_user_tables, pg_stat_all_indexes, pg_stat_replication, pg_stat_gssapi, pg_stat_ssl, pg_statio_all_tables, pg_statio_all_indexes, pg_statistic_ext and pg_stats — are the ones that resolve. The _user_ and _sys_ index and I/O variants (pg_stat_user_indexes, pg_stat_sys_tables, pg_statio_user_tables, pg_statio_user_indexes, …), pg_stat_statements, pg_stat_bgwriter and pg_stat_io all raise 42P01. One of the missing relations carries real weight: drizzle-kit push reads pg_stat_user_indexes for each existing table, so every push after the first fails against DB9. See ORM schema and migration tooling.

Do not take either list on trust across builds. A pg_catalog relation resolves exactly when it has a pg_class row in that namespace, so this is the authoritative enumeration for the database you are connected to — anything missing from it raises 42P01:

SQL
SELECT c.relname
FROM pg_class c
JOIN pg_namespace n ON n.oid = c.relnamespace
WHERE n.nspname = 'pg_catalog'
ORDER BY c.relname;

“Resolves” and “has rows” are separate questions, and the second one is per database: pg_extension and pg_publication fill up as you install extensions and create publications. Use SELECT count(*) against the names above rather than assuming, and compare against a name you know is fake (SELECT count(*) FROM pg_catalog.pg_bogus_zzz) so you can tell an empty relation from a missing one.

ViewDescription
tablesTables
columnsTable columns (includes is_generated, generation_expression)
schemataSchemas
sequencesSequences
routinesFunctions and procedures
table_constraintsPRIMARY KEY, FOREIGN KEY, CHECK and UNIQUE constraints (never NOT NULL — see below)
key_column_usagePRIMARY/FOREIGN KEY columns
referential_constraintsForeign key constraints
check_constraintsCHECK constraints
constraint_column_usageColumn constraint usage
table_privilegesTable access privileges
triggersTriggers, with the declared action_timing (BEFORE / AFTER) and action_orientation (ROW / STATEMENT)
viewsViews, with view_definition (the stored query, with relation names schema-qualified and quoted), check_option and is_updatable

The table above is the complete information_schema surface — any other information_schema relation, such as attributes, fails with relation "…" does not exist (42P01).

ViewDescription
cron.jobScheduled cron jobs
cron.job_run_detailsJob execution history
cron.running_jobsCurrently executing jobs (superuser only) — see pg_cron

Practical queries for introspecting your database. Replace 'public' and 'my_table' with your actual schema and table names.

Returns all user-defined tables in a schema with their owner and whether each has indexes, rules, and triggers. The second query lists the same tables from information_schema, which is more portable.

SQL
SELECT
tablename AS table,
tableowner AS owner,
hasindexes AS indexed,
hasrules AS has_rules,
hastriggers AS has_triggers
FROM pg_tables
WHERE schemaname = 'public'
ORDER BY tablename;
SELECT
table_name,
table_type
FROM information_schema.tables
WHERE table_schema = 'public'
AND table_type = 'BASE TABLE'
ORDER BY table_name;

Returns all columns with their data type, nullability, and default value.

SQL
SELECT
column_name,
data_type,
character_maximum_length,
is_nullable,
column_default,
ordinal_position
FROM information_schema.columns
WHERE table_schema = 'public'
AND table_name = 'my_table'
ORDER BY ordinal_position;
-- Using pg_attribute for lower-level detail (includes system columns):
SELECT
a.attname AS column,
pg_catalog.format_type(a.atttypid, a.atttypmod) AS type,
a.attnotnull AS not_null,
a.attnum AS position
FROM pg_attribute a
JOIN pg_class c ON c.oid = a.attrelid
JOIN pg_namespace n ON n.oid = c.relnamespace
WHERE n.nspname = 'public'
AND c.relname = 'my_table'
AND a.attnum > 0 -- exclude system columns
AND NOT a.attisdropped
ORDER BY a.attnum;

Lists every index on a table, including the columns covered and whether it is unique or primary.

SQL
SELECT
indexname AS index,
indexdef AS definition
FROM pg_indexes
WHERE schemaname = 'public'
AND tablename = 'my_table'
ORDER BY indexname;
-- More detailed view via pg_index + pg_class:
SELECT
i.relname AS index,
ix.indisunique AS unique,
ix.indisprimary AS primary,
array_agg(a.attname ORDER BY a.attnum) AS columns
FROM pg_index ix
JOIN pg_class t ON t.oid = ix.indrelid
JOIN pg_class i ON i.oid = ix.indexrelid
JOIN pg_attribute a ON a.attrelid = t.oid AND a.attnum = ANY(ix.indkey)
JOIN pg_namespace n ON n.oid = t.relnamespace
WHERE n.nspname = 'public'
AND t.relname = 'my_table'
GROUP BY i.relname, ix.indisunique, ix.indisprimary
ORDER BY i.relname;

The information_schema query returns the PRIMARY KEY, UNIQUE, CHECK and FOREIGN KEY constraints on a table; the pg_constraint variant additionally returns NOT NULL.

SQL
SELECT
tc.constraint_name,
tc.constraint_type,
kcu.column_name,
cc.check_clause
FROM information_schema.table_constraints tc
LEFT JOIN information_schema.key_column_usage kcu
ON kcu.constraint_name = tc.constraint_name
AND kcu.table_schema = tc.table_schema
LEFT JOIN information_schema.check_constraints cc
ON cc.constraint_name = tc.constraint_name
AND cc.constraint_schema = tc.constraint_schema
WHERE tc.table_schema = 'public'
AND tc.table_name = 'my_table'
ORDER BY tc.constraint_type, tc.constraint_name;
-- Concise version using pg_constraint:
SELECT
conname AS constraint,
contype AS type, -- p=PK, u=UNIQUE, c=CHECK, f=FK, n=NOT NULL
pg_get_constraintdef(oid) AS definition
FROM pg_constraint
WHERE conrelid = 'public.my_table'::regclass
ORDER BY contype, conname;

Lists all row-level security policies on a table, including which roles they apply to and their USING / WITH CHECK expressions.

SQL
SELECT
policyname AS policy,
cmd AS command, -- ALL, SELECT, INSERT, UPDATE, DELETE
roles AS applies_to,
qual AS using_expr,
with_check AS check_expr
FROM pg_policies
WHERE schemaname = 'public'
AND tablename = 'my_table'
ORDER BY policyname;
-- Check if RLS is enabled on the table:
SELECT relname, relrowsecurity, relforcerowsecurity
FROM pg_class
WHERE oid = 'public.my_table'::regclass;

Returns all foreign key relationships — both outbound (this table references another) and inbound (other tables reference this one).

SQL
-- Outbound: FK constraints on my_table
SELECT
tc.constraint_name,
kcu.column_name AS fk_column,
ccu.table_name AS references_table,
ccu.column_name AS references_column,
rc.update_rule,
rc.delete_rule
FROM information_schema.table_constraints tc
JOIN information_schema.key_column_usage kcu ON kcu.constraint_name = tc.constraint_name
AND kcu.table_schema = tc.table_schema
JOIN information_schema.referential_constraints rc ON rc.constraint_name = tc.constraint_name
AND rc.constraint_schema = tc.constraint_schema
JOIN information_schema.constraint_column_usage ccu ON ccu.constraint_name = rc.unique_constraint_name
WHERE tc.constraint_type = 'FOREIGN KEY'
AND tc.table_schema = 'public'
AND tc.table_name = 'my_table'
ORDER BY tc.constraint_name;
-- All FK constraints in the schema (both directions):
SELECT
c.conrelid::regclass::text AS from_table,
c.confrelid::regclass::text AS to_table,
c.conname AS constraint,
pg_get_constraintdef(c.oid) AS definition
FROM pg_constraint c
JOIN pg_class t ON t.oid = c.conrelid
JOIN pg_namespace n ON n.oid = t.relnamespace
WHERE c.contype = 'f'
AND n.nspname = 'public'
ORDER BY from_table, c.conname;

Returns all user-defined functions in a schema with their return types.

SQL
SELECT
routine_name AS function,
routine_type AS type,
data_type AS return_type
FROM information_schema.routines
WHERE routine_schema = 'public'
ORDER BY routine_name;
-- More detail via pg_proc:
SELECT
p.proname AS function,
format_type(p.prorettype, NULL) AS return_type,
p.prokind AS kind -- 'f' function, 'a' aggregate, 'p' procedure, 'w' window
FROM pg_proc p
JOIN pg_namespace n ON n.oid = p.pronamespace
WHERE n.nspname = 'public'
ORDER BY p.proname;

pg_get_triggerdef(oid) reconstructs the whole CREATE TRIGGER statement, which is the most readable way to recover a trigger’s timing, events and arguments without decoding the tgtype bitmask:

SQL
SELECT t.tgname, pg_get_triggerdef(t.oid) AS definition
FROM pg_trigger t
JOIN pg_class c ON c.oid = t.tgrelid
WHERE c.relname = 'orders' AND NOT t.tgisinternal
ORDER BY t.tgname;

The definition column comes back fully qualified and ready to replay:

CREATE TRIGGER orders_audit AFTER INSERT ON public.orders FOR EACH ROW EXECUTE FUNCTION public.orders_audit_fn()

Keep the NOT tgisinternal filter. A foreign key installs RI_ConstraintTrigger_* rows on both the referencing table and the one it points at, and pg_get_triggerdef() returns NULL for those. Filter them out with NOT tgisinternal; a <> '' test on the definition compares against NULL and yields NULL, so use IS NOT NULL if you filter on the definition itself.

Like most scalar functions, pg_get_triggerdef() is callable only in the select list; in the FROM clause it reports function pg_get_triggerdef(oid) does not exist (42883). That is ordinary call-position behaviour, not a missing function — a name that really is absent returns 42883 from both positions.

Returns disk usage for every table, largest first. DB9 reports sizes through the storage accounting virtual tables rather than the PostgreSQL size functions.

SQL
SELECT
table_name,
data_bytes,
index_bytes,
total_bytes,
round(total_bytes::numeric / 1024 / 1024, 2) AS total_mb
FROM _DB9_SYS_TABLE_STORAGE_STATS
ORDER BY total_bytes DESC;
-- Database-level totals:
SELECT
database_name,
total_bytes,
round(total_bytes::numeric / 1024 / 1024, 2) AS total_mb,
scanned_at
FROM _DB9_SYS_STORAGE_STATS;