登录
推荐 文章 Go 技术 课程 下载 专题 AI
首页 >  数据库 >  MySQL

MySQL 多列索引遇到 IS NULL 时如何判断顺序

来源:17golang原创

时间:2026-09-15 03:33:03 107浏览 收藏

我在给软删除表补索引时,最容易误判的一点就是:看到 deleted_at IS NULL,就以为 NULL 条件必须放在复合索引第一列。实际判断不是看 WHERE 里谁写在前面,而是看索引定义的左前缀、查询是否提供了前面的列,以及 EXPLAIN 显示优化器真正用了哪些列。IS NULL 本身可以参与索引范围,但它不会自动跳过复合索引前面的空缺列。

要点速览
  • 交换 WHERE 中两个条件的书写位置,通常不会改变复合索引的列顺序。
  • (tenant_id, deleted_at, created_at) 中,tenant_id = 7deleted_at IS NULL 可以共同限定索引前缀。
  • 最终以 keyused_key_partskey_length 和访问类型复查,不凭感觉改索引。

WHERE 条件顺序,不等于索引列顺序

假设表里有租户、软删除时间和创建时间:

CREATE TABLE orders (
  id BIGINT PRIMARY KEY,
  tenant_id BIGINT NOT NULL,
  deleted_at DATETIME NULL,
  created_at DATETIME NOT NULL,
  KEY idx_tenant_deleted_created (tenant_id, deleted_at, created_at)
);

下面两条查询只是条件书写顺序不同:

-- 两个条件都指向同一组业务约束,便于对比 WHERE 文本顺序
SELECT id FROM orders
WHERE tenant_id = 7 AND deleted_at IS NULL;

-- 交换书写位置,不代表要交换索引定义
SELECT id FROM orders
WHERE deleted_at IS NULL AND tenant_id = 7;

索引的顺序仍然是 tenant_iddeleted_atcreated_at。优化器会从可形成的索引条件出发,而不是按 SQL 字符串从左到右机械选列。因此不要为了配合第二条写法,就把索引改成 (deleted_at, tenant_id)

IS NULL 在复合索引里到底占哪一段

MySQL 官方手册把 IS NULL 列为可以用于索引和范围搜索的条件。对 B-Tree 多列索引来说,等值比较、IS NULL 都可能让优化器继续考虑后续键列。上面的例子里,tenant_id = 7 先固定第一段,deleted_at IS NULL 再固定第二段;created_at 是否继续参与范围,要看查询是否写了它以及优化器的成本判断。

MySQL 复合索引中 tenant_id 等值条件、deleted_at IS NULL 与索引前缀和范围的关系示意图
图1:MySQL 多列索引关系示意图,查看查询谓词如何对应索引前缀与范围边界。

关键误区在于“只要条件里出现了某个索引列,就能直接用它”。如果索引是 (tenant_id, deleted_at),查询只有 deleted_at IS NULL 时,deleted_at 并不是左前缀。MySQL 可能因为其他优化策略选择不同访问路径,但不能把“列在索引里”当成“必然按这一列定位”。

索引到底该把 deleted_at 放前面吗

没有脱离查询集合的固定答案。先把最常见的三类请求列出来:

查询形态更直接的左前缀判断重点
tenant_id = ?(tenant_id, ...)租户隔离是主要入口
deleted_at IS NULL(deleted_at, ...)是否经常跨租户查未删除数据
tenant_id = ? AND deleted_at IS NULL两种顺序都要实测兼顾单列前缀、数据分布和其他查询

如果业务几乎所有请求都带租户条件,(tenant_id, deleted_at, created_at) 往往更容易覆盖租户内的列表查询;如果确实存在大量只按未删除状态筛选的跨租户任务,才有理由评估 (deleted_at, tenant_id) 或独立索引。这里不建议仅凭 NULL 比较特殊就固定把它放第一列:NULL 占比、租户数量、排序需求和回表成本都会改变选择。

用 EXPLAIN 判断到底用了哪一列

我通常先看计划,再决定是否调整定义。示例只展示检查方法,不代表任意数据集都会返回相同的行数:

-- 用 JSON 计划查看实际使用的索引列,不执行查询本身
EXPLAIN FORMAT=JSON
SELECT id, created_at
FROM orders
WHERE tenant_id = 7
  AND deleted_at IS NULL
  AND created_at >= '2026-01-01';

-- 传统表格输出便于快速比较 key 和 key_len
EXPLAIN SELECT id, created_at
FROM orders
WHERE tenant_id = 7 AND deleted_at IS NULL;

JSON 结果里重点找 keyused_key_parts;传统格式先看 keykey_lenrowsExtrakey 说明选了哪条索引,used_key_parts 说明实际使用到哪些键列,key_len 可以帮助判断使用了多长的复合键前缀。看到索引被选中,不代表每个索引列都参与定位。

MySQL EXPLAIN 输出中 key、used_key_parts、key_length、访问类型和剩余条件的关系示意图
图2:EXPLAIN 证据示意图,沿着索引证据分组查看实际使用的索引前缀。

复查时还要注意统计信息和真实数据分布。若 rows 估算与实际明显不符,可以先确认表结构、索引定义和统计信息是否处于当前状态,再用同一条业务 SQL 比较候选索引。不要用 FORCE INDEX 直接掩盖计划问题,它更适合做有边界的对照实验。

相关问题

deleted_at IS NULL 写在 WHERE 第一行会更快吗?

通常不会。WHERE 的文本顺序不是复合索引定义,是否更快要看最终执行计划和数据分布。

索引被使用了,为什么查询仍然扫描很多行?

可能只使用了索引的前缀,后续条件作为过滤条件处理,也可能 NULL 值占比很高。结合 used_key_partsrows 和实际执行观察。

只查询 deleted_at IS NULL,一定要新建索引吗?

不一定。先确认这类查询是否频繁、结果集是否足够小,以及现有索引是否能覆盖。用 EXPLAIN 和真实业务 SQL 比较后再决定。

一句话收束:IS NULL 可以参与复合索引定位,但索引顺序应由完整查询模式决定;先看左前缀,再看 EXPLAIN,最后才改索引。

声明:本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>