MySQL 索引优化(一)

索引创建原则

  1. 先写代码,再建索引
    不应该在创建完表之后立马就创建索引,等主体业务开发完毕以后,再把涉及到该表的 sql 分析过后再建立索引。
  2. 联合索引尽量覆盖查询条件
    在设计一个联合索引的时候,让联合索引尽可能的包含 sql 语句中的 whereorder bygroup by 的字段,还要确保这些字段尽量满足 sql 最左前缀原则。
  3. 选取索引字段选值比较多、区分度高的字段
    比如我有一张用户表,它有一个性别字段,只包含男女两种值,这就没有办法进行快速的二分查找,不如全表扫描,索引也就失去意义了。建立索引,尽量选取值比较多的字段,才能更好的发挥 B+ 树二分查找的优势。
  4. 长字符串可以采用前缀索引
    选取索引尽量对索引字段类型较小的列设计索引,字段小,占用磁盘空间也小,搜索的时候性能也会好一些。如果需要给长字符串建立索引,比如 varchar(255),比如这时就可以针对这个字段的前 20 歌字符建立索引,也就是说,把这个字段每个值的前面 20 位放到索引树里,比如 KEY index(name(20),age,position)。
  5. where 和 order by 冲突时优先 where
    在 where 和 order by 出现索引设计冲突时,优先让 where 条件去使用索引快速筛选出一部分数据,再进行排序。因为大多数情况下,基于索引进行 where 筛选往往可以最快速筛选出需要的少部分数据,再进行排序的成本可能就会降低很多。
  6. 数据量足够大的情况下再建立索引
    一般单表超过十万条数据以后,为了改善用户体验,再建立索引。

索引优化案例演示

示例表结构和数据

CREATE TABLE `student` (
	`id` INT(11) NOT NULL AUTO_INCREMENT,
	`name` VARCHAR(24) NOT NULL DEFAULT '' COMMENT '姓名',
	`age` INT(11) NOT NULL DEFAULT '0' COMMENT '年龄',
	`school` VARCHAR(20) NOT NULL DEFAULT '' COMMENT '学校名',
	`start_time` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '入学时间',
	PRIMARY KEY (`id`),
	KEY `idx_name_age_school` (`name`,`age`,`school`) USING BTREE
) ENGINE=INNODB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 COMMENT='学生表';

INSERT INTO student(NAME,age,school,start_time) VALUES('老大',11,'老大小学',NOW());
INSERT INTO student(NAME,age,school,start_time) VALUES('老二', 12,'老大小学',NOW());
INSERT INTO student(NAME,age,school,start_time) VALUES('老三',13,'老大小学',NOW());


DROP PROCEDURE IF EXISTS insert_student;
DELIMITER ;;
CREATE PROCEDURE insert_student()
BEGIN
	DECLARE i INT;
	SET i=1;
	WHILE(i<=100000)DO
		INSERT INTO student(NAME,age,school) VALUES(CONCAT('老',i),i,'老大小学');
		SET i=i+1;
	END WHILE;
END;;
DELIMITER ;
CALL insert_student();

覆盖查询优化

第一个例子,联合索引的首个字段用范围查询不会走索引

EXPLAIN SELECT * FROM student WHERE NAME > '老1' AND age = 11 AND school='老大小学';

    id  select_type  table    partitions  type    possible_keys          key     key_len  ref       rows  filtered  Extra        
------  -----------  -------  ----------  ------  ---------------------  ------  -------  ------  ------  --------  -------------
     1  SIMPLE       student  (NULL)      ALL     idx_name_age_position  (NULL)  (NULL)   (NULL)   97664      0.50  Using where  

从上面的输出结果可以看得到,没有使用索引,mysql 内部可能判断结果集很大,回表效率不高,直接使用全表扫描,我们再来看强制使用索引的情况:

EXPLAIN SELECT * FROM student FORCE INDEX(idx_name_age_school) WHERE NAME > '老1' AND age = 11 AND school='老大小学';

    id  select_type  table    partitions  type    possible_keys        key                  key_len  ref       rows  filtered  Extra                  
------  -----------  -------  ----------  ------  -------------------  -------------------  -------  ------  ------  --------  -----------------------
     1  SIMPLE       student  (NULL)      range   idx_name_age_school  idx_name_age_school  74       (NULL)   48832      1.00  Using index condition  

看似结果集数量少了,但是回表查询效率不高,所以最重的查询效率不一定比全表扫描高,可以自己尝试一下这两条语句,看看执行时间的区别,我就不截图了。

使用覆盖索引查询优化那条查询语句:

