Mysql

关注公众号 jb51net

关闭
首页 > 数据库 > Mysql > mysql索引优化

MySQL 索引优化实战指南(从慢查询到高性能)

作者:小北方城市网

本文详细讲解了MySQL索引的核心原理、创建原则、失效场景以及优化策略,通过SQL示例和执行计划分析,帮助开发者设计高效索引,提升查询性能,感兴趣的朋友跟随小编一起看看吧

MySQL 作为主流关系型数据库,索引是提升查询性能的核心手段。多数开发者仅会创建基础索引,但对索引原理、创建原则、失效场景、慢查询优化理解不足,导致索引 “形同虚设”,甚至因索引设计不当引发性能问题(如索引冗余、写入变慢)。

本文从索引核心原理出发,讲解索引类型、创建原则、失效场景、慢查询分析、分场景优化方案,结合 SQL 示例与执行计划分析,帮你避开索引坑,设计出高效索引,将慢查询优化为毫秒级响应。

一、核心认知:索引的价值与底层原理

1. 核心价值

2. 底层原理(B + 树索引)

MySQL 中最常用的索引类型是 B + 树索引(主键索引、二级索引均基于 B + 树实现),其结构特点决定了高效查询:

3. 索引类型与适用场景

(1)主键索引(聚簇索引,Primary Key)

(2)二级索引(非聚簇索引,Secondary Index)

(3)其他索引类型

二、实战:索引创建原则与最佳实践

1. 核心创建原则(避免无效索引)

(1)高频查询字段优先创建索引

(2)联合索引遵循 “最左前缀原则”

(3)避免创建冗余索引

(4)低频查询、低区分度字段不创建索引

(5)避免对大字段创建索引

2. 分场景索引设计示例

(1)单表查询优化

业务场景SQL 语句索引设计备注
按订单号精确查询SELECT * FROM order WHERE order_no = 'OD123'普通索引idx_order_no (order_no)精确匹配,单字段索引足够
按用户 ID + 时间范围查询SELECT * FROM order WHERE user_id = 123 AND create_time BETWEEN '2024-05-01' AND '2024-05-31'联合索引idx_user_create (user_id, create_time)遵循最左前缀,时间范围放在后面
按状态排序查询SELECT * FROM order WHERE status = 1 ORDER BY create_time DESC联合索引idx_status_create (status, create_time DESC)索引包含排序字段,避免额外排序

(2)多表联查优化

SELECT u.id, u.user_name, o.order_no, o.create_time
FROM user u
LEFT JOIN order o ON u.id = o.user_id
WHERE u.id = 123
ORDER BY o.create_time DESC;
  1. user表主键索引(默认存在,id为主键);
  2. order表创建联合索引idx_user_create (user_id, create_time DESC)(关联字段user_id在前,排序字段在后);

(3)分页查询优化

-- 慢查询:LIMIT offset过大,需扫描前10000条数据,再取10条
SELECT * FROM order ORDER BY create_time DESC LIMIT 10000, 10;
  1. 创建索引idx_create_id (create_time DESC, id)
  2. 优化 SQL:
    SELECT o.* FROM order o
    WHERE o.id < (SELECT id FROM order ORDER BY create_time DESC LIMIT 10000, 1)
    ORDER BY create_time DESC LIMIT 10;
    

三、关键:索引失效场景与避坑

索引创建后若使用不当,会导致索引失效,触发全表扫描,需重点规避以下场景:

1. 场景 1:索引字段使用函数或运算

SELECT * FROM user WHERE LEFT(user_name, 3) = '张'; -- 函数操作索引字段
SELECT * FROM order WHERE create_time + INTERVAL 1 DAY > NOW(); -- 运算操作索引字段
SELECT * FROM user WHERE user_name LIKE '张%'; -- 前缀匹配,索引有效
SELECT * FROM order WHERE create_time > NOW() - INTERVAL 1 DAY;

2. 场景 2:模糊查询以 “%” 开头

SELECT * FROM user WHERE user_name LIKE '%三'; -- %开头,索引失效

3. 场景 3:索引字段存在隐式转换

SELECT * FROM user WHERE user_phone = 13800138000; -- 隐式转换,索引失效
SELECT * FROM user WHERE user_phone = '13800138000';

4. 场景 4:联合索引不满足最左前缀原则

SELECT * FROM order WHERE create_time BETWEEN '2024-05-01' AND '2024-05-31'; -- 仅用第二个字段,索引失效

5. 场景 5:OR 连接非索引字段

SELECT * FROM user WHERE user_name = '张三' OR phone = '13800138000'; -- 索引失效

6. 场景 6:WHERE 条件恒成立 / 恒不成立

