[TOC]html
本文有参考网上其余相关文章,本文最后有附参考的连接mysql
MySQL支持诸多存储引擎,而各类存储引擎对索引的支持也各不相同,所以MySQL数据库支持多种索引类型,如BTree索引,哈希索引,全文索引等等。为了不混乱,本文将只关注于BTree索引,由于这是日常使用MySQL时主要打交道的索引。redis
MySQL官方对索引的定义为:索引(Index)是帮助MySQL高效获取数据的数据结构。提取句子主干,就能够获得索引的本质:索引是数据结构。算法
索引的目的在于提升查询效率,能够类比字典,若是要查“mysql”这个单词,咱们确定须要定位到m字母,而后从下往下找到y字母,再找到剩下的sql。若是没有索引,那么你可能须要把全部单词看一遍才能找到你想要的,若是我想找到m开头的单词呢?或者ze开头的单词呢?是否是以为若是没有索引,这个事情根本没法完成?sql
我们去图书馆借书也是同样,若是你要借某一本书,必定是先找到对应的分类科目,再找到对应的编号,这是生活中活生生的例子,通用索引,能够加快查询速度,快速定位。数据库
全部索引原理都是同样的,经过不断的缩小想要得到数据的范围来筛选出最终想要的结果,同时把随机的事件变成顺序的事件,也就是咱们老是经过同一种查找方式来锁定数据。缓存
数据库也是同样,但显然要复杂许多,由于不只面临着等值查询,还有范围查询(>、<、between)、模糊查询(like)、并集查询(or)、多值匹配(in【in本质上属于多个or】)等等。数据库应该选择怎么样的方式来应对全部的问题呢?咱们回想字典的例子,能不能把数据分红段,而后分段查询呢?最简单的若是1000条数据,1到100分红第一段,101到200分红第二段,201到300分红第三段……这样查第250条数据,只要找第三段就能够了,一会儿去除了90%的无效数据。但若是是1千万的记录呢,分红几段比较好?稍有算法基础的同窗会想到搜索树,其平均复杂度是lgN,具备不错的查询性能。但这里咱们忽略了一个关键的问题,复杂度模型是基于每次相同的操做成原本考虑的,数据库实现比较复杂,数据保存在磁盘上,而为了提升性能,每次又能够把部分数据读入内存来计算,由于咱们知道访问磁盘的成本大概是访问内存的十万倍左右,因此简单的搜索树难以知足复杂的应用场景。安全
任何一种数据结构都不是凭空产生的,必定会有它的背景和使用场景,咱们如今总结一下,咱们须要这种数据结构可以作些什么,其实很简单,那就是:每次查找数据时把磁盘IO次数控制在一个很小的数量级,最好是常数数量级。那么咱们就想到若是一个高度可控的多路搜索树是否能知足需求呢?就这样,b+树应运而生。bash
浅蓝色的块咱们称之为一个磁盘块,能够看到每一个磁盘块包含几个数据项(深蓝色所示)和指针(黄色所示),如磁盘块1包含数据项17和35,包含指针P一、P二、P3,P1表示小于17的磁盘块,P2表示在17和35之间的磁盘块,P3表示大于35的磁盘块。真实的数据存在于叶子节点即三、五、九、十、1三、1五、2八、2九、3六、60、7五、7九、90、99。非叶子节点不存储真实的数据,只存储指引搜索方向的数据项,如1七、35并不真实存在于数据表中。服务器
如图所示,若是要查找数据项29,那么首先会把磁盘块1由磁盘加载到内存,此时发生一次IO,在内存中用二分查找肯定29在17和35之间,锁定磁盘块1的P2指针,内存时间由于很是短(相比磁盘的IO)能够忽略不计,经过磁盘块1的P2指针的磁盘地址把磁盘块3由磁盘加载到内存,发生第二次IO,29在26和30之间,锁定磁盘块3的P2指针,经过指针加载磁盘块8到内存,发生第三次IO,同时内存中作二分查找找到29,结束查询,总计三次IO。真实的状况是,3层的b+树能够表示上百万的数据,若是上百万的数据查找只须要三次IO,性能提升将是巨大的,若是没有索引,每一个数据项都要发生一次IO,那么总共须要百万次的IO,显然成本很是很是高。
经过上面的分析,咱们知道间越小,数据项的数量越多,树的高度越低。这就是为何每一个数据项,即索引字段要尽可能的小,好比int占4字节,要比bigint8字节少一半。这也是为何b+树要求把真实的数据放到叶子节点而不是内层节点,一旦放到内层节点,磁盘块的数据项会大幅度降低,致使树增高。当数据项等于1时将会退化成线性表。
当b+树的数据项是复合的数据结构,好比(name,age,sex)的时候,b+数是按照从左到右的顺序来创建搜索树的,好比当(张三,20,F)这样的数据来检索的时候,b+树会优先比较name来肯定下一步的所搜方向,若是name相同再依次比较age和sex,最后获得检索的数据;但当(20,F)这样的没有name的数据来的时候,b+树就不知道下一步该查哪一个节点,由于创建搜索树的时候name就是第一个比较因子,必需要先根据name来搜索才能知道下一步去哪里查询。好比当(张三,F)这样的数据来检索时,b+树能够用name来指定搜索方向,但下一个字段age的缺失,因此只能把名字等于张三的数据都找到,而后再匹配性别是F的数据了, 这个是很是重要的性质,即索引的最左匹配特性。
在MySQL中,索引属于存储引擎级别的概念,不一样存储引擎对索引的实现方式是不一样的,本文主要讨论MyISAM和InnoDB两个存储引擎的索引实现方式。
MyISAM引擎使用B+Tree做为索引结构,叶节点的data域存放的是数据记录的地址。 下图是MyISAM索引的原理图:
这里设表一共有三列,假设咱们以Col1为主键,则上图即是一个MyISAM表的主索引(Primary key)示意图。能够看出MyISAM的索引文件仅仅保存数据记录的地址。在MyISAM中,主索引和辅助索引(Secondary key)在结构上没有任何区别,只是主索引要求key是惟一的,而辅助索引的key能够重复。若是咱们在Col2上创建一个辅助索引,则此索引的结构以下图所示:
一样也是一颗B+Tree,data域保存数据记录的地址。所以,MyISAM中索引检索的算法为首先按照B+Tree搜索算法搜索索引,若是指定的Key存在,则取出其data域的值,而后以data域的值为地址,读取相应数据记录。
MyISAM的索引方式也叫作“非汇集”的,之因此这么称呼是为了与InnoDB的汇集索引区分。
虽然InnoDB也使用B+Tree做为索引结构,但具体实现方式却与MyISAM大相径庭。
第一个重大区别是InnoDB的数据文件自己就是索引文件。从上文知道,MyISAM索引文件和数据文件是分离的,索引文件仅保存数据记录的地址。而在InnoDB中,表数据文件自己就是按B+Tree组织的一个索引结构,这棵树的叶节点data域保存了完整的数据记录。这个索引的key是数据表的主键,所以InnoDB表数据文件自己就是主索引。
上图是InnoDB主索引(同时也是数据文件)的示意图,能够看到叶节点包含了完整的数据记录。这种索引叫作汇集索引。由于InnoDB的数据文件自己要按主键汇集,因此InnoDB要求表必须有主键(MyISAM能够没有),若是没有显式指定,则MySQL系统会自动选择一个能够惟一标识数据记录的列做为主键,若是不存在这种列,则MySQL自动为InnoDB表生成一个隐含字段做为主键,这个字段长度为6个字节,类型为长整形。
第二个与MyISAM索引的不一样是InnoDB的辅助索引data域存储相应记录主键的值而不是地址。换句话说,InnoDB的全部辅助索引都引用主键做为data域。例如,下图为定义在Col3上的一个辅助索引:
这里以英文字符的ASCII码做为比较准则。汇集索引这种实现方式使得按主键的搜索十分高效,可是辅助索引搜索须要检索两遍索引:首先检索辅助索引得到主键,而后用主键到主索引中检索得到记录。
了解不一样存储引擎的索引实现方式对于正确使用和优化索引都很是有帮助,例如知道了InnoDB的索引实现后,就很容易明白为何不建议使用过长的字段做为主键,由于全部辅助索引都引用主索引,过长的主索引会令辅助索引变得过大。再例如,用非单调的字段做为主键在InnoDB中不是个好主意,由于InnoDB数据文件自己是一颗B+Tree,非单调的主键会形成在插入新记录时数据文件为了维持B+Tree的特性而频繁的分裂调整,十分低效,而使用自增字段做为主键则是一个很好的选择。
一个最重要的原则是最左前缀原理,在提这个以前要先说下联合索引,MySQL中的索引能够以必定顺序引用多个列,这种索引叫作联合索引,通常的,一个联合索引是一个有序元组<a1, a2, …, an>,其中各个元素均为数据表的一列。另外,单列索引能够当作联合索引元素数为1的特例。
索引匹配的最左原则具体是说,假如索引列分别为A,B,C,顺序也是A,B,C:
- 那么查询的时候,若是查询【A】【A,B】 【A,B,C】,那么能够经过索引查询
- 若是查询的时候,采用【A,C】,那么C这个虽然是索引,可是因为中间缺失了B,所以C这个索引是用不到的,只能用到A索引
- 若是查询的时候,采用【B】 【B,C】 【C】,因为没有用到第一列索引,不是最左前缀,那么后面的索引也是用不到了
- 若是查询的时候,采用范围查询,而且是最左前缀,也就是第一列索引,那么能够用到索引,可是范围后面的列没法用到索引
复制代码
由于索引虽然加快了查询速度,但索引也是有代价的:索引文件自己要消耗存储空间,同时索引会加剧插入、删除和修改记录时的负担,另外,MySQL在运行时也要消耗资源维护索引,所以索引并非越多越好
在使用InnoDB存储引擎时,若是没有特别的须要,请永远使用一个与业务无关的自增字段做为主键。若是从数据库索引优化角度看,使用InnoDB引擎而不使用自增主键绝对是一个糟糕的主意。
InnoDB使用汇集索引,数据记录自己被存于主索引(一颗B+Tree)的叶子节点上。这就要求同一个叶子节点内(大小为一个内存页或磁盘页)的各条数据记录按主键顺序存放,所以每当有一条新的记录插入时,MySQL会根据其主键将其插入适当的节点和位置,若是页面达到装载因子(InnoDB默认为15/16),则开辟一个新的页(节点)。若是表使用自增主键,那么每次插入新的记录,记录就会顺序添加到当前索引节点的后续位置,当一页写满,就会自动开辟一个新的页。以下:
这样就会造成一个紧凑的索引结构,近似顺序填满。因为每次插入时也不须要移动已有数据,所以效率很高,也不会增长不少开销在维护索引上。
若是使用非自增主键(若是身份证号或学号等),因为每次插入主键的值近似于随机,所以每次新纪录都要被插到现有索引页得中间某个位置,以下:
此时MySQL不得不为了将新记录插到合适位置而移动数据,甚至目标页面可能已经被回写到磁盘上而从缓存中清掉,此时又要从磁盘上读回来,这增长了不少开销,同时频繁的移动、分页操做形成了大量的碎片,获得了不够紧凑的索引结构,后续不得不经过OPTIMIZE TABLE来重建表并优化填充页面。
所以,只要能够,请尽可能在InnoDB上采用自增字段作主键。
最左前缀匹配原则,很是重要的原则,mysql会一直向右匹配直到遇到范围查询(>、<、between、like)就中止匹配,好比a = 1 and b = 2 and c > 3 and d = 4 若是创建(a,b,c,d)顺序的索引,d是用不到索引的,若是创建(a,b,d,c)的索引则均可以用到,a,b,d的顺序能够任意调整。
=和in能够乱序,好比a = 1 and b = 2 and c = 3 创建(a,b,c)索引能够任意顺序,mysql的查询优化器会帮你优化成索引能够识别的形式
尽可能选择区分度高的列做为索引,区分度的公式是count(distinct col)/count(*),表示字段不重复的比例,比例越大咱们扫描的记录数越少,惟一键的区分度是1,而一些状态、性别字段可能在大数据面前区分度就是0,那可能有人会问,这个比例有什么经验值吗?使用场景不一样,这个值也很难肯定,通常须要join的字段咱们都要求是0.1以上,即平均1条扫描10条记录
索引列不能参与计算,保持列“干净”,好比from_unixtime(create_time) = ’2014-05-29’就不能使用到索引,缘由很简单,b+树中存的都是数据表中的字段值,但进行检索时,须要把全部元素都应用函数才能比较,显然成本太大。因此语句应该写成create_time = unix_timestamp(’2014-05-29’);
尽可能的扩展索引,不要新建索引。好比表中已经有a的索引,如今要加(a,b)的索引,那么只须要修改原来的索引便可,固然要考虑原有数据和线上使用状况
配置优化指的MySQL 的 server端的配置,通常对于业务方而言,能够不用关注,毕竟会有专门的DBA来处理,可是对于原理的了解,我想,咱们开发,是须要了解的
通常要进行SQL调优,那么就说有慢查询的SQL,系统或者server能够开启慢查询日志,尤为是线上系统,通常都会开启慢查询日志,若是有慢查询,能够经过日志来过滤。可是知道了有须要优化的SQL后,下面要作的就是如何进行调优
在平常工做中,咱们有时会开慢查询去记录一些执行时间比较久的SQL语句,找出这些SQL语句并不意味着完事了,咱们经常用到explain这个命令来查看一个这些SQL语句的执行计划,查看该SQL语句有没有使用上了索引,有没有作全表扫描,这均可以经过explain命令来查看。因此咱们深刻了解MySQL的基于开销的优化器,还能够得到不少可能被优化器考虑到的访问策略的细节,以及当运行SQL语句时哪一种策略预计会被优化器采用。
使用explain 只须要在原有select 基础上加上explain关键字就能够了,以下:
mysql> explain select * from servers;
+----+-------------+---------+------+---------------+------+---------+------+------+-------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+---------+------+---------------+------+---------+------+------+-------+
| 1 | SIMPLE | servers | ALL | NULL | NULL | NULL | NULL | 1 | NULL |
+----+-------------+---------+------+---------------+------+---------+------+------+-------+
1 row in set (0.03 sec)
复制代码
简要解释下explain各个字段的含义
EXPLAIN的特性
假若有以下表结构
circlemessage_idx_0 | CREATE TABLE `circlemessage_idx_0` (
`circle_id` bigint(20) unsigned NOT NULL COMMENT '群组id',
`from_id` bigint(20) unsigned NOT NULL COMMENT '发送用户id',
`to_id` bigint(20) unsigned NOT NULL COMMENT '指定接收用户id',
`msg_id` bigint(20) unsigned NOT NULL COMMENT '消息ID',
`type` tinyint(3) unsigned NOT NULL DEFAULT '0' COMMENT '消息类型',
PRIMARY KEY (`msg_id`,`to_id`),
KEY `idx_from_circle` (`from_id`,`circle_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin
复制代码
经过执行计划explain分析以下查询语句
mysql> explain select msg_id from circlemessage_idx_0 where to_id = 113487 and circle_id=10019063 and msg_id>=6273803462253938690 and from_id != 113487 order by msg_id asc limit 30;
+----+-------------+---------------------+-------+-------------------------+---------+---------+------+--------+-------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+---------------------+-------+-------------------------+---------+---------+------+--------+-------------+
| 1 | SIMPLE | circlemessage_idx_0 | range | PRIMARY,idx_from_circle | PRIMARY | 16 | NULL | 349780 | Using where |
+----+-------------+---------------------+-------+-------------------------+---------+---------+------+--------+-------------+
1 row in set (0.00 sec)
复制代码
mysql> explain select msg_id from circlemessage_idx_0 where to_id = 113487 and circle_id=10019063 and from_id != 113487 order by msg_id asc limit 30;
+----+-------------+---------------------+-------+-----------------+---------+---------+------+------+-------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+---------------------+-------+-----------------+---------+---------+------+------+-------------+
| 1 | SIMPLE | circlemessage_idx_0 | index | idx_from_circle | PRIMARY | 16 | NULL | 30 | Using where |
+----+-------------+---------------------+-------+-----------------+---------+---------+------+------+-------------+
1 row in set (0.00 sec)
复制代码
经过上面两个执行计划能够发现当没有msg_id >= xxx这个查询条件的时候,检索的rows要少不少,而且二者查询的时候都用到了索引,并且用到的还只是主键索引。那说明索引应该是不合理的,没有发挥最大做用。
分析这个执行计划能够看到,当包含msg_id >= xxx 查询条件的时候,rows有34w多行,这种状况,说明检索太多,要么就是表里面确实有这么大,要么就是索引不合理没有用到索引,大都状况是没用合理用到索引。列中所用到的索引也是PRIMARY,那就多是(msg_id
,to_id
)的其中一个,注意咱们创建表的时候msg_id索引的顺序是在to_id前面的,所以MySQL查询必定会优先用msg_id索引,在使用了msg_id索引后,就已经检索出了34w行,而且因为msg_id的查询条件是大于等于,所以,再这个查询条件后,就不能再用到to_id的索引。
而后再看key_len长度为16,结合 key为PRIMARY,那么能够分析得知,只有一个主键索引被用到。
最后看看 type 值,是range,那么就说明这个查询要么是范围查询,要么就是多值匹配。
请注意,from_id != xxx 这样的语句,是没法用到索引的。 只有from_id = xxx就能够用到因此,所以from id 的索引其实能够不用,创建索引的时候就要考虑清楚
既然知道索引不合理,那么就要分析并调整索引。通常而言,咱们既然要从单表里面查询,那么就须要可以知道大致,单表里面大体会有哪些数据,如今的量级大概是多少。
而后开始下一步的分析,既然msgid是被设置为了主键,那必定是全局惟一的,全部,有多少数据量就至少会有多少条msgid;那么检索msg_id基本就是检索整个表了。咱们要作的优化就是要尽可能减小索引,减小查询的行数;那么就须要思考,经过查询哪些字段才可以减小行数?好比,一个张表里面,所属某个用户的数据,会不会比查询msgid的行数要少? 查询某个用户而且是属于某个圈子的,那会不会就更少了? 等等。。。
而后根据实际状况分析,单表里面命中to_id 的行数应该是会小于命中msg_id的,所以要首先保证可以使用到to_id的索引,为此,能够设置主键的时候把msg_id和to_id的顺序交互一下;可是,因为已是线上的表,已经有了大量数据,而且业务开始运行,这种状况下,修改主键会引起不少问题(固然修改索引是OK的),所以,不建议直接修改主键。那么,为了保证有效使用to_id的索引,就要新建一个联合索引;那么新建的联合索引的第一索引字段必然是to_id,针对此业务场景,最好可以再加上circle_id索引,这样能够快速索引;这样就获得了新的联合索引(to_id,circle_id)的索引,而后,由于要找msg_id,为此,在此基础上,再加上msg_id。最终获得的联合索引为(to_id,circle_id,msg_id);这样的话,就可以快速检索这样的查询语句了:where to_id = xxx and circle_id = xxx and msgId >= xxx
固然,索引的创建,也不是说某个sql 语句须要啥索引,就创建某个联合索引,这样的话,索引太多的话,写的性能受影响(插入、删除、修改),而后存储空间也会相应增大;另外mysql在运行时也会消耗资源维护索引,因此,索引并非越多越好,须要结合查询最频繁、最影响性能的sql来创建合适的索引。须要再说明的是,一个联合索引或者一组主键就是一个btree,多个索引就是多个btree
首先咱们须要深刻理解索引的原理和实现,当理解了原理后,才可以更有助于咱们创建合适的索引。而后咱们创建索引的时候,不要想固然,要先想清楚业务逻辑,再创建对应的表结构和索引。 须要再次强调以下几点:
感谢参考文章的原做者
【"欢迎关注个人微信公众号:Linux 服务端系统研发,后面会大力经过微信公众号发送优质文章"】