SaaS architecture
column does not exist in PostgreSQL: causes, measured
PostgreSQL's 'column does not exist', measured on 12 to 18: capitals, double quotes, aliases in WHERE, UPDATE SET, renamed columns, psql's daticulocale.
ERROR: column "userid" does not exist (SQLSTATE 42703) means PostgreSQL could not find a column of that name in any table the statement refers to. When the column is plainly there, the usual causes are a column created with capital letters in double quotes and queried without them, a string written in double quotes ("bob" is a column name, 'bob' is a value), and a SELECT alias used in WHERE. A HINT line under the error often names the column PostgreSQL thinks you meant; read it first.
We reproduced each cause on PostgreSQL 17.11, most of them also on 12, 15 and 18, and the psql variant (column d.daticulocale does not exist) across client and server versions.
Why does PostgreSQL say a column does not exist?
| What the query did | Message (17.11) | HINT |
|---|---|---|
select userId from t, column created as "userId" | column "userid" does not exist | Perhaps you meant to reference the column "t.userId". |
where name = "bob" | column "bob" does not exist | none |
select emial from users | column "emial" does not exist | Perhaps you meant to reference the column "users.email". — only for small typos |
select price * qty as total … where total > 10 | column "total" does not exist | none |
… on o.user_id = u.user_id, where user_id is a column of o only | column u.user_id does not exist | Perhaps you meant to reference the column "o.user_id". — only when the two aliases are similar; customers.user_id against a table orders got no hint |
with c as (select id, name from users) select email from c | column "email" does not exist | none — the CTE (or subquery) did not carry the column along |
update users set users.name = 'x' | column "users" of relation "users" does not exist | 17 and 18 only: SET target columns cannot be qualified with the relation name. |
insert into users (emial) … | column "emial" of relation "users" does not exist | none |
select tenant_id from users, but the search path finds a different users | column "tenant_id" does not exist | none, and no table name |
Capitals. Unquoted names are folded to lower case: userId becomes userid. A column created as "userId" — by an ORM, or by a tool that quotes everything — only answers to "userId". The hint shows the real spelling as "t.userId"; type it as t."userId" or "userId" — pasting "t.userId" as one quoted name failed again, with the same hint. For a table with capitals the same applies to '"Users"'::regclass.
A column that is a reserved word behaves differently again: with a column "user", select user from r raised no error and returned current_user, and where user = 'bob' silently matched nothing; order gave a syntax error. r.user and r."order" worked.
Double quotes. In SQL, double quotes mark an identifier and single quotes a string. "bob" in a WHERE or in VALUES is a column called bob, which is why the message names your value as if it were a column.
The hint has limits. In our tests it appeared only for small differences: emial for email got it, emailxxxx (four characters off) did not, and neither did eml, two off in a three-letter word. The pattern held for all 26 names we tried — at most three characters different and at most half the length of what you typed — but we did not check it against the source code. In a qualified name, the difference between the aliases counted towards the three. When two columns were equally close, the hint named both; with three equally close, it named none.
Aliases. PostgreSQL accepts a SELECT alias only as a bare name in GROUP BY and ORDER BY: where total > 10, having total > 10 and order by total + 1 failed, group by total and order by total worked. If the alias has the name of a real column, WHERE silently uses the column: select price * qty as price from o where price > 10 filtered on price, not on the product. Repeat the expression in WHERE, or wrap the query in a subquery and filter outside. GROUP BY has the same preference for the real column, without an error; that case, and the error a column outside the GROUP BY gives, is in must appear in the GROUP BY clause.
UPDATE … SET table.column. The SET list takes bare column names; with the table name in front, the message names the table as if it were a column. Only 17 and 18 add a hint that says what is wrong.
The wrong table. With a public.users without tenant_id and an app.users with it, a role whose search path starts at public got column "tenant_id" does not exist — no hint, no table name. Check which table the name resolved to: select 'users'::regclass and \d users. Why a search path can find the wrong table, and how a missing USAGE makes a schema drop out of it, is in relation does not exist in PostgreSQL.
Why does psql say column d.daticulocale does not exist?
Because psql 15 and 16 ask for pg_database.daticulocale, a column PostgreSQL 17 renamed to datlocale. Exactly psql 15 and psql 16 against a PostgreSQL 17 or 18 server gave, on \l:
ERROR: column d.daticulocale does not exist
HINT: Perhaps you meant to reference the column "d.datlocale".
\d, \dt and \du worked in the same combinations, and psql 12, 13, 14, 17 and 18 worked against every server we tried — older clients too, so it is these two versions, not age as such. The fix is a psql at least as new as the server; the error says nothing about your database.
What does the HINT line say?
- A column with capitals, like
"t.userId"Quote the nameselect "userId" from t. - A similar nameA typo, or a renamed columnUse the column it names; check the table too if it is from the other side of a join.
- No hint, and the message says
of relation "users"AnINSERTcolumn list orUPDATE … SETDrop a table prefix inSET; compare the spelling with\d. - No hint, and the "column" is a value you wroteDouble quotes around a stringUse single quotes.
- No hint, the name is an alias from your
SELECTAlias used inWHEREorHAVINGRepeat the expression, or filter in an outer query. - No hint, the column really exists somewhereAnother table of the same nameCheck
'users'::regclassand the search path.
Each branch measured on PostgreSQL 17.11. For \l in psql, see the daticulocale section above.
What happens to views and functions after a column is renamed?
They differ. After alter table products rename column name to naam, a view on the table followed along and kept working, and so did a function with a SQL-standard body (begin atomic … end). A SQL function with a string body and a PL/pgSQL function kept the old name and failed when called, with column "name" does not exist and a hint naming the new column. They also differ at creation: a PL/pgSQL function that referred to a column that did not exist was created without complaint, while a string-body SQL function (with the default check_function_bodies = on; restores from pg_dump turn it off) and a begin atomic function (PostgreSQL 14 and later) failed at once. So the error can surface long after the migration, in the first request that calls the function.
How do you see the exact column names?
\d users shows the names but not whether they need quotes. This shows the spelling you have to type:
select attnum, quote_ident(attname) as write_as, format_type(atttypid, atttypmod) as type
from pg_attribute
where attrelid = 'users'::regclass and attnum > 0 and not attisdropped
order by attnum;
For the table with "userId" and "Name", write_as showed them with their quotes, and the lower-case columns without.
What we did not measure
How specific ORMs quote names. Generated columns and columns added by an extension. JSON keys: data.name gives a different error (missing FROM-clause entry for table "data"), and data->>name without quotes gives this one — the key needs single quotes, data->>'name'.
All measurements are our own, on PostgreSQL 17.11, 18.6, 15.19, 14.24 and 12.22 in containers, with psql 12 to 18 as clients (16 only as a client), on 22 October 2026.