触发器必须挂在 users 表上;需先创建返回 TRIGGER 类型的函数,再创建 BEFORE UPDATE 触发器,用 NEW.updated_at := NOW() 赋值并 RETURN NEW,避免事务控制和时区混乱。

触发器该挂在哪张表上?
必须明确:触发器属于表级对象,不是数据库或模式级别的。想让 users 表的 updated_at 字段自动更新,就得在 users 上建触发器,不能挂在别的表上,也不能“全局生效”。
常见错误是误以为建一次就能覆盖所有表——PostgreSQL 不支持跨表触发逻辑,每张需要自动时间戳的表都得单独定义触发器和对应的函数。
为什么必须先写函数再建触发器?
PostgreSQL 要求触发器必须关联一个已存在的函数,且该函数返回类型必须是 TRIGGER。直接写内联逻辑不被允许,也没有 BEFORE UPDATE SET updated_at = NOW() 这种语法。
实操建议:
- 函数名建议带表名前缀,比如
update_updated_at_column_users,避免后续维护时混淆 - 函数体里用
NEW.updated_at := NOW()(注意是:=赋值,不是=) - 函数最后必须有
RETURN NEW,否则UPDATE会把整行变成 NULL - 不要在函数里加
COMMIT或ROLLBACK—— 触发器运行在父事务中,手动控制事务会报错ERROR: cannot commit while a trigger is running
BEFORE UPDATE 还是 AFTER UPDATE?
必须用 BEFORE UPDATE。因为只有在这个时机,你才能修改即将写入的 NEW 行记录;如果用 AFTER,数据已经落盘,再改就无效了。
另外注意:
-
BEFORE UPDATE触发器对INSERT无影响,所以created_at需要另配一个BEFORE INSERT触发器,或直接设默认值DEFAULT NOW() - 如果同时需要处理
INSERT和UPDATE,别偷懒合并在一个触发器里——PostgreSQL 的触发器事件类型是互斥的,INSERT OR UPDATE是合法语法,但函数内部仍需用IF TG_OP = 'UPDATE' THEN ...分支判断 - 触发器激活条件可以加
WHEN (OLD.* IS DISTINCT FROM NEW.*)避免空更新也触发,但要注意 JSON/BLOB 类型字段可能让这个判断失效
时间戳字段类型选 TIMESTAMP WITHOUT TIME ZONE 还是 TIMESTAMP WITH TIME ZONE?
推荐统一用 TIMESTAMP WITH TIME ZONE(即 TIMESTAMPTZ),尤其当应用部署在多时区或使用连接池(如 PgBouncer)时。
原因很实际:
-
NOW()返回的是带时区的时间戳,存进WITHOUT TIME ZONE字段会隐式丢掉时区信息,后续按本地时区解析可能错乱 - 如果客户端连接设置了
timezone = 'UTC',而服务端是'Asia/Shanghai',用WITHOUT TIME ZONE会导致同一行在不同会话里显示不同时间 - 迁移成本低:现有字段可直接
ALTER COLUMN updated_at TYPE TIMESTAMPTZ USING updated_at AT TIME ZONE 'UTC'(按你原始时区调整)
真正容易被忽略的是:触发器函数里调用 NOW() 的行为依赖于当前会话的 timezone 设置。如果没统一配置,凌晨批量任务跑出来的时间戳可能跨天。

