SELECT * FROM user WHERE 1 = 1; -- 恒成立,索引失效,全表扫描
SELECT * FROM user WHERE id = 123 AND 1 = 0; -- 恒不成立,索引失效

四、慢查询分析工具与流程

1. 开启慢查询日志(定位慢查询)

MySQL 默认关闭慢查询日志,需手动开启,记录执行时间超过阈值(默认 1 秒)的 SQL:

-- 临时开启(重启MySQL失效)
SET GLOBAL slow_query_log = ON; -- 开启慢查询日志
SET GLOBAL long_query_time = 1; -- 慢查询阈值(单位:秒)
SET GLOBAL slow_query_log_file = '/var/lib/mysql/slow.log'; -- 日志存储路径
-- 永久开启(修改my.cnf配置文件,重启生效)
[mysqld]
slow_query_log = ON
long_query_time = 1
slow_query_log_file = /var/lib/mysql/slow.log
log_queries_not_using_indexes = ON -- 记录未使用索引的查询

2. 分析慢查询日志(EXPLAIN 执行计划)

通过EXPLAIN关键字分析 SQL 执行计划,判断索引是否生效、是否全表扫描:

(1)EXPLAIN 使用方法

在 SQL 前加EXPLAIN,示例:

EXPLAIN SELECT * FROM order WHERE user_id = 123 AND create_time BETWEEN '2024-05-01' AND '2024-05-31';

(2)核心字段解读(判断索引是否生效)

3. 慢查询优化流程

  1. 开启慢查询日志,收集慢查询 SQL;
  2. 对慢查询 SQL 执行EXPLAIN,分析执行计划,定位问题(索引失效、全表扫描、额外排序等);
  3. 优化索引(创建新索引、调整联合索引顺序、删除冗余索引);
  4. 改写 SQL(避免索引失效场景、优化分页逻辑);
  5. 验证优化效果(重新执行EXPLAIN,对比扫描行数、执行时间)。

五、避坑指南

1. 坑点 1:索引越多越好

2. 坑点 2:主键用 UUID 而非自增 ID

3. 坑点 3:忽略覆盖索引(避免回表)

-- 查询字段:user_id、create_time、order_no
-- 覆盖索引:idx_user_create_no (user_id, create_time, order_no)
SELECT user_id, create_time, order_no FROM order WHERE user_id = 123;

4. 坑点 4:索引字段允许 NULL 值

5. 坑点 5:未定期维护索引

六、终极总结:索引优化的核心是 “精准与平衡”

MySQL 索引优化不是 “盲目创建索引”,而是在 “查询性能” 与 “写入性能” 之间寻找平衡 —— 既要通过索引加速查询,又要控制索引数量,降低写入维护成本。

核心原则总结:

  1. 索引设计贴合业务:基于高频查询场景设计,避免无意义索引;
  2. 规避失效场景:熟练掌握索引失效规则,改写 SQL 避免触发;
  3. 善用工具:通过慢查询日志、EXPLAIN 定位问题,验证优化效果;
  4. 持续维护:定期清理冗余索引、优化索引碎片,适配业务迭代。

记住:最好的索引不是 “最复杂的”,而是 “最贴合业务、最高效、维护成本最低的”。

MySQL 索引失效场景速查表

常见失效场景分类,含错误 SQL 示例核心失效原因可落地解决方案,附关键备注,快速避坑、优化 SQL。

