mysql中created_at仅靠default current_timestamp无法100%保证非空,显式传null会绕过;需用before insert触发器在new.created_at is null时设为now();postgresql同理但语法不同,且主从时间可能不一致。

MySQL 中 created_at 字段不能只靠 DEFAULT CURRENT_TIMESTAMP?
能,但仅限第一次插入;如果业务逻辑里手动 INSERT 时显式传了 NULL 或空字符串,DEFAULT 就被绕过了。触发器是唯一能「强制拦截并补全」的手段。
常见错误现象:INSERT INTO user (name, created_at) VALUES ('Alice', NULL) 导致该字段存入 NULL,后续查 created_at IS NOT NULL 就漏数据。
- 使用场景:需要 100% 保证时间戳不为空,且不允许应用层写死或 ORM 自动填充(比如直连数据库跑脚本、旧系统迁移)
-
DEFAULT CURRENT_TIMESTAMP只在列值完全未指定时生效,显式传NULL不触发 - 触发器优先级高于默认值,能覆盖所有 INSERT 路径
写 BEFORE INSERT 触发器时必须检查 NEW.created_at 是否为 NULL
不检查就直接赋值,会把用户本来想存的合法时间(比如导入历史数据时指定的创建时间)给覆盖掉。
正确做法是只在字段为空时才注入:
CREATE TRIGGER set_created_at_before_insert
BEFORE INSERT ON user
FOR EACH ROW
BEGIN
IF NEW.created_at IS NULL THEN
SET NEW.created_at = NOW();
END IF;
END;
- 必须用
BEFORE INSERT,AFTER已无法修改NEW值 - 判断条件只能是
IS NULL,不能用= ''或='' OR IS NULL—— 时间类型和空字符串比较会隐式转成 0,引发意外赋值 - 用
NOW()而非CURRENT_TIMESTAMP(),二者行为一致,但前者更直观,避免和列定义里的默认值关键字混淆
PostgreSQL 怎么做等效处理?别用 DEFAULT 就完事
PostgreSQL 的 DEFAULT CURRENT_TIMESTAMP 同样会被显式 NULL 绕过,但触发器写法不同:它没有 NEW 的直接赋值语法,得用 EXECUTE 或函数封装。
最简方案是建一个触发器函数:
CREATE OR REPLACE FUNCTION set_created_at()
RETURNS TRIGGER AS $$
BEGIN
IF NEW.created_at IS NULL THEN
NEW.created_at := NOW();
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
再绑定触发器:
CREATE TRIGGER set_created_at_before_insert BEFORE INSERT ON user FOR EACH ROW EXECUTE FUNCTION set_created_at();
- PostgreSQL 必须显式
RETURN NEW,否则插入失败 - 函数语言必须是
plpgsql,sql类型不支持变量赋值 - 触发器名和函数名可以同名,但不要省略
EXECUTE FUNCTION中的括号,否则 PostgreSQL 14+ 会报错
触发器可能拖慢批量插入,特别是没加索引的 created_at 字段
触发器本身开销极小,但如果你在 created_at 上建了普通 B-Tree 索引(比如为了按时间排序查最新记录),每次插入都会触发索引更新 —— 百万级批量导入时,这比触发器逻辑本身更耗时。
- 性能影响主要来自索引维护,不是触发器执行;可先
DROP INDEX,导入完再重建 - 如果只是用来过滤(如
WHERE created_at > '2024-01-01'),考虑用分区表替代单字段索引 - MySQL 8.0+ 和 PostgreSQL 12+ 支持函数索引,但
NOW()是 volatile 函数,无法用于函数索引,别试
真正容易被忽略的是:触发器不会自动同步到从库的延迟复制场景——主库执行成功,但从库重放时若时间不同步,NOW() 会取从库本地时间,导致主从 created_at 不一致。需要严格时间一致的话,得在应用层生成时间戳传入。










