能,但需按数据库差异处理:oracle用sequence.nextval配合before each row;postgresql用nextval()原子生成;mysql须用独立计数表+on duplicate key update模拟序列,禁用max()+1或last_insert_id()拼接。

触发器里不能用 NEXTVAL 直接拼流水号?
很多刚写触发器的人会直接在 BEFORE INSERT 里写 NEW.order_no := 'ORD' || seq_order.NEXTVAL,结果报错:ORA-04091: table is mutating(Oracle)或 MySQL 报错 Can't update table in stored function/trigger。这不是语法错,是数据库引擎限制:触发器执行时表正被修改,不能再查/改同表或依赖序列的当前值(尤其 PostgreSQL 的 NEXTVAL 在某些隔离级别下也不安全)。
真正能用的方案只有两种:
- Oracle:用
SEQUENCE.NEXTVAL是允许的,但必须确保触发器是BEFORE EACH ROW,且序列未被其他事务长时间持有锁 - MySQL / PostgreSQL:不能依赖
NEXTVAL或AUTO_INCREMENT当前值拼前缀,得先取序列值再拼,或改用应用层生成
MySQL 触发器拼流水号的正确写法
MySQL 没有原生序列对象,靠 AUTO_INCREMENT 主键 + 函数拼接最稳妥。假设表 orders 主键是 id,想生成形如 ORD202405200001(日期+当日序号)的流水号,不能用 LAST_INSERT_ID(),因为它是会话级的、不可靠。
推荐做法:用独立计数表 + INSERT ... ON DUPLICATE KEY UPDATE 模拟序列:
CREATE TABLE order_seq ( date_key CHAR(8) PRIMARY KEY, counter INT DEFAULT 0 );
触发器里这样写:
DELIMITER $$
CREATE TRIGGER tr_gen_order_no
BEFORE INSERT ON orders
FOR EACH ROW
BEGIN
DECLARE today CHAR(8);
SET today = DATE_FORMAT(NOW(), '%Y%m%d');
INSERT INTO order_seq (date_key, counter)
VALUES (today, 1)
ON DUPLICATE KEY UPDATE counter = counter + 1;
SELECT CONCAT('ORD', today, LPAD(counter, 4, '0'))
INTO NEW.order_no
FROM order_seq
WHERE date_key = today;
END$$
DELIMITER ;
注意:LPAD(counter, 4, '0') 控制序号位数;ON DUPLICATE KEY UPDATE 保证并发安全;但该方案在高并发下仍可能因间隙锁导致短暂阻塞。
PostgreSQL 中用 nextval() 和 to_char() 组合
PostgreSQL 支持序列函数,但直接在触发器里调 nextval('seq') 是安全的,只要不同时读写同一行。典型错误是试图用 CURRENT_DATE 拼日期段再查当日最大值——这会引发竞态条件。
更稳的做法:序列值全局唯一,用函数格式化:
CREATE SEQUENCE order_seq START 1;
触发器:
CREATE OR REPLACE FUNCTION gen_order_no()
RETURNS TRIGGER AS $$
BEGIN
NEW.order_no := 'ORD' || TO_CHAR(CURRENT_DATE, 'YYYYMMDD')
|| LPAD(nextval('order_seq')::TEXT, 6, '0');
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
绑定触发器:
CREATE TRIGGER tr_order_no BEFORE INSERT ON orders FOR EACH ROW EXECUTE FUNCTION gen_order_no();
关键点:nextval() 是原子操作,不会重复;TO_CHAR(CURRENT_DATE, 'YYYYMMDD') 稳定;LPAD(..., 6, '0') 补零长度固定。缺点是流水号不按日重置,如果业务强制要求“每日从 0001 开始”,就得用带日期的复合序列或额外计数表。
为什么别在触发器里查 MAX(order_no)?
这是最常见也最危险的写法:先 SELECT MAX(order_no) FROM orders WHERE order_no LIKE 'ORD20240520%',再截取数字+1。问题在于:
- 并发插入时两个事务查到同一个最大值,生成相同流水号
- 即使加
SELECT ... FOR UPDATE,也会严重拖慢性能,锁表时间变长 - 索引无法高效支持
LIKE 'ORD20240520%'前缀查询,全表扫描风险高 - 一旦某天没单,第二天查不到记录,逻辑崩掉
真要按日重置序号,宁可用独立计数表或应用层缓存当天计数,也不要让数据库承担这种脆弱逻辑。
流水号本质是业务标识,不是主键;生成逻辑越简单、越少依赖运行时状态,越不容易出错。触发器只是搬运工,不是调度中心。