EXPLAIN SELECT NAME, age, school FROM student WHERE NAME > '老1' AND age = 11 AND school='老大小学';

    id  select_type  table    partitions  type    possible_keys        key                  key_len  ref       rows  filtered  Extra                     
------  -----------  -------  ----------  ------  -------------------  -------------------  -------  ------  ------  --------  --------------------------
     1  SIMPLE       student  (NULL)      range   idx_name_age_school  idx_name_age_school  74       (NULL)   48832      1.00  Using where; Using index  

可以看到我们使用了 idx_name_age_school 索引,查询的数目也少了,extra 列也显示出使用了索引,提高了查询速度。

like kk%一般会使用索引

EXPLAIN SELECT * FROM student_copy WHERE NAME LIKE '老%' AND age = 11 AND school='老大小学';

    id  select_type  table         partitions  type    possible_keys        key                  key_len  ref       rows  filtered  Extra                  
------  -----------  ------------  ----------  ------  -------------------  -------------------  -------  ------  ------  --------  -----------------------
     1  SIMPLE       student_copy  (NULL)      range   idx_name_age_school  idx_name_age_school  140      (NULL)       3     33.33  Using index condition  

可以看到在 key 的字段里,使用了索引,这个主要是用到了索引下推优化。

正常联合索引(name, age, school)是按照最左原则,上面这个查询语句会查询 name 字段的索引,根据 name 字段过滤完,得到的索引行里的 age 和 school 是无序的,没有办法很好的利用。MySQL 5.6 之前的版本,这个查询语句只能查询名字是开头的索引,然后拿着这些索引的主键一个个回表,到主键索引上找出相应的记录,再对比 age 和 school 这两个字段是否符合。

MySQL 5.6 引入索引下推,可以在遍历索引过程中,对索引中包含的所有字段先判断,过滤掉不符合的记录后再进行回表 ,能够有效的减少回表次数。使用索引下推优化后,上面那个查询在联合索引里匹配开头的索引之后,同时也会在索引里过滤 age 和 school 两个字段,拿着过滤完剩下的索引对应的主键 id 再回表查整行数据。

Order by 的优化

EXPLAIN SELECT * FROM student WHERE NAME = '老1' AND school = '老大小学' ORDER BY age;

    id  select_type  table    partitions  type    possible_keys        key                  key_len  ref       rows  filtered  Extra                  
------  -----------  -------  ----------  ------  -------------------  -------------------  -------  ------  ------  --------  -----------------------
     1  SIMPLE       student  (NULL)      ref     idx_name_age_school  idx_name_age_school  74       const        1     10.00  Using index condition  

利用最左前缀原则,查询匹配的条件中并不包含 age 字段,只有 name 和 school 两个条件,key_len=74,可以看到 Extra 字段的结果是 Using index condition 而不是 Using filesort。

EXPLAIN SELECT * FROM student WHERE NAME = '老1' ORDER BY school;

    id  select_type  table    partitions  type    possible_keys        key                  key_len  ref       rows  filtered  Extra           
------  -----------  -------  ----------  ------  -------------------  -------------------  -------  ------  ------  --------  ----------------
     1  SIMPLE       student  (NULL)      ref     idx_name_age_school  idx_name_age_school  74       const        1    100.00  Using filesort  

看结果 key_len=74,查询使用了 name 索引,由于用了 school 进行排序,跳过了age,extra 结果 Using filesort。

对比下一条语句

EXPLAIN SELECT * FROM student WHERE NAME = '老1' ORDER BY age, school;

    id  select_type  table    partitions  type    possible_keys        key                  key_len  ref       rows  filtered  Extra   
------  -----------  -------  ----------  ------  -------------------  -------------------  -------  ------  ------  --------  --------
     1  SIMPLE       student  (NULL)      ref     idx_name_age_school  idx_name_age_school  74       const        1    100.00  (NULL)                                                                                                                                        

查询只用了索引 name,age 和 school 用于排序,没有 Using filesort。

将 age 和 school 调换

EXPLAIN SELECT * FROM student WHERE NAME = '老1' ORDER BY school, age;

    id  select_type  table    partitions  type    possible_keys        key                  key_len  ref       rows  filtered  Extra           
------  -----------  -------  ----------  ------  -------------------  -------------------  -------  ------  ------  --------  ----------------
     1  SIMPLE       student  (NULL)      ref     idx_name_age_school  idx_name_age_school  74       const        1    100.00  Using filesort  

又出现了 Using filesort,因为索引的创建顺序是 name,age,school,但是排序的顺序颠倒了,不符合最左前缀原则, 又使用 Using filesort。

