负向查询到底能不能走索引
在排查数据库慢查询的时候,经常会看到类似这样的 SQL:
SELECT * FROM users WHERE status != 1;或者更常见的写法:
SELECT * FROM users WHERE NOT status = 1;这类查询就是典型的“负向查询”,也就是通过 !=、<>、NOT IN、NOT EXISTS 等方式排除某些条件。问题来了:这种查询能用上索引吗?
索引的工作原理决定它的偏好
索引本质上是为“快速定位符合条件的数据”设计的。比如 B+ 树索引,适合做等值匹配或范围扫描,像 WHERE status = 1 或 WHERE create_time > '2024-01-01' 都能高效利用索引。
但负向查询不同。WHERE status != 1 想找的是除了 1 以外的所有状态。如果 status 字段只有 1 和 2 两种值,那这个条件其实相当于 WHERE status = 2,理论上可以走索引。但如果 status 有几十种取值,!= 1 就会命中大量数据,数据库优化器很可能觉得全表扫描比走索引更快。
实际测试看效果
假设 users 表有 100 万条数据,status 字段上有普通索引,执行:
EXPLAIN SELECT * FROM users WHERE status != 1;你会发现 type 是 index,说明走了索引扫描,但 key 是 status_idx,而 rows 显示接近 100 万。这意味着它把整个索引都过了一遍,并没有真正“高效”。
换一种情况,如果 status 大部分都是 1,只有少量不是 1,这时候优化器可能直接放弃索引,选择全表扫描。
NOT IN 更容易踩坑
比如这句:
SELECT * FROM users WHERE id NOT IN (1, 2, 3);看起来简单,但如果 NOT IN 后面是个子查询,尤其是关联表数据多的时候,性能会急剧下降。而且一旦子查询结果中包含 NULL,整个 NOT IN 的结果就会变成 UNKNOWN,导致查不出任何数据。
有没有办法让负向查询也快
有时候可以换个思路。比如原本要查“未处理的订单”,写成:
SELECT * FROM orders WHERE status != 'done';但如果“未处理”的状态是明确的几种,比如 'pending', 'processing',完全可以改成正向查询:
SELECT * FROM orders WHERE status IN ('pending', 'processing');这样不仅能走索引,执行计划也更稳定。
另外,组合索引也可以帮忙。比如经常按时间查未完成的任务,可以建一个 (status, create_time) 的联合索引,先把正向状态固定住,再加时间范围,效率高得多。
别迷信索引,要看执行计划
有没有走索引,不能靠猜。每次写完查询,特别是带 NOT 或 != 的,一定要用 EXPLAIN 看一眼。type 是 ref 还是 range,key 有没有命中,rows 扫了多少,这些才是真正判断依据。
有时候加了索引反而更慢,因为索引维护有成本,查询时回表也可能变多。别一上来就加索引,先看执行路径。
负向查询不是完全不能用索引,而是得看数据分布、查询频率和具体写法。理解它的局限,才能避开性能陷阱。