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....
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
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.
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);
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.
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.
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;
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
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.
SELECT "userId" FROM app_accounts LIMIT 1;
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
SELECT count(*) AS rows_for_user_42 FROM app_accounts WHERE user_id = 42;
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
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.
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.