PostgreSQL ERROR: relation "x" does not exist

Quick answer

  • Run SELECT to_regclass('users');NULL means the name doesn't resolve through your current search_path, not that the table exists nowhere.
  • Error quotes a lower-case name you didn't type? The table was created with double quotes — query it as "Users".
  • Table exists but only when schema-qualified? It isn't on your search_path.
  • Still NULL when you schema-qualify it? Wrong database, or the migration never ran.

The exact error string

ERROR:  relation "users" does not exist
LINE 1: SELECT * FROM users;
                      ^

-- the same 42P01 error from a write, with the caret in a different column:
ERROR:  relation "missing_tbl" does not exist
LINE 1: INSERT INTO missing_tbl (id) VALUES (1);
                    ^

-- and from a sequence, which is also a "relation":
ERROR:  relation "orders_id_seq" does not exist
LINE 1: SELECT nextval('orders_id_seq');
                       ^

PostgreSQL raises this whenever a relation name can't be resolved from the current session. That last part is the whole difficulty: the message says "does not exist," but what it actually means is "I could not find this name using this database, this search_path, and this role." The object frequently does exist — three feet away, in a schema you aren't looking in.

The error code is SQLSTATE 42P01, whose symbolic name is undefined_table. Its close neighbour 42703 (undefined_column) produces column "x" does not exist and means something quite different — the table resolved fine, the column inside it didn't.

Anatomy of the message

Two parts of the output carry diagnostic weight, and both are routinely skipped over:

Notably, PostgreSQL offers no hint pointing you at another schema. If the table sits in reporting and you query it unqualified, the message is bare — identical to the one you'd get if the table had never been created. Distinguishing those two situations is what the rest of this page is about.

Triage: three queries, in this order

SELECT current_database(), current_schemas(true); right database? which schemas actually resolve? SELECT to_regclass('users'); NULL = doesn't resolve via search_path (never raises) SELECT n.nspname FROM pg_class c JOIN pg_namespace n ... which schema does it really live in? Found in another schema qualify it, or add that schema to search_path Found in no schema wrong database, or the migration never ran

Steps 1–2 tell you whether the name resolves; only step 3 tells you whether the relation exists. The two halves have completely different fixes.

-- 1. Where am I, and what does the path actually resolve to?
SELECT current_database(), current_user, current_schemas(true);
--  appdb | postgres | {pg_catalog,public}

-- 2. Non-throwing existence check (NULL = cannot be resolved from here)
SELECT to_regclass('users');

-- 3. Find it in ANY schema, whatever kind of relation it is.
--    lower() = lower() matches case variants (so a quoted "Users" still
--    shows up) without ILIKE's pattern semantics, where _ and % are wildcards.
SELECT n.nspname AS schema, c.relname AS name,
       CASE c.relkind WHEN 'r' THEN 'table'   WHEN 'v' THEN 'view'
                      WHEN 'm' THEN 'matview' WHEN 'S' THEN 'sequence'
                      WHEN 'p' THEN 'partitioned table'
                      ELSE c.relkind::text END AS kind
FROM pg_class c
JOIN pg_namespace n ON n.oid = c.relnamespace
WHERE lower(c.relname) = lower('users')
ORDER BY 1;

Read the name column carefully rather than just noting that a row came back: if you searched for users and the result says Users, you've found the case-folding problem in Fix 2. Two deliberate variants of that WHERE clause are worth knowing:

-- exact, case-sensitive: "is there a relation named precisely this?"
WHERE c.relname = 'users'

-- fuzzy, when you only remember part of the name (here _ and % ARE wildcards)
WHERE c.relname ILIKE '%user%'

current_schemas(true) is the query most troubleshooting guides omit, and it's more useful than SHOW search_path: it shows the effective, resolved list after "$user" expansion and after dropping schemas that don't exist or that you can't access. A search_path of "$user", public resolving to just {pg_catalog,public} tells you the $user schema isn't contributing anything.

Fix 1: it genuinely isn't there yet (migrations)

The most common cause is the least interesting one: the table hasn't been created in the database you're connected to. This is nearly always a migration problem, and the tell is that step 3 above returns zero rows.

Check that migrations ran against this database, not just that they ran. Every ORM keeps its own bookkeeping table — schema_migrations (Rails, Django), _prisma_migrations (Prisma), alembic_version (Alembic), flyway_schema_history (Flyway). If that table is missing too, no migration has ever run here; if it's present but short, migrations ran partially:

-- does the migration bookkeeping table itself exist?
SELECT to_regclass('schema_migrations'), to_regclass('_prisma_migrations');

