Fix "ERROR: relation does not exist" in PostgreSQL
Why PostgreSQL says a relation does not exist when the table is right there: quoted mixed-case names, the wrong schema or database, or search_path.
Quick Answer
PostgreSQL can't find a table, view or sequence with that exact name in the schemas it searches. The usual cause is a table created with a quoted mixed-case name. Quote it the same way:
-- ERROR: relation "users" does not exist
SELECT * FROM Users;
-- works if the table was created as "Users"
SELECT * FROM "Users";
If that isn't it, the table is in another schema or another database. The checks below find out which.
What the error means
ERROR: relation "orders" does not exist
LINE 1: SELECT * FROM orders;
^
A relation is PostgreSQL's word for anything that holds rows: tables, views, materialized views, sequences and indexes. The error fires when PostgreSQL looks up the name, before the query runs. The SQLSTATE is 42P01 (undefined_table).
| Cause | How to tell | Fix |
|---|---|---|
| The name has capitals and was created with quotes | The table shows up as "Orders" in \dt | Quote it: "Orders" |
| The table is in a different schema | \dt *.* shows sales.orders | Qualify it: sales.orders, or set search_path |
| You are connected to a different database | SELECT current_database(); | Reconnect with \c dbname |
| The table really doesn't exist yet | Nothing in \dt *.*orders* | Run the migration or CREATE TABLE |
Cause 1: mixed-case names and double quotes
PostgreSQL folds unquoted identifiers to lower case, so SELECT * FROM Orders looks for orders. A name in double quotes is stored exactly as written, so a table created as "Orders" can only be reached as "Orders".
ORMs are the usual source. We checked: Sequelize turns a model named User into CREATE TABLE IF NOT EXISTS "Users" (quoted and pluralised), and Prisma turns the same model into CREATE TABLE "User". Anyone querying those tables by hand has to quote them.
-- see the exact stored name
SELECT table_schema, table_name
FROM information_schema.tables
WHERE lower(table_name) = 'orders';
ALTER TABLE "Orders" RENAME TO orders; -- or rename once and stop quoting
Renaming breaks code that still uses the quoted name. In an ORM, change its table mapping (tableName in Sequelize, @@map in Prisma) at the same time.
Cause 2: the table is in another schema
Unqualified names are looked up in the schemas listed in search_path, which defaults to "$user", public. A table in any other schema is invisible unless you name the schema.
SHOW search_path; -- "$user", public
\dt *.* -- every table in every schema
SELECT * FROM sales.orders; -- qualify it
SET search_path TO sales, public; -- for this session
ALTER ROLE app_user SET search_path TO sales, public; -- permanently, for one role
Cause 3: connected to the wrong database
Each PostgreSQL database is separate, and a query can't see tables in another database on the same server. It's easy to connect to postgres by default and look for tables that live in myapp. Check with SELECT current_database();, list with \l and switch with \c myapp. If it only fails in your app, print the connection string it actually uses: a missing DATABASE_URL that falls back to a default is common.
Cause 4: permissions, or the table was never created
If your role has no USAGE on a schema, PostgreSQL skips that schema when it resolves an unqualified name, so the table looks missing instead of forbidden. Qualify the name (sales.orders) and you get the real error, permission denied for schema sales. To fix it: GRANT USAGE ON SCHEMA sales TO app_user;
If no schema has the table at all, a migration hasn't run against this database, or ran inside a transaction that rolled back. Check your migration tool's history table.
What we found testing this
nextval('Orders_id_seq')can silently use the wrong sequence. Names inside the string are case-folded too. In a database with only"Orders", it failed withrelation "orders_id_seq" does not exist. In a database that also had a lower-caseorderstable, it succeeded and returned the next id fromorders_id_seq, a different table's sequence.pg_get_serial_sequence('Orders', 'id')made the same swap. Quote inside the string:nextval('"Orders_id_seq"')andpg_get_serial_sequence('"Orders"', 'id').- PL/pgSQL functions only fail when called.
CREATE FUNCTIONsucceeded for a body that reads a table that didn't exist yet. The error came onSELECT later(), withCONTEXT: PL/pgSQL function later() line 1 at RETURNnaming the function. - A failed migration takes its tables with it. In PostgreSQL,
CREATE TABLEis transactional: a table created in a transaction that later hit an error was gone after the rollback, and the next query got this error. - Temporary tables are per connection. A
CREATE TEMP TABLE scratchin one session gaverelation "scratch" does not existin another. With a connection pool, or PgBouncer in transaction mode, the next query can land on a different connection, so temp tables work locally and fail in production.
How each client reports it: node-postgres sets err.code === '42P01' and err.position (a 1-based character offset, as a string). psycopg raises psycopg.errors.UndefinedTable with e.sqlstate, and psycopg2 psycopg2.errors.UndefinedTable with e.pgcode.
Frequently Asked Questions
Why does PostgreSQL say relation instead of table?
Relation is the general term for anything that holds rows, including tables, views, materialized views and sequences. The same error appears for all of them.
The table shows up in \dt but my query still fails. Why?
Almost always quoting. \dt shows the stored name; if it has capital letters, you must put it in double quotes in every query.
How do I call nextval on a mixed-case sequence?
Put double quotes inside the string: nextval('"Orders_id_seq"'). Without them the name is lower-cased, which either fails or picks up a different sequence that happens to have the lower-case name.
Why does it work in psql but not in my app?
The app is connected to a different database, as a different role with a different search_path, or it created a temp table on a different pooled connection. Compare current_database(), current_user and SHOW search_path in both.