MySQL索引——最左前端

一、索引的概念

索引是一种特殊的文件(InnoDB数据表上的索引是表空间的一个组成部分),它们包含着对数据表里所有记录的引用指针。更通俗的说,数据库索引好比是一本书前面的目录,能加快数据库的查询速度。
索引分为聚集索引非聚集索引两种。聚集索引是按照数据存放的物理位置为顺序的,而非聚集索引就不一样了;聚集索引能提高多行检索的速度,而非聚集索引对于单行的检索很快。
什么是聚集索引?
汉语字典的正文本身就是一个聚集索引。比如,我们要查“安”字,就会很自然的翻开字典的前几页,因为“安”的拼音是“an”,而按照拼音排序汉子的字典是以英文字母“a”开头并以“z”结尾的,那么“安”字就自然地排在字典的前部。如果您翻完了所有以“a”开头的部分仍未找到这个字,那么就说明您的字典中没有这个字;同样的,如果查“张”字,那您也会将您的字典翻到最后部分,因为“张”的拼音是“zhang”。也就是说,字典的正文部分本身就一个目录,您不需要再去查其他目录来找到您需要找的内容。正文内容本身就是一个按照一定规则排列的目录称为“聚集索引”。每个表只能有一个聚集索引,因为目录只能按照一种方法进行排序。
什么是非聚集索引?
如果您认识某个字,您可以快速地从自动中查到这个字。但您也可能会遇到您不认识的字,不知道它的发音,这时候,您就不能按照刚才的方法找到您要查的字,而需要去根据“偏旁部首”查到您要找的字,然后根据这个字后的页码直接翻到某页来找到您要找的字。但您结合“部首目录”和“检字表”而查到的字的排序并不是真正的正文的排序方法,比如您查“张”字,我们可以看到查部首之后的检字表中“张”的页面是672页,检字表中“张”的上面是“驰”字,但页码却是63页,“张”的下面是“弩”字,页码是390页。很显然,这些并不是真正的分别位于“张”字的上下方,现在您看到的连续的“驰”、“张”、“弩”三字实际上就是他们在非聚集索引中的排序,是字典正文中的字在非聚集索引中的映射。我们可以通过这种方式来找到您所需要的字,但它需要两个过程,先找到目录中的结果,然后再翻到您所需要的页码。我们把这种目录纯粹是目录,正文纯粹是正文排序方式称为“非聚集索引”。
存储特点:
1、聚集索引。表数据按照索引的顺序来存储的,也就是说索引项的顺序与表中记录的物理顺序一致。对于聚集索引,叶子结点即存储了真是的数据行,不再有另外单独的数据页。在一张表上最多只能创建一个聚集索引,因为真实数据的物理顺序只能有一种。
2、非聚集索引。表数据存储顺序与索引顺序无关。对于非聚集索引,叶节点包含索引字段值及指向数据页数据行的逻辑指针,其行数量与数据表行数量一致。
总结一下:聚集索引时一种稀疏索引,数据页上一级的索引存储页存储的是叶指针,而不是行指针。而对于非聚集索引,则是密集索引,在数据页的上一级索引页它为每一个数据行存储一条索引记录。
更新表数据:
1、向表中插入新数据行
如果一张表没有聚集索引,那么它被称为“堆集”。这样的表中的数据行没有特定的顺序,所有的新行将被添加到表的末尾位置。
而建立了聚集索引的数据表则不同:最简单的情况下,插入操作根据索引找到对应的数据页,然后通过挪动已有的记录为新数据腾出空间,最后插入数据。如果数据页已满,则需要拆分数据页,调整索引指针(且如果表还有非聚集索引,还需要更新这些索引指向新的数据页)。而类似于自增列为聚集索引的,数据库系统可能并不拆分数据页,,而只是简单的新添数据页。
2、从表中删除数据行
对删除数据行来说:删除行将导致其下方的数据行向上移动以填充删除记录造成的空白。如果删除的行是该数据页中的最后一行,那么该数据页将被回收,相应的索引中的记录将被删除。对于数据的删除操作,可能导致索引页中仅有一条记录,这是,该记录可能会被移至邻近的索引页中,原索引页将被回收,即所谓的“索引合并”。
Cardinality:
该值表示索引中唯一值记录数量的预估值,如索引值有以下取值:1、3、7、3、5、7,则cardinality=4;
在实际应用中,cardinality/rows_num应尽可能接近1,若该值非常小,则需考虑是否要建索引。
innodb存储引擎cardinality的更新策略:
a.表中1/16数据发生变化;
b.stat_modified_counter>2000000000(发生变化的次数)。