-- list every table the current session can actually see
SELECT schemaname, tablename FROM pg_tables
WHERE schemaname NOT IN ('pg_catalog','information_schema')
ORDER BY 1, 2;

A frequent variant in test suites: the test database was created but never migrated, so the suite fails on the first query while the development database is perfectly healthy. Another: the app reads DATABASE_URL from an environment file the migration command doesn't load, so the two act on different databases.

Fix 2: unquoted names are folded to lower case

PostgreSQL folds unquoted identifiers to lower case before lookup. The SQL standard says to fold to upper case, so this is a documented incompatibility, not a bug. Double-quoting an identifier both preserves its case and makes it case-sensitive:

CREATE TABLE "Users" (id int);   -- real name is: Users

-- ❌ fails — "Users" folds to "users", which doesn't exist
--    SELECT * FROM Users;
--    ERROR:  relation "users" does not exist

-- ✅ works — quoted, so the case is preserved
SELECT * FROM "Users";

Note how the failing message quoted "users" even though the query said Users. Whenever the case in the error doesn't match the case you typed, this is your cause. It shows up most often when a schema was created by a GUI tool or an ORM that quotes everything (older Entity Framework and some Rails configurations both do), and then someone queries it by hand.

The long-term fix is to pick one convention: use snake_case everywhere and never quote, which is idiomatic PostgreSQL, or quote consistently in every single reference. Mixing the two is what produces this error. If you inherit a mixed-case schema and want out, rename rather than quote forever — ALTER TABLE "Users" RENAME TO users;.

Fix 3: the table is in a schema that isn't on your search_path

An unqualified name is resolved against search_path, whose default is "$user", public. The "$user" entry means "a schema named after the current role," and is silently ignored when no such schema exists. So by default only public is really searched — anything in app, reporting, or a tenant schema is invisible unless you qualify it:

CREATE SCHEMA app;
CREATE TABLE app.orders (id int);

-- ❌ ERROR:  relation "orders" does not exist
--    SELECT * FROM orders;

-- ✅ schema-qualified — always works, regardless of search_path
SELECT * FROM app.orders;

-- ✅ or put the schema on the path for this session
SET search_path = app, public;

-- ✅ or persist it for a role / database
ALTER ROLE myapp SET search_path = app, public;
ALTER DATABASE appdb SET search_path = app, public;

A per-role or per-database search_path is a perfectly sound convention and plenty of well-run applications rely on one. The thing to avoid is depending on a session-level setting: for critical application queries and migrations, schema qualification makes name resolution explicit rather than inheriting state that survives locally and vanishes behind a connection pooler handing you a different backend on the next request.

Fix 4: you're connected to a different database than you think

Step 1 of the triage already told you which database you're on; this section is about how you ended up on the wrong one. The usual causes:

If you're chasing an intermittent version of this in a containerised setup, the connection may be failing over to a different host entirely — see FATAL: sorry, too many clients already for the related pooling failure mode, and ERR_CONNECTION_REFUSED when nothing is listening at all.

Fix 5: it isn't a table — sequences, views and matviews

"Relation" in PostgreSQL means any entry in pg_class: tables, views, materialized views, sequences, indexes and partitioned tables all qualify. So this same message appears when:

The relkind column in the step-3 query above tells you what kind of object actually exists under that name, which resolves the case where you're issuing a table operation against a view (or the reverse).

Fix 6: temp tables and pooled connections

Temporary tables live in a per-session schema (pg_temp_1, pg_temp_3, and so on) and disappear when that session ends. Another connection cannot see them — not a permissions issue, they're genuinely not in its namespace:

-- session 1
CREATE TEMP TABLE staging_rows (id int);
SELECT count(*) FROM staging_rows;   -- works

-- session 2 (a different connection)
SELECT count(*) FROM staging_rows;
-- ERROR:  relation "staging_rows" does not exist

This becomes a production-only bug behind PgBouncer in transaction pooling mode, or any pool that doesn't pin a connection for the life of a request: you create the temp table on one backend and the follow-up query is handed a different one. If you need a scratch table that survives across statements in a pooled environment, use a real table with a cleanup strategy, or ensure the whole unit of work runs inside a single transaction on a pinned connection.

Fix 7: the privilege case that looks identical

This distinction is worth knowing precisely, because it's widely mis-stated. Missing privileges usually produce their own error, not this one:

SituationError you get
Schema-qualified query, no USAGE on the schemapermission denied for schema private
USAGE granted, but no SELECT on the tablepermission denied for table noselect
Unqualified name, schema on search_path, no USAGErelation "secrets" does not exist
CREATE in public by a role with no explicit grant (PostgreSQL 15+)permission denied for schema public