EXPLAIN SELECT * FROM student WHERE NAME = '老1' AND age = 11 ORDER BY school, age;

    id  select_type  table    partitions  type    possible_keys        key                  key_len  ref            rows  filtered  Extra   
------  -----------  -------  ----------  ------  -------------------  -------------------  -------  -----------  ------  --------  --------
     1  SIMPLE       student  (NULL)      ref     idx_name_age_school  idx_name_age_school  78       const,const       1    100.00  (NULL)                                                                                                                                          

虽然排序的字段与索引顺序还是不一样,但 age 是常量,在排序中被优化了,所以索引未颠倒,不会出现 Using filesort。

EXPLAIN SELECT * FROM student WHERE NAME = '老1' ORDER BY age ASC, school DESC;

    id  select_type  table    partitions  type    possible_keys        key                  key_len  ref       rows  filtered  Extra           
------  -----------  -------  ----------  ------  -------------------  -------------------  -------  ------  ------  --------  ----------------
     1  SIMPLE       student  (NULL)      ref     idx_name_age_school  idx_name_age_school  74       const        1    100.00  Using filesort  

虽然排序的字段和索引顺序一样了,但在排序的时候 age 正序,school 倒序,这与索引中的排序方式不同,从而产生 Using file sort。

EXPLAIN SELECT * FROM student WHERE NAME > '老1' ORDER BY NAME;

    id  select_type  table    partitions  type    possible_keys        key     key_len  ref       rows  filtered  Extra                        
------  -----------  -------  ----------  ------  -------------------  ------  -------  ------  ------  --------  -----------------------------
     1  SIMPLE       student  (NULL)      ALL     idx_name_age_school  (NULL)  (NULL)   (NULL)   97664     50.00  Using where; Using filesort  

使用范围查找的时候,这种情况可以通过使用覆盖索引优化,避免 Using filesort

EXPLAIN SELECT NAME, age, school FROM student WHERE NAME > '老1' ORDER BY NAME;

    id  select_type  table    partitions  type    possible_keys        key                  key_len  ref       rows  filtered  Extra                     
------  -----------  -------  ----------  ------  -------------------  -------------------  -------  ------  ------  --------  --------------------------
     1  SIMPLE       student  (NULL)      range   idx_name_age_school  idx_name_age_school  74       (NULL)   48832    100.00  Using where; Using index  

Group by 和 Order by 类似的,以上的情况都差不多,实质是先排序后分组 ,遵照索引创建顺序的最左前缀原则。对于 Group by 的优化如果不需要排序的可以加上 Order by null 禁止排序。能写在 where 中的不要用 having。

order by 总结

MySQL 支持用两种方式排序,filesort 和 index。Using index 指的是只需要扫描索引本身就能完成排序。index效率高,filesort 效率低。

order by 满足两种情况会使用 Using index。

  1. order by 语句满足最左前缀原则。
  2. where 子句和 order by 子句组合使用最左前缀原则。
    尽量在索引列上完成排序,遵循索引建立时 的最左前缀原则。
    能用覆盖索引尽量用覆盖索引。

总结

这篇文章包含了建立索引的原则,联合索引查询优化,like XX% 类型查询以及 order by、group by 查询的优化。最重要的部分还是最左前缀原则的理解,如果把最左前缀原则理解通透,这些优化都会融会贯通。下一篇继续索引优化。

相关推荐
明月看潮生19 分钟前
青少年编程与数学 02-007 PostgreSQL数据库应用 11课题、视图的操作
数据库·青少年编程·postgresql·编程与数学
阿猿收手吧!27 分钟前
【Redis】Redis入门以及什么是分布式系统{Redis引入+分布式系统介绍}
数据库·redis·缓存
奈葵30 分钟前
Spring Boot/MVC
java·数据库·spring boot
leegong2311138 分钟前
Oracle、PostgreSQL该学哪一个?
数据库·postgresql·oracle
中东大鹅44 分钟前
MongoDB基本操作
数据库·分布式·mongodb·hbase
夜光小兔纸1 小时前
Oracle 普通用户连接hang住处理方法
运维·数据库·oracle
兩尛3 小时前
订单状态定时处理、来单提醒和客户催单(day10)
java·前端·数据库
web2u3 小时前
MySQL 中如何进行 SQL 调优?
java·数据库·后端·sql·mysql·缓存
Elastic 中国社区官方博客4 小时前
使用 Elasticsearch 导航检索增强生成图表
大数据·数据库·人工智能·elasticsearch·搜索引擎·ai·全文检索
小金的学习笔记4 小时前
RedisTemplate和Redisson的使用和区别
数据库·redis·缓存