MySQL索引原理及其优化

目录

前言

网上都说学会mysql须要学会两个部分,索引和事务,其实在最近的Mysql学习过程当中,我以为应该是有三个部分的,索引,查询,事务.其中的查询主要是指查询优化即编写高效率的SQL语句.mysql

本文记录一下学习MySQL的索引过程当中的一些知识.主要为阅读《高性能MySQL》的一些理解和扩展.sql

什么是索引

索引是存储引擎用于快速找到记录的一种数据结构.数据库

这是MySQL官方对于索引的定义,能够看到索引是一种数据结构,那么咱们应该怎样理解索引呢?一个常见的例子就是书的目录.咱们都已经养成了看目录的习惯,拿到一本书时,咱们首先会先去查看他的目录,而且当咱们要查找某个内容时,咱们会在目录中查找,而后找到该片断对应的页码,再根据相应的页码去书中查找.若是没有索引(目录)的话,咱们就只能一页一页的去查找了.数组

在MySQL中,假设咱们有一张以下记录的表:缓存

id name age
1 huyan 10
2 huiui 18
3 lumingfei 20
4 chuzihang 15
5 nono 21

若是咱们但愿查找到年龄为15的人的名字,在没有索引的状况下咱们只能遍历全部的数据去作逐一的对比,那么时间复杂度是O(n).bash

而若是咱们在插入数据的过程当中, 额外维护一个数组,将age字段有序的存储.获得以下数组.服务器

[10,15,18,20,21]
 |  |  |  |  |
[x1,x4,x2,x3,x5]
复制代码

下面的x是模拟数据再磁盘上的存储位置.这个时候若是咱们须要查找15岁的人的名字.咱们能够对盖数组进行二分查找.众所周知,二分查找的时间复杂度为O(logn).查找到以后再根据具体的位置去获取真正的数据.数据结构

PS:MySQL中的索引不是使用的数组,而是使用的B+树(后面讲),这里用数组举例只是由于比较好理解.性能

索引能为咱们带来什么?

如上面所说,索引能帮助咱们快速的查找到数据.其次由于索引中的值是顺序储存,那么能够帮助咱们进行orderby操做.并且索引中也是存储了真正的值的,所以有一些的查询直接能够在索引中完成(也就是覆盖索引的概念,后面会提到).学习

总结一下索引的优势就是(《高性能》书中总结的):

  • 减小查询须要扫描的数据量(加快了查询速度)
  • 减小服务器的排序操做和建立临时表的操做(加快了groupby和orderby等操做)
  • 将服务器的随机IO变为顺序IO(加快查询速度).

索引有哪些缺点呢?

首先索引也是数据,也须要存储,所以会带来额外的存储空间占用.其次,在插入,更新和删除操做的同时,须要维护索引,所以会带来额外的时间开销.

总结一下:

  • 索引占用磁盘或者内存空间
  • 减慢了插入更新操做的速度

实际上,在必定数据范围内(索引没有超级多的状况下),创建索引带来的开销是远远小于它带来的好处的,可是咱们仍然要防止索引的滥用.

都有哪些类型的索引?

对于MySQL来讲,在服务器层并不实现索引,而是交给存储引擎来实现的,所以不一样的存储引擎实现的索引类型不太同样.InnoDB做为当前使用最为普遍的存储引擎,使用的是B+树索引,所以咱们大部分时间提到的索引也都是指的它.

MySQL主要有如下几种索引:

  • B-树索引/B+树索引
  • 哈希索引
  • 空间数据索引
  • 全文索引

本文只学习B-树索引和B+树索引.

B-树索引和B+树索引

这里不会特别详细的解释B-树和B+树的数据结构原理,有兴趣的小伙伴能够移步参考文章中的文章.或者经过google自行了解.

B-树

B-树是一棵多路平衡查找树,对于一棵M阶的B-树有如下的性质:

  1. 根节点至少有两个子女.
  2. 每一个节点包含k-1个元素和k个孩子,其中m/2 <= k <= m.
  3. 每个叶子节点都包含k-1个元素,其中m/2 <= k <= m.
  4. 全部的叶子节点位于同一层.
  5. 每一个节点中的元素从小到大排列,那么k-1个元素正好是k个孩子包含的值域的划分.

这么说可能会有一些难理解,能够将B-树理解为一棵更加矮胖的二叉搜索树.