Only the third row produces our error. When a schema is on your search_path but you lack USAGE on it, PostgreSQL skips it during name resolution rather than reporting a privilege problem — so the object is invisible and the message is the generic "does not exist." Granting the schema privilege fixes it:

GRANT USAGE ON SCHEMA private TO lowpriv;
GRANT SELECT ON private.secrets TO lowpriv;

A related trap: the PostgreSQL 15 public schema change

This one does not emit 42P01 directly — it's one step removed, which is exactly why it wastes so much time. Since PostgreSQL 15, the public schema no longer grants CREATE to the PUBLIC role by default (earlier versions did, and upgraded clusters keep the old grant). The chain runs:

  1. A migration tries to create a table in public as a role that no longer has the implicit grant → it fails with permission denied for schema public.
  2. That failure is swallowed, retried, or simply scrolled past in CI output.
  3. The application then queries the table that was never created → relation "x" does not exist.

So the 42P01 you're looking at is a symptom two steps downstream of a privilege failure. If a migration suite started failing only after moving to a fresh 15+ cluster, read the first error in the migration log rather than the last one your application reported:

-- grant it explicitly to the migrating role (preferred over re-opening it to PUBLIC)
GRANT CREATE ON SCHEMA public TO myapp;

Related errors, and how to tell them apart

MessageSQLSTATEMeans
relation "x" does not exist42P01Name couldn't be resolved from this session
column "x" does not exist42703Relation resolved; the column inside it didn't
function x(...) does not exist42883No function with that name and argument types
permission denied for table x42501Object found; your role can't touch it

If you're staring at a raw driver exception rather than clean psql output, paste the trace into the Error Log Analyzer — it matches the message against this reference set and routes you to the matching page.

Debugging checklist

Frequently Asked Questions

How do I fix "relation does not exist" in PostgreSQL?

Run SELECT to_regclass('your_table'); first. If it returns NULL the name cannot be resolved from this session, so search every schema with a pg_class query. If the table turns up in another schema, either schema-qualify the query or add that schema to search_path. If it turns up nowhere, you are connected to the wrong database or the migration that creates it has not run.

Why does the error show my table name in lower case?

Because PostgreSQL folds unquoted identifiers to lower case before looking them up, and the error quotes the folded name it actually searched for. If you wrote SELECT * FROM Users and the error says relation "users" does not exist, that lower-case spelling is the tell: the table was created as "Users" with double quotes, so its real name is mixed case and only a quoted reference will match it.

Does this error mean I lack permission on the table?

Usually not. Missing table privileges produce permission denied for table x, and a schema-qualified query against a schema you lack USAGE on produces permission denied for schema x. There is one exception: if the schema is on your search_path but you lack USAGE on it, PostgreSQL skips that schema during name resolution and you get relation does not exist instead. Granting USAGE on the schema resolves that case.

Why does the query work in psql but fail from my application?

The two connections almost always differ in database, role, or search_path. psql may connect as a superuser whose role name matches a schema, satisfying the "$user" entry in the default search_path, while the application connects as a different role. Run SELECT current_database(), current_user, current_schemas(true); from both and compare the output rather than assuming they match.

Does "relation" mean only tables?

No. In PostgreSQL a relation is any object in pg_class: tables, views, materialized views, sequences, indexes, and partitioned tables. That is why nextval('users_id_seq') on a missing sequence reports relation "users_id_seq" does not exist rather than something sequence-specific, and why the same message appears for a view or a materialized view you have not created yet.

How do I check whether a table exists without triggering the error?

Use to_regclass, which returns NULL instead of raising: SELECT to_regclass('public.users'); It resolves the name through the same search_path rules a query would use, so a NULL result tells you the name is genuinely unresolvable from this session rather than merely absent from one schema. It is safe inside a transaction because it never aborts one.

Why did this start after restoring a database dump?

A dump restores objects into the schemas they came from, which may not be the schemas your application expects, and pg_dump writes its own SET search_path statements during restore. If the source database kept tables in a named schema and the target role's search_path does not include it, every unqualified query fails even though the restore reported success. Check where the objects actually landed with a pg_class query before assuming the restore was incomplete.

References

More database & backend errors

Browse the full reference — exact message, cause, and fix — or paste a stack trace and let the analyzer find the match.

All Error References Error Log Analyzer duplicate key value violates unique constraint
About the author

Pasindu Ishan is a software developer based in Sri Lanka. He builds privacy-first developer tools at JSON Dev Tools.