Get Started Free

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.

Updated October 28, 2026 ยท Tested on PostgreSQL 16.14, node-postgres 8.23, psycopg 3.3, Sequelize 6, Prisma 6

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).

CauseHow to tellFix
The name has capitals and was created with quotesThe table shows up as "Orders" in \dtQuote it: "Orders"
The table is in a different schema\dt *.* shows sales.ordersQualify it: sales.orders, or set search_path
You are connected to a different databaseSELECT current_database();Reconnect with \c dbname
The table really doesn't exist yetNothing 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 with relation "orders_id_seq" does not exist. In a database that also had a lower-case orders table, it succeeded and returned the next id from orders_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"') and pg_get_serial_sequence('"Orders"', 'id').
  • PL/pgSQL functions only fail when called. CREATE FUNCTION succeeded for a body that reads a table that didn't exist yet. The error came on SELECT later(), with CONTEXT: PL/pgSQL function later() line 1 at RETURN naming the function.
  • A failed migration takes its tables with it. In PostgreSQL, CREATE TABLE is 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 scratch in one session gave relation "scratch" does not exist in 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.

VisuaLeaf

See every schema and table in VisuaLeaf

VisuaLeaf lists PostgreSQL databases, schemas and tables in one tree, with exact names as stored, and draws ER diagrams from your foreign keys so you can see where every table lives.