PostgreSQL中GENERATED ALWAYS AS列禁止INSERT时显式赋值,建表要求表达式IMMUTABLE,生成列实时计算无缓存,STORED可索引但不可逆更改。

GENERATED ALWAYS AS 在 INSERT 时直接报错 “cannot insert into column”
PostgreSQL 不允许显式向 GENERATED ALWAYS AS 列写入值,哪怕你传了 NULL 或默认值。只要 INSERT 语句里列名列表中包含了该列,或者 VALUES 中对应位置给了值(哪怕为空),就会触发这个错误。
常见误操作:
- 用 ORM 自动生成的 INSERT 语句,没过滤掉计算列字段
- 从旧表 COPY 数据时,列名全写成
*或未排除计算列 - 使用
INSERT ... SELECT *且源表结构含同名列但非生成列
正确做法是:显式列出所有**非生成列**,确保 GENERATED ALWAYS AS 列不在列名列表中。例如:
INSERT INTO users (name, email, created_at) VALUES ('Alice', 'a@example.com', NOW());而不是:
INSERT INTO users (id, name, email, full_name, created_at) VALUES (DEFAULT, 'Alice', 'a@example.com', NULL, NOW()); -- ❌ id 和 full_name 都可能触发错误
函数体里调用 volatile 函数导致 CREATE TABLE 失败
GENERATED ALWAYS AS 要求表达式必须是 IMMUTABLE 或至少 STABLE,但 PostgreSQL 实际只接受 IMMUTABLE —— 否则建表会报错:ERROR: generation expression is not immutable。
典型踩坑点:
- 用了
NOW()、CURRENT_TIMESTAMP、RANDOM()等 volatile 函数 - 调用了自定义函数但没加
IMMUTABLE属性 - 用了
TO_CHAR(created_at, 'YYYY-MM')—— 表面看是确定的,但TO_CHAR默认是STABLE,需显式 cast 或换函数
修复方式:
- 用
EXTRACT(YEAR FROM created_at)替代TO_CHAR(...)(前者是 IMMUTABLE) - 自定义函数必须声明为
IMMUTABLE,且内部不能调用任何非 IMMUTABLE 函数 - 避免在生成表达式中引用序列、配置参数或 session 变量
UPDATE 时更新基础列但生成列没刷新
生成列的值在查询时实时计算,不是物理存储(除非加了 STORED),所以它“看起来没变”只是因为每次 SELECT 都重新算。但如果你发现 UPDATE 基础列后,SELECT 里生成列还是旧值,那大概率是你在用缓存层(比如应用层 ORM 缓存、连接池的 statement cache)或没重查。
注意点:
- PostgreSQL 本身不会缓存生成列结果;每次读都是实时计算
- 如果用了
pgbouncer在 transaction 模式下,且语句被重用,可能因 plan cache 导致结果延迟(少见但存在) - 某些 ORM(如 Django 的
update_fields)若没刷新实例,会继续显示旧对象属性,和数据库无关
验证是否真有问题?直接连 psql 执行:
UPDATE users SET name = 'Bob' WHERE id = 1;
SELECT id, name, full_name FROM users WHERE id = 1;
如果这里 full_name 是对 name 的拼接,它一定立刻更新 —— 否则说明表达式本身依赖了不可变字段或写错了逻辑。
想持久化又怕性能差,该不该加 STORED
不加 STORED:每次读都计算,节省空间,但增加 CPU 开销,尤其表达式复杂或高频查询时;加了 STORED:值物理写入磁盘,INSERT/UPDATE 时多一次计算+写入,但读快、可建索引、支持部分谓词下推。
选型关键判断点:
- 表达式是否简单?比如
first_name || ' ' || last_name—— 不加 STORED 通常够用 - 是否需要在该列上建索引?必须加
STORED,否则报错index on generated column must be stored - 是否频繁
WHERE或JOIN这个列?加 STORED + 索引能显著提升性能 - 写入是否远多于读取?慎用 STORED,避免写放大
一旦加了 STORED,后续改回非 STORED 无法直接 ALTER,只能 DROP COLUMN + ADD COLUMN 重建,数据会丢失(除非提前导出)。这点容易被忽略。