B+树

B+树是B-树的进阶版本,在B-树的基础上又作了以下的限制:

  1. 每一个中间节点不保存数据,只用来索引,也就意味着全部非叶子节点的值都被保存了一份在叶子节点中.
  2. 叶子节点之间根据自身的顺序进行了连接.

这样能够带来什么好处呢?

  1. 中间节点不保存数据,那么就能够保存更多的索引,减小数据库磁盘IO的次数.
  2. 由于中间节点不保存数据,因此每一次的查找都会命中到叶子节点,而叶子节点是处在同一层的,所以查询的性能更加的稳定.
  3. 全部的叶子节点按顺序连接成了链表,所以能够方便的话进行范围查询.

怎样建立高性能的索引?

因为优化索引和优化查询通常是分不开的,所以这一块可能会包含部分的查询优化内容.

前缀索引和索引选择性

若是但愿给一个很长的字符串上添加索引,那么能够考虑使用前缀索引.在正式介绍前缀索引以前,咱们先大概考虑一下索引的工做步骤,数据库使用索引进行查找的时候,通常是以下几步:

  1. 在索引的B+树上找到对应的值,好比找到学校名称为卡塞尔学院的一条记录,而且拿到这条数据在磁盘上的地址.
  2. 根据地址去磁盘上查找,拿到该条数据全部的值.

那么假如在全部的学校名称的值中,卡塞尔就能够惟一的标识这条数据,那么用卡塞尔来作索引是否能够达到和卡塞尔学院作索引相同的效果?

答案是确定的,而使用卡塞尔的话,是能够减小索引的大小到原来的60%的.这就是前缀索引的做用.

前缀索引: 在对一个比较长的字符串进行索引时,能够仅索引开始的一部分字符,这样能够大大的节约索引空间,从而提升索引效率.可是这样也会下降索引的选择性.

索引的选择性: 不重复的值/全部的值. 能够看出索引的选择性为0-1,最高的就是该列惟一,没有重复值.因此惟一索引的效率是比较好的.

可是在通常状况下,较长的字符串的一些前缀的选择性也是比较好的,这个咱们能够算出来.使用下面的语句:

select 
    count(distinct left(school_name,3))/count(*) as sch3, 
    count(distinct left(school_name,4))/count(*) as sch4,
    count(distinct left(school_name,5))/count(*) as sch5,
    count(distinct school_name)/count(*) as original
from 
    user;
复制代码

其中查找到的original就是本来的选择性,sch3,sch4,sch5分别是取该列的前3,4,5个字符做为索引的时候的选择性.逐步增长这个数值,当选择性与原来相差不大的时候,就是一个比较合适的前缀索引的长度.(通常状况下是这样,可是也有例外,当数据极其不均匀时,这样的前缀索引会在某个特殊的case上表现不好劲).

找到合适的长度以后,就能够建立一个前缀索引了:alter table user add index sch_pre3(`school(3)`)

注意:前缀索引和覆盖索引是很难一块儿使用的,我今天早上刚试过,对索引的优化进行到这一步以后无功而返,具体的缘由在下面介绍完覆盖索引以后解释.

联合索引

通常咱们都是有对多个列进行索引的需求的,由于查询的需求多种多样.这个时候咱们能够选择创建多个独立的索引或者创建一个联合索引.大多数时候都是联合索引更加合适一些.

假设咱们要执行这个语句:select * from user where school_name = '卡塞尔' and age > 20,咱们在schoolage上分别创建两个独立的索引,那么咱们预期这条查询语句会命中两个索引,可是使用explain命令查看会发现不必定.这是一个玄学的过程.我的没有研究清楚.

从理论上来说,MySQL在5.0以后的版本里面对支持合并索引,也就是同时使用两个索引,可是MySQL的优化器不必定这样认为,他可能会认为,查询两次B+树的代价高于查询一次索引以后去数据表进行过滤,所以会选择只用一个索引.(我在本身的5张表上作了相似此case的测试,结果都是只使用了一个索引.)

建立联合索引的语法:alter table user add index school_age(`school`,`age`).

使用联合索引的时候,有一个很是重要的因素就是全部的索引列只能够进行最左前缀匹配,例如上面的school_age联合索引,当仅使用age做为查询条件的时候是不能使用的,也就是说select * from user where age =20是不能命中上面的联合索引的.

