LEFT JOIN时软删除字段必须写在ON子句中(如p.deleted_at IS NULL),而非WHERE,否则会过滤左表行、破坏LEFT JOIN语义;每层关联右表均需显式排除已删记录,且应建联合索引保障性能。

LEFT JOIN 时软删除字段没过滤,关联结果不准确
软删除(比如 is_deleted = 1 或 deleted_at IS NOT NULL)只标记逻辑删除,数据仍存在表中。但用 LEFT JOIN 关联时,如果被关联表(右表)有软删除记录,它们仍会参与连接,导致返回不该出现的“已删除”关联行 —— 这不是你想要的“有效关联”,而是脏数据泄露。
常见错误写法:
SELECT u.name, p.title<br>FROM users u<br>LEFT JOIN posts p ON u.id = p.user_id;
即使
posts 表里有 deleted_at IS NOT NULL 的记录,它们照样被连出来。
- 正确做法是把软删除条件写进
ON子句,而不是WHERE -
ON中加条件能控制“哪些右表行允许被连上”,WHERE则是连接完再过滤,会把整行(含左表)干掉,破坏LEFT JOIN的语义 - 若右表多条件(如状态 + 软删除),全部塞进
ON,用AND连接
ON 子句里怎么写软删除条件才安全
取决于你的软删除字段定义方式,关键是要让“未删除”成为连接成立的前提之一。不能只写 deleted_at IS NULL 就完事,得确认字段类型和默认值是否可靠。
- 如果用
TINYINT(1)字段如is_deleted,推荐写is_deleted = 0(而非!= 1,避免 NULL 值干扰) - 如果用
DATETIME字段如deleted_at,必须写deleted_at IS NULL;写deleted_at = NULL是错的(SQL 中 NULL 不等于任何值) - 如果字段允许 NULL 且业务上“NULL = 未删除”,那没问题;但如果历史数据有
deleted_at = '0000-00-00 00:00:00',就得额外加判断
示例(MySQL):
SELECT u.name, p.title<br>FROM users u<br>LEFT JOIN posts p ON u.id = p.user_id AND p.deleted_at IS NULL;
LEFT JOIN 多层关联时软删除容易漏掉一层
比如查用户 → 文章 → 分类,其中 categories 表也有 is_active = 0 标记。很多人只给 posts 加过滤,忘了 categories 也要控。
- 每层
LEFT JOIN的右表,只要带软删除,就必须在对应ON里显式排除 - 漏掉一层,就可能让“已停用分类”的文章显示成“正常分类”,前端看到的就是错的归属
- 别试图在最后用
WHERE统一过滤:那样会把整个用户行干掉(哪怕他还有其他有效文章)
三层写法示意:
SELECT u.name, p.title, c.name AS category_name<br>FROM users u<br>LEFT JOIN posts p ON u.id = p.user_id AND p.deleted_at IS NULL<br>LEFT JOIN categories c ON p.category_id = c.id AND c.is_active = 0;
性能影响:加了软删除条件后索引还生效吗
加了 AND deleted_at IS NULL 这类条件,不一定走索引 —— 取决于你的索引是否覆盖该字段。很多表只有 (user_id) 单列索引,现在要查 user_id + deleted_at,就得建联合索引。
- 对高频关联的右表,建议建复合索引,顺序按
JOIN条件字段 + 软删除字段,例如:INDEX idx_user_deleted (user_id, deleted_at) - 如果软删除字段选择性很低(比如 99% 都是 NULL),某些旧版 MySQL 可能放弃使用该索引,需结合
EXPLAIN看执行计划 - PostgreSQL 对
IS NULL的索引支持更好,但同样依赖索引是否包含该列
别假设“加了条件就自动优化”,上线前用真实数据量跑 EXPLAIN 看一眼 type 和 key 字段。
最常被忽略的是:软删除条件写在 WHERE 里看似省事,实则悄悄把 LEFT JOIN 变成了 INNER JOIN 效果 —— 这个坑在线上查半天数据对不上时才暴露出来。

















