你想在终端里查一下某张表的数据,敲了 select * from Article,PostgreSQL 回你一句 relation "article" does not exist。你去数据库里看,表明明在。
现象
在命令行里查询某张表报错:
ERROR: relation "article" does not exist
LINE 1: select * from Article;
^
但用图形化工具打开数据库,表实实在在就在那里。而且应用跑起来读写这张表完全正常——只有你手敲 SQL 的时候不行。
这个"工具能用、命令行不行"的对比是关键线索,但很多人第一反应是"表被删了"或者"连错库了"。
有一个细节能立刻帮你排除"连错库":报错信息里那个箭头 ^ 指向的是你写的表名,而且它把折叠成小写之后的 article 显示了出来——如果只是连错库,报错通常不会这样"复述"你的表名。看到 relation "xxx" does not exist 且 xxx 恰好是你的表名的小写形式,就该往大小写方向想了。
根因
PostgreSQL 对标识符(表名、列名)的大小写处理规则是特殊的。
PostgreSQL 在解析时会把未加引号的标识符折叠成小写。所以 select * from Article 实际等价于 select * from article——它找的是小写的 article。
而你的表是 ORM 创建的,ORM 的模型名就是表名:模型叫 Article,创建出来的表名就是加了引号的 "Article"——这是一个大小写敏感的、真的叫大写的表。
于是:表叫 "Article"(大写),你查的是 article(小写),两者不匹配,就报"不存在"。
为什么图形化工具和 ORM 都没问题? 因为它们读取了表结构元数据后,会自动给标识符加上引号,写成 "Article"。而命令行是手写的,没人替你加。
这也解释了错误信息为什么看起来像"表不存在"——PostgreSQL 报的是"关系找不到",但真实原因是名字的大小写不匹配。
要把机制理解得更透一点:这个折叠规则来自 SQL 标准里"标识符大小写不敏感"的约定,PostgreSQL 选择的方向是"折叠到小写"。而双引号的作用就是关闭折叠,让标识符按字面意思理解——所以 "Article" 是大小写敏感的字面名,Article 是折叠成小写的名字,两者是不同的标识符。
解决
在命令行里用双引号写表名。 双引号的含义是"这是一个大小写敏感的字面标识符,不要折叠":
select * from "Article";
select * from "Article" where id = 1;
同理,列名如果也是大写(ORM 里模型字段常是驼峰),也要加引号:
select "createdAt", "updatedAt" from "Article";
反过来,如果表是小写的,就别加引号:
select * from article; -- 小写表,不需要引号
不确定表名到底是什么大小写时,查一下元数据:
-- 列出所有表,注意表名会原样显示(含大小写)
\dt
-- 或者直接查系统表
select tablename from pg_tables where schemaname = 'public';
-- 查询本身就是大小写敏感的,要精确匹配
select tablename from pg_tables where tablename = 'Article';
\dt 的输出最能说明问题:它会用双引号把需要大小写敏感的表名标出来,一眼就能看出到底是 article 还是 "Article"。
补一个小细节:这里的引号必须是英文半角双引号。在中文输入法下很容易敲出全角的 “Article”,那会被当成普通字符,起不到关闭折叠的作用,报错依旧。
延伸与预防
这条规律的推广是:PostgreSQL 里大写表名必须加引号,而多数工具会替你加,命令行不会。
写脚本或直接用 SQL 时,遇到"表不存在",先怀疑大小写,而不是先怀疑数据被删了。
根治的办法:如果不是历史包袱,建议在 ORM 层配置"表名全小写":
model Article {
id Int @id
title String
@@map("article") // 数据库里实际建小写的表
}
这样表名在任何客户端都不需要引号,彻底避开这个坑。代价是模型名和表名不一致,需要在 ORM 里显式映射——但这是一次性的成本,换来的是后续所有手写 SQL 都不踩坑。
同样的规则也适用于列名,以及 MySQL 在某些配置下的表名大小写敏感性(虽然 MySQL 默认在 Windows 上不区分、在 Linux 上区分,行为更不一致,坑更深)。跨数据库项目里,统一用全小写加下划线的命名是最省心的选择。