List PostgreSQL Object Comments with SQL
List PostgreSQL Object Comments with SQL
pg_description is where PostgreSQL keeps every comment attached with COMMENT ON, whether the target is a table, a column, an index, or a function. Psql shows these comments, and so does any tool that reads the catalog, which makes them a cheap way to keep a schema self-documenting.
This SQL query lists the comments on columns of ordinary tables in your database, showing the schema, table name, column name, and the comment text. Partitioned tables, foreign tables, and views are not covered. It is useful for auditing schema documentation, onboarding new team members, and verifying that comments are in place before a schema handover.
SQL Script
1SELECT
2 c.table_schema,
3 c.table_name,
4 c.column_name,
5 pgd.description
6FROM
7 pg_catalog.pg_statio_all_tables AS st
8INNER JOIN
9 pg_catalog.pg_description pgd
10 ON (pgd.objoid = st.relid)
11INNER JOIN
12 information_schema.columns c
13 ON (pgd.objsubid = c.ordinal_position
14 AND c.table_schema = st.schemaname
15 AND c.table_name = st.relname);
Notes: Works on all supported PostgreSQL versions. Returns one row per commented column of an ordinary table. Columns with no comment do not appear because the join to pg_description is an INNER JOIN. Changing that join to a LEFT JOIN is not enough to list uncommented columns, because the later join on pgd.objsubid drops the rows with no comment. To list every column, start from information_schema.columns instead:
1SELECT
2 c.table_schema,
3 c.table_name,
4 c.column_name,
5 pgd.description
6FROM
7 information_schema.columns c
8JOIN
9 pg_catalog.pg_class cl
10 ON (cl.relname = c.table_name
11 AND cl.relnamespace = c.table_schema::regnamespace)
12LEFT JOIN
13 pg_catalog.pg_description pgd
14 ON (pgd.objoid = cl.oid
15 AND pgd.classoid = 'pg_class'::regclass
16 AND pgd.objsubid = c.ordinal_position)
17WHERE
18 c.table_schema NOT IN ('pg_catalog', 'information_schema');
Code Breakdown
pg_catalog.pg_statio_all_tables
This view provides one row per table, including both user tables and system catalog tables. Here it is used as a source of table OIDs (relid) and schema/table name pairs (schemaname, relname). It is a convenient way to get the OID for joining with pg_description; the second query below uses pg_class directly, which is the more usual route.
pg_catalog.pg_description
The pg_description catalog stores the comments created with COMMENT ON for objects in the current database. Comments on shared objects such as databases, roles, and tablespaces are stored cluster-wide in pg_shdescription instead. Key columns used here:
objoid— the OID of the object the comment is attached toobjsubid— for column comments, this is the column's ordinal position (1-based). For table-level comments,objsubidis 0.description— the comment text itself
pgd.objoid = st.relid
Matches each comment to its parent table by OID.
pgd.objsubid = c.ordinal_position
Links the comment to the specific column by its ordinal position number. PostgreSQL stores column comments against the table OID with the column's position as the sub-object identifier.
information_schema.columns
A standard SQL view listing the columns of tables, views, and foreign tables that the current user can access. The ordinal_position column is the 1-based position of the column within its table. PostgreSQL defines it as the column's attnum, which is the same number stored in pgd.objsubid, so the two match even when dropped columns leave gaps.
c.table_schema = st.schemaname AND c.table_name = st.relname
Ensures the column metadata from information_schema is matched to the correct table in the correct schema, avoiding cross-schema name collisions.
Key Points
- Only columns that have a comment defined will appear. An empty result does not prove that no column comments exist: comments on columns of partitioned tables, foreign tables, and views are not covered, and
information_schema.columnshides columns the current user cannot access. - Table-level comments (
objsubid = 0) never match this join, becauseordinal_positionstarts at 1. Use thepg_classquery in the next section to list them. - The query covers all schemas. Add
WHERE c.table_schema = 'public'to limit results to one schema. - A comment removed with
COMMENT ON COLUMN ... IS NULLis gone, so that column simply drops out of these results.
Insights and Explanations
Comments are added with the COMMENT ON command:
1-- Comment on a table
2COMMENT ON TABLE public.orders IS 'Records every customer order. One row per order.';
3
4-- Comment on a column
5COMMENT ON COLUMN public.orders.status IS 'Order lifecycle state: pending, confirmed, shipped, delivered, cancelled.';
To remove a comment, set it to NULL:
1COMMENT ON COLUMN public.orders.status IS NULL;
Column comments serve several practical purposes:
- Onboarding: A new developer can query this view to understand what each column means without reading application code or external documentation.
- Data dictionaries: Because comments live in the catalog, data catalog and documentation tools can read them from
pg_descriptionrather than from a separate document. - Audit and compliance: Documenting sensitive columns (PII, financial data) with comments creates an in-database record of what data is stored and why.
To list table-level comments:
1SELECT n.nspname AS schema_name, c.relname AS table_name, d.description
2FROM pg_class c
3JOIN pg_namespace n ON n.oid = c.relnamespace
4JOIN pg_description d ON d.objoid = c.oid
5 AND d.classoid = 'pg_class'::regclass
6 AND d.objsubid = 0
7WHERE n.nspname NOT IN ('pg_catalog', 'information_schema')
8ORDER BY n.nspname, c.relname;
To find ordinary tables that have no table-level comment defined (partitioned and foreign tables are not listed, because of the relkind = 'r' filter):
1SELECT n.nspname AS schema_name, c.relname AS table_name
2FROM pg_class c
3JOIN pg_namespace n ON n.oid = c.relnamespace
4LEFT JOIN pg_description d ON d.objoid = c.oid AND d.objsubid = 0
5WHERE c.relkind = 'r'
6 AND n.nspname NOT IN ('pg_catalog', 'information_schema')
7 AND d.description IS NULL
8ORDER BY n.nspname, c.relname;
Additional Considerations
- Permissions: There is no security mechanism for viewing comments: any user connected to a database can see all the comments for objects in that database. The query also reads
information_schema.columns, which shows only columns the current user owns or has some privilege on, so a restricted role can see fewer rows. - Performance: All joins are on system catalogs, so run time depends on the number of objects in the database, not on the amount of data in user tables.
- Functions and indexes: Comments on functions, indexes, sequences, and other objects are also stored in
pg_description, keyed by the OID of the object in its own catalog (pg_procfor functions,pg_classfor indexes and sequences). They need different joins, and this query covers only table columns. - psql shortcut: The
\d+ tablenamecommand in psql displays column comments inline with the table definition, which is a quick way to check comments interactively.
References
Posts in this series
- How Many Connections Can Your PostgreSQL Database Handle?
- PostgreSQL Backend Connections via pg_stat_database
- pg_blocking_pids — Find Blocking Queries in PostgreSQL
- List PostgreSQL Databases by Size with Access Check
- Assess PostgreSQL Database Sizes Quickly and Easily
- Unveiling Your PostgreSQL Server - A Diagnostic Powerhouse
- Keep Your PostgreSQL Database Clean, Identify Idle Connections
- Query the PostgreSQL Configuration
- pg_is_in_recovery — Monitor PostgreSQL Standby Status
- ALTER SEQUENCE RESTART WITH in PostgreSQL — Examples
- Monitor Running Queries in PostgreSQL using pg_stat_activity
- Monitor PostgreSQL Active Sessions with pg_stat_activity
- PostgreSQL Error Handling Settings via pg_settings
- PostgreSQL File Location Settings Query via pg_settings
- Inspect PostgreSQL Lock Management Settings with pg_settings
- PostgreSQL Logging Configuration Query via pg_settings
- Monitor PostgreSQL Memory Settings with pg_settings
- PostgreSQL Table Row Count Estimates with SQL
- List PostgreSQL Tables by Size with SQL
- PostgreSQL WAL Settings Query Guide
- log_parser_stats, log_planner_stats, log_executor_stats — PostgreSQL
- PostgreSQL SSL Settings Query Guide
- PostgreSQL Resource Settings Query Guide
- PostgreSQL Replication Settings Query Guide
- PostgreSQL Query Planning Settings Query Guide
- PostgreSQL Preset Options Settings Query Guide
- PostgreSQL Miscellaneous Settings Query Guide
- Count PostgreSQL Sessions by State with SQL
- Kill Idle PostgreSQL Sessions with SQL
- GRANT SELECT on All Tables in PostgreSQL — with Examples
- pg_stat_user_tables — Find Insert-Only Tables in PostgreSQL
- Detect Soft Delete Patterns in PostgreSQL
- List PostgreSQL Object Comments with SQL
- List Foreign Key Constraints in PostgreSQL
- List PostgreSQL Enum Types and Their Values with SQL
- List All Views in a PostgreSQL Database with SQL
- Find PostgreSQL Tables Without a Primary Key
- List PostgreSQL Partitioned Tables with SQL
- List All Schemas in Your PostgreSQL Database
- pg_stat_database — Query PostgreSQL Database Statistics
- List PostgreSQL Roles and Their Privileges
- Scrubbing Email PII in PostgreSQL for GDPR Compliance
- List Installed Extensions in PostgreSQL
- List PostgreSQL Collations with pg_collation and ICU
- PostgreSQL Replica Identity for Logical Replication
- Monitor PostgreSQL Vacuum Progress with pg_stat_progress_vacuum
- Monitor PostgreSQL Wait Events Using pg_stat_activity
- Monitor PostgreSQL Replication Lag with pg_stat_replication
- List PostgreSQL Wait Events with the pg_wait_events View
- PostgreSQL Column-Level Permissions Audit Query
- List All PostgreSQL Triggers with Their State
- timestamptz and tzdata: Avoid Shifted PostgreSQL Timestamps
- Inspect PostgreSQL Sequences with the pg_sequences View
- List PostgreSQL Functions with pg_proc
- Query PostgreSQL Tablespace Info with pg_tablespace
- Audit PostgreSQL Authentication with pg_hba_file_rules
- List PostgreSQL Materialized Views with pg_matviews
- Audit Row-Level Security Policies with pg_policies