数据库触发器不能直接发邮件,须通过异步解耦方式实现:MySQL禁用系统命令且存在事务风险;PostgreSQL推荐写入通知表再由外部进程消费;SQL Server的sp_send_dbmail不参与事务且易阻塞,官方亦不推荐;所有场景下触发器仅应做校验、维护字段或写日志,发邮件等操作必须移出事务边界。

数据库本身不能直接发邮件;所谓“触发器自动发邮件”,本质是把发信动作委托给外部服务或数据库内置扩展,且必须绕过事务一致性风险。
MySQL 触发器里调用 sys_exec 或 sys_eval 会失败
很多开发者看到社区示例就尝试在触发器里写:SELECT sys_exec('curl http://notify/api?event=order') ——这行不通。原因有三:
- MySQL 默认禁用所有系统命令执行函数,需手动编译启用
lib_mysqludf_sys,生产环境几乎没人开 - 即使启用,
sys_exec是异步非事务型操作:触发器所在事务回滚了,HTTP 请求早已发出,通知无法撤回 - 触发器执行期间不允许产生网络 I/O,多数 MySQL 配置会直接报错
ERROR 1370 (42000): execute command denied to user
PostgreSQL 的 dblink + 外部队列才是可行路径
真正能落地的方案,是让触发器只做一件事:往一张轻量通知表插入待办记录,再由独立进程轮询消费。例如:
CREATE TABLE notify_queue (
id SERIAL PRIMARY KEY,
event_type TEXT NOT NULL,
payload JSONB NOT NULL,
created_at TIMESTAMPTZ DEFAULT NOW(),
status TEXT DEFAULT 'pending' -- 'sent', 'failed'
);
<p>CREATE OR REPLACE FUNCTION queue_order_notification()
RETURNS TRIGGER AS $$
BEGIN
INSERT INTO notify_queue (event_type, payload)
VALUES ('new_order', jsonb_build_object(
'order_id', NEW.id,
'customer_id', NEW.customer_id,
'total', NEW.total
));
RETURN NEW;
END;
$$ LANGUAGE plpgsql;</p><p>CREATE TRIGGER trig_after_insert_order
AFTER INSERT ON orders
FOR EACH ROW
EXECUTE FUNCTION queue_order_notification();</p>
后续用 Python 脚本或 pg_cron 定时查 notify_queue WHERE status = 'pending',调用 SMTP 或企业微信 API 发送,成功后 UPDATE ... SET status = 'sent'。这样既解耦,又保事务安全。
SQL Server 的 sp_send_dbmail 看似直接,但有严重陷阱
SQL Server 提供 sp_send_dbmail,允许在存储过程甚至触发器中调用:
EXEC msdb.dbo.sp_send_dbmail @profile_name = 'default', @recipients = 'admin@example.com', @subject = 'New order arrived', @body = 'Order ID: ' + CAST(@order_id AS VARCHAR);
但要注意:
- 该过程本身不参与当前事务:哪怕你刚插入的订单因下游失败而回滚,邮件已发出去
- 它依赖 Database Mail 服务必须运行且配置正确,任意环节卡住(如 SMTP 认证失败)会导致触发器阻塞,拖慢整个
INSERT - 微软官方文档明确建议:“不要在触发器中调用
sp_send_dbmail”,推荐改用 Service Broker 异步投递
触发器里不该放任何业务逻辑,包括“调用存储过程”
常见错误是写这样的触发器:
CREATE TRIGGER trig_after_order
AFTER INSERT ON orders
FOR EACH ROW
BEGIN
CALL send_email_notification(NEW.id); -- ❌ 危险!
END;
问题不止于性能。一旦 send_email_notification 抛出异常,整个事务会中断,用户收到“插入失败”,但实际可能只是邮件发不出——这不是数据层该管的事。触发器只应做三件事:
- 基于
NEW/OLD做简单校验(比如库存不足拒绝插入) - 维护衍生字段(如更新
orders.updated_at) - 写入审计日志或事件队列表(如上文
notify_queue)
发邮件、调外部 API、生成 PDF 这类动作,必须移出事务边界。最容易被忽略的一点是:**触发器的执行时间算在客户端 SQL 响应延迟里**——你以为只是“加个通知”,其实悄悄把用户等待时间从 5ms 拉到了 800ms。










