感谢阅读!❤️
如果这篇文章对你有帮助,欢迎 **点赞** 👍 和 **关注** ⭐,获取更多实用技巧和干货内容!你的支持是我持续创作的动力!
**关注我,不错过每一篇精彩内容!**
目录
- 一、MySQL优化手段
- 二、性能分析工具
-
- [2.1 查看数据库整体情况](#2.1 查看数据库整体情况)
- [2.2 慢查询日志](#2.2 慢查询日志)
- [2.3 Show Profiles](#2.3 Show Profiles)
- [2.4 Explain](#2.4 Explain)
- 三、索引优化
-
- [3.1 加索引vs不加索引](#3.1 加索引vs不加索引)
- [3.3 最左前缀原则](#3.3 最左前缀原则)
- 四、索引失效情况
-
- [4.1 索引列使用函数](#4.1 索引列使用函数)
- [4.2 索引列参与运算](#4.2 索引列参与运算)
- [4.3 隐式类型转换](#4.3 隐式类型转换)
- [4.4 模糊匹配前缀为通配符](#4.4 模糊匹配前缀为通配符)
- [4.5 OR连接非索引列条件](#4.5 OR连接非索引列条件)
- [4.6 非等值判断与负向查询](#4.6 非等值判断与负向查询)
- [4.7 is null和is not null的索引失效问题](#4.7 is null和is not null的索引失效问题)
- [4.8 联合索引使用违规导致失效](#4.8 联合索引使用违规导致失效)
- [4.9 其他特殊场景导致失效](#4.9 其他特殊场景导致失效)
一、MySQL优化手段
MySQL 优化是提升数据库性能、稳定性和可扩展性的关键环节。优化可以从多个层面进行,包括 SQL语句优化 、索引优化 、配置参数调优 、架构设计优化 等。
MySQL数据库的优化手段通常包括但不限于:
- SQL语句优化:这是最低成本的优化手段,通过优化查询语句、适当添加索引等方式进行。并且效果显著。
- 库表结构优化:通过规范化设计、优化索引和数据类型等方式进行库表结构优化,需要对数据库结构进行调整和改进
- 系统配置优化:根据硬件和操作系统的特点,调整最大连接数、内存管理、IO调度等参数
- 硬件优化:升级硬盘、增加内存容量、升级处理器等硬件方面的投入,需要购买和替换硬件设备,成本较高
主要掌握:SQL语句优化
二、性能分析工具
2.1 查看数据库整体情况
在 MySQL 中,SHOW GLOBAL STATUS 是一个非常重要的性能监控命令,用于查看服务器级别的运行状态变量(status variables)。这些变量记录了自 MySQL 启动以来各种操作的累计次数、资源使用情况等信息。
sql
SHOW GLOBAL STATUS LIKE 'Com_select';
SHOW GLOBAL STATUS LIKE 'Com_insert';
SHOW GLOBAL STATUS LIKE 'Com_delete';
SHOW GLOBAL STATUS LIKE 'Com_update';
以及通配形式:
SQL
-- COM + 7个'_'通配符。('_'(下划线)匹配任意单个字符)
SHOW GLOBAL STATUS LIKE 'Com_______';
在 MySQL 中,所有以
Com_开头的状态变量表示的是 客户端发起的 SQL 命令的执行次数。
Com_select:执行 SELECT 语句的总次数(包括子查询中的 SELECT)。Com_insert:执行 INSERT 语句的总次数(包括 INSERT ... ON DUPLICATE KEY UPDATE)。Com_update:执行 UPDATE 语句的总次数。Com_delete:执行 DELETE 语句的总次数。
这些结果反映了从MySQL服务器启动到当前时刻,所有SELECT、INSERT、UPDATE、DELETE语句执行的总次数。对于MySQL性能优化来说,通常查看Com_select的值可以了解 SELECT 查询在整个 MySQL 服务期间所占比例的情况:
- 如果
Com_select次数过高,可能说明查询表中的每条记录都会返回过多的字段。 - 如果
Com_select次数很少,同时insert或delete或update的次数很高,可能说明服务器运行的应用程序过于依赖写入操作和少量读取操作。
总之,通过查看 Com_select 的值,可以了解 MySQL 服务器的长期执行情况,并在优化查询性能时,帮助我们了解 MySQL 的性能瓶颈。
2.2 慢查询日志
慢查询日志 是关系型数据库提供的性能监控与优化核心工具 ,专门用来记录执行时间超过设定阈值的SQL语句,同时可以配置记录未使用索引的查询。其核心价值是定位数据库性能瓶颈,为 SQL 优化、索引调整提供精准依据。
MySQL 慢查询日志核心参数:
MySQL 的慢查询日志功能通过一系列系统参数控制,参数可分为临时生效(会话 / 全局) 和永久生效(配置文件) 两种方式。
| 参数名 | 核心作用 | 默认值 | 配置说明 |
|---|---|---|---|
slow_query_log |
慢查询日志开关 | OFF |
设为ON开启,OFF关闭; 临时配置 :SET GLOBAL slow_query_log = 1;(需重新连接会话生效) |
slow_query_log_file |
慢查询日志文件路径 | 取决于系统(如/var/log/mysql/slow.log) |
指定日志存储位置,需确保MySQL进程有读写权限; 临时配置 :SET GLOBAL slow_query_log_file = '/data/mysql/slow.log'; |
long_query_time |
慢查询时间阈值 | 10(单位:秒) |
执行时间超过该值 的SQL会被记录; 支持小数 (如0.5代表500毫秒); 临时配置 :SET GLOBAL long_query_time = 0.5;(需重新连接会话生效) |
log_queries_not_using_indexes |
是否记录未使用索引的查询 | OFF |
设为ON时,即使执行时间未超阈值 ,未使用索引的SQL也会被记录; 注意:开启后可能产生大量日志,建议结合log_throttle_queries_not_using_indexes限制 |
log_throttle_queries_not_using_indexes |
未用索引查询的日志频率限制 | 0(无限制) |
单位时间内(1分钟)最多记录的未用索引查询数量; 设为10表示每分钟仅记录前10条,避免日志暴涨 |
min_examined_row_limit |
扫描行数阈值 | 0 |
扫描行数少于该值 的慢查询,即使超时也不记录; 设为100可过滤掉扫描少量数据的"伪慢查询" |
配置方法:
-
临时配置(重启MySQL后失效)
适用于临时排查问题,无需重启服务
SQL-- 1. 开启慢查询日志 SET GLOBAL slow_query_log = 1; -- 2. 设置日志文件路径 SET GLOBAL slow_query_log_file = '/data/mysql/slow.log'; -- 3. 设置慢查询阈值为500毫秒 SET GLOBAL long_query_time = 0.5; -- 4. 记录未使用索引的查询(可选) SET GLOBAL log_queries_not_using_indexes = 1; -- 5. 限制未用索引查询的日志频率(可选) SET GLOBAL log_throttle_queries_not_using_indexes = 10;注意 :修改
GLOBAL参数后,需重新连接MySQL会话 才能看到参数生效后的状态(可通过SHOW GLOBAL VARIABLES LIKE '%slow%';查看)。 -
永久配置(重启后生效)
修改MySQL配置文件(Linux为
/etc/my.cnf或/etc/mysql/my.cnf,Windows为my.ini),添加以下配置项:TOML[mysqld] # 开启慢查询日志 slow_query_log = ON # 日志文件路径 slow_query_log_file = /data/mysql/slow.log # 慢查询阈值:0.5秒 long_query_time = 0.5 # 记录未使用索引的查询 log_queries_not_using_indexes = ON # 每分钟最多记录10条未用索引的查询 log_throttle_queries_not_using_indexes = 10 # 扫描行数少于100的慢查询不记录 min_examined_row_limit = 100配置完成后,重启MySQL服务生效:
Bash# Linux 重启命令 systemctl restart mysqld # 或 service mysqld restart
慢查询日志内容解读:
MySQL慢查询日志为文本格式,每条记录包含执行上下文和SQL语句,以下是典型的日志片段及字段说明:
Plain
# Time: 2026-01-18T10:30:00.000000Z
# User@Host: root[root] @ localhost [] Id: 123
# Query_time: 8.50 Lock_time: 0.02 Rows_sent: 20 Rows_examined: 100000
# Rows_affected: 0
SET timestamp=1737186600;
SELECT * FROM order_info WHERE order_date = '2026-01-01';
关键字段解析:
| 字段 | 含义 | 优化参考 |
|---|---|---|
Time |
SQL执行的时间戳 | 用于定位业务高峰期的慢查询 |
User@Host |
执行SQL的用户和客户端地址 | 排查特定用户的低效操作 |
Query_time |
SQL执行耗时(单位:秒) | 核心指标,数值越大说明性能越差 |
Lock_time |
锁等待时间(单位:秒) | 若数值过高,说明存在锁竞争(如行锁、表锁冲突) |
Rows_sent |
向客户端返回的行数 | 若Rows_sent远小于Rows_examined,说明SQL过滤效果差 |
Rows_examined |
扫描的行数 | 若Rows_examined远大于表数据量,大概率是全表扫描,需加索引 |
Rows_affected |
受影响的行数(仅DML语句) | 用于分析INSERT/UPDATE/DELETE的效率 |
SET timestamp |
SQL执行的UNIX时间戳 | 可转换为具体时间,便于关联业务场景 |
| 最后一行 | 被记录的SQL语句 | 优化的直接对象 |
慢查询日志分析工具:
- 自带工具:
mysqldumpslow - 第三方工具:
pt-query-digest
-
自带工具:
mysqldumpslowmysqldumpslow是MySQL内置的日志分析工具,可对慢查询日志进行统计汇总,支持按执行时间、锁时间、返回行数等维度排序。-
常用参数
-
-s:排序方式(t按执行时间,l按锁时间,r按返回行数,c按执行次数) -
-t:显示前N条慢查询 -
-g:正则匹配SQL语句(区分大小写)
-
-
示例
Bash# 按执行时间排序,显示前10条慢查询 mysqldumpslow -s t -t 10 /data/mysql/slow.log # 按执行次数排序,筛选包含"order_info"表的慢查询 mysqldumpslow -s c -g "order_info" /data/mysql/slow.log -
-
第三方工具:
pt-query-digestpt-query-digest是Percona Toolkit 中的核心工具,功能比mysqldumpslow更强大,支持按SQL指纹聚合、生成详细分析报告,是生产环境的首选工具。-
安装(以Linux为例)
Bash# 安装Percona Toolkit yum install percona-toolkit -y -
示例
Bash# 分析慢查询日志,生成报告 pt-query-digest /data/mysql/slow.log > slow_analysis_report.log # 分析最近1小时的慢查询 pt-query-digest --since 1h /data/mysql/slow.log
报告中会包含:SQL执行次数 、平均耗时 、扫描行数 、索引使用情况等关键指标,直接给出优化建议。
-
2.3 Show Profiles
Show Profiles 是 MySQL 提供的轻量级 SQL 执行性能分析工具,用于跟踪和统计单个 SQL 语句执行过程中各阶段的耗时、CPU 占用、IO 开销等细节。与慢查询日志(侧重捕获超时 SQL)不同,它能深入 SQL 执行内部流程,精准定位"解析、优化、执行、锁等待"等具体环节的性能瓶颈,是 SQL 优化的核心辅助工具之一。
适用场景:排查单条 SQL 执行缓慢的根因、分析 SQL 执行计划的实际耗时分布、验证索引优化效果、定位锁等待或资源竞争问题。
核心特性与优势:
-
细粒度耗时统计:不仅能查看 SQL 总执行时间,还能拆分到"发送数据""排序结果""表锁等待"等 30+ 执行阶段,明确瓶颈所在。
-
低侵入性:默认关闭,开启后仅对当前会话生效,不影响全局数据库性能,适合生产环境临时排查问题。
-
支持多维度统计:除耗时外,可扩展统计 CPU、IO、内存、上下文切换等资源开销(需结合特定参数)。
-
操作简单:无需复杂配置,通过简单 SQL 命令即可开启、查看和分析,上手成本低。
开启与配置:
Show Profiles 功能默认关闭,且仅对当前会话生效(重启会话或 MySQL 服务后需重新开启),无需修改全局配置文件。
-
查看当前数据库是否支持
Show Profiles操作SQLSELECT @@have_profiling; -
查看当前Profiling状态
SQLSELECT @@profiling;或者
SQLSHOW VARIABLES LIKE 'profiling'; -
开启Profiling
SQLSET profiling = 1;
说明:
profiling是会话级参数,仅对当前连接有效,其他会话需单独开启;若需全局默认开启,可在 MySQL 配置文件(my.cnf/my.ini)中添加profiling = 1,重启服务后生效(不推荐生产环境全局开启,避免额外性能开销)
配置历史记录数量:
通过 profiling_history_size 参数控制最多保留的 SQL 性能记录条数,默认值为 15,最大值为 100。
sql
-- 查看当前保留条数
SHOW VARIABLES LIKE 'profiling_history_size';
-- 修改为保留 50 条记录(仅当前会话生效)
SET profiling_history_size = 50;
注意:超过设定条数后,新记录会覆盖最早的记录,需及时保存关键分析结果。
示例:
开启 profiling 后,执行需要分析的 SQL 语句,再通过系列命令查看性能详情,流程如下:
-
执行目标 SQL
SQL-- 先执行待分析的 SQL(SELECT/INSERT/UPDATE/DELETE 均可) SELECT * FROM EMP; SELECT EMPID,ENAME FROM EMP; SELECT * FROM EMP WHERE EMPID = 1033; SELECT COUNT(*) FROM EMP; -
查看 SQL 性能记录列表(SHOW PROFILES)
执行以下命令,获取当前会话中已记录的所有 SQL 性能摘要:
sqlSHOW PROFILES;典型输出结果:
Query_ID Duration Query 1 0.00020075 SELECT * FROM EMP 2 0.00009025 SELECT EMPID,ENAME FROM EMP 3 0.000092 SELECT * FROM EMP WHERE EMPID = 1033 4 0.00058375 SELECT COUNT(*) FROM EMP - Query_ID:SQL 语句的唯一标识,用于后续查看详细性能数据。
- Duration:SQL 总执行时间(单位:秒),直观反映 SQL 整体耗时。
- Query:具体执行的 SQL 语句,便于对应分析目标。
-
查看单条SQL详细性能
通过
Query_ID定位目标 SQL,查看其执行各阶段的详细耗时,语法:sql-- 基础语法:查看指定 Query_ID 的各阶段耗时 SHOW PROFILE FOR QUERY 1; -- 扩展语法:查看包含 CPU 开销的详细信息 SHOW PROFILE CPU FOR QUERY 1;输出结果:
Status Duration CPU_user CPU_system starting 0.000020 0.000000 0.000000 checking permissions 0.000005 0.000000 0.000000 Opening tables 0.000010 0.000000 0.000000 init 0.000015 0.000000 0.000000 System lock 0.000008 0.000000 0.000000 optimizing 0.000006 0.000000 0.000000 statistics 0.000012 0.000000 0.000000 preparing 0.000009 0.000000 0.000000 executing 0.000003 0.000000 0.000000 Sending data 0.849900 0.800000 0.050000 end 0.000005 0.000000 0.000000 query end 0.000004 0.000000 0.000000 closing tables 0.000006 0.000000 0.000000 freeing items 0.000010 0.000000 0.000000 cleaning up 0.000003 0.000000 0.000000 SHOW PROFILE输出的Status列对应 SQL 执行的各阶段,以下是常见阶段及异常分析:-
Sending data:最常见的耗时阶段,代表 MySQL 向客户端传输数据的过程,也包含数据查询和排序的耗时。若此阶段耗时过长,可能是返回数据量过大、未命中索引导致全表扫描,或需要优化排序逻辑。
-
Waiting for table lock:表锁等待,说明存在表级锁竞争(如 MyISAM 引擎的写操作阻塞读),需优化存储引擎(改为 InnoDB)、减少长事务,或调整 SQL 执行顺序。
-
Waiting for row lock:行锁等待,InnoDB 引擎下的行锁竞争,多由并发更新同一行数据导致,需优化事务粒度、避免长时间持有锁,或调整索引减少锁冲突范围。
-
Creating sort index:创建排序索引,代表 SQL 执行了文件排序(Using filesort),若耗时过长,需添加合适的索引避免排序,或优化排序字段。
-
Copying to tmp table:复制数据到临时表,多由 GROUP BY、DISTINCT 等操作触发,临时表可能在内存或磁盘中,磁盘临时表耗时极高,需优化索引或 SQL 写法减少临时表使用。
-
Opening tables :打开表的过程,若此阶段频繁耗时,可能是表连接数过多,或表缓存(table_open_cache)设置过小,需调整
table_open_cache参数。
-
2.4 Explain
EXPLAIN 是 MySQL 核心的 SQL 执行计划分析工具 ,用于直观展示 MySQL 优化器对 SQL 语句的执行计划。通过它可快速判断索引使用情况 、表连接顺序 、数据扫描方式 等关键信息,精准定位全表扫描 、索引失效 、文件排序 等性能问题,是 SQL 优化的必备前置工具,与慢查询日志、SHOW PROFILES 配合使用可形成完整的性能优化闭环。根据执行计划可以做出相应的优化措施。提高执行效率。
核心价值:无需执行 SQL(对写操作仅分析计划不落地数据),就能预判 SQL 执行效率,提前规避性能瓶颈,而非等 SQL 执行缓慢后再排查。
EXPLAIN 可用于 SELECT、DELETE、UPDATE、INSERT 语句,核心语法及扩展用法
sql
-- 1. 基础用法:分析 SELECT 语句执行计划
EXPLAIN SELECT * FROM EMP WHERE EMPID = 1033;
-- 2. 分析写操作(仅生成计划,不实际执行删除/更新)
EXPLAIN DELETE FROM EMP WHERE EMPID = 1033;
EXPLAIN UPDATE EMP SET ENAME = 'TestName' WHERE EMPID = 1033;
-- 3. 扩展用法:输出 JSON 格式详细计划(适合复杂场景分析)
EXPLAIN FORMAT=JSON SELECT * FROM EMP WHERE EMPID = 1033;
-- 4. MySQL 8.0+ 增强:实际执行并返回精确计划(有性能开销,生产慎用)
EXPLAIN ANALYZE SELECT * FROM EMP WHERE EMPID = 1033;
说明:多表连接、子查询场景下,
EXPLAIN会返回多行结果,每行对应一张表的处理逻辑,需结合id字段判断执行顺序。
核心输出字段解读:
EXPLAIN 输出包含 10 个关键字段,每个字段对应执行计划的核心逻辑,掌握这些字段是 SQL 优化的关键。以下结合实际场景解读各字段含义及优化建议:
- id :SQL 执行顺序标识
- 相同 id:按从上到下顺序执行;
- 不同 id:id 越大优先级越高,先执行(子查询通常 id 更大);
- NULL:为其他语句提供结果集(如子查询的临时表)。
- select_type :查询类型,标识 SQL 的复杂程度(如简单查询、子查询、联合查询等)。常用的值:
SIMPLE:简单的 SELECT 查询,不包含子查询或联合PRIMARY:复杂查询中的主查询(外层查询)SUBQUERY:子查询(内层查询,不依赖外层结果DERIVED:衍生表查询(from 子句中的子查询,会生成临时表)UNION:UNION 语句中的第二个及后续查询UNION RESULT:UNION 结果集的合并操作DELETE:DELETE 语句的执行计划UPDATE:UPDATE 语句的执行计划
- table :当前行对应的表名,反映了这个查询操作的是哪个表(多表连接时显示表名/别名)
- 结合
id字段判断表连接顺序,若顺序不合理,可通过STRAIGHT_JOIN强制指定连接顺序(慎用,需充分测试)
- 结合
- partitions :分区表相关,显示匹配的分区
- 非分区表显示 NULL,分区表显示匹配的分区名称,用于排查分区表的分区命中情况(未命中分区会导致全分区扫描)。
- type :表的访问类型(SQL 扫描方式),直接决定执行效率。从优到差排序(重点关注是否出现低效类型):
NULL:效率最高,一般不可能优化到这个级别,只有查询时没有查询表的时候,访问类型是NULL。例如:select 1;system:表仅 1 行数据(MyISAM 引擎特有,最优);const:通过主键/唯一索引查询,仅返回 1 行(理想状态);eq_ref:多表连接时,通过主键/唯一索引匹配,每行仅匹配 1 行;ref:通过普通索引匹配,可能返回多行(常规合理状态);range:索引范围查询(如 >、<、BETWEEN、IN),性能较好;index:全索引扫描(比全表扫描略优,需优化);ALL:全表扫描(性能最差,优先优化,需添加索引或调整 SQL)。
- possible_keys :MySQL 优化器认为可能适用的索引列表
- 非 NULL:列出所有匹配查询条件的索引
- NULL:无可用索引,大概率走全表扫描
- 若
possible_keys非 NULL 但key为 NULL,说明索引失效(需排查失效原因)
- key :实际被 MySQL 使用的索引名称
- 非 NULL:表示索引被成功使用,取值为索引名称
- NULL:未使用任何索引(需排查:是否有可用索引?索引是否失效?)
key与possible_keys可能不一致,优化器会选择最优索引。
- key_len :实际使用索引的长度(字节数),长度越长,说明索引覆盖的字段越全
- 用于判断复合索引的使用情况:如复合索引
(a,b),若key_len仅对应a的长度,说明仅使用了部分索引 - 需结合字段类型判断(如 INT 占 4 字节,VARCHAR(20) 占 20+2 字节)
- key_len 计算规则:
- 对于 VARCHAR(n)
- 存储长度 = n × max_bytes_per_char + length_prefix
- length_prefix = 2 字节
- utf8mb4 → max_bytes_per_char = 4
- 用于判断复合索引的使用情况:如复合索引
- ref :与索引匹配的列或常量
- 常量(如
const):通过常量匹配索引(高效) - 列名(如
order.user_id):通过其他表的列匹配索引(多表连接场景)
- 常量(如
- rows :MySQL 预估扫描的行数(非精确值,参考意义重大)
- 数值越小,执行效率越高
- 若数值远大于实际数据量,说明表统计信息过时,执行
ANALYZE TABLE 表名更新统计信息 - 多表连接时,该字段可判断连接逻辑的高效性
- filtered :预估符合条件的行数占扫描行数的百分比(0-100)
- 数值越高,过滤效果越好(大部分数据被 WHERE 条件过滤,效率高
- 数值越低(如 <10%),说明过滤效果差,需优化 WHERE 条件或索引,减少无效数据扫描
- 结合
rows字段计算预估匹配行数:rows × (filtered/100),辅助判断执行效率
- Extra :额外执行信息,包含关键优化提示(最能反映潜在问题)。常见取值及分析:
Using index:覆盖索引(仅通过索引获取数据,无需回表,最优);Using where:通过 WHERE 条件过滤数据(正常场景);Using filesort:文件排序(未通过索引排序,性能差,需优化索引);Using temporary:使用临时表(多由 GROUP BY/DISTINCT 触发,性能差 - 化);Using join buffer:表连接使用缓冲区(无合适索引,需优化连接索引);Using index condition:索引条件过滤(ICP 优化,正常场景)。
三、索引优化
3.1 加索引vs不加索引
假设有一个USER表,有ID和NAME字段,其中ID是主键,NAME是订单名字并且NAME没有添加索引。表中有100万条记录。
-
根据Id查询(Id是主键,有索引)
SQL-- 走索引,效率高 SELECT * FROM USER WHERE Id = 100001; -
根据NAME查询(NAME字段无索引)
SQL-- 没有索引,全表扫描,效率低 SELECT * FROM USER WEHRE NAME = 'Tom'; -
给NAME添加索引
SQLCREATE INDEX IDX_NAME ON USER(NAME); -
再次根据NAME查询(NAME字段有索引)
SQL-- 有索引,走索引,效率高 SELECT * FROM USER WEHRE NAME = 'Tom';
3.3 最左前缀原则
MySQL 联合索引的最左前缀原则 (Leftmost Prefix Principle) :当使用联合索引时,数据库会优先匹配联合索引中「最左侧的字段」,然后依次向右匹配后续字段 ;如果查询条件中「跳过了左侧的某个字段」,或者「不满足左侧字段的匹配条件」,那么从跳过的位置开始,右侧所有字段的索引都会完全失效,无法走索引查询,只能走全表扫描/回表。
通俗解释:
联合索引就像一本按「省份→城市→区县」排序的通讯录:
你可以快速查到
「广东省」的所有人(只匹配最左的「省份」);可以快速查到
「广东省+广州市」的所有人(匹配最左2个:省份+城市);可以快速查到
「广东省+广州市+天河区」的所有人(匹配全部3个);但你无法直接查到「广州市」的所有人 (跳过了最左的「省份」),也无法直接查
「天河区」的人,因为通讯录没有按城市/区县单独排序,只能一页页翻(全表扫描)。联合索引
(a,b,c)等价于这个通讯录的「省份(a)→城市(b)→区县©」,规则完全一致!
核心底层原理(为什么会有最左前缀原则?):
所有索引规则的本质,都是由索引的底层存储结构决定的**,最左前缀原则也不例外。
MySQL的联合索引在B+树中存储时,遵循两个核心规则:
-
排序规则 :联合索引的B+树,首先按照「最左侧字段」排序,左侧字段值相同的情况下,再按第二个字段排序,第二个字段相同的情况下,再按第三个字段排序,以此类推。
例:联合索引
(age,name,phone)的B+树,先按age从小到大排,age相同的记录,再按name字典序排,name也相同的,再按phone排。 -
索引的匹配逻辑 :B+树的索引查询,是「有序区间匹配」,必须从最左侧的有序起点开始,才能命中索引;一旦跳过左侧字段,后续的字段在B+树中是「无序的」,数据库无法利用索引快速定位。
最左前缀原则不是MySQL的「语法限制」,而是B+树索引的「物理存储特性」导致的必然结果。,所有基于B+树的数据库(MySQL、Oracle、SQLServer)都遵循这个原则!
最左前缀原则的「核心匹配规则」:
用一个测试表+联合索引做案例,所有规则都基于这个环境,方便理解:
SQL
DROP TABLE IF EXISTS `User`;
-- 创建测试表
CREATE TABLE `User` (
id INT PRIMARY KEY AUTO_INCREMENT,
age INT NOT NULL,
name VARCHAR(20) NOT NULL,
phone VARCHAR(11) NOT NULL,
address VARCHAR(50)
);
-- 创建联合索引:最左字段age → 中间name → 最右phone
CREATE INDEX idx_age_name_phone ON `User` (age, name, phone);
- age: INT → 4 字节
- name: VARCHAR(20) → 最大 20 个字符
- phone: VARCHAR(11) → 最大 11 个字符
- 字符集未显式指定,默认是 utf8mb4(MySQL 8.0+ 默认),每个字符最多 4 字节
-
规则1:「完全匹配最左前缀」- 索引完全生效(最优)
查询条件严格包含从最左侧开始的连续字段,不管是1个还是2个还是全部,索引都会完全生效,匹配的字段越多,索引效率越高
SQL-- 场景1:只查最左字段 age → 索引生效,使用部分索引✅,type = ref; key_len = 4字节 EXPLAIN SELECT * FROM User WHERE age = 25;

SQL
-- 场景2:查最左2个字段 age + name → 索引生效,使用部分索引✅,type = ref; key_len = 4+20×4+2 = 86(匹配度更高)
EXPLAIN SELECT * FROM User WHERE age = 25 and name = 'Tom';

SQL
-- 场景3:查询全部字段 age + name + phone → 索引完全生效,完全使用索引✅,type = ref; key_len = 4+20×4+2+11×4+2 = 132(最优匹配)
EXPLAIN SELECT * FROM User WHERE age = 25 AND name = 'Tom' AND phone = '13800000000';

SQL
-- 场景4:查询全部字段 age + name + phone,但phone 传值类型不是字符串 → age和name索引生效,因隐式类型转换,phone 无法使用索引✅,type = ref; key_len = 4+20×4+2 = 86
EXPLAIN SELECT * FROM User WHERE age = 25 AND name = 'Tom' AND phone = 13800000000;

SQL
-- 场景5:查询全部字段 age,但age传值类型是字符串 → age仍然索引生效
EXPLAIN SELECT * FROM User WHERE age = '25';

-
规则2:「跳过左侧字段」- 索引直接完全失效(高频坑点)
核心禁忌 :查询条件中跳过了联合索引中「左侧的某个字段」 ,那么整个联合索引直接失效,哪怕包含右侧的字段,也无法走索引,只能走全表扫描。
SQL-- 场景1:跳过最左侧的 age字段,只查name → 索引完全失效❌,type = ALL,全表扫描 EXPLAIN SELECT * FROM User WHERE name = 'Tom';
SQL--场景2:跳过左侧字段,只查最右 phone → 索引完全失效❌,type = ALL,全表扫描 EXPLAIN SELECT * FROM User WHERE phone = '13800000000';
SQL--场景3:跳过中间name字段,只查age + phone → 只走age索引,phone索引失效❌,使用部分索引✅,type = ref; key_len = 4。需回表。 -- 解析:age是最左字段能匹配,但是跳过了name,phone的索引完全用不上,phone属于无效条件 EXPLAIN SELECT * FROM User WHERE age = 25 AND phone = '13800000000';
SQL-- 场景4:跳过age字段,只查name + phone → 索引完全失效❌,type = ALL,全表扫描 EXPLAIN SELECT * FROM User WHERE name = 'Tom' AND phone = '13800000000';
结论:联合索引的字段,必须「从左到右连续匹配」,不能跳字段!
-
规则3:「字段顺序不影响」- and条件下,查询条件的顺序无关紧要
MySQL的查询优化器(Optimizer)会对
AND连接的查询条件做「自动重排」,最终会按照「联合索引的字段顺序」重新匹配。也就是说:**联合索引的字段顺序是 **(a,b,c)**,查询条件写 **where b=? and a=? and c=?,和写 **where a=? and b=? and c=?** 效果完全一样,索引都会完全生效。SQL-- 三个写法效果都是一样的 -- 写法1:按索引顺序写条件 → 索引完全生效 ✅ EXPLAIN SELECT * FROM User WHERE age=25 AND name='Jack' AND phone='13800000000'; -- 写法2:打乱顺序写条件 → 优化器重排后,和写法1一致,索引完全生效 ✅ EXPLAIN SELECT * FROM User WHERE name='Jack' AND phone='13800000000' AND age=25; -- 写法3:部分打乱 → 索引完全生效 ✅ EXPLAIN SELECT * FROM User WHERE age=25 AND phone='13800000000' AND name='Jack';
-
联合索引中「范围查询」的特殊规则(重中之重,高频考点)
这是最左前缀原则中最难、最容易踩坑、面试必问的核心知识点,没有之一!
在联合索引的匹配中:
遇到「范围查询」(
>、<、>=、<=、between...and)的字段后,该字段本身可以走索引,但是「该字段右侧的所有字段的索引都会直接失效」。⚠️ 补充:
=等值查询不会阻断后续字段的索引匹配,只有「范围查询」会!SQL-- 场景1:查询age > 20(范围) + name = 'Tom'(等值) → age走索引,name索引失效 -- 解析:age是范围查询,匹配出age>25的所有记录后,这些记录的name字段在B+树中是「无序的」,无法用索引匹配name EXPLAIN SELECT * FROM User WHERE age > 20 AND name = 'Tom';
SQL-- 场景2:查询age >= 20(范围) + name = 'Tom'(等值) → age走索引,name索引失效 EXPLAIN SELECT * FROM User WHERE age >= 20 AND name = 'Tom';
虽然 >= 查询时
key_len = 86,看起来像是用了 name 字段,但实际上这并不表示 name 被用于索引查找(Index Lookup),而是 MySQL 使用了Index Condition Pushdown(ICP)机制,导致 key_len 显示为更大值。正确理解是 :
当 age >= 20 且 name = 'Tom' 时,MySQL 尝试将 name = 'Tom' 条件"推入"到存储引擎层(ICP),即在索引扫描过程中就先过滤掉不符合 name 的行。这称为
Index Condition Pushdown (ICP)。- 在 ICP 中,MySQL 会把 name = 'Tom' 的条件传递给存储引擎
- 存储引擎在遍历索引 (age, name) 时,会检查 name 是否等于 'Tom'
- 如果满足,则返回该行;否则跳过
- 因此,name 字段被用于"索引扫描中的条件判断",所以 key_len 包含了 name 的长度
但注意:这 ≠ "右侧字段可走索引"。
"走索引"通常指"通过索引快速定位目标行",比如
ref或eq_ref类型。age >= 20 是范围扫描(
type = range)。name = 'Tom' 被用于 ICP 过滤,但不能加速定位,仍然需要扫描大量 age >= 20 的记录,并逐个判断 name 是否匹配,所以,name 没有真正"走索引",只是在索引扫描中提前过滤。如果能使用
>=就不使用>.同理between...and和>=原理相同SQL-- 场景3:age=25(等值) + name like 'T%'(范围-前缀匹配) + phone=13800000000 → age、name索引生效,phone索引失效 ❌ EXPLAIN SELECT * FROM User WHERE age = 25 AND name LIKE 'T%' AND phone = '13800000000';
phone = '13800000000' 在执行计划中会被用于索引过滤(通过 ICP),但它 不能用于索引查找(Index Lookup),即不能加速定位数据行。从"最左前缀 + 范围查询"规则看,它本质上已经"失效"了,只是被 ICP 优化了而已。
补充一个高频细节,和最左前缀原则强相关:
name like 'T%':前缀匹配 → 属于「有序范围查询」,能走联合索引的name字段;name like '%T':后缀匹配 → 无序,索引失效;name like '%T%':全模糊匹配 → 无序,索引失效。name like 'T':等值匹配 → 能走联合索引的name字段。
四、索引失效情况
用一个测试表+联合索引做案例,所有规则都基于这个环境,方便理解:
SQL
DROP TABLE IF EXISTS `User`;
-- 创建user表
CREATE TABLE `User` (
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`name` VARCHAR(50) NOT NULL COMMENT '姓名',
`phone` VARCHAR(20) NOT NULL COMMENT '手机号(字符串类型)',
`age` TINYINT UNSIGNED NOT NULL COMMENT '年龄',
`dept` VARCHAR(50) NOT NULL COMMENT '部门',
`position` VARCHAR(50) NOT NULL COMMENT '职位',
`salary` DECIMAL(10,2) NOT NULL COMMENT '薪资',
`status` TINYINT NOT NULL DEFAULT 0 COMMENT '状态(0-禁用,1-正常,2-待审核)',
`create_time` DATETIME NOT NULL COMMENT '创建时间',
`email` VARCHAR(100) NOT NULL COMMENT '邮箱',
`comment` VARCHAR(100) COMMENT '备注',
PRIMARY KEY (`id`) COMMENT '主键索引',
-- 单字段索引(对应单字段查询场景)
INDEX `idx_user_create_time` (`create_time`) COMMENT '创建时间索引,用于函数操作测试',
INDEX `idx_user_age` (`age`) COMMENT '年龄索引,用于运算测试',
INDEX `idx_user_phone` (`phone`) COMMENT '手机号索引,用于隐式类型转换测试',
INDEX `idx_user_status` (`status`) COMMENT '状态索引,用于非等值/负向查询测试',
INDEX `idx_user_name` (`name`) COMMENT '姓名索引,用于模糊匹配测试',
INDEX `idx_user_email` (`email`) COMMENT '邮箱索引,用于OR条件测试',
-- 联合索引(对应联合索引违规场景)
INDEX `idx_dept_pos` (`dept`, `position`, `salary`) COMMENT '部门-职位-薪资联合索引,测试最左前缀原则',
INDEX `idx_dept_pos_sal` (`dept`, `position`, `salary`) COMMENT '调整顺序后的联合索引,测试范围查询后列失效场景',
INDEX `idx_user_comment` (`comment`) COMMENT 'IS NULL和IS NOT NULL索引失效场景'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='索引失效测试核心表';
4.1 索引列使用函数
失效原因:对索引列包裹函数后,数据库需遍历所有索引值计算结果再匹配条件,无法借助索引有序性快速定位,等价于"破坏"索引原始结构。
示例 :用户表User的create_time列建索引,查询2024年1月注册用户,错误SQL:
sql
-- type = ALL
EXPLAIN SELECT * FROM User WHERE YEAR(create_time) = 2024 AND MONTH(create_time) = 1;

规避方案:避免对索引列用函数,将条件转换为索引列本身的范围判断:
sql
-- type = range
EXPLAIN SELECT * FROM User WHERE create_time BETWEEN '2024-01-01 00:00:00' AND '2024-01-31 23:59:59';

4.2 索引列参与运算
失效原因:索引列参与数学运算或逻辑运算时,数据库无法直接匹配索引值,需逐行计算后对比,导致索引失效。
示例 :User表age列建索引,查询年龄加5等于30的用户,错误SQL:
sql
-- type = ALL
EXPLAIN SELECT * FROM User WHERE age + 5 = 30;

规避方案:将运算逻辑转移到条件右侧,保留索引列原始形态:
sql
-- type = ref
EXPLAIN SELECT * FROM User WHERE age = 30 - 5;
-- 或
EXPLAIN SELECT * FROM User WHERE age = 25;

4.3 隐式类型转换
失效原因:查询条件值与索引列类型不匹配时,数据库会对索引列做隐式转换(等价于加函数处理),破坏索引有序性,是开发中极易忽略的场景。
示例 :User表phone列(字符串类型,存手机号)建索引,错误SQL(条件值为数字):
sql
-- type = ALL
EXPLAIN SELECT * FROM User WHERE phone = 13800138000;

规避方案:确保条件值与索引列类型完全一致,将数字转为字符串:
sql
-- type = ref
EXPLAIN SELECT * FROM User WHERE phone = '13800138000';

4.4 模糊匹配前缀为通配符
失效原因:B+树按索引列字符顺序存储,前缀为通配符(%开头)时,数据库无法确定查询起始位置,只能遍历所有索引值匹配。
示例 :User表name列建索引,错误SQL:
sql
EXPLAIN SELECT * FROM User WHERE name LIKE '%明%';
或 '%明'
sql
EXPLAIN SELECT * FROM User WHERE name LIKE '%明';

规避方案:
-
业务允许时用前缀匹配(通配符在末尾):
name LIKE '李%',索引可正常使用; -
需任意位置匹配时,用全文索引(如MySQL FULLTEXT)或搜索引擎(Elasticsearch)替代普通B+树索引。
4.5 OR连接非索引列条件
失效原因 :OR两侧条件若存在无索引列,数据库无法通过索引合并优化结果集,为减少开销会选择全表扫描。
示例 :id建索引,email无索引,错误SQL:
sql
-- type = ALL
EXPLAIN SELECT * FROM User WHERE id = 100 OR email = 'test@example.com';

规避方案:
-
为OR两侧列均建索引;
-
拆分为UNION ALL(推荐,避免冗余索引):
sql
EXPLAIN SELECT * FROM User WHERE id = 100 UNION ALL SELECT * FROM User WHERE email = 'test@example.com';

4.6 非等值判断与负向查询
失效原因:B+树索引擅长等值(=)和范围(BETWEEN、>、<)查询,而非等值判断(!=、<>)、负向查询(NOT IN、NOT LIKE)会导致优化器无法快速定位,若不符合条件的数据占比高,会选择全表扫描。
示例 :User表status列建索引,错误SQL:
sql
SELECT * FROM User WHERE status != 1; -- 或 NOT IN (1)
规避方案:
-
将非等值转为范围判断:
status < 1 OR status > 1(列无NULL值时); -
NOT IN改用LEFT JOIN替代:
SELECT t1.* FROM t1 LEFT JOIN t2 ON t1.id = t2.id WHERE t2.id IS NULL; -
结合高选择性范围条件(如时间、ID)缩小扫描范围,让索引生效。
4.7 is null和is not null的索引失效问题
失效原因:走索引还是不走索引,根数据分布有很大关系,如果符合条件的记录占比较大,会考虑使用全表扫描,而放弃走索引。
示例 :User表建一个索引idx_user_comment(comment),失效场景:
sql
-- 场景1:将User表的comment字段值全部更新为NULL
UPDATE SET comment = NULL;
-- 验证此时条件使用is null是否走索引:
-- 结论:不走索引
EXPLAIN SELECT * FROM USER WHERE comment IS NULL;
-- 验证此时条件使用is not null是否走索引:
-- 结论:走索引
EXPLAIN SELECT * FROM USER WHERE comment IS NOT NULL;
sql
-- 场景1:将User表的comment字段值全部更新为10001
UPDATE SET comment = "10001";
-- 验证此时条件使用is null是否走索引:
-- 结论:走索引
EXPLAIN SELECT * FROM USER WHERE comment IS NULL;
-- 验证此时条件使用is not null是否走索引:
-- 结论:不走索引
EXPLAIN SELECT * FROM USER WHERE comment IS NOT NULL;
4.8 联合索引使用违规导致失效
-
违反最左前缀原则:
失效原因:联合索引需从最左侧列开始使用,且不能跳过中间列,否则无法匹配索引的有序结构。
示例 :
User表建联合索引idx_dept_pos(dept, position, salary),失效场景:sqlSELECT * FROM User WHERE position = '工程师'; -- 跳过最左列dept SELECT * FROM User WHERE dept = '技术部' AND salary > 10000; -- 跳过中间列position规避方案:严格按最左前缀顺序设计查询条件,或调整索引列顺序(将高频查询列放左侧)。
-
范围查询后列失效:
失效原因:联合索引中,范围查询(>、<、BETWEEN)后的列会因索引有序性被破坏,无法继续使用索引。示例:基于上述联合索引,SQL如下:
sqlSELECT * FROM User WHERE dept = '技术部' AND salary > 10000 AND position = '工程师';规避方案 :调整索引列顺序,将范围查询列放最右侧,如
idx_dept_pos_sal(dept, position, salary)。
4.9 其他特殊场景导致失效
-
索引选择性过低:
失效原因:索引列区分度低(如性别列仅M/F),查询结果占表数据比例高时,优化器认为全表扫描比索引扫描更高效,会放弃索引。
规避方案 :不单独为低选择性列建索引,可组合其他列建联合索引提升选择性(如
idx_gender_age(gender, age))。 -
表数据量过小:
失效原因:表记录数极少时(如几百行),全表扫描的IO代价低于索引扫描+回表的代价,优化器会主动选择全表扫描。
说明:此为正常优化行为,无需强制干预。
-
JOIN关联字段不匹配:
失效原因:多表JOIN时,关联字段类型、长度、字符集/排序规则不一致,会触发隐式转换,导致索引失效。
示例 :
t1.user_id为INT,t2.user_id为BIGINT,JOIN时索引失效。规避方案:确保关联字段的类型、长度、字符集完全一致,数据库设计阶段统一规范。
-
IN列表值过多:
失效原因:MySQL对IN列表的优化阈值约为1000个值,超过阈值后,优化器认为遍历索引成本高于全表扫描,会放弃索引。
规避方案:拆分IN列表为多个小列表(用UNION ALL),或用临时表+JOIN替代。