在不考虑任何查询的状况下,咱们应该讲选择性高的列放在联合索引的前面,可是实际上咱们更多的是经过查询来反推索引,以使某个固定的查询能够尽量的命中索引以提升查询速度.毕竟咱们创建索引的目的也是为了加快查询的速度.

所以联合索引的优化更多的是根据某个或者某些语句来优化的,不具有一个通用的法则.

最左前缀索引的原理

当数据列有序的时候,mysql可使用索引,那么假设咱们创建了school_age索引,示例数据以下:

school age
a 12
b 12
b 14
b 15
c 1

在这份数据中,school字段是彻底有序的,索引school可使用索引.

而从全表来看,age字段不是有序的,所以没法直接使用索引,那么观察一下数据表,在何时age有序呢?在school进行定值匹配的时候,例如当school=b的时候,对于这三条数据而言,age是有序的,所以可使用age索引.这就是最左前缀的原理.

此外,最左前缀索引只能使用一个范围查询,例如select * from user where school > a, select * from user where school = a and age > 12,都是能够命中索引的,可是select * from user where school > a and age > 12中,仅school能够命中索引,这也能够从上面得出结论.由于当school是范围匹配的时候,mysql没法确认age字段是否严格有序,好比 school的范围匹配命中了b,c的四条数据,那么age就不是有序的.没法使用后续的索引.

聚簇索引

聚簇索引不是一种索引类型,而是一种存储数据的方式.Innodb的聚簇索引是在同一个数据结构中保存了索引和数据.

由于数据真正的数据只能有一种排序方式,因此一个表上只能有一个聚簇索引.Innodb使用主键来进行聚簇索引,没有主键的话就会选择一个惟一的非空索引,若是还尚未,innodb会选择生成一个隐式的主键来进行聚簇索引.为何innodb这么执着的须要搞一个聚簇索引呢,由于一个数据表中的数据总得有且只有一种排序方式来存储在磁盘上,所以这是必须的.

这也是innodb推荐咱们使用自增主键的缘由,由于自增主键自增且连续,在插入的时候只须要不断的在数据后面追加便可.设想一下使用UUID来做为主键,那么每一次的插入操做,都须要找到当前主键在已排序的主键中的位置,而后插入,而且要移动该主键后的数据,以使得数据和主键保持相同的顺序,这无疑是代价很是高的.

也是由于这个缘由,在其余索引的叶子节点中,存储的"数据"其实不是该数据的真实物理地址,而是该数据的主键,查找到主键以后,再根据主键进行一次索引,拿到数据.

聚簇索引和非聚簇索引的区别能够用一个简单的例子来讲明:

当咱们拿到一本书的时候,目录就是主键,是一个聚簇索引,由于在目录中连续的内容,在正文中也是连续的,当咱们想要查看迎着阳光盛大逃亡章节,只须要在目录中找到它对应的页面,好比459,而后去对应的页码查看正文便可.

而非聚簇索引呢,则相似于书后面的附录专有名词索引同样(二级普通索引),当你查找邦达列夫的时候,附录会告诉你,这个名词出如今了迎着阳光盛大逃亡一节,而后你须要去目录(主键索引)中再次查找到对应的页码.

覆盖索引

当一个索引包含(或者说是覆盖)须要查询的全部字段的值时,咱们称之为覆盖索引.

设想有以下的查询语句:

select 
  school_name,age
from  
  user
where 
  school_name = '金色莺尾花学院'
复制代码

这个语句根据学校名称来查询数据行的学校名称和年龄,从上面的数据查询的步骤咱们能够知道,当在索引中找到要求的值的时候,还须要根据主键去进行一次索引,以拿到所有的数据,而后从其中挑选出须要的列,返回.可是如今索引中已经包含了全部的须要返回的列,那么就不用进行回数据表查询的操做了,此外索引的大小通常是远远小于真正的数据大小的,覆盖索引能够极大的减小从磁盘加载数据的数量.

为何前缀索引和覆盖索引没法一块儿使用?

由于前缀索引的目的是用前缀来表明真正的值,他们在选择性上几乎没有区别,可是MySQL仍然没法判断真正的数据是什么,好比阿里巴巴阿里妈妈在前缀为2的时候是同样的,可是为了确保你查询阿里巴巴的时候不会出现阿里妈妈的内容,是须要回到数据表拿到数据再次进行一个精准匹配来进行过滤的.

