Short answer: \dt in psql, or
SELECT schemaname, tablename FROM pg_tables WHERE schemaname NOT IN ('pg_catalog','information_schema');
in any SQL client. Add + for sizes (\dt+) or a schema pattern for one
schema (\dt app.*). If \dt prints
Did not find any relations. and you know the tables are there, your tables are in a
schema that isn’t on your search_path — skip to that section.
hwtables=# \dt
List of relations
Schema | Name | Type | Owner
--------+-----------+-------+----------
public | customers | table | postgres
public | orders | table | postgres
public | scratch | table | postgres
(3 rows)Table of Contents
\dt+ adds size and persistence
hwtables=# \dt+
List of relations
Schema | Name | Type | Owner | Persistence | Access method | Size | Description
--------+-----------+-------+----------+-------------+---------------+------------+-------------
public | customers | table | postgres | permanent | heap | 8192 bytes |
public | orders | table | postgres | permanent | heap | 256 kB |
public | scratch | table | postgres | unlogged | heap | 0 bytes |That Size column is the table heap only — it excludes indexes and TOAST. For the
number you actually want when hunting disk usage, see
finding database and table size in SQL.
The empty result that confuses everyone
This is the single most common reason people search for this page. The tables exist. \dt
says they don’t:
hwempty=# \dt
Did not find any relations.
hwempty=# SELECT schemaname, tablename FROM pg_tables
hwempty-# WHERE schemaname NOT IN ('pg_catalog','information_schema');
schemaname | tablename
------------+--------------
reporting | daily_totals
reporting | errors
(2 rows)Nothing is broken. \dt with no argument only lists tables visible on your
search_path. Run \dt with -E and psql prints the query it is really
issuing — the last line of the WHERE clause gives it away:
$ psql -E -c '\dt'
...
WHERE c.relkind IN ('r','p','')
AND n.nspname <> 'pg_catalog'
AND n.nspname !~ '^pg_toast'
AND n.nspname <> 'information_schema'
AND pg_catalog.pg_table_is_visible(c.oid)
ORDER BY 1,2;Your search_path is almost certainly the default, which does not mention
reporting:
hwempty=# SHOW search_path;
search_path
-----------------
"$user", publicThree fixes, in order of usefulness. Name the schema:
\dt reporting.*. Put it on the path for this session:
SET search_path TO reporting, public;. Or list everything with
\dt *.* — but be warned, that includes the system catalogs. On the two-table database
above it returned 111 rows, 109 of them pg_catalog and information_schema.
\dn lists the schemas so you know what to ask for.
information_schema vs pg_tables vs pg_class
An earlier version of this page recommended information_schema on the grounds that it
is “more standardized”. That advice is actively harmful for the “what tables exist?” question, because
information_schema.tables only shows you tables you hold a privilege on. Same database,
same moment, two different roles:
-- role hw_limited, granted SELECT on public.orders only
hwtables=> SELECT table_name FROM information_schema.tables WHERE table_schema='public';
table_name
------------
orders
(1 row)
hwtables=> SELECT tablename FROM pg_tables WHERE schemaname='public';
tablename
-----------
customers
orders
scratch
(3 rows)One view says the database has one table; the other says three. Both are correct — they answer
different questions. information_schema answers “what may I use?”, and it is the right
choice for portable code that runs on MySQL and SQL Server too. pg_tables answers “what
exists?” and is what you want when you are taking inventory. Note also that
information_schema.tables includes views (table_type = 'VIEW') while
pg_tables excludes them entirely.
pg_class is the raw catalog underneath both, and the only one that distinguishes
every relation kind:
SELECT c.relname, c.relkind, c.relpersistence
FROM pg_class c JOIN pg_namespace n ON n.oid = c.relnamespace
WHERE n.nspname = 'public';
relname | relkind | relpersistence
------------------+---------+----------------
customers_id_seq | S | p
orders_pkey | i | p
orders | r | p
scratch | r | u
recent_orders | v | prelkind is r ordinary table, p partitioned table,
v view, m materialized view, i index, S sequence,
f foreign table. relpersistence is p permanent,
u unlogged, t temporary.
Partitions inflate every one of these lists
A partitioned table shows up alongside each of its partitions, in \dt and in
pg_tables both:
hwtables=# \dt
Schema | Name | Type | Owner
--------+-------------+-------------------+----------
public | customers | table | postgres
public | events | partitioned table | postgres
public | events_2026 | table | postgres
public | orders | table | postgresOn a table partitioned by month that is 60+ rows of clutter. Filter them out with
relispartition, which pg_tables does not expose:
SELECT c.relname
FROM pg_class c JOIN pg_namespace n ON n.oid = c.relnamespace
WHERE n.nspname = 'public' AND c.relkind IN ('r','p') AND NOT c.relispartition
ORDER BY 1;
relname
-----------
customers
events
ordersTwo psql details that trip people up
\dt is a psql feature, not SQL. It does not work in pgAdmin’s query tool, DBeaver, a
JDBC connection or psql -c alongside SQL in the same string — backslash commands need
their own -c:
$ psql -c 'SET search_path TO reporting' -c '\dt'And \dt lists tables in the database you are connected to, nothing else. PostgreSQL
has no cross-database query; if the tables you want are in another database, \c dbname
first. \l lists the databases.
Verified 2026-08-10 on PostgreSQL 17.10 (Homebrew), aarch64-apple-darwin25.6.0.
Every command and every block of output above was executed on a throwaway cluster and pasted unedited,
including the empty result and the two-role privilege comparison. Reproduce all of it with
this script, which builds its own cluster and deletes it
on exit.
