List PostgreSQL Partitioned Tables with SQL
List Partitioned and Ordinary Tables in PostgreSQL
Before you change a retention job, plan a migration, or add a partition for next month, you need a plain list of which tables are partitioned and which are not. pg_class joined to pg_namespace gives that list for the whole database in one query, and a count from pg_inherits shows how many partitions sit under each parent.
Purpose and Overview
Declarative partitioning splits one logical table into many physical pieces. The parent — the partitioned table — holds no rows itself; the PostgreSQL documentation calls it a "virtual" table with no storage of its own. The rows live in its partitions, which are ordinary tables tied to the parent by their partition bounds. On a mature database it is easy to lose track of which large tables were partitioned, which were not, and which partitioning work was never finished.
The system catalogs record all of it. pg_class has a row for every relation, and its relkind column says what kind each one is: p for a partitioned table, r for an ordinary table. Its relispartition flag is true when a table is itself a partition. pg_namespace provides schema names, and pg_inherits records each parent–child link.
The query below returns one row per top-level user table: the schema, the table name, whether it is partitioned or ordinary, the owning role, and how many direct children it has. Partitions themselves are left out, so the list shows each partition tree once, by its root, alongside every ordinary table.
Sample Code
1SELECT
2 n.nspname AS schema_name,
3 c.relname AS table_name,
4 CASE c.relkind
5 WHEN 'p' THEN 'partitioned table'
6 WHEN 'r' THEN 'ordinary table'
7 END AS table_type,
8 pg_get_userbyid(c.relowner) AS owner,
9 (SELECT count(*)
10 FROM pg_inherits i
11 WHERE i.inhparent = c.oid) AS direct_children
12FROM pg_class c
13JOIN pg_namespace n ON n.oid = c.relnamespace
14WHERE c.relkind IN ('p', 'r')
15 AND NOT c.relispartition
16 AND n.nspname NOT IN ('pg_catalog', 'information_schema')
17 AND n.nspname !~ '^pg_'
18ORDER BY schema_name, table_name;
To see the whole tree under one partitioned table — every partition, its parent, whether it is a leaf, its depth, and its size — PostgreSQL 12 and later provide pg_partition_tree():
1SELECT
2 relid,
3 parentrelid,
4 isleaf,
5 level,
6 pg_size_pretty(pg_relation_size(relid)) AS size
7FROM pg_partition_tree('your_table_name')
8ORDER BY level, relid::text;
Notes: The first query works on PostgreSQL 10 and later, the release that introduced declarative partitioning and the relispartition column. It returns ordinary tables and the root of each partition tree — not only partitioned tables. Partitions are hidden, and so is a partitioned table that is itself a partition of another table (sub-partitioning), because relispartition is true for it. The second query needs PostgreSQL 12 or later; replace your_table_name with a partitioned table.
Code Breakdown
The first query reads three catalogs and applies three filters.
pg_class and pg_namespace
pg_class holds one row for every relation in the database — tables, indexes, sequences, views, and more. It stores the schema as an OID in relnamespace, so the join to pg_namespace on n.oid = c.relnamespace turns that number into a readable schema name.
Labelling relkind
relkind is a single character. The query keeps only p (partitioned table) and r (ordinary table), and the CASE expression turns those codes into readable labels. Because the WHERE clause already limits relkind to those two values, the CASE never needs a fallback branch.
Excluding partitions with relispartition
The documentation defines relispartition as "true if table or index is a partition". Filtering on NOT c.relispartition removes every partition from the list, so a table with 365 daily partitions shows up as one row instead of 366. The same flag also hides a sub-partitioned table that sits beneath another parent; only the top of each tree appears.
Schema filter
The nspname conditions drop pg_catalog, information_schema, and every other schema whose name begins with pg_ — names the documentation reserves for system use — so only application tables remain.
Owner with pg_get_userbyid
relowner is the OID of the owning role. pg_get_userbyid() returns the role's name for that OID, which is handy when auditing who can alter or drop each table.
Counting children with pg_inherits
pg_inherits stores one row per child, with inhrelid pointing at the child and inhparent at the parent. The scalar subquery counts the rows whose inhparent is the current table. For a partitioned table this is its number of direct partitions. For an ordinary table it is usually zero — and when it is not, that table is the parent in an older inheritance-based design, which is worth knowing (see Version Compatibility).
Key Partitioning Concepts
A few facts about how PostgreSQL partitioning works explain what the query shows.
Partition methods
PostgreSQL partitions by range, by list, or by hash. Range and list partitioning arrived with declarative partitioning in PostgreSQL 10. Hash partitioning — rows placed by the hash of the key, using a modulus and remainder for each partition — was added in PostgreSQL 11.
Default partitions
From PostgreSQL 11, a partitioned table can have a default partition that stores rows matching no other partition. It shows up as one more child in direct_children and in pg_partition_tree().
Sub-partitioning
A partition can itself be declared as a partitioned table, creating another level. The first query shows only the top-level root; pg_partition_tree() shows every level, with level 0 for the root and isleaf true for the tables that actually hold rows.
Where the storage is
Because the parent has no storage of its own, the size of a partitioned table is the sum of its leaf partitions. In the pg_partition_tree() output, the non-leaf rows carry no data; the sizes that matter are on the rows where isleaf is true.
Practical Applications
The list is a starting point for routine partition housekeeping.
Partition inventory before maintenance
Before writing a job that creates next month's partition or drops old ones, run the first query to confirm which tables are partitioned and how many children each has today. A table with far more children than its retention policy allows is a sign that the drop job is not running.
Finding tables that may need partitioning
Ordinary tables in the list are candidates when they grow very large and are always queried by date or another natural key. Pair the list with a size query to pick the biggest ones first.
Spotting a stalled migration
Moving a large table to a partitioned design often happens in stages. If the new parent shows direct_children of zero, or a table you expected to be partitioned still reads ordinary table, the migration has not finished.
Auditing ownership
The owner column shows which role owns each table. A mix of owners across a schema is worth checking before a maintenance job that creates or drops partitions runs under one account.
Version Compatibility
Declarative partitioning, the p relation kind, and the relispartition column all arrived in PostgreSQL 10, so the first query needs version 10 or later. PostgreSQL 11 added hash partitioning, default partitions, and support for primary keys, foreign keys, indexes, and triggers on partitioned tables. PostgreSQL 12 added the partition introspection functions pg_partition_tree(), pg_partition_ancestors(), and pg_partition_root().
Before version 10, partitioning was built with table inheritance plus triggers or rules to route rows. PostgreSQL still documents that method. Those parents are ordinary tables (relkind = 'r'), and their children are not partitions in the declarative sense, so relispartition is false for them. In this query an inheritance parent appears as an ordinary table with a non-zero direct_children count, and its children appear as ordinary tables too.
Best Practices
- Check the count against the plan — compare
direct_childrenwith your retention rules after every scheduled partition job. - Use pg_partition_tree for depth — for sub-partitioned tables, the tree function is simpler than a recursive query over
pg_inherits. - Size the leaves, not the parent — the parent has no storage, so sum leaf partitions when reporting table size.
- Keep an eye on the default partition — rows landing there usually mean a partition for a new range was never created.
- Watch for legacy inheritance — an ordinary table with children is an older design that needs different maintenance from declarative partitions.
- Filter to one schema when needed — add
AND n.nspname = 'your_schema'to the first query on large multi-tenant databases.
References
- PostgreSQL: Table Partitioning — Range, list and hash partitioning, the partitioned table as a virtual table with no storage, sub-partitioning, and partitioning using inheritance.
- PostgreSQL: pg_class — The
relkindcodes (ppartitioned table,rordinary table) and therelispartitionflag. - PostgreSQL: pg_inherits — The
inhrelidandinhparentcolumns that link each child table to its parent. - PostgreSQL: Database Object Management Functions — Reference for
pg_partition_tree(),pg_partition_root(), andpg_relation_size(). - PostgreSQL 11 Release Notes — Hash partitioning, default partitions, and indexes and keys on partitioned tables.
- depesz: Waiting for PostgreSQL 12 – pg_partition_tree — A walkthrough of the
pg_partition_tree()function when it was added for PostgreSQL 12. - andyatkinson/pg_scripts: list_partitioned_tables.sql — The original catalog query this post's script is based on.
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