所以,覆盖索引没法和列前缀索引一块儿使用,这是我用一个早晨的时间测试得出的结论.

删除掉冗余和重复的索引

有一些索引是从未在查询中使用过,却白白增长数据插入时开销的,对于这种索引咱们应该及时的进行删除.

好比在主键上再创建一个普通索引,无疑是毫无做用的.

还好比在有联合索引school_age的状况下,再创建一个school的独立索引,由于索引的最左前缀匹配原则,school_age是彻底能够命中对school的单独查询的,所以后者能够删掉.

如何查看索引的一些相关信息?

索引信息

在mysql中可使用show index from table_name来查看某个表上的索引,它将会有以下的输出:

2019-06-02-22-28-04

或者使用show create table table_name来查看建表语句,其中包含建立索引的语句.

索引大小

在5.0之后的版本中,咱们能够经过查看information_schema.TABLES表中的数据来获取更加详细的数据.

该表各字段的含义以下表:

字段 含义
Table_catalog 数据表登记目录
Table_schema 数据表所属的数据库名
Table_name 表名称
Table_type 表类型[system view
Engine 使用的数据库引擎[MyISAM
Version 版本,默认值10
Row_format 行格式[Compact
Table_rows 表里所存多少行数据
Avg_row_length 平均行长度
Data_length 数据长度
Max_data_length 最大数据长度
Index_length 索引长度
Data_free 空间碎片
Auto_increment 作自增主键的自动增量当前值
Create_time 表的建立时间
Update_time 表的更新时间
Check_time 表的检查时间
Table_collation 表的字符校验编码集
Checksum 校验和
Create_options 建立选项
Table_comment 表的注释、备注

咱们能够经过一些查询语句来获取详细的信息,好比:

// 查看当前MySQL服务器全部索引的大小(以MB为单位,默认是字节)
SELECT CONCAT(ROUND(SUM(index_length)/(1024*1024), 2), ' MB') AS 'Total Index Size' FROM TABLES
// 查看某一个库的全部大小
SELECT CONCAT(ROUND(SUM(index_length)/(1024*1024), 2), ' MB') AS 'Total Index Size' FROM TABLES  WHERE table_schema = 'XXX';
// 查看某一个表的索引大小
SELECT CONCAT(ROUND(SUM(index_length)/(1024*1024), 2), ' MB') AS 'Total Index Size' FROM TABLES  WHERE table_schema = 'yyyy' and table_name = "xxxxx";  
// 汇总查看一个库中的数据大小及索引大小
SELECT CONCAT(table_schema,'.',table_name) AS 'Table Name', CONCAT(ROUND(table_rows/1000000,4),'M') AS 'Number of Rows', CONCAT(ROUND(data_length/(1024*1024*1024),4),'G') AS 'Data Size', CONCAT(ROUND(index_length/(1024*1024*1024),4),'G') AS 'Index Size', CONCAT(ROUND((data_length+index_length)/(1024*1024*1024),4),'G') AS'Total'FROM information_schema.TABLES WHERE table_schema LIKE 'xxxxx';
复制代码

对tables表的数据的全部查看方式都是能够的,其中还包含了一些表格自己的数据信息,可是由于和本文的主题不符合,这里就不举例子了.

注意:上面的表格是有缓存的,当更新数据库索引以后,最好执行analyze table xxxx,而后再进行查看.MySQL会在表格数据发生较大的变化时才更新此表(大小变化超过1/16或者插入20亿行).

索引碎片

在索引的建立删除过程当中,不可避免的会产品索引碎片,固然还有数据碎片,咱们能够经过执行optimize table xxx来从新整理索引及数据,对于不支持此命令的存储引擎来讲,能够经过一条无心义的alter语句来触发整理,好比:将表的存储引擎更换为当前的引擎,alter table xxxx engine=innodb.

参考文章

书籍《高性能MySQL(第三版)》

B-树

B+树


完。



ChangeLog

2019-06-01 完成

以上皆为我的所思所得,若有错误欢迎评论区指正。

欢迎转载,烦请署名并保留原文连接。

联系邮箱:huyanshi2580@gmail.com

更多学习笔记见我的博客------>呼延十

相关文章
相关标签/搜索