失效场景(错误 SQL 示例)核心失效原因解决方案(优化 SQL / 索引)关键备注
索引字段做函数 / 运算SELECT * FROM user WHERE LEFT(name,3)='张'SELECT * FROM order WHERE create_time + 1 DAY > NOW()函数 / 算术运算会修改索引字段的原始值,MySQL 无法利用 B + 树索引的有序性做快速查找1. 改写 SQL,将函数 / 运算移到查询条件右侧✅ 优化后:SELECT * FROM user WHERE name LIKE '张%'SELECT * FROM order WHERE create_time > NOW()-1 DAY2. 若无法改写,考虑生成列索引(MySQL 5.7+)前缀匹配LIKE '张%'可命中索引,属于特例
模糊查询以 % 开头SELECT * FROM user WHERE name LIKE '%三'SELECT * FROM user WHERE name LIKE '%张%'%开头会破坏索引的有序性,MySQL 无法通过索引定位,只能全表扫描1. 业务允许则改为前缀匹配LIKE '张%')2. 文本模糊查询用全文索引FULLTEXT INDEX)3. 大数据量文本查询,改用 Elasticsearch全文索引适用于TEXT/VARCHAR大字段,替代低效的 % 模糊查询
索引字段存在隐式类型转换SELECT * FROM user WHERE phone=13800138000(phone 为 VARCHAR 类型)MySQL 会对索引字段做隐式转换(如CAST(phone AS UNSIGNED)),转换后字段脱离索引,触发全表扫描1. 保证查询参数与字段类型一致✅ 优化后:SELECT * FROM user WHERE phone='13800138000'2. 统一数据库字段与业务代码的参数类型最易踩坑的场景之一,多发生在字符串 / 数字类型混用
联合索引不满足最左前缀原则联合索引(user_id, create_time)SELECT * FROM order WHERE create_time BETWEEN '2024-01-01' AND '2024-12-31'联合索引的生效规则为从左到右连续匹配,跳过最左字段,后续字段无法命中索引1. 补充最左前缀字段到查询条件2. 若该字段查询频繁,单独创建索引3. 调整联合索引字段顺序(将高频查询字段放左侧)联合索引设计原则:区分度高的字段放前,高频查询字段放前
OR 连接非索引字段SELECT * FROM user WHERE name='张三' OR age=25(name 有索引,age 无索引)OR 的查询逻辑为 “任一满足即可”,若存在非索引字段,MySQL 无法通过索引过滤,直接触发全表扫描1. 为所有 OR 连接的字段创建索引2. 改用UNION/UNION ALL拆分查询(替代 OR)✅ 优化后:SELECT * FROM user WHERE name='张三' UNION ALL SELECT * FROM user WHERE age=25UNION 会去重(性能略低),UNION ALL 不查重(性能更高,确认无重复时用)
IS NULL/IS NOT NULL查询低区分度索引字段SELECT * FROM user WHERE email IS NULL(email 为索引字段,大量 NULL 值)索引对 NULL 值的过滤效率极低,若 NULL 值占比高,查询效率不如全表扫描1. 索引字段设置NOT NULL,用默认值(如空字符串)替代 NULL2. 若必须存 NULL,改用全表扫描(强制FORCE INDEX()反而更慢)设计规范:索引字段尽量设置为 NOT NULL,从根源避免该问题
分页查询LIMIT offset 过大SELECT * FROM order ORDER BY create_time DESC LIMIT 10000,10offset 过大时,MySQL 需扫描前 N 条数据并丢弃,仅返回最后 10 条,全表扫描成本高1. 利用主键 / 唯一索引定位分页起点,避免全表扫描✅ 优化后:SELECT o.* FROM order o WHERE o.id < (SELECT id FROM order ORDER BY create_time DESC LIMIT 10000,1) ORDER BY create_time DESC LIMIT 102. 业务上限制最大分页页数(如最多支持 100 页)核心思路:将偏移量分页改为主键定位分页,利用索引减少扫描行数
联合索引中字段顺序与排序 / 过滤矛盾联合索引(user_id, create_time)SELECT * FROM order WHERE user_id>100 ORDER BY create_time DESC范围查询(>/<)后的索引字段无法用于排序 / 分组,只能全表排序1. 调整联合索引顺序,将排序字段放范围字段前(若排序更频繁)2. 为排序字段单独创建索引3. 用覆盖索引减少回表成本联合索引中:等值查询字段放前,范围查询字段放中,排序字段放最后
NOT IN/NOT EXISTS查询索引字段SELECT * FROM user WHERE id NOT IN (1,2,3)MySQL 对NOT IN的索引支持极差,会默认走全表扫描,替代方案效率更高1. 改用LEFT JOIN + IS NULL替代✅ 优化后:SELECT u.* FROM user u LEFT JOIN temp t ON u.id=t.id WHERE t.id IS NULL2. 改用<>()逐个排除(少量值时)NOT EXISTSNOT IN效率略高,但仍不如LEFT JOIN

补充:索引生效的「黄金规则」

  1. 索引字段直接作为查询条件,不做任何函数 / 运算 / 转换;
  2. 联合索引遵循最左前缀原则,查询条件包含从左到右的连续字段;
  3. 模糊查询仅前缀匹配LIKE 'xxx%')可命中索引;
  4. 查询参数与索引字段类型严格一致,避免隐式转换;
  5. OR 连接的字段全部创建索引,否则整体失效。

快速排查技巧

  1. EXPLAIN分析 SQL,type=ALL 表示全表扫描(索引失效);
  2. 关注Extra字段:Using filesort(额外排序)、Using temporary(临时表)均需优化;
  3. key=NULL,说明未使用任何索引,优先检查上述失效场景。

到此这篇关于MySQL 索引优化实战指南(从慢查询到高性能)的文章就介绍到这了,更多相关mysql索引优化内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

您可能感兴趣的文章:
阅读全文