不能直接用 delete,因逻辑删除需保留数据用于审计、回溯与对账;应改用 update 修改状态字段(如 is_deleted=1),并确保所有查询均过滤 is_deleted=0,配合索引、视图或orm自动注入条件,防止误查、性能下降及约束冲突。

为什么不能直接用 DELETE?
因为业务上常需要保留历史数据,比如订单、用户、日志等,删掉后审计、对账、回溯就没了。逻辑删除本质是把 DELETE 换成 UPDATE 一行标志字段(如 is_deleted 或 deleted_at),让后续查询默认过滤掉“已删”记录。
但直接在存储过程中写 UPDATE ... SET is_deleted = 1 不够——你得确保所有读操作也配合过滤,否则会漏查或误查。
存储过程里怎么安全地 UPDATE 标志位?
核心是:只改目标行、带校验、防重复删、留痕迹。常见做法:
- 用主键或唯一业务键(如
order_no)定位,避免WHERE status = 'pending'这类宽条件误更新 - 加
AND is_deleted = 0条件,防止重复执行导致数据状态混乱 - 更新时顺带写
deleted_at = NOW()和deleted_by = @user_id,便于追踪 - 用
ROW_COUNT()(MySQL)或@@ROWCOUNT(SQL Server)检查是否真改到了行,没改到就抛异常或返回错误码
示例(MySQL):
UPDATE orders SET is_deleted = 1, deleted_at = NOW(), deleted_by = @operator_id WHERE id = @order_id AND is_deleted = 0;
执行后立刻查 ROW_COUNT(),返回 0 就说明该记录不存在或已被删过。
查询时怎么自动忽略逻辑删除的数据?
不能靠开发人员每次手写 WHERE is_deleted = 0——容易遗漏。推荐两种方式:
-
视图封装:建一个
v_orders视图,SELECT * FROM orders WHERE is_deleted = 0,业务代码只查视图 -
应用层统一拦截:ORM(如 MyBatis Plus 的
@TableLogic、EF Core 的全局查询过滤器)自动注入条件;但存储过程本身不走 ORM,所以视图更可控 - 注意:如果已有大量旧 SQL 直接查表,改起来成本高,建议先加数据库级约束(如给
is_deleted加索引),再逐步迁移
容易被忽略的坑:软删后关联查询和索引失效
逻辑删除不是“删”,它让数据体积变大、查询条件变多,几个关键点必须处理:
-
is_deleted字段一定要加索引,尤其当表数据量 >10 万行时,否则WHERE is_deleted = 0会全表扫 - 外键关联(如查订单 + 订单项)时,
order_items表也要有is_deleted,且关联条件要显式带上,否则可能拉出“已删订单”的明细 - 唯一约束(如
UNIQUE(email))需改成函数索引或过滤索引(PostgreSQL/SQL Server 支持),否则逻辑删除后无法插入同邮箱新用户 - 分页排序时,
ORDER BY created_at DESC会把刚逻辑删除的记录排在前面——除非你明确想看到它们,否则加AND is_deleted = 0再排序
软删不是加个字段就完事,它是贯穿 DML、索引、约束、查询路径的一整套约定。最麻烦的往往不是怎么删,而是怎么让所有人(包括老代码、报表、DBA 脚本)都按这个约定走。