二 、索引的类型

1、普通索引

最基本的索引,没有任何限制,是我们大多情况下使用到的索引
创建:
A、直接创建
CREATE INDEX index_name ON table(column(length))
B、修改原有表结构
ALTER TABLE table_name ADD INDEX index_name ON(column(length))
若是char、varchar类型length可不填,默认字段的实际长度,若是blob、text类型则必须指定长度。

2、唯一索引

与普通索引类型,不同之处在于索引列的值必须唯一,但允许有空值(和主键不同之处),若是联合索引,则列值的组合必须唯一
创建:
A、直接创建
CREATE UNIQUE INDEX index_name ON table(column(length))
B、修改原有表结构
ALTER TABLE table ADD UNIQUE index_name ON(column(length))

3、主键索引

不允许有空值,主键索引建立的规则是int优于varchar,一般在建表的时候创建,最好是与表的其他字段不相关的或者是业务不相关的列,一般为int切实AUTO_INCREMENT自增长类型的

4、联合索引

通俗地讲就是,索引包含多个字段但只有一个名称。
创建:
CREATE INDEX index_name ON table_name(column1(length1),column2(length2……))
下面来讲一个联合索引的”最左前缀”

三、联合索引——最左前缀

实例:在students表中建立以<email、phone、create_date>3个字段为联合索引,如下图所示:
在这里插入图片描述
规则1:最左前缀:索引where时的条件,要按照建立索引的时候字段的排列顺序 。
示例如下:
where条件单独使用email字段,符合最左前缀,联合索引起作用:
在这里插入图片描述
where条件使用email与phone字段,符合最左前缀,联合索引起作用:
在这里插入图片描述
由上图可见,key_len由52变成55了,说明用到了索引的第一列和第二列前缀。
where条件使用phone字段,不符合最左前缀,联合索引不起作用:
在这里插入图片描述
where条件使用phone与create_date字段,不符合最左前缀,联合索引不起作用
在这里插入图片描述
规则2:全值=或in匹配where条件可以乱序:索引where时的条件包含了联合索引的全部字段,且是等值匹配或in匹配的时候,where条件的顺序可以打乱,mysql查询优化器会自动优化,调整顺序来使用定义好的索引
实例如下:
where条件顺序为phone、email、create_date的等值匹配
在这里插入图片描述
where条件顺序为phone、email、create_date的in匹配
在这里插入图片描述
规则3:查询条件用到了索引中列的精确匹配,但是中间某个条件未提供
实例如下:
在这里插入图片描述
key_len仍为52。由上可见,因为phone未提供,所以查询只用到了索引的第一列,而后面的create_date虽然也在索引中,但是由于phone不存在而无法和左前缀连接,因此需要对结果进行扫描过滤create_date。
如果想让create_date也使用索引而不是where过滤,可以增加一个辅助索引<email,create_date>,此时上面的查询会使用这个索引。
除此之外,还可以使用一种称之为“隔离列”的优化方法,将email与create_date之间的“坑”填上。
如下图:
在这里插入图片描述
在这种成为“坑”的列值比较少的情况下,可以考虑用“IN”来填补这个“坑”从而形成最左前缀。这次key_len为59,说明索引被用全了,但是从type和rows看出IN实际上执行了一个range查询,这里检查了3个key。
小伙伴们,可以用SHOW PROFILES看下两种查询的性能比较。当然,如果phone的值很多,用填坑就不合适了,必须建立辅助索引。
规则4:范围查询(范围查询后面的列将无法使用索引)
对于范围条件查询,MYSQL无法再使用范围后面的其他索引列了。但对多个等值条件查询则没有这样的限制。
实例如下:
where条件顺序包含了联合索引的所有字段,但其中email字段使用了范围查询,联合索引对email后的所有字段不起作用,如下图:
在这里插入图片描述
where条件顺序包含了联合索引的所有字段,但其中phone字段使用了范围查询,联合索引对phone后的所有字段不起作用,如下图:
在这里插入图片描述
同时,索引最多用于一个范围列,因此如果查询条件中有两个范围列则无法全用到索引。
这里特别要说明MySQL一个有意思的地方,那就是仅用explain可能无法区分范围索引和多值匹配,因为在type中这两者都显示为range。同时,用了“between”并不意味着就是范围查询,例如下面的查询:
在这里插入图片描述
看起来是用了两个范围查询,但作用于email上的“BETWEEN”实际上相当于“IN”,也就是说email实际是多值精确匹配。可以看到这个查询用到了索引全部三个列。因此在MySQL中要谨慎地区分多值匹配和范围匹配,否则会对MySQL的行为产生困惑。
规则5:查询条件中含有函数或表达式
很不幸,如果查询条件中含有函数或表达式,则MySQL不会为这列使用索引(虽然某些在数学意义上可以使用)。例如:
在这里插入图片描述
由上分析可知,由于使用了函数left,则无法为title列应用索引。再如:
在这里插入图片描述
显然这个查询等价于查询phone为2的函数,但是由于查询条件是一个表达式,MySQL无法为其使用索引。看来MySQL还没有智能到自动优化常量表达式的程度,因此在写查询语句时尽量避免表达式出现在查询中,而是先手工私下代数运算,转换为无表达式的查询语句。
规则6:匹配某列的前缀字符串。
在这里插入图片描述
此时可以用到索引,但是如果通配符不是只出现在末尾,则无法使用索引。

