Cookbook/ Schema & Constraints/ Column Does Not Exist Quoted Identifier...
Schema & Constraints · Runbook

Column Does Not Exist Quoted Identifier Case

A query that looks obviously correct returns 'column "<name>" does not exist', where the name in the message is the lowercased form of what you typed....

🧩 5 steps ⏱ ~10 min 📊 Intermediate 🐘 PostgreSQL 14--18 Last reviewed June 26, 2026

In 10 seconds

A new analytics query, written by hand against tables an ORM had created, fails immediately with 'ERROR: column "userid" does not exist' -- yet the column is plainly visible in the schema browser as userId. The tables were generated by a tool that double-quotes every identifier, so the columns are stored with their camelCase exactly: "userId", "createdAt". When the analyst references those columns without quotes, PostgreSQL folds the unquoted name to lowercase, looks for userid, and finds nothing. The data is untouched and reachable; the only problem is that an unquoted identifier and a quoted mixed-case identifier are two different names.

Why This Happens

PostgreSQL follows the SQL standard for identifiers: an unquoted identifier is folded to lowercase before lookup, while a double-quoted identifier is taken verbatim, preserving case. The columns here were created as "userId" and "createdAt" (quoted), so their real names contain uppercase letters. Writing userId without quotes asks PostgreSQL to find userid, which does not exist, so it raises 42703.

The HINT's fuzzy match surfaces the close camelCase name. This is purely a naming-resolution issue -- the rows, types, and indexes are all intact. There are two correct responses: quote the identifier everywhere it is used (immediate, but fragile because every reference must remember the quotes), or rename the columns to lower_snake_case so that an unquoted reference resolves naturally and quoting is never needed again. There are two correct responses: quote the identifier everywhere it is used (immediate, but fragile because every reference must remember the quotes), or rename the columns to lower_snake_case so that an unquoted reference resolves naturally and quoting is never needed again.

DIAGNOSE

Diagnose

Free Tier -- Basic Approach

First, see the columns as they are actually stored and count the ones that are not all-lowercase. Those are precisely the names that must be quoted to be referenced -- the source of the error.

SQL
SELECT column_name FROM information_schema.columns WHERE table_name = 'app_accounts' ORDER BY ordinal_position;
SELECT count(*) AS mixed_case_columns
FROM information_schema.columns
WHERE table_name = 'app_accounts' AND column_name <> lower(column_name);
OUTPUT -- captured on real PostgreSQL
column_name
-------------
 id
 userId
 createdAt
 status
(4 rows)

 mixed_case_columns
--------------------
                  2
(1 row)

List information_schema.columns for the table (id, userId, createdAt, status), then count names where column_name <> lower(column_name); here mixed_case_columns = 2.

This is a Pro lesson

Get every Learning Pathway and cookbook recipe -- grounded in PostgreSQL source code, with diagnostics, fixes, and prevention for each topic.

Continue this lesson to learn:

  • Advanced Approach -- Production-Safe
  • All 36 Learning Pathway lessons
  • 170+ cookbook recipes
  • Source-grounded diagnostics & fixes

Secure checkout Cancel anytime Source-grounded

FIX

Fix

Remove the need for quoting. Rename the camelCase columns to lower_snake_case so an unquoted reference resolves naturally. This is the literal capture; the quick unblock if a rename is not yet possible is simply to quote the names.

SQL
ALTER TABLE app_accounts RENAME COLUMN "userId" TO user_id;
ALTER TABLE app_accounts RENAME COLUMN "createdAt" TO created_at;
SELECT 'unquoted_ok' AS result, id, user_id, status FROM app_accounts WHERE user_id = 42;
OUTPUT -- captured on real PostgreSQL
ALTER TABLE
ALTER TABLE
   result    | id | user_id | status
-------------+----+---------+--------
 unquoted_ok | 42 |      42 | active
(1 row)

ALTER TABLE app_accounts RENAME COLUMN "userId" TO user_id; RENAME COLUMN "createdAt" TO created_at; then SELECT ... WHERE user_id = 42 returns the row tagged unquoted_ok.

VERIFY

Verify

Confirm the healthy state: the old camelCase name no longer exists at all -- even the quoted spelling now errors (the expected proof the rename took effect) -- and the unquoted query returns user 42.

SQL
SELECT "userId" FROM app_accounts LIMIT 1;
OUTPUT -- captured on real PostgreSQL
ERROR:  42703: column "userId" does not exist
LINE 1: SELECT "userId" FROM app_accounts LIMIT 1;
               ^
