17370845950

PostgreSQL GENERATED ALWAYS AS 列的计算列使用坑
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_TIMESTAMPRANDOM() 等 volatile 函数
  • 调用了自定义函数但没加 IMMUTABLE 属性
  • 用了 TO_CHAR(created_at, 'YYYY-MM') —— 表面看是确定的,但 TO_CHAR 默认是 STABLE,需显式 cast 或换函数

修复方式:

  • EXTRACT(YEAR FROM created_at) 替代 TO_CHAR(...)(前者是 IMM

    UTABLE)
  • 自定义函数必须声明为 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
  • 是否频繁 WHEREJOIN 这个列?加 STORED + 索引能显著提升性能
  • 写入是否远多于读取?慎用 STORED,避免写放大

一旦加了 STORED,后续改回非 STORED 无法直接 ALTER,只能 DROP COLUMN + ADD COLUMN 重建,数据会丢失(除非提前导出)。这点容易被忽略。