MySQL学习笔记-索引
Posted YellowSea的博客
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了MySQL学习笔记-索引相关的知识,希望对你有一定的参考价值。
索引
索引(index)是帮助MySQL高效获取数据的数据结构(有序)。在数据之外,数据库系统还维护着满足特定查找算法的数据结构,这些数据结构以某种方式引用(指向)数据,这样就可以在这些数据结构上实现高级查找算法,这种数据结构就是索引。
-
无索引的查找:全表扫描(将整张表遍历一遍),性能极低。
-
有索引的查找:数据库系统在存储数据的同时会维护一种数据结构(如二叉树),当需要查找时,利用该数据结构进行查找,性能较高。
-
索引的优缺点
一. 索引结构
MySQL的索引是在存储引擎层实现的,不同的存储引擎有不同的结构。
- 索引在存储引擎中的支持情况
- 平常所说的索引,如果没有特别指明,一般都是说B+树结构组织的索引。
1. B+树
1.1 二叉树
-
一种经典的数据结构。
-
二叉树的两个缺点:
- 顺序存储二叉树时,会形成一条链表,二叉树的深度很大,效率很低。
- 二叉树的度不大于2,在数据库中存有大量数据的时候深度很大,效率很低。
1.2 红黑树
-
一种自平衡的特殊二叉树。
-
可以解决二叉树可能形成链表的缺点,但是依旧存在数据量大时深度很大的问题。
1.3 B树(平衡多路查找树)
- 一种自平衡的树。可以解决二叉树的两个缺点。
- 一个节点可以拥有两个以上的子节点。
1.4 B+树
- B树的变种。
- B+树与B树的区别:
- 所有的元素都会出现在叶子节点。
- 叶子节点形成了一条单向链表。
1.5 MySQL中的B+树
- 在MySQL中,对B+树进行了优化。在原有基础上,叶子节点改为了双向循环链表,提高区间访问的性能。
2. Hash
哈希索引就是采用一定的Hash算法,将键值换算成新的Hash值,映射到对应的槽位上,然后存储在Hash表中。
如果两个(或多个)键值映射到同一个槽位上,产生了Hash冲突,可以通过链表解决。
2.1 Hash索引的特点
- 只能用于对等比较(=,in),不支持范围查询(between,>,<,...)。
- 无法利用索引完成排序操作。
- 查询效率高,通常只需要一次检索(不出现hash冲突),效率高于B+树。
2.2 存储引擎支持
在MySQL中,支持hash索引的是Memory引擎,而InnoDB中具有自适应hash功能,hash索引是存储引擎根据B+树索引在指定条件下自动构建的。
二. 索引分类
- 在InnoDB存储引擎中,根据索引的存储形式,又可以分为以下两种:
- 聚集索引的选取规则:
- 如果存在主键,主键索引就是聚集索引。
- 如果不存在主键,将适用第一个唯一(UNIQUE)索引作为聚集索引。
- 如果不存在主键,或没有合适的唯一索引,则InnoDB会自动生成一个rowid作为隐藏的聚集索引。
- 聚集索引和二级索引的示意图:
- 在查找时,先走二级索引,找到对应的主键后,再走聚集索引,找到对应的整个行。(回表查询)
三. 索引语法
1. 创建索引
create [unique|fulltext] index 索引名 on 表名 (字段名,...);
- unique 唯一索引 |fulltext 全文索引 |不加这两个则说明是常规索引。
- 一个索引可以关联多个字段,如果一个索引只关联一个字段,叫单列索引,如果关联多个字段,叫联合索引(组合索引)。
- 联合索引的字段顺序是有讲究的。
- 索引名一般的命名规则:idx _ 表名 _ 字段名
2. 查看索引
show index form 表名;
3. 删除索引
drop index 索引名 on 表名;
四. SQL性能分析
做性能分析是为了做SQL优化,SQL主要是做查询优化,因为查询操作比增删改多,查询优化的关键在于索引。
1. SQL执行频率
# 查看当前数据库的增删改查的访问频次
show global status like \'Com_______\';
# 模糊匹配\'Com\'后面是7个下划线
- 根据执行频率来判断SQL优化需要在哪方面进行,也就是说这个数据库哪个操作频率高就优化哪个操作。
2. 慢查询日志
慢查询日志记录了所有执行时间超过制定参数 (long_query_time,单位:秒,默认10秒) 的所有sql语句的日志
慢查询日志用于找到执行慢的sql语句,进行针对性优化。
2.1 开启慢查询日志
MySQL的慢查询日志默认没有开启,需要在MySQL的配置文件(/etc/my.cnf) 中配置。
- 查询是否开启
show variables like \'slow_query_log\';
- 开启慢查询日志
在MySQL的配置文件(/etc/my.cnf) 中配置如下信息:
# 开启MySQL慢查询日志开关
show_query_log = 1
# 设置慢查询日志的时间为2秒,SQL语句执行时间超过2秒就会被记录
long_query_time = 2
配置完毕后,需要重启服务器。
# 重启服务器
systemctl restart mysqld
2.2 查看慢查询日志
# 慢查询日志存放地址 Linux下
/var/lib/mysql/localhost-slow.log
3. profile详情
Show profiles 能够在做SQL优化时帮助我们了解时间都耗费到哪里去了。
3.1 查看MySQL是否支持profile操作
select @@have_profiling;
3.2 打开profile开关
# 查看是否打开
select @@profiling;
# 打开profile开关
set [session|global] profiling = 1;
-
profile默认是关闭的。
-
[session|global] 可以指定是会话级别的还是全局的。
3.3 查看profile详情
# 查看每一条SQL的耗时基本情况
show profiles;
# 查看指定query_id的SQL语句各个阶段的耗时情况
show profile for query query_id;
# 查看指定query_id的SQL语句CPU的使用情况
show profile cup for query query_id;
- query_id指的是在profiles中的某一条指令的id,可以在show profiles中看到。
4. explain执行计划
explain 或者 desc命令获取MySQL如何执行select语句的信息,包括在select语句执行过程中表如何连接和连接的顺序。
# 直接在select语句之前加上关键字explain/desc
explain select 字段列表 from 表名 where 条件;
- explain执行计划各字段含义
五. 索引使用
1. 最左前缀法则
-
如果索引了多列(联合索引),要遵循最左前缀法则。最左前缀法则是指查询从索引的最左列开始,并且不跳过索引中的列。
-
如果跳跃某一列,索引将部分失效(后面的字段索引失效)。
-
查询时左边字段存在即符合最左前缀法则,不管它在代码中的位置。
2. 范围查询
- 联合索引中,出现范围查询(> , <),范围查询右侧的列索引失效。
- 用(>= , <=)不会出现失效情况。
3. 索引列运算
- 不要在索引列上进行运算操作,否则索引将失效。
4. 字符串不加引号
- 字符串类型字段使用时,不加引号,索引将失效。
5. 模糊查询
- 如果是尾部进行模糊查询,索引不会失效;如果是头部进行模糊查询,索引会失效。
6. or连接的条件
- 用or分割开的条件,如果or前的条件中的列有索引,二后面的列没有索引,那么涉及的索引都不会被用到。
- 只要把没有索引的建立一个索引就可以解决失效问题。
7. 数据分布影响
- 如果MySQL评估使用索引比全表更慢,则不使用索引。
8. SQL提示
SQL提示是优化数据库的一个重要手段。在SQL语句中加入一些人为的提示来达到优化操作的目的。
8.1 use index
- 使用指定索引(建议)
select * from 表名 use index(索引名) where...;
8.2 ignore index
- 不使用某个索引
select * from 表名 ignore index(索引名) where...;
8.3 force index
- 使用指定索引(必须)
select * from 表名 force index(索引名) where...;
9. 覆盖索引
-
尽量使用覆盖索引(查询使用了索引,并且需要返回的列,在该索引中已经全部能够找到),减少使用select *。
-
使用覆盖索引和没有使用覆盖索引,在explain中的Extra列有不一样的提示:
- (没使用)using index condition : 查找使用了索引,但是需要回表查询数据。
- (使用了)using where; using index : 查找使用了索引,但是需要的数据都在索引列中能够找到,所以不需要回表查询数据。
-
覆盖索引直接在二级索引中获取了返回所需的所有数据,所以不需要回表查询,查询速度快。
-
如果不是覆盖查询,在二级索引中查询到数据后,还需要拿到对应数据的主键,到聚焦索引中查询行数据,这就叫回表查询,所以速度慢。
10. 前缀索引
当字段类型为字符串时,有时候需要存储很长的字符串,如果建立索引,索引会变得很大,浪费大量磁盘IO,影响查询效率。
此时可以只用字符串的一部分前缀来建立索引(前缀索引),可以大大节约索引空间,从而提高效率。
10.1 创建前缀索引
create index 索引名 on 表名(字段名(前缀的字符数));
10.2 前缀长度的选择
-
可以根据索引的选择性来决定。
-
选择性:不重复的索引值和数据表的记录总数的比值。索引选择性越高,效率越高。唯一索引的选择性是1,是性能最好的。
-
求选择性:
select count(distinct substring(字段名,1,截取长度))/count(*) from 表名;
11. 单列索引和联合索引
-
单列索引:一个索引只包含单个列
-
联合索引:一个索引包含了多个列
-
在业务场景中,如果存在多个查询条件,考虑针对查询字段建立索引时,建议使用联合索引。
-
联合索引的存储结构:
六. 索引设计原则
Mysql索引学习笔记
一丶什么是索引
索引是存储引擎快速找到记录的一种数据结构。数据库中的数据可以理解成字典中的单词,而索引就是目录,显而易见这是一种空间换时间的做法,目录占用了空间,但是加快了我们找到单词的速度,正如索引需要空间存储,但是利用索引我们可以快速的找到想要的数据。
InnoDB存储引擎存在几种常见的索引:
- B+树索引
- 全文索引
- 哈希索引
本文主要讨论 B+树索引
二丶索引的数据结构
可以加快查找速度的数据结构很多,为什么mysql使用 B+树
来实现昵,换句话说哈希表,有序数组,跳表,平衡二叉搜索树,B-树等都可以优化搜索效率,为什么偏偏使用 B+树
1.哈希表
哈希表,可以联想Java中的HashMap,在HashMap源码学习中,我们了解到Hash表的数据结构。如下图
哈希表通过hash算法将key映射到数组对应的下表进行存储,不可避免的会产生hash冲突(多个不同的key散列到相同的数组下标中),解决hash冲突常用拉链法,顾名思义,就是把相同hash值的节点组成链表。在根据key查找value的过程中,只需要再次使用相同的hash算法那么就能拿到对应的数组下表,然后遍历链表找到目标值即可,查找的效率是o(1)。
明明存在链表需要遍历为什么说时间复杂度o(1)
首先hash算法计算是常数时间,
hash表会在需要的时候进行扩容,
维持链表长度尽量在一个常数范围,从而保证遍历常数个链表节点
mysql中存在 自适应哈希索引
,由innodb存储引擎自己控制,利用查找O(1)的性质优化等值查询。我们可以看出,hash表并不适合范围查询,对于 Id<10
这种范围查询只能遍历hash表中每一个数据,相当于要进行一次全表扫描。我还有一个想法:从扩容的角度看,每次扩大数组大小后都需要移动一些数组到新的数组空间中,这部分的复制移动的开销也许是hash表不合适的原因(redis为了解决这个问题,使用 渐进式hash
的方式,在扩容的时候生成更大的数组,但是不是一次移动所以数据,而是把新数据的插入都是方法新数组,老数组使用到的数据才会移动到新数组)
2.有序数组
有序数组就是数据中元素有序,正因为有序,索引其在返回查找上非常优秀,正因为要维持有序,在更改数据的时候,也许需要移动大量数组(比如插入一个较小的值,大于此值的数据都需要后移动),所以有序数据只适用于静态数据(比如2020年人口信息,这种不会改变的数据)
3.跳表
为了解决有序数组需要移动元素的问题,我们可以使用链表来维护数组,从而使更改数组效率为o(1),但是链表的查找非常慢,但是链表整体式有序的,那么我们可以使用二分法优化查找效率,如上我们建立多级的节点,在查找的时候我们首先通过多级的所有依次找到最下层。对于范围查找,由于底层数据是有序的,查找 id<7
的数组,首先我们找到 id=7
然后向左遍历集合(可以把跳表最下面一层优化为双向链表,从而让范围查找速度也很快)
哪为什么不使用跳表来做索引昵?其实我觉得完全可以使用跳表,可能是由于跳表在插入数据维护索引比较随机,比如上面的跳表插入数据 6
理论上为了达到 二分的效果 ,每一层的结点数需要是下一层结点数的二分之一。也就是说现在有一个新的数据插入了,它有 50%
的概率需要在 第二层
加入索引,有 25%
的概率需要在 第三层
加个索引,以此类推,直到 最顶层
。
4.平衡二叉搜索树
二叉搜索树,即左子树小于根,根小于右子树,这种结构在查找的时候可以进行二分,根据根节点的值就可以确定期望的数据在左树还是右树。
但是二叉搜索树在插入,删除节点的时候可能出现树极度不平衡的情况,出现树退化成链表。
这个时候就需要维持树的平衡——AVL:在满足二叉搜索树的条件下,要求任何节点的两个子树高度差不超过1。平衡二叉树的查找是趋近于O(log(N)),但是需要维护一颗树为AVL需要进行左旋,右旋的操作,更新的时间复杂度也是 O(log(N))。
为什么不使用AVL做索引:节点存储的数据内容太少。因为操作系统和磁盘之间一次数据交换是以页为单位的,一页 = 4K,即每次IO操作系统会将4K数据加载进内存。但是,在二叉树每个节点的结构只保存一个关键字,一个数据区,两个子节点的引用,并不能够填满4K的内容。幸幸苦苦做了一次的IO操作,却只加载了一个关键字,在树的高度很高,恰好又搜索的关键字位于叶子节点或者支节点的时候,取一个关键字要做很多次的IO。
5.B-树,B+树
B-树就是B树,英文是B-Tree,所以国内有许多人称之为B-树。B树和B+树是 多路平衡查找树 ,之所以 多路
,是为了契合磁盘的io操作——操作系统和磁盘之间一次数据交换是以页为单位的,多路能让读取一页能获取更多的数据,让树的高度更低。
上面两图,我们可以看出B树和B+树的区别
- B+树叶子节点使用双向指针串联起来,这让B+树相比于B树更加适合范围查找
- B+树非叶子节点并不存数据,所以每次查找数据都必须遍历到叶子节点,时间复杂度稳定为O(logN),B-树在运气好的时候可以在根节点直接拿到数据。但是正是因为非叶子节点不存储数据,可以让一次磁盘读取一页中包含的索引数据更多,每个节点能索引的范围更大更精确,让我们可以更改定位到期望的数据。由于B+树的叶子节点的数据都是使用链表连接起来的,而且他们在磁盘里是顺序存储的,所以当读到某个值的时候,磁盘预读原理就会提前把这些数据都读进内存,使得范围查询和排序都很快
B+树在更改数据的时候,为了保证树的平衡可能存在节点的分裂和合并,所以我们一般建议使用自增主键,在插入的时候,不会频繁的发生节点的分裂。
三丶聚集索引和非聚集索引
InnoDB存储引擎是索引组织表——表中的数据按照主键顺序存放。非聚集索引也称做辅助索引,无论是聚集还是非聚集,其原理都是一颗B+树,叶子节点都存储数据,不同的是聚集索引叶子节点存储的是一整行的数据,非聚集索引叶子节点存储的是聚集索引值(主键值)。
如果数据表定义了主键,那么这个索引就是聚集索引,如果没有定义主键,mysql会选择该表的第一个非空唯一的索引构建聚集索引,如果都没有那么mysql会生成一个隐藏的列(6字节的列,并且插入自增)
自增主键会把数据自动向后插入,避免了插入过程中聚集索引节点分裂的问题。节点分裂会带来大范围的数据物理移动,带来磁盘IO的性能损耗,并且我们一般建议尽量不要改动主键,主键的更改也会带来page分裂,产生碎片。
四丶回表查询
如上图,加入我们有一张表存在三个字段 id,age,name
我们在id上建立了主键索引,这时候id主键索引也是聚集索引,在age上建立了普通索引,这时候age索引就是非聚集索引。如果我们执行 select * from table where age=1
这时候先走age索引(如果数据量较大,数据量少直接全表扫描了)那么会找到对应的主键id,继续到主键id索引中找到目标数据,这个操作叫做 回表 。
这就是为什么根据主键查找快于根据其他索引列查找,因为如果其他索引列没有包含我们 select
语句中需要的列(如果是 select id from table where age<10
,那么age索引是可以覆盖到需要的数据的(叶子节点存储了id),那么也不会回表),那么会走主键索引拿到需要的数据,多了一步回表操作。
这里我们也可以看到为什么建议使用 select *
,这意味着查找所有列,如果配合上普通索引,那么大概率这个普通索引不会覆盖到索引列,导致需要回表查询。并且 select*
这种"我全都要"大概率会查询到我们不需要的列,造成不必要的网络资源消耗,增加不必要的io,增加不必要的内存消耗。
五丶联合索引
联合索引是指对表上的多个列建立索引,如上图表存在四个字段 id,address,name,age
,我们在name和age上建立索引,上图我们粗略的展示了联合索引的B+树结构。我们可以观察到在叶子节点中name是有序的,但是age无序,联合索引是按照索引定义的顺序排序的,这就导致 select xxx from table where name=b
是可以根据上面定义的联合索引查找数据的,但是``select xxx from table where age=12 是无法走上面定义的联合索引的。这就是常说的
最左前缀匹配原则`的原理。
- 联合索引可以减少回表
如果我们执行select age,id from table where name=a and age=10
,这个时候由于我们定义的聚集索引一级包含了需要的数据就不需要进行回表操作了(这其实也被称为覆盖索引,即非聚集索引中可以查询到全部需要的列,那么就不需要走聚集索引回表查询数据) - 联合索引可以优化排序
上图中的联合索引,我们可以看到,名称相同的节点,其年龄是有序的
也就是说select * from table where name=a order by age
这个语句将避免多一次的排序操作(select* from table where id=1 order by age
会走主键索引拿到所有符合数据进行排序,这里说的避免一次排序操作指拿到的数据本身就是有序的 所有不需要再次排序) - 索引下推ICP
全称Index Condition PushDown
,mysql 5.6后支持的一种根据索引进行查询优化的操作。mysql数据库会在取出所有数据的同时判断是否进行where条件的过滤,将where的部分过滤放在存储引擎层。
mysql5.6之前如果执行 `select * from table where name like 张% and age=10` 这时候会先从 `name age` 的联合索引中拿到name满足张开头的数据,然后回表,mysql支持ICP后,效果图如下
mysql会根据联合索引中记录的age对数据进行过滤,这时候age不等于10的数据将不会回表,将回表次数从4优化到了2,这就是索引下推。
- 如何安排联合索引的顺序
第一原则是,如果通过调整顺序,可以少维护一个索引,那么这个顺序往往就是需要优先考虑采用的,比如业务中存在两个高频查询,根据name,以及根据name查询后根据age排序,这个时候我们应该建立name age
的联合索引,上面我们说过name,age
的所有其中name是有序的,age只在name相同的情况下才是有序的,这样可以减少建立name的普通索引,并且优化排序,甚至利用索引下推减少回表。如果还存在根据age进行的查询,那么需要单独维护一个age的普通索引
六丶索引建立原则
1.为搜索,排序,分组的列建立索引
一般只为出现在 where
后面的列,连接子句中的列,出现在 order by
,或者 group by
的列进创建索引。不要无脑建立索引,索引是需要存储在磁盘上的,占用空间,并且在新增,删除,修改的时候还需要维护索引,是需要时间的。
比如 select xxx from table where name= a order by user_no
,这条查询语句可以选择在name上建立索引,也可以选择在user_no 上建立索引,后者可以优化排序。
2.考虑列中不重复的个数建立索引
select xxxx from table where sex=1
这里不要为 sex
性别建立索引,性别通常只有男和女,为其建立索引,b+树只有两个节点,查找之后还要对一半的进行回表,不如直接走全表扫描
3.索引列尽可能小
mysql基本数据类型十分丰富,整数类型有 tinyint
, mediumint
, int
, bigint
,我们因该尽量使用占用字节数小的数据类型,这样可以让每次读取磁盘获取一页的数据,可以获得更多的范围信息
4.为列前缀进行索引
比如说有英文名可能很长,每次都是根据FirstName 进行like查找,这时候可以选择为列的前10个字符建立索引( alter table user add index idx_name(name(10))
)。但是十个字符之后将无法使用索引。
5.合理的建立覆盖索引
在聚集索引小节中,我们总结了聚集索引的好处,减少回表,优化排序,索引下推。
6.不要在uuid上建立索引
首先uuid占用字节大,导致每一页范围信息少,并且uuid无序,这会导致插入数据的时候节点的分裂。这里也说明了自增主键优秀的点,不会频繁的节点分裂,并且不要修改主键,避免不必要的节点分裂。相比于uuid作为主键,不如使用分布式自增主键生成的方案
7.存在联合索引的情况下,不要重复建立索引
存在 name,age
的联合索引,那么不需要再为name单独建立索引了,但是可以为age建立索引,原理在联合索引中进行了讲解。
七丶索引失效
上图是mysql的基本架构,其中存在优化器,其作用是不改变sql执行结构的情况下,让sql更加简单,并且根据成本分析,制定执行计划。是否走索引,走什么索引也是优化器来决定的(sql中可以提示使用什么索引,强制使用某一个索引)。
常见索引失效的原因有
- 不满足最左前缀原则
如果存在a,b,c
的联合索引,select * from table where b=2 and a=1
这种时候还是可能走联合索引的,mysql会优化语句,但是select * from table where b=1 and c=2
是无法走联合索引的,因为b,c在b+树中整体无序 - 使用了select*
使用select*需要回表,也许mysql优化器评估后觉得走非聚集索引,不如直接全表扫描 - 索引列上有计算,索引列上使用了函数
- 类型不匹配
select * from table where name = 123
,可以理解成mysql把sql语句添加了类型转换函数,导致无法走索引 - like查询左边有%
以xxx开头是可以走索引的,因为是有序的,但是以xxx结尾和包含xxx是无法走索引的。 - or条件导致失效
select xxx from table where age=10 or name=zhangsan
,age和 name上存在普通索引,但是mysql优化器觉得不如走全表扫描.这时候我们可以使用union,让其走索引 - order by 使用了联合索引中不存在的列,或者顺序不符合最左前缀匹配
- group by 使用了联合索引中不存在的列,或者顺序不符合最左前缀匹配
以上是关于MySQL学习笔记-索引的主要内容,如果未能解决你的问题,请参考以下文章