索引是 MySQL 性能优化的核心,但建了索引≠能用上索引。本文从数据库优化方案、EXPLAIN 执行计划详解、海量数据构造、索引失效场景、关联查询 / 排序 / 分组优化等维度,手把手教你搞定索引优化,让 SQL 查询起飞。
一、数据库整体优化方案
数据库性能瓶颈通常来自这几类,对应解决方案一目了然:
- 索引失效 :没有合理创建 / 使用索引 → 索引优化
- 关联过多 JOIN :表设计或查询不合理 → SQL 语句优化
- 数据量过大 (单表 500W+/2GB+)→ 分库分表
- 服务器配置不合理 → 调整 my.cnf 参数(缓冲、线程数等)
二、性能分析神器:EXPLAIN 执行计划
2.1 什么是 EXPLAIN
使用EXPLAIN + SQL语句可以模拟 MySQL 优化器执行查询,清晰看到 SQL 的执行路径、索引使用情况、性能瓶颈,是索引优化的必备工具。
2.2 测试数据准备
USE atguigudb;
-- 创建测试表
CREATE TABLE t1(id INT AUTO_INCREMENT, content VARCHAR(100), PRIMARY KEY(id));
CREATE TABLE t2 LIKE t1;
CREATE TABLE t3 LIKE t1;
CREATE TABLE t4(id INT AUTO_INCREMENT, content1 VARCHAR(100), content2 VARCHAR(100), PRIMARY KEY(id));
-- 创建普通索引
CREATE INDEX idx_content1 ON t4(content1);
-- 插入测试数据
INSERT INTO t1(content) VALUES(CONCAT('t1_',FLOOR(1+RAND()*1000)));
2.3 EXPLAIN 关键字段详解
1. id
表示查询中SELECT的执行顺序,是优化核心:
- id 相同:从上往下顺序执行
- id 不同:id 越大,优先级越高,越先执行
- id 为 NULL :执行
UNION合并结果集
2. table
表示当前行数据对应的数据表,多表查询中可区分驱动表 和被驱动表。
3. type(★核心优化目标)
查询访问类型,性能从好到坏 :system > const > eq_ref > ref > range > index > ALL
✅ 优化目标 :至少达到range,推荐ref,力争const
ALL:全表扫描(最差)index:全索引扫描range:索引范围查询(between、>、<、in)ref:普通索引等值匹配eq_ref:多表关联,主键 / 唯一索引匹配const:主键 / 唯一索引匹配常量system:单表一行数据(最优)
4. possible_keys & key
possible_keys:可能使用的索引key:实际使用的索引(为 NULL 表示未使用索引)
5. key_len
索引使用的字节数,联合索引中值越大,说明索引利用越充分。计算规则:字段长度 + 字符集字节 + 动态字符串 2 字节 + 允许 NULL1 字节。
6. rows
MySQL 预估需要扫描的行数,值越小越好。
7. Extra(★关键额外信息)
Using where:服务器层过滤数据,未充分利用索引Using filesort:文件排序,性能极差,需优化Using index:覆盖索引,无需回表,性能优秀Using index condition:索引下推,减少回表次数Using join buffer:关联查询未使用索引,使用缓存优化
三、海量测试数据构造(50W 员工 + 1W 部门)
优化必须基于真实数据量,通过函数 + 存储过程快速造数:
3.1 创建表
-- 部门表
CREATE TABLE dept(
id INT PRIMARY KEY AUTO_INCREMENT,
deptName VARCHAR(30),
address VARCHAR(40),
ceo INT
);
-- 员工表
CREATE TABLE emp(
id INT PRIMARY KEY AUTO_INCREMENT,
empno INT NOT NULL,
name VARCHAR(20),
age INT,
deptId INT
);
3.2 创建随机函数
-- 开启函数创建权限
SET GLOBAL log_bin_trust_function_creators=1;
-- 随机字符串
DELIMITER $$
CREATE FUNCTION rand_string(n INT) RETURNS VARCHAR(255)
BEGIN
DECLARE chars_str VARCHAR(100) DEFAULT 'abcdefghijklmnopqrstuvwxyzABCDEFJHIJKLMNOPQRSTUVWXYZ';
DECLARE return_str VARCHAR(255) DEFAULT '';
DECLARE i INT DEFAULT 0;
WHILE i < n DO
SET return_str=CONCAT(return_str,SUBSTRING(chars_str,FLOOR(1+RAND()*52),1));
SET i=i+1;
END WHILE;
RETURN return_str;
END $$
-- 随机数字
DELIMITER $$
CREATE FUNCTION rand_num(from_num INT,to_num INT) RETURNS INT
BEGIN
DECLARE i INT DEFAULT 0;
SET i=FLOOR(from_num+RAND()*(to_num-from_num+1));
RETURN i;
END $$
3.3 创建存储过程并插入数据
-- 插入员工数据(50W条)
DELIMITER $$
CREATE PROCEDURE insert_emp(START INT,max_num INT)
BEGIN
DECLARE i INT DEFAULT 0;
SET autocommit=0;
REPEAT
SET i=i+1;
INSERT INTO emp(empno,name,age,deptid) VALUES((START+i),rand_string(6),rand_num(30,50),rand_num(1,10000));
UNTIL i=max_num END REPEAT;
COMMIT;
END $$
-- 插入部门数据(1W条)
DELIMITER $$
CREATE PROCEDURE insert_dept(max_num INT)
BEGIN
DECLARE i INT DEFAULT 0;
SET autocommit=0;
REPEAT
SET i=i+1;
INSERT INTO dept(deptname,address,ceo) VALUES(rand_string(8),rand_string(10),rand_num(1,500000));
UNTIL i=max_num END REPEAT;
COMMIT;
END $$
-- 执行存储过程
CALL insert_dept(10000);
CALL insert_emp(100000,500000);
3.4 辅助工具
-- 批量删除表索引
CALL proc_drop_index("库名","表名");
-- 开启SQL执行时间统计
SET profiling=1;
SHOW PROFILES;
四、单表索引失效 10 大场景(★高频面试 + 实战)
建了索引却失效,90% 是踩了这些坑:
1. 索引列上使用计算、函数
-- 生效
EXPLAIN SELECT * FROM emp WHERE name LIKE 'abc%';
-- 失效:LEFT函数导致索引失效
EXPLAIN SELECT * FROM emp WHERE LEFT(name,3)='abc';
2. LIKE 以 % 开头(左模糊 / 全模糊)
-- 失效
EXPLAIN SELECT * FROM emp WHERE name LIKE '%ab%';
✅ 规范:严禁使用左模糊,需求改用搜索引擎(ES)。
3. 使用!= / <> 不等于
-- 失效
EXPLAIN SELECT * FROM emp WHERE name<>'abc';
4. IS NOT NULL 多数场景失效
-- 失效
EXPLAIN SELECT * FROM emp WHERE name IS NOT NULL;
特殊:当 NULL 值占比极高时,优化器可能会使用索引。
5. 类型隐式转换
-- 字符串列传入数字,索引失效
EXPLAIN SELECT * FROM emp WHERE name=123;
6. 联合索引:违反最左前缀法则
索引idx_age_deptid_name(age,deptid,name):
-- 只用到age索引,跳过deptid,name失效
EXPLAIN SELECT * FROM emp WHERE age=30 AND name='abcd';
-- 完全失效:缺少带头大哥age
EXPLAIN SELECT * FROM emp WHERE deptid=1 AND name='abcd';
7. 联合索引:范围条件右边列失效
sql
-- 索引 idx_age_deptid_name
-- deptId是范围条件,后面的name索引失效
EXPLAIN SELECT * FROM emp WHERE age=30 AND deptId>1000 AND name='abc';
✅ 优化:范围条件放在联合索引最后。
8. OR 连接非索引列
OR 前后存在未索引列,整体索引失效。
9. 数据区分度极低
重复值极高的列(如性别),建索引无意义。
10. 查询优化器判断全表更快
数据量极小时,优化器会放弃索引,选择全表扫描。
五、多表关联查询优化
核心原则
- 索引建在被驱动表的关联字段上
- 小表驱动大表,减少循环次数
- 关联字段数据类型必须完全一致
- 优先使用 JOIN,替代子查询
左外连接优化
-- 左连接:class是驱动表,book是被驱动表
EXPLAIN SELECT * FROM class LEFT JOIN book ON class.card=book.card;
✅ 关键:给被驱动表 book.card建索引,性能提升最大。
内连接优化
MySQL 优化器会自动选择小表作为驱动表,无需手动指定顺序。
六、子查询优化
严禁使用NOT IN / NOT EXISTS,改用 LEFT JOIN 替代:
-- 低效:NOT IN
SELECT * FROM emp WHERE id NOT IN (SELECT ceo FROM dept WHERE ceo IS NOT NULL);
-- 高效:LEFT JOIN + IS NULL
SELECT emp.* FROM emp LEFT JOIN dept ON emp.id=dept.ceo WHERE dept.id IS NULL;
七、排序(ORDER BY)优化
核心目标:消除Using filesort
排序使用索引的三大铁律:
- 无过滤,不索引:ORDER BY 必须搭配 WHERE 条件 / LIMIT
- 顺序错,不索引:排序顺序必须和联合索引一致
- 方向反,不索引:排序字段升降序必须统一
filesort 算法优化
- 双路排序:两次磁盘 IO,效率低
- 单路排序:一次磁盘 IO,占用内存大
✅ 优化方案:
- 禁止
SELECT *,只查询业务需要的字段 - 适当调大
sort_buffer_size(1M-8M) - 合理设置
max_length_for_sort_data
八、分组(GROUP BY)优化
- 规则与 ORDER BY 基本一致
- GROUP BY无需过滤条件也能使用索引
- 分组前 WHERE 过滤结果集尽量≤1000 行
九、覆盖索引优化
✅ 核心:查询的字段都在索引中,无需回表
- 禁止
SELECT * - 联合索引覆盖查询字段,减少回表 IO
十、索引优化万能口诀
全值匹配我最爱,最左前缀要遵守;
带头大哥不能死,中间兄弟不能断;
索引列上少计算,范围之后全失效;
like百分写最右,覆盖索引不写*;
不等空值还有or,索引失效要少用;
var引号不能丢,SQL高级也不难;