四、索引选择性

既然索引可以加快查询速度,那么是不是只要是查询语句需要,就建上索引?答案是否定的。因为索引虽然加快了查询速度,但索引也是有代价的:索引文件本身要消耗存储空间,同时索引会加重插入、删除和修改记录时的负担,另外,MySQL在运行时也要消耗资源维护索引,因此索引并不是越多越好。一般两种情况下不建议建索引。

第一种情况是表记录比较少,例如一两千条甚至只有几百条记录的表,没必要建索引,让查询做全表扫描就好了。至于多少条记录才算多,这个个人有个人的看法,我个人的经验是以2000作为分界线,记录数不超过 2000可以考虑不建索引,超过2000条可以酌情考虑索引。

另一种不建议建索引的情况是索引的选择性较低。所谓索引的选择性(Selectivity),是指不重复的索引值(也叫基数,Cardinality)与表记录数(#T)的比值:

Index Selectivity = Cardinality / #T

显然选择性的取值范围为(0, 1],选择性越高的索引价值越大,这是由B+Tree的性质决定的。例如,上文用到的students表,如果title字段经常被单独查询,是否需要建索引,我们看一下它的选择性:

SELECT count(DISTINCT(email))/count(*) AS Selectivity FROM students

email的选择性不足0.0001(精确值为0.00001579),所以实在没有什么必要为其单独建索引。

五、索引优化策略之前缀索引

有一种与索引选择性有关的索引优化策略叫做前缀索引,就是用列的前缀代替整个列作为索引key。当前缀长度合适时,可以做到既使得前缀索引的选择性接近全列索引,同时因为索引key变短而减少了索引文件的大小和维护开销。
下面以students表为例介绍前缀索引的选择和使用,students表只有一个联合索引<email、phone、create_date>。
如果我们想按名字搜索一个人,就只能全表扫描了,如下图所示:
在这里插入图片描述
如果频繁按名字搜索员工,这样显然效率很低,因此我们可以考虑建索引。有两种选择,建<first_name>或<first_name, last_name>,看下两个索引的选择性:

SELECT count(DISTINCT(first_name))/count(*) AS Selectivity FROM students

Selectivity值为0.0042

SELECT count(DISTINCT(concat(first_name, last_name)))/count(*) AS Selectivity FROM students

Selectivity值为 0.9313
<first_name>显然选择性太低,<first_name, last_name>选择性很好,但是first_name和last_name加起来长度为30,有没有兼顾长度和选择性的办法?可以考虑用first_name和last_name的前几个字符建立索引,例如<first_name, left(last_name, 3)>,看看其选择性:

SELECT count(DISTINCT(concat(first_name, left(last_name, 3))))/count(*) AS Selectivity FROM students

Selectivity值为 0.7879
选择性还不错,但离0.9313还是有点距离,那么把last_name前缀加到4:

SELECT count(DISTINCT(concat(first_name, left(last_name, 4))))/count(*) AS Selectivity  FROM students

Selectivity值为0.9007
这时选择性已经很理想了,而这个索引的长度只有18,比<first_name, last_name>短了接近一半,我们把这个前缀索引 建上:

ALTER TABLE students
ADD INDEX `first_name_last_name4` (first_name, last_name(4));

此时再执行一遍按名字查询,比较分析一下与建索引前的结果:

SHOW PROFILES;

性能的提升是显著的,查询速度提高了120多倍。
前缀索引兼顾索引大小和查询速度,但是其缺点是不能用于ORDER BY和GROUP BY操作,也不能用于Covering index(即当索引本身包含查询所需全部数据时,不再访问数据文件本身)。
在这里插入图片描述

六、覆盖索引:

覆盖索引是select的数据列只用从索引中就能够取得,不必读取数据行,换句话说查询列要被所建的索引覆盖。(针对特定select语句而言的联合索引)
若mysql使用了覆盖索引,只需要通过索引便可返回数据,无需在查到索引之后进行回表查询,减少I/O,提高效率。
当在explain select…执行计划中的extra列看到using index提示时,说明该select查询使用了覆盖索引。
在这里插入图片描述
在这里插入图片描述
覆盖索引的限制,遇到以下情况不会使用覆盖索引:
a、索引中的列不能全部覆盖select查询所需的所有列
b、where条件中不能含有对索引进行like的操作
总结:覆盖索引只能使特殊定义的查询性能大幅度提升,在生产环境上并不是理想的索引,索引包含过多的列也会给mysql维护索引带来一定得麻烦,降低写操作的性能

七、全文索引

a、mysql版本5.6以下全文索引仅可用于MyISAM表,5.6以上MyISAM和InnoDB都支持,可以从CHAR、VARCHAR或TEXT列中座位CREATE TABLE语句的一部分被创建,或是随后使用ALTER TABLE或CREATE INDEX被添加
b、用处/场景:当查询条件为where column like ‘%xxx%’时,会让索引失效,此时全文索引便派上用场了
c、创建语句:ALTER TABLE table_name ADD FULLTEXT(column1,column2…)
d、简单查询用法:SELECT * FROM table_name WHERE MATCH(column1,column2) AGAINST (‘xxx’,‘sss’,‘ddd’)
该语句将column1与column2列中有xxx、sss、ddd的数据记录全部查出来
创建索引的几大原则:
a、最左前缀匹配原则: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的顺序可以任意调整
b、=和in可以乱序,比如a=1 and b=2 and c=3建立(a,b,c)索引可以任意顺序,mysql的查询优化器会优化成索引可识别的形式
c、尽量选择区分度高(cardinality越大越好)的列作为索引,区分度的公式是count(distinct col)/count(*),表示字段不重复的比例,比例越大我们扫描的记录数越少,唯一键的区分度是1,一般需要join的字段都要求是0.1以上,当然,使用场景不同,该值也难以确定
d、索引列不能是表达式的一部分货mysql函数的参数,保持列“干净”,比如from_unixtime(create_time)=‘2014-05-24’就不能使用到索引,因为b+树种存的都是数据表中的字段值,但进行检索时,需要把所有元素都应用函数才能比较,成本太大,故不能走索引,所以语句应写成create_time=unix_timestamp(‘2014-05-24’)
e、尽量的扩展索引,不能新建索引,比如表中已经有a的索引,现在要加(a,b)的索引,那么只需要修改原来的索引即可

八、简单例子体验下联合索引:

场景是这样的:T_news新闻表有大概250W条记录,需要查询经常查询某一时间段内,指定频道下已审核的新闻列表,sql语句是这样的,select news_id,platform_id,media_name,channel_id,labels,title,image,ischecked,create_time,audit_time,author_id
from t_news_innodb n where 1=1 and n.ischecked = 3 and n.channel_id = 1 and n.audit_time > ‘2017-01-08 16:05:00’
and n.create_time > ‘2017-01-08 16:05:00’ ORDER BY n.audit_time DESC,没涉及到多表关联或者其他的复杂查询,仅仅是where条件+order by排序而已,很简单的一条单表查询语句,由于公司没有DBA,因此之前开发对该表索引的建立都是where条件包含哪些字段就在那些字段上建个单列索引,于是乎执行了上面的语句,效果如下:
在这里插入图片描述
完全懵逼有木有~~就37条记录额,花了整整将近178s,是人都不能忍额 ,再来使用explain看看执行计划
在这里插入图片描述
重点关注下type、key、rows、extra这4列,type列中的值为index_merge说明该查询将索引进行合并了,key列中的值说明使用到了ischecked与channel_id字段的索引,rows列表示这个查询预计要扫描9987条记录,最后的extra列告诉我们该查询将ischecked与channel_id这两个索引进行合并,合并完还不算完,最后order by的排序字段由于用不到索引,于是使用到了mysql自带的文件排序算法,于是这个查询的时间就这么蹭蹭蹭地上天了!!!
最后看下详细的执行计划,看下这条查询的主要时间是花在什么哪一步操作上额
在这里插入图片描述
可以看到,绝大部分的时间是花在对记录进行排序上面,因此我们的查询语句应当尽量避免文件排序,尽可能地让索引帮我们排序 。
再来看下使用了ischecked、channel_id、audit_time这3字段为联合索引后的查询效果,执行相同的语句,效果如下:
在这里插入图片描述
查询效率的提升相比于上面的简直是恐怖有木有!!!用了大概1/10秒,相比于上面的,查询速度约是它的1780倍额 。
再来使用explain看看执行计划
在这里插入图片描述
相比于上面的,因为where条件与order by的排序字段都使用到了联合索引,于是什么索引合并、文件排序通通都木有了,只需要用到where过滤掉不符合的记录,而且预计需要扫描的行数仅仅47,查询速度自然不再同一级别 。
最后看下详细的执行计划
在这里插入图片描述
PS:这个例子只是想说明,在很多情况下,一个好的,适合查询语句的索引可以使查询的效率上升一个甚至多个数量级,索引并不是越多越好,千万不要觉得where条件有什么字段就建个索引下去,这样往往帮助不大,反而会使得索引过多影响MDL操作效率,合适才是最重要的,因为本示例只是单纯地想对比下单列索引与联合索引,因此在建立联合索引的时候,需要综合考虑指定表中常用的查询语句,建立可以适用多个查询而效率又不太低的联合索引,适当时候改写下sql语句使得其用上联合索引。

九、索引的优缺点及使用情况

A、使用索引的缺点
1.在创建索引和维护索引 会耗费时间,随着数据量的增加而增加
2.索引文件会占用物理空间,除了数据表需要占用物理空间之外,每一个索引还会占用一定的物理空间
3.当对表的数据进行 INSERT,UPDATE,DELETE 的时候,索引也要动态的维护,这样就会降低数据的维护速度,(建立索引会占用磁盘空间的索引文件。一般情况这个问题不太严重,但如果你在一个大表上创建了多种组合索引,索引文件的会膨胀很快)。
B、使用索引需要注意的地方
在建立索引的时候应该考虑索引应该建立在数据库表中的哪些列上面,哪一些索引需要建立,哪一些所以是多余的。一般来说:
1.在经常需要搜索的列上,可以加快索引的速度。
2.主键列上可以确保列的唯一性。
3.在表与表的连接条件上加上索引,可以加快连接查询的速度。
4.在经常需要排序(order by)、分组(group by)和distinct 列上加索引 可以加快排序查询的时间, (单独order by 用不了索引,索引考虑加where 或加limit)。
5.在一些where 之后的 <、 <= 、> 、>=、 BETWEEN IN 以及某个情况下的like 建立字段的索引(B-TREE)。
6.like语句,如果你对nickname字段建立了一个索引.当查询的时候的语句是 nickname lick ‘%ABC%’ 那么这个索引讲不会起到作用.而nickname lick ‘ABC%’ 那么将可以用到索引。
7.索引不会包含NULL列,如果列中包含NULL值都将不会被包含在索引中,复合索引中如果有一列含有NULL值那么这个组合索引都将失效,一般需要给默认值0或者 ’ '字符串。
8.使用短索引,如果你的一个字段是Char(32)或者int(32),在创建索引的时候指定前缀长度,比如前10个字符 (前提是多数值是唯一的…)那么短索引可以提高查询速度,并且可以减少磁盘的空间,也可以减少I/0操作。
9.不要在列上进行运算,这样会使得mysql索引失效,也会进行全表扫描。
10.选择越小的数据类型越好,因为通常越小的数据类型通常在磁盘,内存,cpu,缓存中占用的空间很少,处理起来更快。
C、什么情况下不创建索引
1.查询中很少使用到的列,不应该创建索引,如果建立了索引然而还会降低mysql的性能和增大了空间需求。
2.很少数据的列也不应该建立索引,比如一个性别字段 0或者1,在查询中,结果集的数据占了表中数据行的比例比较大,mysql需要扫描的行数很多,增加索引,并不能提高效率。
3.定义为text和image和bit数据类型的列不应该增加索引。
4.当表的修改(UPDATE,INSERT,DELETE)操作远远大于检索(SELECT)操作时不应该创建索引,这两个操作是互斥的关系。

参考文献:
mysql索引之四:复合索引之最左前缀原理,索引选择性,索引优化策略之前缀索引
MySQL中B+Tree索引原理
mysql索引优化
细说MySQL索引
Mysql索引之-cardinality
MySQL Explain详解
MYSQL EXPLAIN解析一 EXTRA中的USING INDEX,USING WHERE,USING INDEX CONDITION

没有更多推荐了,返回首页