MySQL知识梳理图,一图看完整篇文章: html
今天接上一篇『MySQL』揭开索引神秘面纱 讨论了索引的实现原理,了解了大概的原理,接下来了解一下高性能索引的优化策略,这也是面试中常常会问到的问题。python
在详细总结MySQL的索引优化策略以前,先给你们介绍一个工具,方便在查慢查询的过程,排查大部分的问题:Explain。有关Explain的详细介绍,能够查看官网地址: dev.mysql.com/doc/refman/… 。这里再给你们推荐一个学习方法,就是必定要去官网学习第一手资料,若是以为英语阅读有挑战的朋友,建议仍是平时都积累看看英文文章,英语对于程序员来讲很重要,先进的技术和理论不少资料都是英文版,而官网也是很是全的,要想成为技术大牛,这是必须须要修炼的。 扯淡就到这里,下面我简单描述一下Explain怎么使用。 举例:mysql
mysql> explain select * from user where name="xiao" and age=9099 and birthday="1980-08-02";
+----+-------------+-------+------------+------+---------------+------------+---------+-------------------+------+----------+-------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------+------------+------+---------------+------------+---------+-------------------+------+----------+-------+
| 1 | SIMPLE | user | NULL | ref | unique_key | unique_key | 249 | const,const,const | 1 | 100.00 | NULL |
+----+-------------+-------+------------+------+---------------+------------+---------+-------------------+------+----------+-------+
复制代码
Explain 结果有好几列,简单说一下经常使用的列:select_type, type, key, key_len, ref, rows。其他列能够参考官网介绍。程序员
CREATE TABLE `user` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`gender` varchar(16) DEFAULT NULL,
`name` varchar(64) DEFAULT NULL,
`birthday` varchar(16) NOT NULL,
`age` int(11) unsigned NOT NULL,
PRIMARY KEY (`id`),
KEY `unique_key` (`name`,`age`,`birthday`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
复制代码
往表里插入了一些数据,方便下面问题的分析面试
B-Tree索引,按上一篇原理分析知道是按顺序存储数据的,因此并非只要查询语句中用了索引就能起做用的,下面来看看具体的场景和限制sql
mysql> explain select * from user where name="xiao" and age=9099 and birthday="1980-08-02";
+----+-------------+-------+------------+------+---------------+------------+---------+-------------------+------+----------+-------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------+------------+------+---------------+------------+---------+-------------------+------+----------+-------+
| 1 | SIMPLE | user | NULL | ref | unique_key | unique_key | 249 | const,const,const | 1 | 100.00 | NULL |
+----+-------------+-------+------------+------+---------------+------------+---------+-------------------+------+----------+-------+
复制代码
全值匹配,即按照索引的全部列均精确匹配,从ref和key_len看出,从语句用到了三个索引。理论上索引对顺序是比较敏感的,但实际上执行下面语句能够看看结果:缓存
explain select * from user where age=9099 and birthday="1980-08-02" and name="xiao";
复制代码
结果答案是同样的,由于MySQL查询优化器会自动调整where子句的条件顺序,从而匹配最适合的索引。bash
mysql> explain select * from user where name="xiao";
+----+-------------+-------+------------+------+---------------+------------+---------+-------+-------+----------+-------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------+------------+------+---------------+------------+---------+-------+-------+----------+-------+
| 1 | SIMPLE | user | NULL | ref | unique_key | unique_key | 195 | const | 15170 | 100.00 | NULL |
+----+-------------+-------+------------+------+---------------+------------+---------+-------+-------+----------+-------+
复制代码
能够看到用到了name这个索引。若是没有匹配最左前缀,结果是怎么样了:服务器
mysql> explain select * from user where birthday="1980-08-02";
+----+-------------+-------+------------+------+---------------+------+---------+------+-------+----------+-------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------+------------+------+---------------+------+---------+------+-------+----------+-------------+
| 1 | SIMPLE | user | NULL | ALL | NULL | NULL | NULL | NULL | 30340 | 10.00 | Using where |
+----+-------------+-------+------------+------+---------------+------+---------+------+-------+----------+-------------+
复制代码
能够看到若是没有用name查询索引,则变成了全表查询。函数
mysql> explain select * from user where name like "xiao-1%";
+----+-------------+-------+------------+-------+---------------+------------+---------+------+------+----------+-----------------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------+------------+-------+---------------+------------+---------+------+------+----------+-----------------------+
| 1 | SIMPLE | user | NULL | range | unique_key | unique_key | 195 | NULL | 1111 | 100.00 | Using index condition |
+----+-------------+-------+------------+-------+---------------+------------+---------+------+------+----------+-----------------------+
复制代码
能够看到类型是range, 使用key=unique_key的联合索引。 若是是name like "%xiao-1%" 则就不能使用索引了,其中缘由能够根据B-Tree的特性想一下。
mysql> explain select * from user where name > "xiao-1" and name <= "xiao-200";
+----+-------------+-------+------------+-------+---------------+------------+---------+------+------+----------+-----------------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------+------------+-------+---------------+------------+---------+------+------+----------+-----------------------+
| 1 | SIMPLE | user | NULL | range | unique_key | unique_key | 195 | NULL | 1113 | 100.00 | Using index condition |
+----+-------------+-------+------------+-------+---------------+------------+---------+------+------+----------+-----------------------+
复制代码
能够看出type=range,用到了unique_key索引。
mysql> explain select * from user where name="xiao" and age > 1 and age < 100;
+----+-------------+-------+------------+-------+---------------+------------+---------+------+------+----------+-----------------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------+------------+-------+---------------+------------+---------+------+------+----------+-----------------------+
| 1 | SIMPLE | user | NULL | range | unique_key | unique_key | 199 | NULL | 98 | 100.00 | Using index condition |
+----+-------------+-------+------------+-------+---------------+------------+---------+------+------+----------+-----------------------+
复制代码
能够从key_len的长度参考上一条范围匹配,发现key_len的长度变长了,实际就是用到了name和age2个索引,name是精准匹配,age是范围匹配。 思考: 若是sql语句变成:
select * from user where name="xiao" and age > 1 and age < 100 and birthday="2000-08-02";
复制代码
birthday的索引会用到吗?
刚才上面也提到了B-Tree索引有一些限制,如今总结一下:
先总结一下索引的优势:
说了三大优势,是否是以为只要是个表,是个列就所有加上索引就行了?
这样显示是不对的,虽然索引虽然加快了查询速度,但索引也是有代价的:索引文件自己要消耗存储空间,同时索引会加剧插入、删除和修改记录时的负担,另外,MySQL在运行时也要消耗资源维护索引,所以索引并非越多越好。 只要当索引帮助存储引擎快速查找到记录带来的好处大于其带来的额外工做时,索引才是比较有效的。
那是否有什么办法知道何时该用索引,何时不应用了?
表记录比较少,简单的全表扫描更高效。少的界定的话通常也是靠经验,没有明确多少行算少,我的以为2000行之内就ok的,实际业务中不少配置表,明显以为不会有2000行的均可以。
索引选择性。高性能MySQL(第三版)对索引选择性的定义是:不重复的索引值(也称为基数,cardinality)和数据表的记录总数(#T)的比值,范围从1/#T到1之间。索引选择性越高则查询效率越高,由于选择性高的索引可让MySQL在查找时过滤掉更多的行。惟一索引的选择性是1,下面举例看一下如何计算选择性:
mysql> select count( distinct name) / count(1) from user;
+----------------------------------+
| count( distinct name) / count(1) |
+----------------------------------+
| 0.6632 |
+----------------------------------+
mysql> select count( distinct birthday) / count(1) from user;
+--------------------------------------+
| count( distinct birthday) / count(1) |
+--------------------------------------+
| 0.0002 |
+--------------------------------------+
mysql> select count( distinct id) / count(1) from user;
+--------------------------------+
| count( distinct id) / count(1) |
+--------------------------------+
| 1.0000 |
+--------------------------------+
复制代码
上面能够看出,user表里,name索引的选择性还蛮高,id自增主键选择性就是1,birthday的选择性很低,其实没有必要作索引了。说白了就是不能有效区分数据的列不适合作索引列(如性别,男女未知,最多也就三种,区分度很是低)。
接下来总结一下常见的索引策略:
mysql> select count( distinct left(name, 8)) / count(1) from user;
+-------------------------------------------+
| count( distinct left(name, 8)) / count(1) |
+-------------------------------------------+
| 0.3648 |
+-------------------------------------------+
mysql> select count( distinct left(name, 9)) / count(1) from user;
+-------------------------------------------+
| count( distinct left(name, 9)) / count(1) |
+-------------------------------------------+
| 0.6630 |
+-------------------------------------------+
复制代码
能够看到当采用name,前缀8个字符时,选择性还比较低,当变成9个字符时,选择性就高了不少,修改索引为left(name, 9),看一下索引的长度下降了多少了。 将索引改成(name(9), age, birthday)
mysql> explain select * from user where name="xiao" and age=9099 and birthday="1980-08-02";
+----+-------------+-------+------------+------+---------------+-------------+---------+-------------------+------+----------+-------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------+------------+------+---------------+-------------+---------+-------------------+------+----------+-------------+
| 1 | SIMPLE | user | NULL | ref | unique_key2 | unique_key2 | 84 | const,const,const | 1 | 100.00 | Using where |
+----+-------------+-------+------------+------+---------------+-------------+---------+-------------------+------+----------+-------------+
复制代码
能够对比全值匹配中的explain语句,key_len从249缩小到了84,缩小了三倍,大大减小了索引文件的大小,提升了效率。可是也有缺点,前缀索引对ORDER BY or GROUP BY操做无效。
选择合适的索引列顺序,从场景分析中看,对于B-Tree索引是按顺序存储数据,因此选择一个最合适的顺序索引列对查询很是有帮助,但这个也没有比较直观的方法,通常考虑选择性和业务需求的特性。好比上面的例子,name的选择性>age>birthday,且一般业务中按某个用户的name查询的场景会居多,因此索引的顺序就是(name, age, birthday)。说白了就是较频繁做为查询条件的字段才去建立索引。
聚簇索引的特性, 从上一篇索引的原理分析,InnoDB引擎使用的B-Tree索引就是聚簇索引,这类索引有什么特性了,上一篇也提到过,InnoDB数据是按主键汇集,若是表没有显示定义主键,则InnoDB会优先选择一个惟一的非空索引代替,若是找不到这样的索引,会隐式定义一个主键来聚簇索引。 因此在选择主键的时候,建议参考如下:
有些人以为使用业务中的惟一字段做为主键便可,不必选一个跟业务无关的自增id做为主键,但我我的建议最好使用跟业务无关的自增ID做为主键。缘由以下:
InnoDB数据按主键顺序汇集存储,数据记录自己被存于主索引的叶子节点上。这就要求同一个叶子节点内(大小为一个内存页或磁盘页)的各条数据记录按主键顺序存放,所以每当有一条新的记录插入时,MySQL会根据其主键将其插入适当的节点和位置,若是页面达到装载因子(InnoDB默认为15/16),则开辟一个新的页(节点)。 若是表使用自增主键,那么每次插入新的记录,记录就会顺序添加到当前索引节点的后续位置,当一页写满,就会自动开辟一个新的页。 若是用业务的惟一主键,可能非自增主键(如身份证号或学号等),因为每次插入主键的值近似于随机,所以每次新纪录都要被插到现有索引页得中间某个位置,此时MySQL不得不为了将新记录插到合适位置而移动数据,甚至目标页面可能已经被回写到磁盘上而从缓存中清掉,此时又要从磁盘上读回来,这增长了不少开销,同时频繁的移动、分页操做形成了大量的碎片,获得了不够紧凑的索引结构,后续不得不经过OPTIMIZE TABLE来重建表并优化填充页面。可见插入的消耗是巨大的。
为何主键的字符要小了,由于二级索引是根据主键来检索数据,则叶子节点存储了主键列,也就是说二级索引的访问须要访问二次主键索引,若是主键索引很大,二级索引的可能比想象的要大不少,从而影响性能。
更新频繁的列最好不要做为索引,若是更新频繁的列做为索引,每次更新,为了保持有顺,须要调整整个索引B-Tree树,这样的消耗也是挺大的。
冗余和重复索引 MySQL容许相同列上建立多个索引,有时候看到建了一个UNIQUE KEY (name, age),而后还建了个 KEY (name),这样name这个索引就重复了,发现则须要删除单独的索引,能够减小不少开销。索引越多,会致使插入数据变慢。
未使用的索引 在设计表的时候,刚开始需求可能须要用到某个字段的去查询,就将此字段增长了索引,可能最后需求变动的时候,这个字段基本不多有场景去查,这时候常常会忘记去删除此索引,致使不必的开销。因此不必的索引,最好是删除。
若是一张表百万级以上,索引是须要额外的维护成本,由于索引文件是单独存在的文件,因此当咱们对数据的增删改,都会产生额外的对索引文件的操做,这些操做须要消耗额外的IO,会下降增删改的执行效率。且删除数据的速度跟建立的索引的数量是成正比的。有一个小技巧,能够参考:
上面三个步骤比直接删除确定是要快一点,若是直接删除数据的过程当中删除失败,致使事务回滚,那消耗就成倍增长了。
索引策略就说这么多,下一篇总结MySQL增删改查和多表查询优化。
更多精彩文章,请关注公众号: 「天澄技术杂谈」