
JOIN时漏加IsDeleted = 0导致查出已逻辑删除的数据
逻辑删除字段(如IsDeleted)不参与JOIN条件,是线上数据错乱的常见源头。比如用户表Users和订单表Orders都带IsDeleted,但写LEFT JOIN Orders ON Users.Id = Orders.UserId时没过滤Orders.IsDeleted = 0,结果会把已删除的订单也关联进来——即使主表Users本身没删,关联结果里就混入了脏数据。
关键不是“要不要加”,而是“加在ON里还是WHERE里”:前者控制关联行为,后者只过滤结果行。对LEFT JOIN来说,WHERE Orders.IsDeleted = 0会把没订单的用户也过滤掉(变成内连接效果),必须写进ON子句。
-
LEFT JOIN Orders ON Users.Id = Orders.UserId AND Orders.IsDeleted = 0→ 正确:保留用户,只关联未删除订单 -
LEFT JOIN Orders ON Users.Id = Orders.UserId WHERE Orders.IsDeleted = 0→ 错误:丢失无订单用户 - 若用
INNER JOIN,ON和WHERE效果等价,但统一写在ON更利于逻辑复用和阅读
多表JOIN时每个表的IsDeleted都要显式判断
三个及以上表关联时,很容易只顾主表和第一个从表,忽略后续表的删除标记。例如查询用户→角色→权限链路:Users、UserRoles、Roles、RolePermissions、Permissions,其中UserRoles和RolePermissions是中间关系表,也需逻辑删除控制。
典型错误是只写AND Roles.IsDeleted = 0,却漏了UserRoles.IsDeleted = 0和RolePermissions.IsDeleted = 0,导致关联到已被停用的角色关系或权限绑定。
- 关系表(如
UserRoles)的IsDeleted代表“该用户是否还拥有此角色”,必须参与ON - 实体表(如
Roles)的IsDeleted代表“角色本身是否已停用”,同样要检查 - 建议给每个JOIN的
ON条件换行缩进,把IsDeleted判断和关联条件并列写清楚,避免漏项
SQL Server中IsDeleted字段类型影响索引使用效率
如果IsDeleted是BIT类型且未建索引,WHERE/ON中用IsDeleted = 0可能触发全表扫描——尤其当表数据量大、删除比例低时(比如95%数据IsDeleted = 0),优化器常放弃索引。这不是语法问题,而是性能隐患。
解决方式取决于实际分布:
- 若
IsDeleted = 1占比极小(如不到1%),建INCLUDE索引更高效:CREATE INDEX IX_Orders_UserId_Incl ON Orders(UserId) INCLUDE (IsDeleted) - 若需高频按
IsDeleted筛选,单独建索引:CREATE INDEX IX_Orders_IsDeleted ON Orders(IsDeleted) WHERE IsDeleted = 0(SQL Server 2016+支持筛选索引) - 避免在
IsDeleted上用函数,比如ISNULL(IsDeleted, 0) = 0会阻止索引查找
ORM(如EF Core)生成SQL时容易忽略IsDeleted的自动过滤
EF Core默认开启全局查询过滤(HasQueryFilter),但仅对直接DbSet<t></t>查询生效。一旦用FromSqlRaw、Join方法手写关联,或跨DbContext查询,过滤器不会自动注入IsDeleted = 0。
更隐蔽的问题是导航属性加载:即使Users配置了HasQueryFilter(x => !x.IsDeleted),context.Users.Include(x => x.Orders)仍可能加载出Orders.IsDeleted = 1的数据——因为EF Core默认不对导航目标表应用主表的过滤器。
- 手写SQL或存储过程时,必须人工补全所有
IsDeleted条件,不能依赖ORM习惯 - 用
Include时,确认目标实体(如Orders)自身也配置了HasQueryFilter - 测试阶段用SQL Profiler或EF日志输出真实执行语句,逐行核对
IsDeleted是否出现
IsDeleted都是独立开关,少一个就可能让整条关联链失效。最麻烦的是那种“看起来有数据、但业务上不该存在”的情况——它不会报错,只会悄悄污染结果。










