软删除应使用 BEFORE DELETE 触发器中 UPDATE is_deleted 字段(如设为 1),必须用 OLD 引用被删行、加 WHERE 精确匹配主键(如 id = OLD.id),避免误更新;MySQL 5.7+ 允许同表 UPDATE,PostgreSQL 需 RETURN NULL 阻止物理删除。

触发器里怎么自动更新 is_deleted 字段
软删除不是真删数据,而是把 is_deleted(或类似字段)设为 1 或 TRUE。触发器要在 DELETE 语句执行时拦截并转成 UPDATE。关键点是:不能在 BEFORE DELETE 触发器里直接 DELETE FROM,否则会递归或报错;必须用 UPDATE 替代原操作。
常见错误是写成 DELETE 后再手动 UPDATE,结果行被删了,UPDATE 找不到目标行。正确做法是在 BEFORE DELETE 中取消删除动作,并改写逻辑:
- MySQL / PostgreSQL 需用
NEW(PostgreSQL)或OLD(MySQL)引用即将被删的行 - PostgreSQL 支持
BEFORE DELETE中RETURN NULL阻止删除,再显式UPDATE - MySQL 不允许在触发器中修改触发表本身,所以必须用
BEFORE DELETE+UPDATE同表(5.7+ 允许,但需确保条件精准匹配OLD.id)
MySQL 的 BEFORE DELETE 触发器怎么写才不报错
MySQL 对“触发表自修改”限制严格,BEFORE DELETE 中直接 UPDATE 当前表默认报错 Can't update table 'xxx' in stored function/trigger。绕过方式是确保 UPDATE 条件只命中当前行,且避免子查询引用本表:
CREATE TRIGGER user_soft_delete BEFORE DELETE ON users FOR EACH ROW UPDATE users SET is_deleted = 1, deleted_at = NOW() WHERE id = OLD.id;
注意三点:
- 必须用
OLD.id而非id = id,否则可能误更新多行 -
deleted_at字段要提前建好,类型为DATETIME或TIMESTAMP - 如果表有主键自增但无
id字段(比如用uuid),WHERE 条件得换成uuid = OLD.uuid
PostgreSQL 的 BEFORE DELETE 触发器为什么总返回空结果
PostgreSQL 触发器函数必须显式返回 NULL(取消原 DELETE)或 NEW(仅对 INSERT/UPDATE 有效)。若函数末尾没 RETURN NULL,DELETE 仍会执行,导致数据真删、标记失效。
典型写法:
CREATE OR REPLACE FUNCTION soft_delete_user() RETURNS TRIGGER AS $$ BEGIN UPDATE users SET is_deleted = true, deleted_at = NOW() WHERE id = OLD.id; RETURN NULL; -- 关键!阻止原 DELETE END; $$ LANGUAGE plpgsql;
容易漏掉的点:
- 函数返回类型必须是
TRIGGER,且调用时绑定FOR EACH ROW - 若忘记
RETURN NULL,日志里看不到错误,但数据就没了 - UPDATE 语句没加
WHERE?会把整张表标为已删除
软删除后查询为什么还查不到数据
触发器只管“删”的动作转化,不管“查”。所有 SELECT 必须显式过滤 is_deleted = 0,否则逻辑上已删的数据仍会出现在结果里。这不是触发器能解决的,是应用层契约。
建议统一处理方式:
- 在视图里封装:如
CREATE VIEW active_users AS SELECT * FROM users WHERE is_deleted = 0 - ORM 层全局加默认 scope(如 Laravel 的
softDeletes()、Django 的QuerySet.filter(is_deleted=False)) - 绝不能依赖触发器让 SELECT 自动忽略已删行——SQL 标准不支持这种“透明过滤”
最常被忽略的是:触发器生效后,旧业务代码里的 SELECT * FROM users 会突然暴露出大量“已删”脏数据,必须全量检查查询语句。

















