Mysql优化-索引
1. 索引的本质
MySQL官方对索引的定义为:索引是帮助MySQL高效获取数据的数据结构。
数据库查询是数据库的最主要功能之一。我们都希望查询数据的速度尽可能的快,因此
数据库系统的设计者会从查询算法的角度进行优化。最基本的查询算法是顺序查找(liner search),这种复杂度为o(n)的算法在数据量大时,速度很慢。但是我们有更优秀的查找算法,例如二分查找(binary search)、二叉树查找(binary tree search)等。如果稍微分析一下会发现,每种查找算法都只能用于特定的数据结构之上,例如二分查找要求被检索数据有序,而二叉查找只能应用于二叉查找树上,但是数据本身的组织结构不可能满足各种数据结构(例如,一般不可能同时将两列都按顺序进行组织),所以在数据之外,数据库系统还维护着满足特定查找算法的数据结构,这些数据结构以某种方式引用(指向)数据,这样就可以在这些数据结构上实训高级查找算法。这种数据结构,就是索引。
为了加快查询速度,可以维护一个二叉查找树,每个节点分别包含索引键值和一个指向对应数据记录物理地址的指针,这样就可以运用二叉查找在o(logN)的复杂度内获取到相应数据。
虽然这是一个货真价实的索引,但是实际的数据库系统几乎没有使用二叉查找树实现的。。。
二叉排序树
二叉排序树的排序规则如下:
1)若左子树不空,则左子树上所有节点的值均小于它的根节点的值
2)若右子树不空,则右子树上所有节点的值均大于它的根节点的值
3)它的左右子树也分别是二叉排序树(递归定义)
二叉排序树组织数据时,用于查找是比较方便的,因为每次经过一次节点时,最多可以减少一半的可能,不过极端情况的出现所有节点都位于同一侧,直观上看就是一条直线,那么这种查询的效率就比较低了,因此需要对二叉树左右子树的高度进行平衡化处理,于是就有了平衡二叉树(Balanced Binary Tree)。
所谓的“平衡”,说的是这棵树的各分支的高度是均匀的,它的左右子树高度差的绝对值小于等于1,这样就不会出现一条支路特别长的情况。于是,在这样的平衡树种进行查找是,总共比较节点的次数不超过数的高度,这就确保了查询的效率(时间复杂度为o(logN))
B树
B树实际上是一种平衡的多叉查找树,也就是说最多可以开 m 个叉(m >= 2),我们称之为 m 阶B树。
B树的查询过程和二叉排序树比较类似,从根节点依次比较每个节点,因为每个节点中的关键字和左右子树都是有序的,所以只要比较节点中的关键字,或者沿着指针就能很快的找到指定的关键字,如果查找失败,则会返回叶子节点,即空指针。
B树的特点可以总结如下:
1)关键字集合分布在整棵树中;
2)任何一个关键字出现且只出现在一个节点中。
3)搜索有可能在非叶子节点结束
4)其搜索性能等价于在关键字集合内做二分查找
5)B树在插入删除新的数据记录会破坏B-Tree的性质,因为在插入删除时,需要对树进行一个分裂、合并、转义等操作,以保持B-Tree的性质。
B+ 树
作为B树的加强版,B+树与B树的差异在于
1)有 n 棵子树的节点含有 n 个关键字(n-1个关键字)
2)所有的关键字全部存储在叶子节点上,且叶子节点本身根据关键字自小而大顺序连接
3)非叶子节点可以看成索引部分,节点中仅含有其子树(根节点)中的最大(或最小)关键字。
B+ 树的查找过程,与B树类似,只不过查找时,如果在非叶子节点上的关键字等于给定值,并不终止,而是继续沿着指针直到叶子节点位置。因此在B+树,不管查找是否成功,每次查找都是走了一条从根到叶子节点的路径。
B+ 树的特性如下:
1)所有关键字都存储在叶子节点上,且链表中的关键字恰好是有序的。
2)不可能非叶子节点命中返回。
3)非叶子节点相当于叶子节点的索引,叶子节点相当于是存储(关键字)数据的数据层。
4)更适合文建索引系统。
带有顺序访问指针的B+Tree
一般在数据库系统或文件系统中使用的 B+Tree 结构都在经典 B+Tree 的基础上进行了优化,增加了顺序访问指针。
在 B+Tree 的每个叶子节点增加一个指向相邻叶子节点的指针,就形成了带有顺序访问指针的 B+Tree。做这个优化的目的是为了提高区间访问的性能。如上图,要查询key从18到49的所有数据记录,当找到18后,只需顺着节点和指针顺序遍历就可以一次性访问到所有数据节点,极大提高了区间查询效率。
MySQL为什么使用B树(B+树)
一般来说,索引本身也很大,不可能全部存储在内存中,因此索引往往以索引文件的形式存储在磁盘上。这样的话,索引查找过程中就要产生磁盘 I/O 消耗,相对于内存存取,I/O存取的消耗要高几个数量级,所以评价一个数据结构作为索引的优劣最重要的指标就是在查找过程中磁盘 I/O 的操作次数的渐进复杂度。换句话说,索引的结构组织要尽量减少查找过程中磁盘 I/O 的存取次数。
由于存储介质的特性,磁盘本身存取就比主存慢很多,再加上机械运动耗费,磁盘的存取速度往往是主存的几百分之一,因此为了提高效率,要尽量减少磁盘 I/O 。为了达到这个目的,磁盘往往不是严格按需读取,而是每次都会预读,即使只需要一个字节,磁盘也会从这个位置开始,顺序往后读取一定长度的数据放入内存。这样做的理论依据是计算机科学中注明的局部性原理:
当一个数据被用到时,其附近的数据也通常会马上被使用。
所以,程序运行期间所需要的数据通常应当比较集中。
由于磁盘顺序读取的效率很高(不需要寻道,只需要很少的旋转时间),因此对于具有局部性的程序来说,预读可以提高 I/O 效率。预读的长度一般为页的整数倍。页是计算机管理存储器的逻辑块,硬件及操作系统往往将主存和磁盘存储区分割成连续的大小相等的块,每个存储块称为一页(大小通常为4K),主存和磁盘以页为单位交换数据。当程序要读取的数据不在主存中时,会触发一个缺页异常,此时系统会向磁盘发出读盘信号,磁盘会找到数据的起始位置并向后连续读取一页或几页载入内存中,然后异常返回,程序继续运行。
B-/+Tree 索引的性能分析
上文说一般使用磁盘 I/O 次数评价索引结构的优劣。
B-Tree 检索一次最多需要访问 h 个节点。数据库系统的设计者巧妙利用了磁盘预读原理,将一个节点的大小设为等于一个页,这样每个节点只需要一次 I/O 就可以完全载入。为了达到这个目的,在实际实现 B-Tree 还需要使用如下技巧:
每次新建节点时,直接申请一个页的空间,这样就保证一个节点物理上也存储在一个页里,加之计算机存储分配都是按页对齐的,就实现一个 node 只需一次 I/O 。
B-Tree 中一次检索最多需要 h-1 次 I/O (根节点常驻内存),渐进复杂度为 o(h)=o(logN) d 为底。实际,出度 d 是非常大的数字,通常超过100,因此 h 非常小(通常不超过3)。(h 表示树的高度 & 出度 d 表示的是树的度,即树中各节点度的最大值)
综上,用 B-Tree 作为索引结构效率是非常高的。
而红黑树(二叉平衡查找树),h 明显深得多。由于逻辑上很近的节点(父子)物理上可能很远,无法利用局部性,所以,红黑树的 I/O 渐进复杂度也为 o(h),效率明显比 B-Tree 差很多。
上文还说,B+Tree 更适合外存索引,原因和内节点出度 d 有关。从上面分析可知,d 越大索引性能越好,而出度的上限取决于节点内 key 和 data 的大小:
dmax = floor(pagesize / (keysize + datasize + pointsize))
由于 B+Tree 内节点去掉了data 域,因此可以拥有更大的出度,有更好的性能。
MySQL索引实现
在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 索引实现
虽然 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域。例如,下图的辅助索引:
聚集索引这种实现方式使得按主键搜索十分高效,但是辅助索引需要检索两遍索引:先检查辅助索引获得主键,然后用主键到住索引中检索获得记录。
了解不同的存储引擎实现方式对于正确使用和优化索引都有帮助,例如知道了 InnoDB 的索引实现后,就容易明白为什么不建议使用过长的字段作为主键,因为所有辅助索引都引用主索引,过长的主索引会令辅助索引变得过大。再例如,用非单调的字段作为主键在 InnoDB 中不太好, 因为 InnoDB 数据文件本身是一棵 B+Tree,非单调的主键会造成在插入新记录时数据文件为了维护 B+Tree 的特性而频繁的分裂调整,十分低效,而使用自增字段作为主键则是一个很好的选择。
2. 索引的类型
mysql索引有四种类型:主键索引、唯一索引、普通索引和全文索引。通过给字段添加索引,可以提高数据的读取速度,提高项目的并发能力和抗压能力。索引优化是 mysql 的一种优化方式。索引的作用相当于图书的目录,可以根据目录中的页码快速找到所需的内容。
主键索引:主键是一种唯一性索引,但它必须指定为 PRIMARY KEY,每个表只能有一个主键。
alter table tabname add primary key('字段名');
唯一索引:索引列的所有值都只能出现一次,即必须唯一,值可以为 空。
alter table tabname add unique index('字段名');
普通索引:基本的索引类型,值可以为空,没有唯一性的限制。
alter table tabname add index('字段名');
全文索引:全文索引的索引类型为 FULLTEXT。全文索引可以在 varchar、char、text 类型的列上创建。可以通过 ALTER TABLE 或者 CREATE INDEX 命令创建。对于大规模的数据集,通过 ALTER TABLE(或者CREATE INDEX)命令创建全文索引要比把记录插入带有全文索引的空表更快。MyIsam 支持全文索引,InnoDB 在mysql5.6 后支持了全文索引。
全文索引不支持中文,需要借 sphinx(coreseek)或讯搜<、code>技术处理中文。
alter table tabname add fulltext('字段名');
查看表的所有索引和删除
show indexes from 表名;
show keys from 表名;
alter table 表名 drop index 索引名;
3. 执行计划explain
1)id
执行编号,标识 select 所属的行。如果语句中没有子查询或关联查询,只有唯一的 select ,每行都将显示1.否则,内层的 select 语句一般会顺序编号,对应于其在原始语句中的位置。
列数字越大越先执行,如果数字一样大,那么就从上到下执行,id列为 null 的就表示这是一个结果集,不需要使用它来进行查询。
- 关联优化器会为查询选择关联顺序,左侧深度优先
- 当 from 中有子查询的时候,表名是 derivedN 的形式,N指向子查询,也就是 explain 结果中的下一列
- 当有 union result 的时候,表名是 union 1,2 等的形式,1,2 表示参与 union 的query id
4. 索引的使用场景
1)MySQL目前支持前导列
mysql 只支持最左前缀原则进行筛选
创建复合索引
2)索引列上的范围查找
对于某个条件进行范围查找时,如果这个列上有索引,且使用 where ... between and ... >,< 等范围操作,那么可能用到索引范围查找,如果索引范围查找的成本太高,数据库可能会选择全表扫描。
IN 也是范围查找的范畴,但是带操作函数的 IN 不属于查找的范畴
3)join 列
在联合查询两个表时,比如查询语句为 select a.col1, b.col2 from a join b on a.id = b.id,其中id 为两个表的主键,如果 a 是小表,那么 a 就被视为驱动表,那么数据库可能全表扫描 a 表,并用 a 表的每一个 id 去探测 b 表的索引查询匹配的记录。
4)where 子句
形如:
where a = ? and b = ? and c > 1000
where a = ? and b = ? and c = ? and d > 1000
where 子句的条件列是复合索引前面的索引列 + 另一个列的范围查找
create index idx_a_b_c_d on tb1(a, b, c, d);
上面才会用到这个索引。
下面的两个查询:
where a=? and b=? and c>1000 and d<10000 这个例子中 d < 10000 这个操作并不会走索引
where a>? and b=? and c>1000 and d<10000 这个例子 a 上有范围查找,那么 b、c、d 列上的索引信息都不能被利用
注,创建索引,考虑把复合索引的范围查找放到最后。
5)mysql 优化器
mysql 优化器会做一些特殊优化,比如对于索引查找 max(索引列)可以直接进行定位。遇到 max, min 是可以在列上做索引。
5、索引失效
https://www.jianshu.com/p/aedf4c0972e8
https://www.jianshu.com/p/3ccca0444432
1)where 条件中的索引列不能是表达式的一部分,mysql 不支持函数索引,即对查询的列上有运算或者函数
2)InnoDB 二级索引底层叶子节点存储的是索引+主键值
InnoDB的非主键索引存储的不是实际的记录的指针,而是主键的值,所以主键最好是整数型,如自增ID,基于主键存取数据是最高效的,使用二级索引存取数据则需要进行二次索引查找。
3)索引尽量是高选择性的,如性别,男女两种情况不宜使用索引,基本会全表扫描
4)查询条件中有 or , 仍是全表扫描
5)like 查询是以 ‘%’ 开头
6)如果列类型是字符串,那一定要在条件中将数据使用引号引起来,否则不使用索引。
explain select name, age, address from user where name=10 ---- 这样的不使用索引,走全表扫描。这里的原因和上面第一条是一样的,首先 mysql 有个类型转换规则就是将字符串转成数字 explain select name, age, address from user where cast(name as signed)=10。很明显,name上有函数,所以不会走索引。
7)左连接查询或者右连接查询关联的字段编码格式不一样
explain select a.name, b.job from user a left join job b on a.name=b.name
如果 user 表的 name 字段编码是 utf8mb4, 而 job 表的 name 字段编码是 gbk。执行左外连接查询 b 表仍然走的是全表扫描。
8)如果mysql 估计使用全表扫描要比使用索引快,则不使用索引
数据量很少的情况。
9)连接查询中,按照优化器顺序的第一张表不会走索引
条件:a 表 name 有索引, b 表name 没有索引
结果: explain a.name, a.age, b.name, b.job from user a left join job —— a表 b表都是全表扫描
explain select a.name, a.age, b.name, b.job from user a right join job b on a.name=b.name——a表使用上了索引
explain select a.name, a.age, b.name, b.job from user a inner join job b on a.name = b.name——a表使用到了索引
从上面3个连接查询来看只有左外连接a 表没有用到索引的,这就是因为由于是左外连接,所以优化器的执行顺序是 a 表、 b 表,也就是说首先全表扫描 a 表,再根据 a 表的 name 查询 b 表的值,所以 a 表无法用到索引。所以,一般这种连接查询,A表关联B表,要在将优化器顺序的第二张表上关联的字段加索引,而第一张不用加,无用的索引也会影响性能。而第三个例子中的内连接就不一样了,如果是内连接优化器本身就会根据索引情况,连接表的大小去选择执行顺序,所以上例中的内连接执行顺序是 b, a,这样仍然可以用到 a 的索引。
10)如果查询中没有用到联合索引的第一个字段,则不会走索引。
注意:
索引尽量是高选择性的
使用更短的索引:可以考虑前缀索引,但应确保选择的前缀的长度可以保证大部分值是唯一的。衡量不同前缀索引唯一值比例,select count(distinct left(col_name, 5))/count(*)) as sele5; select count(distinct left(col_name, 6))/count(*)) as sele6; select count(distinct left(col_name, 7))/count(*)) as sele7;
避免创建过多的索引:索引过多可能会浪费大量空间,尤其本身字段量较大的字符串,索引过多可能会浪费空间,且降低修改数据的速度,所以不要创建过多的索引,也不要创建重复的索引。
如果是唯一值的列,创建唯一索引会更佳,也可以确保不会出现重复数据
使用覆盖索引能大大提高性能:所有数据都可以从索引中得到,而不需要去读物理记录。
利用索引排序:mysql 有两种方法可以产生有序结果,一种是使用文件排序,另一种是扫描有序的索引,我们尽量使用索引来排序:1)尽量保证索引列和 order by 的列相同,且各列按照相同的顺序排序,2)如果连接多张表,那么 order by 引用的列需要在表连接顺序的首张表内
添加冗余索引要权衡:如果一个索引 column A ,那么新的索引(columnA, columnB)就是冗余索引。一般情况下不论是新增冗余索引,还是扩展原索引为冗余索引,都会导致索引文件的增大,并且增加了维护索引的开销。比如更改了列值也要对此列所以在索引进行重新排序。