目录
MySQL索引深入总结
InnoDB中的索引复习
聚集索引/聚簇索引
问题:如果我们没有定义主键呢?
问题:分析一下B+树三层和四层的性能差异?
辅助索引/二级索引
回表
问题:为什么我们还需要一次回表操作呢?直接把完整的用户记录放到辅助索引d的叶子节点不就好了么?
问题:什么时候采用全表扫描的方式,什么时候使用采用二级索引 + 回表的方式去执 行查询呢?
MRR
联合索引/复合索引
自适应哈希索引
全文检索之倒排索引
什么是全文检索(Full-Text Search)?
MySQL中的全文索引
创建全文索引
使用全文索引
总结:MySQL有哪些索引类型?
面试题:什么是密集索引和稀疏索引?
辨析覆盖索引/索引覆盖
深入思考索引在查询中的使用
索引的代价
空间上的代价
时间上的代价
高性能的索引创建策略
索引列的类型尽量小
利用索引选择性和前缀索引
索引的选择性/离散性
前缀索引
只为用于搜索、排序或分组的列创建索引
合理设计多列索引
尽可能设计三星索引
三星索引概念
达成三星索引
达不成三星索引
主键尽量是很少改变的列
处理冗余和重复索引
删除未使用的索引
补充资料:磁盘和B+树
MySQL索引深入总结
InnoDB中的索引复习
InnoDB中的索引是按照B+树来组织的。我们知道B+树的叶子节点是用来存放数据的,但是放什么数据呢?索引自然是要存放的,因为B+树的作用本来就是为了快速检索数据而提出的一种数据结构,不放索引放什么呢?但是数据库中的表,数据才是我们真正需要的数据,索引只是辅助数据的,甚至于一个表可以没有自定义的索引,但是不可以没有数据。InnoDB的数据到底是如何进行组织的呢?
聚集索引/聚簇索引
InnoDB中使用了聚集索引,就是将表的主键用来构建一棵B+树,并且将整张表的行记录数据存放在该B+树的叶子节点中。也就是所谓的索引即数据,数据即索引。由于聚集索引是利用表的主键构建的,所以每张表只能拥有一个聚集索引。
聚集索引的叶子节点就是数据页,换句话说,数据页上存放的是完整的每行记录。(一个数据页是16KB,磁盘一次大概读取512个字节,也就是说一个数据页需要读取32次)
因此聚簇索引的一个优点即是:通过过聚集索引能获取完整的整行数据。另一个优 点是:对于主键的排序查找和范围查找速度非常快。
问题:如果我们没有定义主键呢?
MySQL会使用唯一性索引,没有唯一性索引,MySQL也会 创建一个隐含列RowID来做主键,然后用这个主键来建立聚集索引。
所有的数据先读到内存,再从内存中读取到cpu的寄存器进行操作。
问题:分析一下B+树三层和四层的性能差异?
对于三层B+树而言,前面两层都可以看作是在内存的操作读写,速度相对很快。但是到第三层后,就是I/O类型的操作。
对于四层B+树而言,前面两层都可以看作是在内存的操作读写,速度相对很快。但是到第三层和第四层后,就都是是I/O类型的操作,等于说多了一次I/O操作。
辅助索引/二级索引
上边介绍的聚簇索引只能在搜索条件是主键值时才能发挥作用,因为B+树中的数据都是按照主键进行排序的,那如果我们想以别的列作为搜索条件怎么办?我们一般 会建立多个索引,这些索引被称为辅助索引/二级索引。
对于辅助索引(也称之为二级索引,非聚集索引),叶子节点并不包含行记录的全部数据,叶子节点除了包含键值以外,每个叶子节点中的索引行还包含了与之行数据记录对应的聚簇索引键(即是主键字段)
比如说:辅助索引为index(leo),那么该辅助索引对应的B+数据的叶子节点中包含的数据就包含了(主键字段和leo这个字段)
回表
辅助索引的存在并不影响数据在聚集索引中的组织,因此每张表可以有多个辅助索引。
当通过辅助索引来寻找数据时,InnoDB存储引擎就会遍历辅助索引并且通过叶级别的指针获得指向主键索引的主键,然后通过主键索引(聚集索引)来找到一个完整的行记录。这个过程也被称之为回表。
也就是根据辅助索引叶子节点中对应的主键字段的值进行查询到一条完整的用户记录,需要使用到两棵B+树,一次辅助索引,一次聚集索引。
问题:为什么我们还需要一次回表操作呢?直接把完整的用户记录放到辅助索引d的叶子节点不就好了么?
(1) 如果把完整的用户记录放到叶子节点是可以不用回表,但是太占地方了,相当于每建立一棵B+树都需要把所有的用户记录再都拷贝一遍,这就有点太浪费存储空间了。
(2) 为了保证每一处数据的一致性(因为同一个字段数据不止在一处进行拷贝记录了),每次对数据的变化要在所有包含数据的索引中全部都修改一 次,性能也非常低下。
问题:什么时候采用全表扫描的方式,什么时候使用采用二级索引 + 回表的方式去执 行查询呢?
(1) 很明显,回表的记录越少,性能提升就越高,需要回表的记录越多,使用二级索引 的性能就越低,甚至让某些查询宁愿使用全表扫描也不使用二级索引。
(2) 这个就是查询优化器做的工作,查询优化器会事先对表中的记录计算一 些统计数据,然后再利用这些统计数据根据查询的条件来计算一下需要回表的记录 数,需要回表的记录数越多,就越倾向于使用全表扫描,反之倾向于使用二级索引 + 回表的方式。具体怎么算的,我们后面会详细说到。
MRR
MRR是MySQL底层使用的一种优化措施。
从上文就可以看出,每一次从二级索引中读取到一条记录后,就会根据该记录的主键值执行回表操作。而在某个扫描区间中的二级索引记录的主键值都是无序的,也就是说这些二级索引记录对应的聚簇索引记录所在的页的页号是无序的。
每一次执行回表操作时都相当于是要进行随机读取一个聚簇索引的页,而这些随机I/O操作带来的性能开销是比较大。MySQL中提出了一个名为Disk-Sweep Multi-Range Read (MRR,多范围 读取)的优化措施,即先读取一部分二级索引记录,将它们的主键值排好序之后再统一执 行回表操作。
相对于每读取一条二级索引记录就立即执行回表操作,这样会节省一些IO开销。使用这 个 MRR优化措施的条件比较苛刻,所以我们直接认为每读取一条二级索引记录就立即执行回表操作。MRR的详细信息,可以查询官方文档。
联合索引/复合索引
前面我们对索引的描述,隐含了一个条件,那就是构建索引的字段只有一个,但实 践工作中构建索引的完全可以是多个字段。所以,将表上的多个列组合起来进行索 引我们称之为联合索引或者复合索引,比如index(a,b)就是将a,b两个列组合起来 构成一个索引。
千万要注意一点,建立联合索引只会建立1棵B+树,多个列分别建立索引会分别以 每个列则建立B+树,有几个列就有几个B+树,比如,index(note)、index(b),就 分别对note,b两个列各构建了一个索引。
index(note,b)在索引构建上,包含了两个意思:
1、先把各个记录按照note列进行排序。
2、在记录的note列相同的情况下,采用b列进行排序
自适应哈希索引
自适应哈希索引同样是MySQL底层使用的一种优化措施。
InnoDB存储引擎除了我们前面所说的各种索引,还有一种自适应哈希索引,我们知 道B+树的查找次数,取决于B+树的高度,在生产环境中,B+树的高度一般为3~4层,故 需要3~4次的IO查询。
所以在InnoDB存储引擎内部自己就会去监控整一个索引表,如果监控到某个索引对应的数据记录值经常进行使用,那么就会被认为是热数据,然后内部自己就会创建一个hash索引,称之为自适应哈希索引,创建以后,如果下一次又查询到这个索引,那么就会直接通过hash算法推导出该数据记录的地址,直接一次就能查询到数据,比重复的去B+Tree索引中查询三四次节点的效率高出了不少。
InnoDB存储引擎使用的哈希函数采用除法散列方式,其冲突机制采用链表方式。注 意,对于自适应哈希索引仅是数据库自身创建并使用的,我们并不能对其进行干 预。通过命令show engine innodb status\G可以看到当前自适应哈希索引的使用 状况,如:
哈希索引只能用来搜索等值的查询,如 SELECT* FROM table WHERE index co=xxx。而对于其他查找类型,如范围查找,是不能使用哈希索引的, 因此这里会显示non- hash searches/s的统计情况。通过 hash searches: nonhash searches可以大概了解使用哈希索引后的效率。
由于AHI是由 InnoDB存储引擎控制的,因此这里的信息只供我们参考。不过我们可 以通过观察 SHOW ENGINE INNODB STATUS的结果及参数
innodb_adaptive_hash_index来考虑是禁用或启动此特性,默认AHI为开启状态。 什么时候需要禁用呢?如果发现监视索引查找和维护哈希索引结构的额外开销远远 超过了自适应哈希索引带来的性能提升就需要关闭这个功能。
同时在MySQL 5.7中,自适应哈希索引搜索系统被分区。每个索引都绑定到一个特 定的分区,每个分区都由一个单独的 latch 锁保护。
分区由 innodb_adaptive_hash_index_parts 配置选项控制 。在早期版本中,自适应哈希 索引搜索系统受到单个 latch 锁的保护,这可能成为繁重工作负载下的争用点。
innodb_adaptive_hash_index_parts 默认情况下,该 选项设置为8。最大设置为 512。当然禁用或启动此特性和调整分区个数这个应该是DBA的工作,我们了解即可。
全文检索之倒排索引
什么是全文检索(Full-Text Search)?
它是将存储于数据库中的整本书或整篇文章中 的任意内容信息查找出来的技术。它可以根据需要获得全文中有关章、节、段、句、词 等信息,也可以进行各种统计和分析。我们比较熟知的Elasticsearch、Solr等就是全文 检索引擎,底层都是基于Apache Lucene的。
举个例子,现在我们要保存唐宋诗词,数据库中我们们会怎么设计?
诗词表我们可能的设计如下:
要根据朝代或者作者寻找诗,都很简单,比如“select 诗词全文 from 诗词表 where作者=‘李白’”,如果数据很多,查询速度很慢,怎么办?我们可以在对应 的查询字段上建立索引加速查询。
其实,上述诗词的中每个字都可以作为关键字,然后建立关键字和文档之间的对应 关系,也就是标识关键字被哪些文档包含。
所以,倒排索引就是,将文档中包含的关键字全部提取处理,然后再将关键字和文 档之间的对应关系保存起来,最后再对关键字本身做索引排序。用户在检索某一个 关键字是,先对关键字的索引进行查找,再通过关键字与文档的对应关系找到所在文档。
在存储在关系型数据库中的数据,需要我们事先分析将数据拆分为不同的字段,而 在es这类的存储中,需要应用程序根据规则自动提取关键字,并形成对应关系。 这些预先提取的关键字,在全文检索领域一般被称为term(词项),文档的词项提 取在es中被称为文档分析,这是全文检索很核心的过程,必须要区分哪些是词项, 哪些不是,比如很多场景下,apple和apples是同一个东西,望和看其实是同一个 动作。
MySQL中的全文索引
MySQL 5.6 以前的版本,只有 MyISAM 存储引擎支持全文索引。从InnoDB 1.2.x版本开 始,InnoDB存储引擎开始支持全文检索,对应的MySQL版本是5.6.x系列。
注意:
不管是哪一种引擎,只有字段的数据类型为:char , varchar ,text及其系列才可以建立全文索引
不过MySQL从设计之初就是关系型数据库,存储引擎虽然支持全文检索,整体架构 上对全文检索支持并不好而且限制很多,比如每张表只能有一个全文检索的索引, 不支持没有单词界定符( delimiter)的语言,如中文、日语、韩语等。
创建全文索引
创建表时创建全文索引
create table fulltext_test (
id int(11) NOT NULL AUTO_INCREMENT,
content text NOT NULL,
tag varchar(255),
PRIMARY KEY (id),
FULLTEXT KEY content_tag_fulltext(content,tag)
) DEFAULT CHARSET=utf8;
在已存在的表上创建全文索引
create fulltext index content_tag_fulltext
on fulltext_test(content,tag);
通过 SQL 语句 ALTER TABLE 创建全文索引
alter table fulltext_test
add fulltext index content_tag_fulltext(content,tag);
使用全文索引
和常用的模糊匹配使用的like+%不同,全文索引有自己的语法格式,使用match和against关键字,比如:
select * from fulltext_test where match(content,tag) against('xxx xxx');
总结:MySQL有哪些索引类型?
从数据结构角度可以分为B+树索引,哈希索引,以及FULLTEXT索引(全文检索索引,现如今MyISAM和InnoDB引擎都支持了全文检索索引)和R-Tree索引(用于对GIS数据类型创建SPATIAL索引);
从物理存储角度可以分为聚集索引和非聚集索引
从逻辑角度可以分为主键索引,普通索引,或者单列索引,多列索引,唯一索引,非唯一索引等等
面试题:什么是密集索引和稀疏索引?
面试中还会被问到什么是密集索引和稀疏索引。
密集索引的定义:密集索引其实就是聚集索引(聚簇索引)。密集索引对应的底层数据存储结构B+树的叶子节点保存的不只是键值字段的数据,还进行保存了位于同一行记录里的其它列字段的数据信息,由于密集索引决定了表的物理排列顺序,一个表只有一个物理排列顺序,所以一个表只能进行创建一个密集索引。
稀疏索引:稀疏索引其实就是非聚集索引。密集索引对应的底层数据存储结构B+树的叶子节点只是进行保存了主键字段和索引对应的字段的数据信息,有的稀疏索引只是进行保存了键位信息机器主键。接着可以敞开说一下回表,底层索引优化时是否选择走稀疏索引。
MyISAM存储引擎:不管是主键索引,唯一键索引还是普通索引都是稀疏索引。
为什么?这里涉及MyISAM引擎对应的表的底层存储方式,它是在磁盘上搞两个文件:一个文件存储表的索引相关信息,一个文件存储表的数据。
InnoDB存储引擎:有且只有一个密集索引。
为什么?这里涉及MyISAM引擎对应的表的底层存储方式,它是在磁盘上只搞了一个文件:是一个ibd文件,这个ibd文件进行存储表对应的索引和数据。
总结:
密集索引就是InnoDB存储引擎里的聚簇索引,稀疏索引就是InnoDB存储引擎里的普通二级索引。
辨析覆盖索引/索引覆盖
既然多个列可以组合起来构建为联合索引,那么辅助索引自然也可以由多个列组成。
覆盖索引也是我们经常见到的名词,InnoDB存储引擎支持覆盖索引(或称索引覆盖),即是从辅助索引中就可以得到select查询语句查询的记录,而不需要再进行回表查询聚集索引中的记录。
使用覆盖索引的一个好处就是辅助索引不包含整行记录的所有数据,所以覆盖索引的大小是要远小于聚集索引的。因此使用覆盖索引是可以减少大量的IO操作的(因为每一次回表操作是需要进行磁盘IO读写操作的)。
所以记住:使用覆盖索引可以视为是索引优化的一种方式,而并不是索引类型的一种。
除了覆盖索引这个概念外,在索引优化的范围内,还有前缀索引,三星索引等一系列的概念。
如图:这里的二级索引是index(note,b),这里使用到的字段为:note,b。所以在二级索引中都有,因此这就是一种覆盖索引的优化。
深入思考索引在查询中的使用
索引在查询中的作用到底是什么?在我们的查询中发挥着什么样的作用呢?
请记住:
1.一个索引就是一个B+树,索引让我们查询可以快速定位和扫描到我们需要的数据记录上,加快查询的速度。
2.一个select查询语句在执行过程中一般最多能使用一个二级索引来加快查询,即使在where条件中使用了多个二级索引。(因为二级索引回表操作时造成的磁盘IO的读写性能消耗是十分巨大的)
索引的代价
世界上从来没有只有好处没有坏处的东西,如果你有,请你一定要告诉我,让我也感受 一下。虽然索引是个好东西,在学习如何更好的使用索引之前先要了解一下使用它的代价,它在空间和时间上都会拖后腿。
空间上的代价
这个是显而易见的,每进行建立一个索引时,我们都需要为它进行建立一个B+树作为底层的数据存储结构。每一棵B+树的每一个节点都是一个数据页,一个页默认会占用16KB的存储空间,一棵很大的B+树有许多个数据页组成会占据很多的存储空间。
时间上的代价
每一次对表的数据进行增,删,改的操作时,为了保证数据的一致性,我们都需要进行修改各个B+树的索引。而且我们讲过,B+树每层节点都是按照索引列的数据值从小到大排序而进行组成了双向链表。
不论是叶子节点中的记录,还是非叶子内的节点中的记录都是按照索引列的值从小到大的顺序而形成了一个单向链表。
然而增,删,改的操作可能会对节点和记录的排序造成破坏,所以存储引擎需要额外的时间进行一些记录移位,页面分裂,页面回收的操作来维护好节点和记录的顺序。如果我们建立了许多索引,每一个索引对应的B+树都需要进行相关的维护操作以此保证数据的一致性。这必然会对性能造成巨大的影响。
既然索引这么有用,我们是不是创建越多越好?既然索引有代价,我们还是别创建了吧?
当然不是!按照经验,一般来说,一张表6-7个索引以下都能够取得比较好的性能权衡。
那么创建索引的时候有什么好的策略让我们充分利用索引呢?
高性能的索引创建策略
正确地创建和使用索引是实现高性能查询的基础。前面我们已经了解了索引相关的数据 结构,各种类型的索引及其对应的优缺点。现在我们一起来看看如何真正地发挥这些索 引的优势。
索引列的类型尽量小
我们在定义表结构的时候要进行显式的指定列的类型,以整形类型为例,有TTNYINT、 NEDUMNT、INT、BIGTNT这么几种,它们占用的存储空间依次递增,我们这里所说的类型 大小指的就是该类型表示的数据范围的大小。能表示的整数范围当然也是依次递增,如 果我们想要对某个整数列建立索引的话,在表示的整数范围允许的情况下,尽量让索引 列使用较小的类型,比如我们能使用INT就不要使用BIGINT,能使用NEDIUMINT就不要使 用INT。
这是因为:
(1)数据类型越小,在查询时进行的比较操作就越快(CPU层面)
(2)数据类型越小,索引占用的存储空间就越小,在一个数据页内就可以放下更多的记录,从而减
少磁盘IO带来的性能损耗(一次IO操作仅仅只可以读取数据页中一个扇形区域(大概为512byte)),
也就是意味着可以把更多的数据页缓存在内存中,从而加快读写效率。
这个建议对于表的主键来说更加适用,因为不仅对于聚簇索引中会进行存储主键字段的数据值,其它的所有的二级索引的叶子节点处都会进行存储一份记录的主键字段数据值,如果主键字段适用更小的数据类型,也就是意味着可以节省更多的存储空间和更加高效的进行IO操作。
利用索引选择性和前缀索引
索引的选择性/离散性
创建索引应该选择:选择性/离散性高的列。
索引的选择性/离散性是指:
不重复的索引值的数量(也称为基数,cardinality)和数据表的记录总数(N)的比值,范围值在1/N到1之间。
举一个例子:
sex性别字段的选择性/离散性求解:
sex性别字段的值要么是男,要么是女。所以不重复的索引值的数量为2。数据表的记录总数我们设为N。则选择性/离散性=2/N。这个值是极低的!
结论:
(1) 索引的选择性越高则查询效率越高,因为选择性高的索引可以让MySQL在查找时过滤掉更多的记录行。
(2) 唯一索引的选择性是1(为最大的选择性值),所以性能是最好的。
(3) 很差的索引选择性就是列中的数据重复度很高,比如性别字段,不考虑政治正确的情况下,只有两种情况,性别字段的值要么是男,要么是女。所以不重复的索引值的数量为2。数据表的记录总数我们设为N。则选择性/离散性=2/N。这个值是极低的!。那么当我们进行查询时,即使使用这个索引,从概率的角度来说,依然可能查出一半的数据出来。
比如下面这个表:
上表中哪个列做为索引字段最好?
当然是姓名字段,因为里面的数据没有任何重复,性别字段是 最不适合做索引的,因为数据的重复度非常高。
怎么算索引的选择性/离散性?
比如order_exp这个表:
select COUNT(DISTINCT order_no)/count(*) cnt from order_exp;
select COUNT(DISTINCT order_status)/count(*) cnt from order_exp;
很明显,order_no列上的索引就比order_status列上的索引的选择性就要好,原因很简 单,因为order_status列中的值只有-1,0,1三种。
前缀索引
有时候需要索引很长的字符列,这会让索引变得大且慢。一个策略是前面提到过的模拟 哈希索引。
模拟哈希索引:
order_exp表中order_note字段很长,我们想把它作为一个索引,我们可以增加一个order_not_hash字段来进行存储order_note的哈希值,然后在order_not_hash上进行建立索引,相对于之前的索引速度会有明显提升,一个是对完整的order_note做索引,而后者是用整数哈希值做索引,显然数字的比较比字符串的匹配要高效的多。
但是缺陷也很明显:
1.需要额外维护order_not_hash字段
2.哈希算法的选择决定了哈希冲突的概率,不良的哈希算法会导致重复值很多
3.不支持范围查询。(哈希底层可知,我们哈希插入一个数据时,都是通过哈希算法进行生成一个哈希值,这个哈希值确定键值key,key确定插入数据的位置,但是随机性较高,所以不支持范围查询,范围查询指的是对有一定规律顺序的数据值进行的查询)
还可以做些什么改进呢?
我们可以不进行索引字段的全部,而是进行索引开始的部分字符,这样可以大大的节省索引的空间,从而提高索引的效率。但是这样也会降低索引的选择性。一般情况下,我们需要保证某一个列前缀的选择性也是足够高的,以此满足查询性能。(尤其是对于BLOB,TEXT或者很长的varchar类型的列,应该使用前缀索引,因为MySQL不允许索引这些列的完整长度)
诀窍在于要选择足够长的前缀以保证较高的选择性,同时又不能太长(以便节约空间)。 前缀应该足够长,以使得前缀索引的选择性接近于索引整个列。
按照《阿里最新Java编程规范泰山版》中《(二) 索引规约》中的说法:
阿里开发规范中建议,这个前缀的长度为20比较合适,但是其实后面的“count(distinct left(列名, 索引长度))/count(*)的区分度来确定”会更合适点。在工程实践中具体如何做呢?
可以这样:
注释:COUNT(DISTINCT LEFT(order_note,3))/COUNT(*)就是在求order_note字段前三个字符前缀的选择性是多大。
分析图可知:
可以看见,从第10个开始选择性的增加值很高,随着前缀字符的越来越多,选择度也在 不断上升,但是增长到第15时,已经和第14没太大差别了,选择性提升的幅度已经很小 了,都非常接近整个列的选择性了。
那么针对这个字段做前缀索引的话,从第13到第15都是不错的选择,甚至第12也不是不 能考虑。
在上面的示例中,已经找到了合适的前缀长度,如何创建前缀索引:
ALTER TABLE order_exp ADD KEY (order_note(14));
建立前缀索引后查询语句并不需要更改:
select * from order_exp where order_note = 'xxxx' ;
前缀索引补充:
前缀索引是一种能使索引更小,更快的有效办法,但是另外一方面也要缺点就是MySQL无法使用前缀索引做ORDER BY和GROUP BY,也无法使用前缀索引做覆盖扫描。
有时候后缀索引 (suffix index)也有用途(例如,找到某个域名的所有电子邮件地 址)。MySQL原生并不支持反向索引,但是可以把字符串反转后存储,并基于此建立前缀 索引。可以通过触发器或者应用程序自行处理来维护索引。
只为用于搜索、排序或分组的列创建索引
也就是说,只为出现在where子句中的列字段,连接子句的连接列创字段进行创建索引,而出现在查询列表中的列一般就没必要建立索引了,除非是需要使用覆盖索引。又或者为出现在ORDER BY 或 GROUP BY子句的列创建索引,这句话是什么意思呢?比如:
SELECT * FROM order_exp ORDER BY insert_time, order_status,expire_time;
查询的结果集需要先按照insert_time字段进行排序,如果insert_time字段值相同,则需要按照order_status字段值进行排序。如果order_status的值相同,则需要按照expire_time排序。 回顾一下联合索引的存储结构,u_idx_day_status索引本身就是按照上述规则排好序 的,所以直接从索引中提取数据,然后进行回表操作取出该索引中不包含的列就好了。
当然ORDER BY的子句后边的列的顺序也必须按照索引列的顺序给出,如果给出ORDER BY order_status,expire_time, insert_time的顺序,那也是用不了B+树索引的。解释一下这句话:因为对于联合索引而言,它底层只是创建一颗B+树,对应叶子节点存储数据时的规则我们举一个例子结合分析:比如创建联合索引index(a,b,c),对于该联合索引就是在a字段排好序的情况下再对b进行排序,c同理。
SELECT insert_time, order_status,expire_time,count(*) FROM order_exp GROUP BY insert_time, order_status,expire_time;
这个查询语句相当于做了3次分组操作:
先把记录按照insert_time值进行分组,所有insert_time值相同的记录划分为一组。 将每个insert_time值相同的分组里的记录再按照order_status的值进行分组,将 order_status值相同的记录放到一个小分组里。
再将上一步中产生的小分组按照expire_time的值分成更小的分组。
然后针对最后的分组进行统计,如果没有索引的话,这个分组过程全部需要在内存里实现,但是如果有了索引的话,恰巧这个分组顺序又和我们的u_idx_day_status索引中的索引列的顺序是一致的,而我们的B+树优势按照索引列排好序的,这不正好么,所以 可以直接使用B+树索引进行分组。和使用B+树索引进行排序是一个道理,分组列的顺序 也需要和索引列的顺序一致。
合理设计多列索引
很多人对多列索引的理解都不够。一个常见的错误就是,为每个列创建独立的索引,或 者按照错误的顺序创建多列索引。
我们遇到的最容易引起困惑的问题就是索引列的顺序。正确的顺序依赖于使用该索引的 查询,并且同时需要考虑如何更好地满足排序和分组的需要。反复强调过,在一个多列 B-Tree索引中,索引列的顺序意味着索引首先按照最左列进行排序,其次是第二列,等 等。所以,索引可以按照升序或者降序进行扫描,以满足精确符合列顺序的ORDER BY、 GROUP BY和DISTINCT等子句的查询需求。
所以多列索引的列顺序至关重要。对于如何进行选择索引的列顺序有一个经验法则:将选择性最高的列放到索引的最前列。当不需要进行考虑排序和分组时,将选择性最高的列放在前面通常是很好的。这时候索引的作用只是用于优化where条件的查询。在这种情况下,这样设计的索引确实能够最快地过滤出需要的行,对于在WHERE子句中只使用了索引部分前缀 列的查询来说选择性也更高。
然而,性能不只是依赖于索引列的选择性,也和查询条件有关。可能需要根据那些运 行频率最高的查询来调整索引列的顺序,比如排序和分组,让这种情况下索引的选择性 最高。
同时,在优化性能的时候,可能需要使用相同的列但顺序不同的索引来满足不同类型的 查询需求。
尽可能设计三星索引
三星索引概念
对于一个查询而言,一个三星索引,可能是其最好的索引。
如果查询使用三星索引,一次查询通常只需要进行一次磁盘随机读以及一次窄索引片的 扫描,因此其相应时间通常比使用一个普通索引的响应时间少几个数量级。
对于三颗星的定义:
索引将相关的记录放到一起则获得一星。
如果索引中的数据顺序和查找中的排列顺序一致则获得二星。
如果索引中的列包含了查询中需要的全部列则获得三星。(即是覆盖索引的应用)
二星(排序星):
在满足一星的情况下,当查询需要排序,group by、 order by,如果查询所需的顺序与索引是一致的(索引本身是有序的),是不是就可以不用再另外排序了,一般来说排序可是影响性能的关键因素。
举个例子:索引为:index(age,name),然后排序为:order by age,name ,则符合二星的标准。
三星(宽索引星):
在满足了二星的情况下,如果索引中所包含了这个查询所需的所有列(包括where子句和select子句中所需的列,也就是覆盖索引),这样一来,查询就无需进行回表操作了,减少了查询的步骤和IO请求的次数,性能几乎可以提升一倍。
一星按照原文稍微有点难以理解,其实它的意思就是:
如果一个查询相关的索引行是相 邻的或者至少相距足够靠近的话,必须扫描的索引片宽度就会缩至最短,也就是说,让 索引片尽量变窄,也就是我们所说的索引的扫描范围越小越好。
这三颗星,哪颗最重要?
第三颗星。因为将一个列排除在索引之外可能会导致很多磁盘 随机读(回表操作)。第一和第二颗星重要性差不多,可以理解为第三颗星比重是50%, 第一颗星为27%,第二颗星为23%,所以在大部分的情况下,会先考虑第一颗星,但会根 据业务情况调整这两颗星的优先度。
达成三星索引
现在有表
create table customer(
cno int,
lname varchar(10),
fname varchar(10),
sex int,
weight int,
city varchar(10));建立索引
create index idx_cust on customer(city,lname,fname,cno);
对于下面的SQL而言,这是个三星索引
select cno,fname from customer where lname =’xx’ and city =’yy’ order by
fname;
来评估下:
第一颗星:我们进行where条件查询时使用的是等值查询,where=等值查询和where like '%字段'模糊查询半天,前者where过滤后索引扫描的范围缩的很窄并且是组合开头的列,所以符合一星。但是若使用后者则不符合一星,因为like'%字段'过滤的数据集是很大的 不符合一星。
但是对于后者模糊查询有一个注释:如果是where like'字段%'的话,那么等价于是where 字段=‘字段值’。
本题使用的是等值,所以符合一星。
第二颗星:order by的fname字段在组合索引中且是索引自动排序好的,符合。
第三颗星:select中的cno字段、fname字段以及where中的lname,city以及order by中的fname都在组合索引中存在,符合。
达不成三星索引
现在有表
CREATE TABLE `test` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`user_name` varchar(100) DEFAULT NULL,
`sex` int(11) DEFAULT NULL,
`age` int(11) DEFAULT NULL,
`c_date` datetime DEFAULT NULL,
PRIMARY KEY (`id`),
) ENGINE=InnoDB AUTO_INCREMENT=12 DEFAULT CHARSET=utf8;
SQL语句如下:
select user_name,sex,age from test where user_name like 'test%' and sex =1
ORDER BY age
如果我们建立索引index(user_name,sex,age):
第三颗星,满足
第一颗星,满足。where user_name like 'test%'等价于是where user_name = 'test',这是MySQL底层的优化,所以where过滤后的数据集是很小的,并且索引片是很窄的。
第二颗星,不满足,user_name 采用了模糊范围匹配,所以user_name不是有序的。sex 是过滤列,由于联合索引第一个字段user_name不是有序的,所以sex字段不满足有序。age有序是建立在user_name和sex字段依次有序的基础上的,所以此时age 列无法保证有序的。
上述我们看到,此时索引(user_name,sex,age)并不能满足三星索引中的第二颗星(排 序)。
于是我们改改,建立索引(sex, age,user_name):
第一颗星,不满足,只可以匹配到sex,sex选择性很差,意味着是一个宽索引片
第二颗星,满足,等值sex 的情况下,age是有序的。由新更改的联合索引可知,age字段有序是在sex字段有序的基础上,sex为等值查询过滤的 所以sex是有序的。所以age有序。所以满足。
第三颗星,满足,select查询的列都在索引列中,
总结:
对于索引(sex,age,user_name)我们可以看到,此时无法满足第一颗星,窄索引片的需 求。
以上2个索引,都是无法同时满足三星索引设计中的三个需求的,我们只能尽力满足2 个。而在多数情况下,能够满足2颗星,已经能缩小很大的查询范围了,具体最终要保留 那一颗星(排序星 or 窄索引片星),这个就需要看查询者自己的着重点了,无法给出 标准答案。
主键尽量是很少改变的列
我们知道,行是按照聚集索引物理排序的,如果主键字段频繁改变的话,物理顺序会改变,MySQL要不断的调整B+树,并且中间可能会产生页面的分裂和合并等等,并且会导致性能急剧下降。
处理冗余和重复索引
MySQL允许在相同列上创建多个索引,无论是有意的还是无意的。MySQL需要单独维护重 复的索引,并且优化器在优化查询的时候也需要逐个地进行考虑,这会影响性能。重复 索引是指在相同的列上按照相同的顺序创建的相同类型的索引。应该避免这样创建重复 索引,发现以后也应该立即移除。
有时会在不经意间创建了重复索引,例如下面的代码:
CREATE TABLE test (
ID INT NOT NULL PRIMARY KEY,
A INT NOT NULL,
B INT NOT NULL,
UNIQUE(ID),
INDEX(ID)
) ENGINE=InnoDB;
这里创建了一个主键,又加上唯一限制,然后再加上索引以供查询使用。事实上,MySQL 的唯一限制和主键限制都是通过索引实现的,因此,上面的写法实际上在相同的列上创 建了三个重复的索引。通常并没有理由这样做,除非是在同一列上创建不同类型的索引 来满足不同的查询需求。
冗余索引和重复索引有一些不同。如果创建了索引(A B),再创建索引(A)就是冗余索 引,因为这只是前一个索引的前缀索引。因此索引(AB)也可以当作索引(A)来使用(这种 冗余只是对B-Tree索引来说的)。但是如果再创建索引 (B,A),则不是冗余索引,索引 (B)也不是,因为B不是索引(A,B)的最左前缀列。
已有的索引(A),扩展为(A,ID),其中ID是主键,对于InnoDB来说主键列已经包含在二 级索引中了,所以这也是冗余的。
解决冗余索引和重复索引的方法很简单,删除这些索引就可以,但首先要做的是找出这 样的索引。可以通过写一些复杂的访问INFORMATION_SCHEMA表的查询来找。
删除未使用的索引
除了冗余索引和重复索引,可能还会有一些服务器永远不用的索引。这样的索引完全是 累赘,建议考虑删除。
补充资料:磁盘和B+树
为什么关系型数据库都选择了B+树,这个和磁盘的特性有着非常大的关系。
如果我们简化一下,可以这么看:
一个磁盘由大小相同并且同轴的圆形盘片组成,磁盘可以转动(各个磁盘必须同步移动)。在磁盘的一侧有磁头支架,磁头支架固定了一组磁头,每一个磁头负责存取一个磁盘的内容。磁头不能转动,但是可以沿磁盘半径方向运动。
盘片被划分成一系列同心环,圆心是盘片中心,每一个同心环叫做一个磁道,所有半径相同的磁道组成了一个柱面。磁道被沿半径线划分成一个个小的段,每一个段叫做一个扇区,每一个扇区是磁盘的最小存储单元也是最小读写单元。现在磁盘扇区一般是512字节~4k个字节
磁盘上数据必须用一个三维地址唯一标识:
柱面号,盘面号,扇区号
读/写磁盘上某一指定数据需要下面步骤:
(1) 首先移动臂根据柱面号使得磁头移动到所需要的柱面上,这一过程被称为定位或者查找
(2) 所有磁头都定位到磁道上后,这时根据盘面号来进行确定指定盘面上的具体磁道
(3) 盘面确定以后,盘片开始旋转,将指定块号的磁道段移动至磁头下。
经过上面步骤,指定数据的存储位置就被找到。这时就可以开始读/写操作了。
可以看见,磁盘读取依靠的是机械运动,分为寻道时间,旋转延迟,传输时间三个部分,这三个部分耗时相加就是一次磁盘IO的时间,一般大概在9ms左右。一次磁盘IO读取的大小为一个扇形区域大小。
寻道时间(seek)是将读写磁头移动到正确的磁道上所需要的时间,这部分的时间代价最高。旋转延迟时间(rotation)是磁盘旋转将目标扇区移动到读写磁头下方所需的时间,取决于磁盘转速。数据传输时间(transfer)是完成传输数据所需要的时间,取决于接口的数据传输率,在纳秒级别,远小于前两部分所消耗的时间。磁盘读取时间成本是访问内存的几百倍到几万倍之间。
为了提高效率,要尽量减少磁盘IO。为了达到这个目的,磁盘往往不是严格按需读取,而是每次都会进行预读操作,所谓预读就是:即使只需要一个字节,磁盘也会从这个位置开始,顺序向后读取一定长度的数据放入到内存中,这个称之为预读。预读的作用就是为了顺序读取磁盘的数据。
这样做的理论依据是著名的局部性原理:
当一个数据被使用到时,其附近的数据也通常会马上被使用到。程序运行期间所需要的数据通常是比较集中的。
可以试运行下面这段代码:
public static void main(String[] args) {
int[][] arr = new int[10000][10000];
int sum = 0;
long startTime = System.currentTimeMillis();
for (int i = 0; i < arr.length; i++) {
for (int j = 0; j < arr[0].length; j++) {
/*按行访问数组*/ //相当于是顺序读写
sum += arr[i][j];
} }
System.out.println("按行耗时:" + (System.currentTimeMillis() - startTime) + "ms");
sum = 0;
startTime = System.currentTimeMillis();
for (int i = 0; i < arr.length; i++) {
for (int j = 0; j < arr[0].length; j++) {
/*按列访问数组*/ //相当于随机读写
sum += arr[j][i];
}
}
System.out.println("按列耗时:" + (System.currentTimeMillis() - startTime) + "ms");
}
这一测试案例就可以看到局部性原理对程序性能的影响。
由于磁盘顺序读取的效率很高(不需要寻道时间,只需要很少的旋转时间即可),一般来说,磁盘的顺序读的效率是随机读的40到400倍都有可能,顺序写是随机写的10到100倍(SSD盘则差距要小的多,顺序读写的效率是随机读写效率的7到10倍,但是有评测表明机械硬盘的顺序写性能稍 优于SSD。总的来说Mysql数据库如果由硬盘由机械的换成SSD的,性能会有很大的提升), 因此对于具有局部性的程序来说,预读可以提高I/O效率。
预读的长度一般为页(page)的整数倍。页是计算机管理存储器的逻辑块,硬件以及操作系统往往将主存和磁盘存储区分割为连续的大小相等的块,每个存储块称之为是一页,页大小通常为4k当然也有16k的页,主存和磁盘以页为单位进行交换数据。当程序要读取的数据不在主存中时,会触发一个缺页异常,此时系统会向磁盘发出读盘信号,磁盘会找到数据的起始位置并且向后连续读取一页或几页载入到内存中,然后异常返回,程序继续运行。
按照磁盘的这种性质,如果是一个页存放一个B+树的节点,自然是可以存放很多的数据的,比如InnoDB引擎里,默认定义的B+树的节点大小为16KB,这就是说,假如一个Key为8个字节,那么一个节点就可以存放大约1000个Key,意味着B+树可以有1000个分叉。同时InnoDB每一次磁 盘I/O,预读进行读取的都是 16KB的整数倍的数据。也就是说InnoDB在节点的读写上是可以充分利用 磁盘顺序IO的高速读写特性。
同时按照B+树逻辑结构来说,在叶子节点一层,所有记录的主键按照从小到大的 顺序排列,并且形成了一个双向链表。同一层的非叶子节点也互相串联,形成了一 个双向链表。那么在实际读写的时候,很大的概率相邻的节点会放在相邻的页上, 又可以充分利用磁盘顺序IO的高速读写特性。所以我们对MySQL优化的一大方向 就是尽可能的多让数据顺序读写,少让数据随机读写。