前言
索引对有一定开发经验的同学来说并不陌生,合理使用索引,能大大提升sql查询的性能,可以这么讲,随着业务数据量的不断增长,优化系统的响应速度,很大程度上可以说就是集中在索引的优化上;
mysql索引原理
在正式了解与学习mysql索引之前,先对mysql的索引原理再次回顾下;
我们知道,目前大多数使用的mysql引擎为 innodb,而innodb引擎使用的是 B+ Tree,下面通过几张图快速了解下 B+ Tree的结构,
假如存在下面这张表:
那么通过 B+ Tree构建出来的 “ 以ID为主键索引 ” 的树形结构如下:
说明:
- 叶子节点存放的是ID对应的一条完整的记录;
- 查找一条记录时,如果是按照ID搜索,则会采用类似二叉树的方式,最终定位到叶子节点的ID对应的那条记录;
也就是说,如果查询的时候,以ID为参数,则效率是最高的,反之,如果以非主键字段,建立索引,则B+Tree的索引结构将会如下,
说明:
- 主键索引构建出来的 B+Tree 结构保持不变;
- 再以主键ID之外的字段,构建出一个B+Tree结构,其叶子节点存储的是字段的值以及对应的主键值;
以上关于mysql 的inndb的索引结构原理先介绍到这儿,后文中将会用到里面的东西,还请留意;
mysql索引分类
MySQL的索引包括普通索引、唯一性索引、全文索引、单列索引、多列索引和空间索引等;
- 从 功能逻辑 上说,索引主要有 4 种,分别是普通索引、唯一索引、主键索引、全文索引;
- 按照 物理实现方式 ,可以分为 2 种:聚簇索引和非聚簇索引;
- 按照 作用字段个数 进行划分,分成单列索引和联合索引;、
常规分类
- 主键索引,针对表的主键创建的索引,默认建表的时候,自动创建,有且只能有一个;
- 唯一索引,为了避免一个表中的某列数据出现重复的值,可以有多个,关键字:UNIQUE;
- 常规索引,用于快速定位特定字段的数据,可以有多个;
- 全文索引,全文索引常用于查找文本中的关键词,而不是比较索引中的值,可以有多个,关键字FULLTEXT;
补充说明
不同的存储引擎支持的索引类型也不一样
- InnoDB :支持 B-tree、Full-text 等索引,不支持 Hash索引;
- MyISAM : 支持 B-tree、Full-text 等索引,不支持 Hash 索引;
- Memory :支持 B-tree、Hash 等索引,不支持 Full-text 索引;
- NDB :支持 Hash 索引,不支持 B-tree、Full-text 等索引;
- Archive :不支持 B-tree、Hash、Full-text 等索引;
索引创建语法
数据准备,有如下建表sql
CREATE TABLE `user` (`user_id` VARCHAR (32) NOT NULL COMMENT '用户ID',`user_name` VARCHAR (64) DEFAULT NULL COMMENT '用户姓名',`passwd` VARCHAR (64) NOT NULL COMMENT '密码',`email` VARCHAR (64) DEFAULT NULL COMMENT '邮箱',`mobile` VARCHAR (32) DEFAULT NULL COMMENT '手机号',`address` VARCHAR (128) DEFAULT NULL COMMENT '地址',`ID` VARCHAR (18) DEFAULT NULL COMMENT '身份证号',`sex` INT (11) DEFAULT NULL COMMENT '用户性别 1:男 2:女',PRIMARY KEY (`user_id`)) ENGINE = INNODB DEFAULT CHARSET = utf8;
1、创建索引
CREATE [UNIQUE | FULLTEXT | SPATIAL] INDEX index_name ON table_name (col_name[length],...) [ASC | DESC]
- UNIQUE 、 FULLTEXT 和 SPATIAL 为可选参数,分别表示唯一索引、全文索引和空间索引;
- index_name 指定索引的名称,为可选参数,如果不指定,那么MySQL默认col_name为索引名;
- col_name 为需要创建索引的字段列,该列必须从数据表中定义的多个列中选择;
- length 为可选参数,表示索引的长度,只有字符串类型的字段才能指定索引长度;
- ASC 或 DESC 指定升序或者降序的索引值存储;
比如给上表的user_name创建一个唯一索引
CREATE UNIQUE INDEX user_name_idx on `user`(user_name);
2、查看索引
SHOW INDEX FROM table_name;
3、删除索引
DROP INDEX index_name ON table_name;
删除上面的表创建的唯一索引
4、为 username和password创建联合索引
create index name_passwd_index on `user`(user_name, passwd);
5、给user表添加一个info的字段,并为这个字段添加全文索引
ALTERTABLE`user`ADDFULLTEXT ( `info` );
全文索引用match+against方式查询:
SELECT * FROM `user` WHERE MATCH(字段名称) AGAINST (‘查询字符串’);
注意点:
- 使用全文索引前,搞清楚版本支持情况,不同的版本可能对全文索引支持不一样;
- 全文索引比 like + % 快 N 倍,但是可能存在精度问题;
- 如果需要全文索引的是大量数据,建议先添加数据,再创建索引;
已经存在的表创建、删除索引等
现实中,如果之前已经给表创建过相关的索引,现在需要修改或删除,或重新创建,该如何做呢?
1、使用ALTER TABLE语句创建索引
ALTER TABLE table_name ADD [UNIQUE | FULLTEXT | SPATIAL] [INDEX | KEY] [index_name] (col_name[length],…) [ASC | DESC]
比如给 user表的mobile字段添加索引
ALTER TABLE `user` ADD INDEX `idx_mobile` (`mobile`);
2、使用ALTER TABLE语句删除索引
ALTER TABLE table_name DROP INDEX index_name;
或者直接使用drop语句
DROP INDEX index_name ON table_name;
说明
删除表中的列时,如果要删除的列为索引的组成部分,则该列也会从索引中删除。如果组成索引的所有列都被删除,则整个索引将被删除;
常用的索引设计原则
在实际开发中,索引并不是越多越好,创建索引需结合业务情况进行综合考量,下面结合实际经验列举出一些常用的索引设计原则,作为创建索引时的参考;
1、字段值在业务中具备唯一性
业务上具有唯一特性的字段,即使是组合字段,也必须建成唯一索引,比如用户表中,可以通过身份证号或微信号作为 唯一索引;
2、频繁作为 WHERE 查询条件的字段
某字段在SELECT语句WHERE 条件中频繁使用,就需要给这个字段创建索引。尤其是数据量大时,创建索引就可以大幅提升查询的效率;比如用户表的 user_id
3、UPDATE、DELETE 的 WHERE 条件列
我们知道,如果更新的这个列创建了索引,在真正执行数据更新的时候,不仅要更新数据,还要更新这个列的索引信息,在这种情况下,建议只对查询的那个字段创建索引(非索引字段更新的时候速度更快);
4、经常 GROUP BY 和 ORDER BY 的列
索引是让数据按某种顺序进行存储或检索,因此使用 GROUP BY 对数据进行分组查询,或者使用 ORDER BY 对数据排序的时候,就需要 对分组或者排序的字段进行索引 。如果待排序的列有多个,那么可以在这些列上建立 组合索引 ,以提升速度;
5、对DISTINCT 字段创建索引
有时候需要使用DISTINCT对某字段进行去重,那么对这个字段创建索引,也会提升查询效率。这是因为索引会对数据按照某种顺序进行排序,所以有了索引在去重的时候会快很多。
6、使用列的类型小的创建索引
7、使用字符串前缀创建索引
举个例子来说,在user表中存在一个邮箱eamil字段,通常来说,对于一个稳定的业务系统,user标准的email格式可以说格式上是固定的,比如 数字@qq.com,名称@163.com等;
我们知道索引也是要占用存储空间的,字段的长度越长,创建的索引最终占用的空间也越大,当表的数据量大到一定程度,查询时就算是检索走索引也会花费较长时间,这时候就可以考虑使用字符串前缀创建索引了;
可以使用下面的语句创建字符串前缀创建索引:
alter table useradd index(email(6));
这里的问题是,截取多少呢?截取多了,达不到节省索引存储空间的目的;截取少了,重复内容太多,字段的散列度(选择性)会降低。怎么计算不同的长度的选择性呢?下面给出一个经验公式作为参考,
先看一下字段在全部数据中的选择度:
select count(distinct email) / count(*) from user;
通过不同长度去计算,与全表的选择性对比:
count(distinct left(列名, 索引长度))/count(*)
8、使用最频繁的列放到联合索引的左侧
这样可以尽可能的向查询时的最佳左前缀原则靠拢;
9、在多字段都要创建索引的情况下,联合索引优于单值索引
试想当user表中,当user_name,mobile都需要创建索引时,分别创建两个单列索引带来的开销,与创建一个联合索引带来的开销哪个更大呢?
10、 区分度高(散列性高)的列适合作为索引
如果表中存在性别这样的字段,就不太适合创建索引,这个需要注意;
11、多表 JOIN 时,创建索引注意事项
- 连接表数量尽量不要超过 3 张 ,因为每增加一张表就相当于增加了一次嵌套的循环,数量级增长会非常快,严重影响查询的效率;
- 尽可能对 WHERE 后面的条件字段创建索引 ,因为 WHERE 才是对数据条件的过滤;
- 对于连接字段创建索引 ,并且该字段在多张表中的类型必须一致 ,字段类型不一致将会带来较大的查询性能损耗;
12、有大量重复数据的列上不要建立索引
在这样的列上一旦创建了索引,比如表中有50万数据,你需要先访问 50 万次索引,然后再访问 50 万次数据表,这样加起来的开销比不使用索引可能还要大;
一个经验值
当数据重复度大,比如高于 10% 的时候,就不需要对这个字段创建索引;
13、避免对经常更新的表创建过多索引
这个算是常识性的经验了,更新数据时候,不经要更新数据本身,还需要更新索引;
14、不建议用无序的值作为索引
例如身份证、UUID(在索引比较时需要转为ASCII,并且插入时可能造成页分裂)、MD5、HASH、无序长字符串等;
15、不要定义冗余或重复的索引
即对同一个字段而言,不要创建多个不同名称的索引,这样只会增加维护的成本,并不会对搜索有什么好处;
16、及时删除不再使用或者很少使用的索引
减少索引存储空间对整个表空间的开销
索引失效情况总结
1、不要在索引列上进行操作计算
计算、函数、类型转换(自动或手动)导致索引失效,上面的user表中,我们给phone创建了索引,但如果使用下面的函数进行查询,索引将会失效;
explain select * from `user` where SUBSTRING(mobile,10,2) = '12'
2、字符串查询时一定要加引号
字符串查询不加引号时存在隐式转换,将会使索引失效
3、模糊匹配
如果仅仅是尾部的模糊匹配,仍然会走索引(即后like);但如果是头部匹配,索引将会失效;
like在前
like在后
前后都有like
很多同学在实际开发中,习惯性的就写上前后都带有 like的语句,而在表数据量越来越大的情况下,效率将会非常低;
4、使用 or 连接的条件
用or分开的字段作为条件查询时,如果or前面的列有索引,但是后面的列没有索引,那么整条查询将不会使用到索引;
explain select * from `user` where mobile = '13366767812'or email = '16678623@qq.com'
usr表中,email由于没有建索引,所以or 的查询结果中,最终没有用到索引;
5、范围条件右边的列索引失效
对于某些联合索引,如果用到了范围查询但是查询条件的字段未按照联合索引的字段顺序,将会使得索引失效;
如下,user_name 和age创建了联合索引,使用下面这条sql分析可知
explain select * from `user` where age > 25 and user_name = '张小华'
在这种情况下,需要将范围查询条件放置语句最后;
6、不等于(!= 或者)索引失效
尽量避免在大数据量的查询中使用 != 这种查询
7、 is null可以使用索引,is not null无法使用索引
CREATE index idx_mobile on `user`(mobile)
分别使用下面的sql进行分析
8、使用 not in和not exists 这样的关键字导致索引失效
9、不合理的使用order by导致索引失效
其实,这个也很好理解,毕竟需要对全表数据进行排序处理,在网络上看到有说如果order by条件满足最左匹配则会正常走索引, 在当前8.0.18版本中并未出现。所以,在基于order by和limit进行使用时,要特别留意。是否走索引不仅涉及到数据库版本,还要看Mysql优化器是如何处理的。
但是使用主键进行排序,则是可以走索引的,这一点需要注意
10、其他情况
当然,还有其他一些是否走索引的规则,这与索引的类型是B-tree索引还是位图索引也有关系;
这里要说的其他,可以总结为:
Mysql优化器的其他优化策略,比如优化器认为在某些情况下,全表扫描比走索引快,则它就会放弃索引;针对这种情况,一般不用过多理会,当发现问题时再定点排查即可
尽量使用覆盖索引
最后,补充一个知识点,即使用覆盖索引
即查询要返回的结果字段中,尽可能的在索引中使用到(或全部用到),减少使用 “select * ” 这样的查询;
比如下面这条语句
select address,age from `user` where user_id = '1'
尽管使用到了主键查询,但是查询的字段 address 和 age并未包含在索引列中,所以这种情况下,innodb引擎最终需要通过回表的方式才能将数据的结果查出来;
还记得本文开头的这张图吧
当我们查询一条记录时,使用的条件为 name = ‘名字’,尽管对name创建了索引,但是name的索引结构中,其叶子节点存储的是name值以及id值,假如这时,你的查询语句中需要返回的字段不是id和name,而是address和mobile,这时,就需要拿到id再次去查询,一直查到id的索引树下的完整的记录,这就是所谓的回表;
当表的数据量足够大的时候,回表耗费的时间是很长的,这个对整体的查询响应来说一定会成为一个需要优化的点;