软删除需用BEFORE DELETE触发器介入,PostgreSQL可返回NULL跳过删除,MySQL需AFTER补回或禁用DELETE权限;须用deleted_at字段、视图封装、部分索引及手动处理外键级联,并为运维提供hard_delete专用函数。

触发器里不能直接拦截 DELETE,得用 BEFORE DELETE 捕获并改写行为
SQL 标准中没有“拦截并取消 DELETE”的语法,BEFORE DELETE 触发器是唯一能介入删除流程的时机。此时你无法阻止语句执行(除非抛异常),但可以修改行数据、跳过物理删除——关键是:在触发器里执行 UPDATE 并 ROLLBACK 原始 DELETE。不过注意,MySQL 不允许在触发器里显式 ROLLBACK,PostgreSQL 和 SQL Server 支持更灵活的控制。
实操建议:
- PostgreSQL 中可在
BEFORE DELETE触发器函数里返回NULL,这会跳过该行的物理删除 - MySQL 必须用
AFTER DELETE补回数据,但存在竞态风险;更稳妥的是禁用DELETE权限,只开放自定义存储过程 - 字段设计上,软删除标记建议用
deleted_at TIMESTAMP NULL而非is_deleted TINYINT,便于查“何时删的”,也避免布尔值被误判为未初始化
WHERE 条件漏判 deleted_at 是最常见查询遗漏点
启用软删除后,所有 SELECT、UPDATE、DELETE(如果还保留)都必须默认过滤已逻辑删除的记录。一旦漏加 WHERE deleted_at IS NULL,就会读到脏数据或覆盖错误状态。
实操建议:
- 不要依赖应用层拼 WHERE —— 容易漏,尤其 ORM 自动关联查询时
- 用视图封装基础查询:
CREATE VIEW user_active AS SELECT * FROM users WHERE deleted_at IS NULL - 对关键表建索引:
CREATE INDEX idx_users_not_deleted ON users(deleted_at) WHERE deleted_at IS NULL(PostgreSQL 支持部分索引,MySQL 8.0+ 可用函数索引模拟)
外键级联删除会绕过触发器,必须手动处理关联软删除
如果订单表 orders 外键关联用户表 users,且设置了 ON DELETE CASCADE,那么当用户被 DELETE 时,数据库会自动删掉其订单——这个过程完全不经过你的软删除触发器。
实操建议:
- 禁用所有
CASCADE删除,改用SET NULL或RESTRICT - 在主表触发器里手动
UPDATE关联表的deleted_at字段(需确保关联字段可为空或有对应软删除字段) - 对多层关系(如订单→订单项→商品),触发器里递归更新不现实,应改用应用层事务 + 批量
UPDATE控制
硬删除需求仍存在,得提供 bypass 权限或专用命令
运维清理历史数据、迁移修复、GDPR 彻底擦除等场景,真需要物理删除。若所有 DELETE 都被触发器劫持,这类操作就卡死。
实操建议:
- 不靠注释或临时关触发器(易遗忘、无审计),而是建专用函数:
PERFORM hard_delete_user(123),内部用EXECUTE 'DELETE FROM users WHERE id = $1'绕过触发器 - 给 DBA 单独角色,授予对原表的
DELETE权限,但不授予应用账号 - 在触发器开头检查
CURRENT_ROLE或自定义 session 变量(如 PostgreSQL 的current_setting('app.bypass_soft_delete', true)),满足条件则直接 RETURN
软删除看着简单,真正落地时,触发器只是起点;查询一致性、外键行为、权限分层、运维出口,每个环节漏一点,数据状态就不可信。最常被忽略的是:没把软删除逻辑下沉到数据库连接池或 ORM 的默认查询策略里,结果上线后才发现一半接口返回了已删数据。

















