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 = 7与deleted_at IS NULL可以共同限定索引前缀。 - 最终以
key、used_key_parts、key_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_id、deleted_at、created_at。优化器会从可形成的索引条件出发,而不是按 SQL 字符串从左到右机械选列。因此不要为了配合第二条写法,就把索引改成 (deleted_at, tenant_id)。
IS NULL 在复合索引里到底占哪一段
MySQL 官方手册把 IS NULL 列为可以用于索引和范围搜索的条件。对 B-Tree 多列索引来说,等值比较、 和 IS NULL 都可能让优化器继续考虑后续键列。上面的例子里,tenant_id = 7 先固定第一段,deleted_at IS NULL 再固定第二段;created_at 是否继续参与范围,要看查询是否写了它以及优化器的成本判断。

关键误区在于“只要条件里出现了某个索引列,就能直接用它”。如果索引是 (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 结果里重点找 key 和 used_key_parts;传统格式先看 key、key_len、rows 与 Extra。key 说明选了哪条索引,used_key_parts 说明实际使用到哪些键列,key_len 可以帮助判断使用了多长的复合键前缀。看到索引被选中,不代表每个索引列都参与定位。

复查时还要注意统计信息和真实数据分布。若 rows 估算与实际明显不符,可以先确认表结构、索引定义和统计信息是否处于当前状态,再用同一条业务 SQL 比较候选索引。不要用 FORCE INDEX 直接掩盖计划问题,它更适合做有边界的对照实验。
相关问题
把 deleted_at IS NULL 写在 WHERE 第一行会更快吗?
通常不会。WHERE 的文本顺序不是复合索引定义,是否更快要看最终执行计划和数据分布。
索引被使用了,为什么查询仍然扫描很多行?
可能只使用了索引的前缀,后续条件作为过滤条件处理,也可能 NULL 值占比很高。结合 used_key_parts、rows 和实际执行观察。
只查询 deleted_at IS NULL,一定要新建索引吗?
不一定。先确认这类查询是否频繁、结果集是否足够小,以及现有索引是否能覆盖。用 EXPLAIN 和真实业务 SQL 比较后再决定。
一句话收束:IS NULL 可以参与复合索引定位,但索引顺序应由完整查询模式决定;先看左前缀,再看 EXPLAIN,最后才改索引。
-
501 收藏
-
501 收藏
-
501 收藏
-
501 收藏
-
501 收藏
-
181 收藏
-
432 收藏
-
327 收藏
-
284 收藏
-
199 收藏
-
数据库 · MySQL | 8小时前 | MySQL · 数据类型 · JSON · SQL排错 · MEMBER OF · MySQL MEMBER OF MySQL JSON 数组成员判断 MEMBER OF 类型不匹配 JSON 数字字符串区别 MySQL JSON 查询167 收藏
-
数据库 · MySQL | 10小时前 | MySQL · 数据库查询 · JSON 函数 · SQL 边界 · JSON 数组 · JSON_CONTAINS MySQL JSON_OVERLAPS JSON 数组相交 MySQL JSON 类型比较 MySQL NULL 边界130 收藏
-
418 收藏
-
410 收藏
-
452 收藏
-
425 收藏
-
488 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 立即学习 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 立即学习 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 立即学习 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 立即学习 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 立即学习 485次学习