HINT:  Perhaps you meant to reference the column "app_accounts.user_id".
LOCATION:  errorMissingColumn, parse_relation.c:3729
SQL
SELECT count(*) AS rows_for_user_42 FROM app_accounts WHERE user_id = 42;
OUTPUT -- captured on real PostgreSQL
rows_for_user_42
------------------
                1
(1 row)

SELECT "userId" FROM app_accounts now fails with 42703 and a HINT pointing at user_id (expected); then count(*) WHERE user_id = 42 returns 1.

⏪ Rollback

All work happens in a throwaway database on a disposable test environment, so cleanup is just dropping that database; the persistent demo containers are never touched. The failing SELECT changed nothing, so there is nothing to undo from the break itself. The fix used here is ALTER TABLE ... RENAME COLUMN, which is a fast catalog-only change (no table rewrite, no data movement) and is fully reversible -- you can rename back to the quoted camelCase form if some quoting-aware client still depends on it. Renaming a column does, however, break code that hard-codes the old name, so coordinate the rename with the application's references (the same code that previously relied on quoting).

📦 Version Matrix (PG 14--18)

Run the same break-and-fix on PostgreSQL 14, 15, 16, 17, and 18: an unquoted reference to a quoted camelCase column raises the identical 'column "userid" does not exist' on every version, and after renaming to lower_snake_case the unquoted query resolves identically. Identifier case-folding is uniform across all supported releases.

Version Behavior Version Notes
PG 14.23 broken=column "userid" does not exist ✗ Not available -- use stats_reset age
PG 15.18 broken=column "userid" does not exist ✗ Not available -- use stats_reset age
PG 16.14 broken=column "userid" does not exist ✗ Not available -- use stats_reset age
PG 17.10 broken=column "userid" does not exist ✗ Not available -- use stats_reset age
PG 18.4 broken=column "userid" does not exist ✗ Not available -- use stats_reset age

Common Mistakes

Adding quotes in only some places: if one reference uses "userId" and another uses userId, the second still folds to userid and fails. Quoting must be consistent across every statement that touches the column.
Assuming the column is missing and re-creating it: the column exists under its mixed-case name. Adding a new userid column would leave you with two columns and split the data.
Renaming a column without updating the application: ALTER ... RENAME COLUMN is instant, but any code (including the ORM mappings that created the quoted name) that still references the old spelling will then break.
Trusting that an unquoted UPPERCASE reference matches a quoted UPPERCASE column: USERID, UserId, and userId all fold to userid when unquoted, so none of them match a column stored as "userId".
Letting the ORM keep generating quoted camelCase columns: the problem recurs on every new table. Configure the naming strategy to emit lower_snake_case so the database side never needs quotes.
🔒 Pro · Prevent

Stop this incident before it pages you

The free runbook clears today's failure. Pro adds the standing guardrails -- the alert query, the dashboard, and the managed-service knobs -- so it never becomes an incident again.

  • A ready-to-paste alert query with the right thresholds.
  • The topology change that prevents recurrence.
  • The exact knobs on RDS, Azure Flexible Server & Cloud SQL.
Unlock Pro$24.99/mo · $199/yr

Frequently Asked Questions

Why does the error say userid when I clearly typed userId?

An unquoted identifier is case-folded to lowercase before PostgreSQL looks it up, so userId, UserId, and USERID all become userid. The column was created quoted as "userId", which keeps its uppercase, so the lowercased lookup does not match and you get 42703.

Can I just always quote the columns instead of renaming?

Yes, that works: "userId" matches the stored name exactly. The downside is that every reference -- in every query, view, function, and client -- must remember the quotes, and one unquoted slip fails again. Renaming to lower_snake_case removes that ongoing burden.

Will RENAME COLUMN rewrite the table or lock it for long?

No. RENAME COLUMN only updates the catalog; it does not touch table data, so it is effectively instant. It does take a brief exclusive lock on the table to update the catalog, but there is no rewrite. Application code that names the old column must be updated to match.

How do I find every column that will have this problem?

Query the catalog for column names that are not all-lowercase: SELECT attname FROM pg_attribute WHERE attrelid = 'app_accounts'::regclass AND attnum > 0 AND NOT attisdropped AND attname <> lower(attname). Any row returned is a column that must be quoted to reference, which is your rename candidate list.

References

Keep going

Related & next steps