Fix: PostgreSQL ERROR: relation "table_name" does not exist
Quick Answer
How to fix the PostgreSQL 'relation does not exist' error caused by schema search_path issues, case sensitivity, wrong database connections, missing migrations, and more.
The Error
You run a query against a PostgreSQL database and get:
ERROR: relation "users" does not exist
LINE 1: SELECT * FROM users;Or you see it through an ORM or application framework:
ProgrammingError: relation "orders" does not existERROR: relation "app_settings" does not exist
LINE 1: INSERT INTO app_settings (key, value) VALUES ('theme', 'dark');psql:migrations/001.sql:5: ERROR: relation "products" does not existAll of these mean the same thing: PostgreSQL cannot find the table (or view, sequence, or index) you referenced in your query. The object either doesn’t exist in the current database, lives in a different schema, or is spelled differently than you think.
Why This Happens
PostgreSQL uses a concept called schemas to organize database objects. Every table lives inside a schema. When you create a table without specifying a schema, it goes into the public schema by default. When you query a table without specifying a schema, PostgreSQL searches through a list of schemas defined by the search_path setting.
Here’s where things go wrong. If your table exists in a schema that isn’t in the search_path, PostgreSQL won’t find it. If you created the table with double-quoted uppercase letters but query it with lowercase, PostgreSQL treats them as different identifiers. If you’re connected to the wrong database entirely, the table simply isn’t there.
The most common causes:
- Schema mismatch. The table is in a custom schema (like
myapporanalytics) but yoursearch_pathonly includespublic. - Case sensitivity. You created the table as
"Users"(with double quotes) but query it asusers(without quotes). PostgreSQL folds unquoted identifiers to lowercase. - Wrong database. You’re connected to
postgres(the default database) instead of your application database. This happens more often than you’d think — similar to connection issues covered in Fix: PostgreSQL Connection Refused. - Pending migrations. Your migration tool (Django, Rails, Flyway, Alembic) hasn’t run yet, so the table was never created.
- Temporary table scope. You created a temporary table in one session and are trying to access it from another.
- Transaction rollback. A
CREATE TABLEinside a transaction was rolled back, so the table was never committed.
Fix 1: Check the Schema and search_path
The table might exist but in a different schema than PostgreSQL is searching. First, find out which schema the table actually lives in:
SELECT schemaname, tablename
FROM pg_tables
WHERE tablename = 'users';This queries the pg_catalog system table, which knows about every table in the database regardless of search_path. If the table shows up under a schema like myapp or analytics, that’s your problem.
Check your current search_path:
SHOW search_path;The default output is usually:
search_path
--------------
"$user", publicThis means PostgreSQL looks first in a schema matching your username, then in public. If your table is in myapp, it won’t be found.
Fix it for the current session:
SET search_path TO myapp, public;Fix it permanently for a specific user:
ALTER ROLE myuser SET search_path TO myapp, public;Fix it permanently for the entire database:
ALTER DATABASE mydb SET search_path TO myapp, public;After changing the search_path, reconnect or start a new session for database-level and role-level changes to take effect.
Alternatively, you can fully qualify the table name in every query:
SELECT * FROM myapp.users;This bypasses search_path entirely and is the most explicit approach.
Pro Tip: If you manage multiple schemas, always fully qualify table names in application code and migrations. Relying on
search_pathworks for interactivepsqlsessions, but it’s a common source of deployment bugs when the database role in production has a differentsearch_paththan your local dev environment.
Fix 2: Handle Case Sensitivity Correctly
PostgreSQL folds unquoted identifiers to lowercase. This means:
CREATE TABLE Users (id SERIAL PRIMARY KEY);Actually creates a table called users (lowercase). You can query it as Users, USERS, or users — all work because they all fold to users.
But if you used double quotes when creating the table:
CREATE TABLE "Users" (id SERIAL PRIMARY KEY);Now the table is literally stored as Users with a capital U. Querying it without quotes fails:
SELECT * FROM Users;
-- ERROR: relation "users" does not existYou must use the exact same quoting:
SELECT * FROM "Users";To check the actual stored name, query pg_class:
SELECT relname FROM pg_class WHERE relname ILIKE '%users%';The ILIKE operator does a case-insensitive search, so it will find the table regardless of how it was stored.
If you inherited a database with mixed-case table names and want to fix it permanently:
ALTER TABLE "Users" RENAME TO users;Warning: Renaming tables requires updating every query, view, function, and ORM mapping that references the old name. Do this carefully, especially in production.
Fix 3: Verify You’re Connected to the Right Database
This is one of the most common and most overlooked causes. A single PostgreSQL server can host multiple databases. Your table might exist in myapp_production while you’re connected to postgres or myapp_staging.
Check which database you’re connected to:
SELECT current_database();In psql, you can also see it in the prompt:
myapp_production=#If you’re in the wrong database, reconnect:
psql -h localhost -U myuser -d myapp_productionOr from inside psql:
\c myapp_productionThis problem is especially common in environments with multiple databases — staging, production, test. It’s analogous to the MySQL access issues described in Fix: MySQL Access Denied for User, where connection parameters point to the wrong target.
To list all databases on the server:
SELECT datname FROM pg_database WHERE datistemplate = false;Then check which database actually contains your table:
psql -h localhost -U myuser -d myapp_production -c "SELECT tablename FROM pg_tables WHERE tablename = 'users';"Fix 4: Run Pending Migrations
If you use a migration framework, the table may not have been created yet. The relation does not exist error is exactly what Django reports as ProgrammingError: relation "myapp_mymodel" does not exist — covered in detail in Fix: Django OperationalError: no such table.
Django:
python manage.py makemigrations
python manage.py migrateRails:
rails db:migrateAlembic (SQLAlchemy / Flask):
alembic upgrade headFlyway:
flyway migratePrisma:
npx prisma migrate deployAfter running migrations, verify the table exists:
\dt usersOr:
SELECT EXISTS (
SELECT FROM pg_tables
WHERE schemaname = 'public'
AND tablename = 'users'
);If migrations ran but the table still doesn’t exist, check for migration errors in the output. A failed migration might have partially executed — creating some tables but not others. Some frameworks (like Django) wrap each migration in a transaction, but others don’t.
Fix 5: Query the Right Object Type
The word “relation” in PostgreSQL covers more than just tables. It includes views, sequences, indexes, materialized views, and composite types. If your error says relation "users_id_seq" does not exist, you’re looking for a sequence, not a table.
Check what type of object exists with a given name:
SELECT relname, relkind
FROM pg_class
WHERE relname = 'users';The relkind column tells you the type:
| relkind | Meaning |
|---|---|
r | Ordinary table |
v | View |
m | Materialized view |
S | Sequence |
i | Index |
c | Composite type |
t | TOAST table |
f | Foreign table |
p | Partitioned table |
If the query returns no rows, the object genuinely doesn’t exist in the current database. If it returns a row with a different relkind than expected, you might be using the wrong SQL syntax for that object type.
Fix 6: Handle Temporary Tables Correctly
Temporary tables only exist within the session that created them. If you create a temp table in one connection and try to access it from another, you get the relation does not exist error.
-- Session 1
CREATE TEMP TABLE staging_data (id INT, name TEXT);
INSERT INTO staging_data VALUES (1, 'test');
-- Session 2
SELECT * FROM staging_data;
-- ERROR: relation "staging_data" does not existThis is by design. Temporary tables live in a special schema (pg_temp_N) that’s visible only to the session that created them. They are automatically dropped when the session ends.
If you need to share data between sessions, use a regular table or an unlogged table (which is faster but not crash-safe):
CREATE UNLOGGED TABLE staging_data (id INT, name TEXT);Note: Connection poolers like PgBouncer in transaction mode create a new server-side session for each transaction. Temporary tables created in one transaction won’t exist in the next, even if your application thinks it’s using the same connection. If you’re using PgBouncer, either switch to session mode or avoid temp tables entirely.
Fix 7: Check for Transaction Rollbacks and DDL Timing
PostgreSQL supports transactional DDL. This means CREATE TABLE can be rolled back:
BEGIN;
CREATE TABLE orders (id SERIAL PRIMARY KEY, total NUMERIC);
-- some error occurs
ROLLBACK;
-- The table was never committed
SELECT * FROM orders;
-- ERROR: relation "orders" does not existThis catches people off guard because MySQL and most other databases don’t support transactional DDL — once you CREATE TABLE, it exists even if you roll back.
Check if there’s an active transaction that might be holding uncommitted DDL:
SELECT pid, state, query
FROM pg_stat_activity
WHERE state = 'idle in transaction';If you see idle-in-transaction sessions, they might have created tables that haven’t been committed. Either commit them or investigate why they’re stuck. This is related to the kind of constraint and transaction issues described in Fix: PostgreSQL Duplicate Key Violates Unique Constraint.
Fix 8: Inspect pg_catalog for the Full Picture
When nothing else works, go straight to the source. PostgreSQL stores all metadata in the pg_catalog schema. Here’s a comprehensive query to find any object matching a name:
SELECT
n.nspname AS schema,
c.relname AS name,
CASE c.relkind
WHEN 'r' THEN 'table'
WHEN 'v' THEN 'view'
WHEN 'm' THEN 'materialized view'
WHEN 'S' THEN 'sequence'
WHEN 'i' THEN 'index'
WHEN 'f' THEN 'foreign table'
WHEN 'p' THEN 'partitioned table'
ELSE c.relkind::TEXT
END AS type
FROM pg_catalog.pg_class c
JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace
WHERE c.relname ILIKE '%users%'
ORDER BY n.nspname, c.relname;This searches across all schemas and shows exactly where every matching object lives. If the table exists, you’ll see its schema. If it doesn’t exist at all, the query returns nothing.
You can also list all tables in all schemas:
SELECT schemaname, tablename
FROM pg_tables
ORDER BY schemaname, tablename;Or use psql shortcuts:
\dt *.* -- all tables in all schemas
\dt myschema.* -- all tables in a specific schema
\dn -- list all schemasCommon Mistake: Using
\dtwithout a schema qualifier only shows tables in schemas listed in yoursearch_path. If your table is in a custom schema not insearch_path,\dtwon’t show it. Always use\dt *.*or querypg_tablesdirectly when debugging missing tables.
Fix 9: Handle Schema Permissions
Even if the table exists and the schema is in your search_path, you’ll get a relation does not exist error if your database role doesn’t have USAGE permission on the schema. PostgreSQL treats permission errors on schemas as “does not exist” to avoid leaking information about the database structure.
Check schema permissions:
SELECT nspname, nspacl
FROM pg_namespace
WHERE nspname = 'myapp';Grant the necessary permissions:
GRANT USAGE ON SCHEMA myapp TO myuser;
GRANT SELECT ON ALL TABLES IN SCHEMA myapp TO myuser;To also grant access to tables created in the future:
ALTER DEFAULT PRIVILEGES IN SCHEMA myapp
GRANT SELECT ON TABLES TO myuser;This is a subtle issue because the error message doesn’t mention permissions at all. If you’ve confirmed the table exists (by logging in as a superuser and running \dt myapp.*) but a specific role can’t see it, permissions are almost certainly the problem.
Still Not Working?
If none of the fixes above resolved the error, work through this checklist:
1. Confirm the exact table name:
SELECT tablename FROM pg_tables WHERE tablename ILIKE '%your_table%';Watch for typos, underscores vs. hyphens, and singular vs. plural names (user vs. users).
2. Check if the table was dropped:
If you have access to PostgreSQL logs, search for DROP TABLE statements. Someone might have dropped it accidentally.
3. Look for database restore issues:
After restoring from a backup with pg_restore, tables might end up in unexpected schemas. Run:
pg_restore --list backup.dump | grep "your_table"This shows which schema the backup expects the table in.
4. Check extension-created objects:
Some PostgreSQL extensions (like PostGIS, pg_trgm, or uuid-ossp) create objects in specific schemas. If you’re referencing an extension function or type, make sure the extension is installed:
SELECT * FROM pg_extension;And that its schema is in your search_path.
5. Docker and container environments:
If you’re running PostgreSQL in Docker, make sure you’re connecting to the right container and the right volume is mounted. Data stored in an ephemeral container disappears when the container restarts. This is similar to connectivity issues described in Fix: MongoDB connect ECONNREFUSED where containerized databases require explicit network configuration.
docker exec -it my-postgres psql -U myuser -d mydb -c "\dt"6. Connection pooler interference:
If you use PgBouncer or Pgpool-II, make sure the pooler is routing to the correct database. Some poolers have their own database mappings that might differ from what your application expects.
7. Check for schema migration conflicts:
In team environments, one developer might have run a migration that another developer’s branch depends on. Pull the latest code, check the migration history, and run any pending migrations:
-- Check the last applied migration (Django example)
SELECT * FROM django_migrations ORDER BY applied DESC LIMIT 5;If the table genuinely doesn’t exist and no migration creates it, you’ll need to create it manually or generate a new migration that includes it.
Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.
Was this article helpful?
Related Articles
Fix: MySQL ERROR 1064 (42000): You have an error in your SQL syntax
How to fix MySQL syntax error 1064 caused by typos, reserved words, wrong quotes, missing commas, version-incompatible syntax, and ORM-generated queries.
Fix: MySQL ERROR 1146 (42S02): Table 'database.table' doesn't exist
How to fix MySQL error 1146 Table doesn't exist caused by wrong database selection, case sensitivity issues, missing migrations, InnoDB tablespace corruption, and cross-OS imports.
Fix: PostgreSQL ERROR: deadlock detected
How to fix PostgreSQL deadlock detected error caused by concurrent transactions, conflicting row locks, foreign key locks, and lock ordering issues.
Fix: PostgreSQL permission denied for table (or relation, schema, sequence)
How to fix the PostgreSQL error 'permission denied for table' by granting privileges, fixing default permissions, resolving schema and ownership issues, RLS policies, and role inheritance.