数据库学习笔记一:索引篇

目录

一、索引篇

InnoDB 是如何存储数据的?

B+ 树是如何进行查询的?

数据页很多时怎么办?建立页索引

B+ 树的特点:

聚簇索引和二级索引

总结

为什么 MySQL 采用 B+ 树作为索引?

怎样的索引的数据结构是好的?

什么是二分查找树?

什么是自平衡二叉树(AVL树)?

什么是 B 树?

什么是 B+ 树?

1、单点查询

2、插入和删除效率

3、范围查询

MySQL 中的 B+ 树

总结

索引失效有哪些?

索引存储结构长什么样?

对索引使用左或者左右模糊匹配

对索引使用函数

对索引进行表达式计算

对索引隐式类型转换

联合索引非最左匹配

WHERE 子句中的 OR

总结

MySQL 使用 like “%x“,索引一定会失效吗?

题目一

题目二

count(*) 和 count(1) 有什么区别?哪个性能最好?

哪种 count 性能最好?

为什么要通过遍历的方式来计数?

如何优化 count(*)?


学习资料来源:小林coding

仅用于个人学习,侵权删除

一、索引篇

InnoDB 是如何存储数据的?

记录是按照行来存储的,但是数据库的读取并不以「行」为单位,否则一次读取(一次 I/O 操作)只能处理一行数据,效率会非常低。

因此,InnoDB 的数据是按「数据页」为单位来读写的,也就是说,当需要读一条记录的时候,并不是将这个记录本身从磁盘读出来,而是以页为单位整体读入内存

数据库的 I/O 操作的最小单位是页,InnoDB 数据页的默认大小是 16KB,意味着数据库每次读写都是以 16KB 为单位的,一次最少从磁盘中读取 16K 的内容到内存中,一次最少把内存中的 16K 内容刷新到磁盘中。

数据页包括七个部分,结构如下图:

数据库学习笔记一:索引篇_第1张图片

 在 File Header 中有两个指针,分别指向上一个数据页和下一个数据页,连接起来的页相当于一个双向的链表,如下图所示:数据库学习笔记一:索引篇_第2张图片

数据页中的记录按照「主键」顺序组成单向链表,单向链表的特点就是插入、删除非常方便,但是检索效率不高,最差的情况下需要遍历链表上的所有节点才能完成检索。

因此,每个数据页中有一个页目录,起到记录的索引作用,就像书本一样,针对书中内容的每个章节设立了一个目录,想看某个章节的时候,可以查看目录,快速找到对应的章节的页数,而数据页中的页目录就是为了能快速找到记录。

那 InnoDB 是如何给记录创建页目录的呢?页目录与记录的关系如下图:数据库学习笔记一:索引篇_第3张图片

页目录创建的过程如下:

  1. 将所有的记录分组,这些记录包括最小记录和最大记录,但不包括标记为“已删除”的记录
  2. 每个记录组的最后一条记录就是组内最大的那条记录,并且最后一条记录的头信息中会存储该组一共有多少条记录,作为 n_owned 字段(上图中粉红色字段)
  3. 页目录用来存储每组最后一条记录的地址偏移量,这些地址偏移量会按照先后顺序存储起来,每组的地址偏移量也被称之为槽(slot)每个槽相当于指针指向了不同组的最后一个记录

从图可以看到,页目录就是由多个槽组成的,槽相当于分组记录的索引。然后,因为记录是按照「主键值」从小到大排序的,所以我们通过槽查找记录时,可以使用二分法快速定位要查询的记录在哪个槽(哪个记录分组),定位到槽后,再遍历槽内的所有记录,找到对应的记录,无需从最小记录开始遍历整个页中的记录链表。

疑问

  • 「槽对应的值都是这个组的主键最大的记录,如何找到组里最小的记录」
  • 解决办法是:首选确定记录所在槽,比如确定在槽3,我们通过槽 3 找到 槽 2 对应的记录,再往下。
  • 槽内记录会不会很多,因为记录是单向链表串联,遍历时速度会变慢?
  • InnoDB 对每个分组中的记录条数都是有规定的,槽内的记录就只有几条:第一个分组中的记录只能有 1 条记录;最后一个分组中的记录条数范围只能在 1-8 条之间;剩下的分组中记录条数范围只能在 4-8 条之间

B+ 树是如何进行查询的?

上面我们都是在说一个数据页中的记录检索,因为一个数据页中的记录是有限的,且主键值是有序的,所以通过对所有记录进行分组,然后将组号(槽号)存储到页目录,使其起到索引作用,通过二分查找检索记录所在分组,确定分组之后遍历记录的单链表。来降低检索的时间复杂度。

数据页很多时怎么办?建立页索引

InnoDB 采用了 B+ 树作为索引。磁盘的 I/O 操作次数对索引的使用效率至关重要,因此在构造索引的时候,我们更倾向于采用“矮胖”的 B+ 树数据结构,这样所需要进行的磁盘 I/O 次数更少,而且 B+ 树 更适合关键字的范围查询

InnoDB 里的 B+ 树中的每个节点都是一个数据页,结构示意图如下:

数据库学习笔记一:索引篇_第4张图片

B+ 树的特点

  • 只有叶子节点存放数据,非叶子节点存放目录项作为索引
  • 非叶子节点分为不同层次,通过分层来降低每一层的搜索量
  • 所有叶子节点按照索引键大小排序,构成一个双向链表,便于范围查询;

我们再看看 B+ 树如何实现快速查找主键为 6 的记录,以上图为例子:

  • 从根节点开始,通过二分法快速定位到符合页内范围包含查询值的页,因为查询的主键值为 6,在[1, 7)范围之间,所以到页 30 中查找更详细的目录项;
  • 在非叶子节点(页30)中,继续通过二分法快速定位到符合页内范围包含查询值的页,主键值大于 5,所以就到叶子节点(页16)查找记录;
  • 接着,在叶子节点(页16)中,使用二分法快速定位要查询的记录在哪个槽(哪个记录分组),定位到槽后,再遍历槽内的所有记录,找到主键为 6 的记录。

总结:在定位记录所在哪一个页时(二分法)。再在该页内二分法快速定位记录所在的分组(槽号),最后在分组内进行遍历查找。

聚簇索引和二级索引

另外,索引又可以分成聚簇索引和非聚簇索引(二级索引),它们区别就在于叶子节点存放的是什么数据

  • 聚簇索引的叶子节点存放的是实际数据,所有完整的用户记录都存放在聚簇索引的叶子节点;
  • 二级索引的叶子节点存放的是主键值,而不是实际数据。

因为表的数据都是存放在聚簇索引的叶子节点里,所以 InnoDB 存储引擎一定会为表创建一个聚簇索引,且由于数据在物理上只会保存一份,所以聚簇索引只能有一个

InnoDB 在创建聚簇索引时,会根据不同的场景选择不同的列作为索引

  • 如果有主键,默认会使用主键作为聚簇索引的索引键;
  • 如果没有主键,就选择第一个不包含 NULL 值的唯一列作为聚簇索引的索引键
  • 在上面两个都没有的情况下,InnoDB 将自动生成一个隐式自增 id 列作为聚簇索引的索引键;

一张表只能有一个聚簇索引,那为了实现非主键字段的快速搜索,就引出了二级索引(非聚簇索引/辅助索引),它也是利用了 B+ 树的数据结构,但是二级索引的叶子节点存放的是主键值,不是实际数据。

二级索引的 B+ 树如下图,数据部分为主键值:

数据库学习笔记一:索引篇_第5张图片

 因此,如果某个查询语句使用了二级索引,①查询的数据不是主键值这时在二级索引找到主键值后,需要去聚簇索引中获得数据行,这个过程就叫作「回表」,也就是说要查两个 B+ 树才能查到数据②当查询的数据是主键值时因为只在二级索引就能查询到,不用再去聚簇索引查,这个过程就叫作「索引覆盖」,也就是只需要查一个 B+ 树就能找到数据

总结

InnoDB 的数据是按「数据页」为单位来读写的,默认数据页大小为 16 KB数据页之间通过双向链表组织起来,物理上不连续,逻辑上连续。

数据页内每个记录之间用单向链表的方式组织起来,为了加快在数据页内高效查询记录,设计了一个页目录,页目录存储各个槽(分组),且主键值是有序的,于是可以通过二分查找提高效率。

为了高效查询记录所在的数据页:

InnoDB 采用 b+ 树作为索引,每个叶子节点都是一个数据页

如果叶子节点存储的是实际数据的就是聚簇索引,一个表只能有一个聚簇索引;如果叶子节点存储的不是实际数据,而是主键值则就是二级索引,一个表中可以有多个二级索引

在使用二级索引进行查找数据时:

①找的是记录的主键,那么就是「索引覆盖」操作,②找的不是记录的主键,就需要先在二级索引找到主键值,需要去聚簇索引中获得数据行,这个过程就叫作「回表」

为什么 MySQL 采用 B+ 树作为索引?


要解释这个问题,其实不单单要从数据结构的角度出发,还要考虑磁盘 I/O 操作次数,因为 MySQL 的数据是存储在磁盘中的嘛。

怎样的索引的数据结构是好的?

MySQL 的数据是持久化的,意味着数据(索引+记录)是保存到磁盘上的,因为这样即使设备断电了,数据也不会丢失。

内存的访问速度是纳秒级别的,而磁盘访问的速度是毫秒级别的,相差上万倍,甚至几十万倍

磁盘读写的最小单位是扇区,扇区的大小只有 512B 大小,操作系统一次会读写多个扇区,所以操作系统的最小读写单位是块(Block)。Linux 中的块大小为 4KB,也就是一次磁盘 I/O 操作会直接读写 8 个扇区。

由于数据库的索引是保存到磁盘上的,因此当我们通过索引查找某行数据的时候,就需要①先从磁盘读取索引到内存②通过索引从磁盘中找到某行数据再读入到内存,也就是说查询过程中会发生多次磁盘 I/O,而磁盘 I/O 次数越多,所消耗的时间也就越大。

所以,我们希望索引的数据结构能在尽可能少的磁盘的 I/O 操作中完成查询工作,因为磁盘 I/O 操作越少,所消耗的时间也就越小。

另外,MySQL 是支持范围查找的,所以索引的数据结构不仅要能高效地查询某一个记录,而且也要能高效地执行范围查找。

所以,要设计一个适合 MySQL 索引的数据结构,至少满足以下要求:

  • 能在尽可能少的磁盘的 I/O 操作中完成查询工作
  • 要能高效地查询某个记录范围查找

什么是二分查找树?

用数组来实现线性排序的数据虽然简单好用,但是插入新元素的性能太低

其次,有序的数组在使用二分查找的时候,每次查找都要不断计算中间的位置

所以我们需要一种非线形且适合二分查找的数据结构:

有的,请看下图这个神奇的操作,找到所有二分查找中用到的所有中间节点,把他们用指针连起来,并将最中间的节点作为根节点。

数据库学习笔记一:索引篇_第6张图片

 二叉查找树的优点:是一个节点的左子树的所有节点都小于这个节点,右子树的所有节点都大于这个节点①在查询数据时,不需要计算中间节点的位置了,只需将查找的数据与节点的数据进行比较。②二叉查找树解决了插入新节点的问题因二叉查找树是一个跳跃结构,不必连续排列。

二叉查找树的缺点:当插入元素升序或者降序时,二叉查找树就退化成一条链表,查找数据的时间复杂度变成了 O(n)。

由于树是存储在磁盘中的,访问每个节点,都对应一次磁盘 I/O 操作(假设一个节点的大小「小于」操作系统的最小读写单位块的大小),也就是说树的高度就等于每次查询数据时磁盘 IO 操作的次数所以树的高度越高,就会影响查询性能。

总结:二叉查找树由于存在退化成链表的可能性,会使得查询操作的时间复杂度从 O(logn)降低为 O(n)。而且会随着插入的元素越多,树的高度也变高,意味着需要磁盘 IO 操作的次数就越多,这样导致查询性能严重下降,再加上不能范围查询,所以不适合作为数据库的索引结构。

什么是自平衡二叉树(AVL树)?

为了解决二叉查找树会在极端情况下退化成链表的问题。

条件约束:节点的左子树和右子树的高度差<=1,通过旋转【LL/RR/LR/RL】维持平衡。红黑树也是一种自平衡的二叉树,约束条件更加复杂。

但是!!!!不管平衡二叉查找树还是红黑树,都会随着插入的元素增多—>树的高度变高(根本原因是每个节点智能保存两个子节点)—>磁盘 I/O 操作次数多,会影响整体数据查询的效率。

什么是 B 树?

为了降低树的高度,B 树的每一个节点最多可以包括 M (M>2)个子节点,M 称为 B 树的阶,所以 B 树就是一个多叉树,从而降低树的高度。

假设 M = 3,那么就是一棵 3 阶的 B 树,特点就是每个节点最多有 2 个(M-1个)数据和最多有 3 个(M个)子节点具体特点这里不再阐述。超过这些要求的话,就会分裂节点,比如下面的的动图:

缺点:

B 树的节点(索引+记录),当用户的记录数据大小 >> 索引数据大小时,需要①花费更多的磁盘 I/O 操作次数来读到「有用的索引数据」。而且,在我们查询位于底层的某个节点(比如 A 记录)过程中,「非 A 记录节点」里的记录数据会从磁盘加载到内存,我们只想读取这些节点的索引数据来做比较查询,这样不仅增多磁盘 I/O 操作次数,也②占用内存资源。另外,如果③使用 B 树来做范围查询的话,需要中序遍历,这会涉及多个节点的磁盘 I/O 问题,从而导致整体速度下降。

什么是 B+ 树?

B+ 树与 B 树差异:

  • 叶子节点(最底部的节点)才会存放实际数据(索引+记录),非叶子节点只会存放索引;
  • 所有索引都会在叶子节点出现,叶子节点之间构成一个有序链表;
  • 非叶子节点的索引也会同时存在在子节点中,并且是在子节点中所有索引的最大(或最小)。
  • 非叶子节点中有多少个子节点,就有多少个索引;

下面通过三个方面,比较下 B+ 和 B 树的性能区别。

1、单点查询

B 树进行单个索引查询时,最快可以在 O(1) 的时间代价内就查到,而从平均时间代价来看,会比 B+ 树稍快一些。

但是 B 树的查询波动会比较大,因为每个节点即存索引又存记录,所以有时候访问到了非叶子节点就可以找到索引,而有时需要访问到叶子节点才能找到索引。

B+ 树的非叶子节点不存放实际的记录数据,仅存放索引,因此数据量相同的情况下,相比存储即存索引又存记录的 B 树,B+树的非叶子节点可以存放更多的索引,因此 B+ 树可以比 B 树更「矮胖」,查询底层节点的磁盘 I/O次数会更少

2、插入和删除效率

B+ 树删除:由于有冗余节点,可以直接从叶子节点中删除,甚至可以不动非叶子节点,这样删除非常快,树形结构变化很小。B+ 树在删除根节点的时候,由于存在冗余的节点,所以不会发生复杂的树的变形。

B+ 树的插入:有冗余节点,插入可能存在节点的分裂(如果节点饱和),但是最多只涉及树的一条路径。而且 B+ 树会自动平衡,不需要像更多复杂的算法,类似红黑树的旋转操作等。

因此,B+ 树的插入和删除效率更高

3、范围查询

B 树和 B+ 树等值查询原理基本一致,先从根节点查找,然后对比目标数据的范围,最后递归的进入子节点查找。

因为 B+ 树所有叶子节点间还有一个链表进行连接,这种设计有助于范围查找,比如说我们想知道 12 月 1 日和 12 月 12 日之间的订单,这个时候可以先查找到 12 月 1 日所在的叶子节点,然后利用链表向右遍历,直到找到 12 月12 日的节点,这样就不需要从根节点查询了,进一步节省查询需要的时间。

而 B 树没有将所有叶子节点用链表串联起来的结构,因此只能通过树的遍历来完成范围查询,这会涉及多个节点的磁盘 I/O 操作,范围查询效率不如 B+ 树。

因此,存在大量范围检索的场景,适合使用 B+树,比如数据库。而对于大量的单个索引查询的场景,可以考虑 B 树,比如 nosql 的MongoDB。

MySQL 中的 B+ 树

Innodb 使用的 B+ 树有一些特别的点,比如:

  • B+ 树的叶子节点用「双向链表」连接,好处是双向遍历。
  • B+ 树叶节点内容是数据页,数据页里存放了用户的记录以及各种信息,每个数据页默认大小是 16 KB。

Innodb 根据索引类型不同,分为聚集和二级索引。他们区别在于,聚集索引的叶子节点存放的是实际数据,所有完整的用户记录都存放在聚集索引的叶子节点,而二级索引的叶子节点存放的是主键值,而不是实际数据。

因为表的数据都是存放在聚集索引的叶子节点里,所以 InnoDB 存储引擎一定会为表创建一个聚集索引,且由于数据在物理上只会保存一份,所以聚簇索引只能有一个,而二级索引可以创建多个。

总结


MySQL 是会将数据持久化在硬盘,而存储功能是由 MySQL 存储引擎实现的,所以讨论 MySQL 使用哪种数据结构作为索引,实际上是在讨论存储引擎使用哪种数据结构作为索引,InnoDB 是 MySQL 默认的存储引擎,它就是采用了 B+ 树作为索引的数据结构。

要设计一个 MySQL 的索引数据结构,不仅仅考虑数据结构增删改的时间复杂度,更重要的是要考虑磁盘 I/0 的操作次数。因为索引和记录都是存放在硬盘,硬盘是一个非常慢的存储设备,我们在查询数据的时候,最好能在尽可能少的磁盘 I/0 的操作次数内完成。

二分查找树虽然是一个天然的二分结构,能很好的利用二分查找快速定位数据,但是它存在一种极端的情况,每当插入的元素都是树内最大的元素,就会导致二分查找树退化成一个链表,此时查询复杂度就会从 O(logn)降低为 O(n)。

为了解决二分查找树退化成链表的问题,就出现了自平衡二叉树,保证了查询操作的时间复杂度就会一直维持在 O(logn) 。但是它本质上还是一个二叉树,每个节点只能有 2 个子节点,随着元素的增多,树的高度会越来越高。

树的高度决定于磁盘 I/O 操作的次数,因为树是存储在磁盘中的,访问每个节点,都对应一次磁盘 I/O 操作,也就是说树的高度就等于每次查询数据时磁盘 IO 操作的次数,所以树的高度越高,就会影响查询性能。

B 树和 B+ 都是通过多叉树的方式,会将树的高度变矮,所以这两个数据结构非常适合检索存于磁盘中的数据。

但是 MySQL 默认的存储引擎 InnoDB 采用的是 B+ 作为索引的数据结构,原因有:

  • B+ 树的非叶子节点不存放实际的记录数据,仅存放索引,因此数据量相同的情况下,相比存储即存索引又存记录的 B 树,B+树的非叶子节点可以存放更多的索引,因此 B+ 树可以比 B 树更「矮胖」,查询底层节点的磁盘 I/O次数会更少。
  • B+ 树有大量的冗余节点(所有非叶子节点都是冗余索引),这些冗余索引让 B+ 树在插入、删除的效率都更高,比如删除根节点的时候,不会像 B 树那样会发生复杂的树的变化;
  • B+ 树叶子节点之间用链表连接了起来,有利于范围查询,而 B 树要实现范围查询,因此只能通过树的中序遍历来完成范围查询,这会涉及多个节点的磁盘 I/O 操作,范围查询效率不如 B+ 树。

索引失效有哪些?

索引存储结构长什么样?

在创建表时,InnoDB 存储引擎默认会创建一个主键索引(聚簇索引),其它索引都属于二级索引。

MySQL 的 MyISAM 存储引擎支持多种索引数据结构,比如 B+ 树索引、R 树索引、Full-Text 索引。MyISAM 存储引擎在创建表时,创建的主键索引默认使用的是 B+ 树索引。

虽然,InnoDB 和 MyISAM 都支持 B+ 树索引,但是它们数据的存储结构实现方式不同。不同之处在于:

  • InnoDB 存储引擎:B+ 树索引的叶子节点保存数据本身
  • MyISAM 存储引擎:B+ 树索引的叶子节点保存数据的物理地址(数据的指针)

如果使用的是 MyISAM 存储引擎,B+ 树索引的叶子节点保存数据的物理地址,即用户数据的指针,如下图:

图片

 如果使用的是 InnoDB 存储引擎, B+ 树索引的叶子节点保存数据本身,如下图所示:

图片

 InnoDB 存储引擎根据索引类型不同,分为聚簇索引(上图就是聚簇索引)和二级索引。它们区别在于,聚簇索引的叶子节点存放的是实际数据,所有完整的用户数据都存放在聚簇索引的叶子节点,而二级索引的叶子节点存放的是主键值,而不是实际数据。

如果将 name 字段设置为普通索引,那么这个二级索引长下图这样,叶子节点仅存放主键值。

图片

 在我们使用「主键索引」字段作为条件查询的时候,如果要查询的数据都在「聚簇索引」的叶子节点里,那么就会在「聚簇索引」中的 B+ 树检索到对应的叶子节点,然后直接读取要查询的数据。如下面这条语句:

// id 字段为主键索引,查询的数据都在聚簇索引里面
select * from t_user where id=1;

在我们使用「二级索引」字段作为条件查询的时候,如果要查询的数据都在「聚簇索引」的叶子节点里,那么需要检索两颗B+树:

  • 先在「二级索引」的 B+ 树找到对应的叶子节点,获取主键值
  • 然后用上一步获取的主键值,在「聚簇索引」中的 B+ 树检索到对应的叶子节点,然后获取要查询的数据

上面这个过程叫做回表,如下面这条语句:

// name 字段为二级索引
select * from t_user where name="林某";

在我们使用「二级索引」字段作为条件查询的时候,如果要查询的数据在「二级索引」的叶子节点(主键),那么只需要在「二级索引」的 B+ 树找到对应的叶子节点,然后读取要查询的数据,这个过程叫做覆盖索引。如下面这条语句:

// name 字段为二级索引
select id from t_user where name="林某";

对索引使用左或者左右模糊匹配

当我们使用左或者左右模糊匹配的时候,也就是 like %xx 或者 like %xx% 这两种方式都会造成索引失效。

比如下面的 like 语句,查询 name 后缀为「林」的用户,执行计划中的 type=ALL 就代表了全表扫描,而没有走索引。

// name 字段为二级索引
select * from t_user where name like '%林';

如果是查询 name 前缀为林的用户,那么就会走索引扫描,执行计划中的 type=range 表示走索引扫描,key=index_name 看到实际走了 index_name 索引:

// name 字段为二级索引
select * from t_user where name like '林%';

为什么 like 关键字左或者左右模糊匹配无法走索引呢?

因为索引 B+ 树是按照「索引值」有序排列存储的,只能根据前缀进行比较。

如果使用 name like '%林' 方式来查询,因为查询的结果可能是「陈林、张林、周林」等之类的,所以不知道从哪个索引值开始比较,于是就只能通过全表扫描的方式来查询。

对索引使用函数

如果查询条件中对索引字段使用函数,就会导致索引失效。

// name 为二级索引
select * from t_user where length(name)=6;

为什么对索引使用函数,就无法走索引了呢?

因为索引保存的是索引字段的原始值,而不是经过函数计算后的值,自然就没办法走索引了。

不过,从 MySQL 8.0 开始,索引特性增加了函数索引,即可以针对函数计算后的值建立一个索引,也就是说该索引的值是函数计算后的值,所以就可以通过扫描索引来查询数据。

举个例子,我通过下面这条语句,对 length(name) 的计算结果建立一个名为 idx_name_length 的索引。

alter table t_user add key idx_name_length ((length(name)));

对索引进行表达式计算

在查询条件中对索引进行表达式计算,也是无法走索引的。

select * from t_user where id + 1 = 10;

但是,如果把查询语句的条件改成 where id = 10 - 1,这样就不是在索引字段进行表达式计算了,于是就可以走索引查询了。

为什么对索引进行表达式计算,就无法走索引了呢?

因为索引保存的是索引字段的原始值,而不是 id + 1 表达式计算后的值,所以无法走索引,只能通过把索引字段的取值都取出来,然后依次进行表达式的计算来进行条件判断,因此采用的就是全表扫描的方式。

对索引隐式类型转换

如果索引字段是字符串类型,但是在条件查询中,输入的参数是整型的话,你会在执行计划的结果发现这条语句会走全表扫描。

我在原本的 t_user 表增加了 phone 字段,是二级索引且类型是 varchar。

图片然后我在条件查询中,用整型作为输入参数,此时执行计划中 type = ALL,所以是通过全表扫描来查询数据的。

select * from t_user where phone = 1300000001;

但是如果索引字段是整型类型,查询条件中的输入参数即使字符串,是不会导致索引失效,还是可以走索引扫描。

我们再看第二个例子,id 是整型,但是下面这条语句还是走了索引扫描的。

//索引字段是整型,但查询条件是字符串类型 可以 走索引扫描
select * from t_user where id = '1';

为什么第一个例子会导致索引失效,而第二例子不会呢?

MySQL 在遇到字符串和数字比较的时候,会自动把字符串转为数字,然后再进行比较

前面的例子一中的查询语句,我也跟大家说了是会走全表扫描:

//例子一的查询语句
select * from t_user where phone = 1300000001;

这是因为 phone 字段为字符串,所以 MySQL 要会自动把字符串转为数字,所以这条语句相当于:

select * from t_user where CAST(phone AS signed int) = 1300000001;

可以看到,CAST 函数是作用在了 phone 字段,而 phone 字段是索引,也就是对索引使用了函数!而前面我们也说了,对索引使用函数是会导致索引失效的

例子二中的查询语句,我跟大家说了是会走索引扫描:

//例子二的查询语句
select * from t_user where id = "1";

这时因为字符串部分是输入参数,也就需要将字符串转为数字,所以这条语句相当于:

select * from t_user where id = CAST("1" AS signed int);

可以看到,索引字段并没有用任何函数,CAST 函数是用在了输入参数,因此是可以走索引扫描的。

联合索引非最左匹配

对主键字段建立的索引叫做聚簇索引,对普通字段建立的索引叫做二级索引

那么多个普通字段组合在一起创建的索引就叫做联合索引,也叫组合索引

创建联合索引时,我们需要注意创建时的顺序问题,因为联合索引 (a, b, c) 和 (c, b, a) 在使用的时候会存在差别。

联合索引要能正确使用需要遵循最左匹配原则,也就是按照最左优先的方式进行索引的匹配

比如,如果创建了一个 (a, b, c) 联合索引,如果查询条件是以下这几种,就可以匹配上联合索引:

  • where a=1;
  • where a=1 and b=2 and c=3;
  • where a=1 and b=2;

需要注意的是,因为有查询优化器,所以 a 字段在 where 子句的顺序并不重要

但是,如果查询条件是以下这几种,因为不符合最左匹配原则,所以就无法匹配上联合索引,联合索引就会失效:

  • where b=2;
  • where c=3;
  • where b=2 and c=3;

有一个比较特殊的查询条件:where a = 1 and c = 3 ,符合最左匹配吗?

这种其实严格意义上来说是属于索引截断,不同版本处理方式也不一样。

MySQL 5.5 的话,前面 a 会走索引,在联合索引找到主键值后,开始回表,到主键索引读取数据行,然后再比对 c 字段的值。

从 MySQL5.6 之后,有一个索引下推功能,可以在索引遍历过程中,对索引中包含的字段先做判断,直接过滤掉不满足条件的记录,减少回表次数。

大概原理是:截断的字段会被下推到存储引擎层进行条件判断(因为 c 字段的值是在 (a, b, c) 联合索引里的),然后过滤出符合条件的数据后再返回给 Server 层。由于在引擎层就过滤掉大量的数据,无需再回表读取数据来进行判断,减少回表次数,从而提升了性能。

比如下面这条 where a = 1 and c = 0 语句,我们可以从执行计划中的 Extra=Using index condition 得知使用了索引下推功能。

为什么联合索引不遵循最左匹配原则就会失效?

原因是,在联合索引的情况下,数据是按照索引第一列排序,第一列数据相同时才会按照第二列排序。也就是说,如果我们想使用联合索引中尽可能多的列,查询条件中的各个列必须是联合索引中从最左边开始连续的列。如果我们仅仅按照第二列搜索,肯定无法走索引。

WHERE 子句中的 OR

在 WHERE 子句中,如果在 OR 前的条件列是索引列,而在 OR 后的条件列不是索引列,那么索引会失效。

举个例子,比如下面的查询语句,id 是主键,age 是普通列,从执行计划的结果看,是走了全表扫描。

select * from t_user where id = 1 or age = 18;

这是因为 OR 的含义就是两个只要满足一个即可,因此只有一个条件列是索引列是没有意义的,只要有条件列不是索引列,就会进行全表扫描。要解决办法很简单,将 age 字段设置为索引即可

总结

今天给大家介绍了 6 种会发生索引失效的情况:

  • 当我们使用左或者左右模糊匹配的时候,也就是 like %xx 或者 like %xx%这两种方式都会造成索引失效;
  • 当我们在查询条件中对索引列使用函数,就会导致索引失效。
  • 当我们在查询条件中对索引列进行表达式计算,也是无法走索引的。
  • MySQL 在遇到字符串和数字比较的时候,会自动把字符串转为数字,然后再进行比较。如果字符串是索引列,而条件语句中的输入参数是数字的话,那么索引列会发生隐式类型转换,由于隐式类型转换是通过 CAST 函数实现的,等同于对索引列使用了函数,所以就会导致索引失效。
  • 联合索引要能正确使用需要遵循最左匹配原则,也就是按照最左优先的方式进行索引的匹配,否则就会导致索引失效。
  • 在 WHERE 子句中,OR前后都需要是索引列才不会失效。如果在 OR 前的条件列是索引列,而在 OR 后的条件列不是索引列,那么索引会失效。

MySQL 使用 like “%x“,索引一定会失效吗?

图片

题目一

第一条和第二条都会走索引扫描,而且都是选择扫描二级索引(index_name),第三和第四条会发生索引失效,执行计划的结果 type= ALL,代表了全表扫描。

题目二

题目 2 的数据库表特别之处在于,只有两个字段,一个是主键索引 id,另外一个是二级索引 name。

第二条查询语句的执行计划如下(第一条结果与之相同):

图片

 第三条查询语句的执行计划(第四条也是一样的结果):

图片

 从执行计划的结果中,可以看到 key=index_name,也就是说用了二级索引,而且从 Extra 里的 Using index 说明用上了覆盖索引

首先,这张表的字段没有「非索引」字段,所以 select * 相当于 select id,name,然后**这个查询的数据都在*二级索引的 B+ 树*,因为二级索引的 B+ 树的叶子节点包含「索引值+主键值」,所以查*二级索引的 B+ 树*就能查到全部结果了,这个就是覆盖索引

但是执行计划里的 type 是 index,这代表着是通过全扫描二级索引的 B+ 树的方式查询到数据的,也就是遍历了整颗索引树。而第一和第二条查询语句的执行计划中 type 是 range,表示对索引列进行范围查询,也就是利用了索引树的有序性的特点,通过查询比较的方式,快速定位到了数据行。所以,type=range 的查询效率会比 type=index 的高一些

为什么选择全扫描二级索引树,而不扫描全表(聚簇索引)呢?

因为二级索引树的记录东西很少,就只有「索引列+主键值」,而聚簇索引记录的东西会更多,比如聚簇索引中的叶子节点则记录了主键值、事务 id、用于事务和 MVCC 的回流指针以及所有的剩余列。

再加上,这个 select * 不用执行回表操作。

所以, MySQL 优化器认为直接遍历二级索引树要比遍历聚簇索引树的成本要小的多,因此 MySQL 选择了「全扫描二级索引树」的方式查询数据。

为什么这个数据表加了非索引字段,执行同样的查询语句后,怎么变成走的是全表扫描呢?

加了其他字段后,select * from t_user where name like "%xx"; 要查询的数据就不能只在二级索引树里找了,得需要回表操作才能完成查询的工作,再加上是左模糊匹配,无法利用索引树的有序性来快速定位数据,所以得在二级索引树逐一遍历,获取主键值后,再到聚簇索引树检索到对应的数据行,这样实在太累了。

所以,优化器认为上面这样的查询过程的成本实在太高了,所以直接选择全表扫描的方式来查询数据。

总结:

如果数据库表中的字段只有主键+二级索引,那么即使使用了左模糊匹配,也不会走全表扫描(type=all),而是走全扫描二级索引树(type=index)

如果数据库表中的字段都是索引的话,即使查询过程中,没有遵循最左匹配原则,也是走索引扫描的,而且 type 也是为 index。

count(*) 和 count(1) 有什么区别?哪个性能最好?

以下内容基于常用的 innodb 存储引擎来说明。

哪种 count 性能最好?

数据库学习笔记一:索引篇_第7张图片

 count() 是什么?

count() 是一个聚合函数,函数的参数不仅可以是字段名,也可以是其他任意表达式,该函数作用是统计符合查询条件的记录中,函数指定的参数不为 NULL 的记录有多少个

select count(1) from t_order;

这条语句是统计「 t_order 表中,1 这个表达式不为 NULL 的记录」有多少个

1 这个表达式就是单纯数字,它永远都不是 NULL,所以上面这条语句,其实是在统计 t_order 表中有多少个记录

count(主键字段) 执行过程是怎样的?

MySQL 的 server 层会维护一个名叫 count 的变量。server 层会循环向 InnoDB 读取一条记录,如果 count 函数指定的参数不为 NULL,那么就会将变量 count 加 1,直到符合查询的全部记录被读完,就退出循环。最后将 count 变量的值发送给客户端。

InnoDB 是通过 B+ 树来保持记录的,根据索引的类型又分为聚簇索引和二级索引,它们区别在于,聚簇索引的叶子节点存放的是实际数据,而二级索引的叶子节点存放的是主键值,而不是实际数据。

用下面这条语句作为例子:

//id 为主键值
select count(id) from t_order;

如果表里只有主键索引,没有二级索引时,那么,InnoDB 循环遍历聚簇索引,将读取到的记录返回给 server 层,然后读取记录中的 id 值,就会 id 值判断是否为 NULL,如果不为 NULL,就将 count 变量加 1。

如果表里有二级索引时,InnoDB 循环遍历的对象就不是聚簇索引,而是二级索引。

这是因为相同数量的二级索引记录可以比聚簇索引记录占用更少的存储空间所以二级索引树比聚簇索引树小,这样遍历二级索引的 I/O 成本比遍历聚簇索引的 I/O 成本小,因此「优化器」优先选择的是二级索引。

count(1) 执行过程是怎样的?

select count(1) from t_order;

如果表里只有主键索引,没有二级索引。InnoDB 循环遍历聚簇索引(主键索引),将读取到的记录返回给 server 层,但不会读取记录中的任何字段的值,因为 count 函数的参数是 1,不是字段,所以不需要读取记录中的字段值。参数 1 很明显并不是 NULL,因此 server 层每从 InnoDB 读取到一条记录,就将 count 变量加 1。

可以看到,count(1) 相比 count(主键字段) 少一个步骤,就是不需要读取记录中的字段值,所以通常会说 count(1) 执行效率会比 count(主键字段) 高一点。

但是,如果表里有二级索引时,InnoDB 循环遍历的对象就二级索引了。

count(*) 执行过程是怎样的?

count(\*) 其实等于 count(0),也就是说,当你使用 count(*) 时,MySQL 会将 * 参数转化为参数 0 来处理。

所以,count(*) 执行过程跟 count(1) 执行过程基本一样的,性能没有什么差异。

而且 MySQL 会对 count(*) 和 count(1) 有个优化,如果有多个二级索引的时候,优化器会使用key_len 最小的二级索引进行扫描。只有当没有二级索引的时候,才会采用主键索引来进行统计。

count(字段) 执行过程是怎样的?

//name不是索引,普通字段
select count(name) from t_order;

对于这个查询来说,会采用全表扫描的方式来计数,所以它的执行效率是比较差的。

小结

count(1)、 count(*)、 count(主键字段)在执行的时候,如果表里存在二级索引,优化器就会选择二级索引进行扫描。

所以,如果要执行 count(1)、 count(*)、 count(主键字段) 时,尽量在数据表上建立二级索引,这样优化器会自动采用 key_len 最小的二级索引进行扫描,相比于扫描主键索引效率会高一些。

再来,就是不要使用 count(字段) 来统计记录个数,因为它的效率是最差的,会采用全表扫描的方式来统计。如果你非要统计表中该字段不为 NULL 的记录个数,建议给这个字段建立一个二级索引

为什么要通过遍历的方式来计数?

使用 MyISAM 引擎时,执行 count 函数只需要 O(1 )复杂度,这是因为每张 MyISAM 的数据表都有一个 meta 信息有存储了row_count值,由表级锁保证一致性,所以直接读取 row_count 值就是 count 函数的执行结果。

InnoDB 存储引擎是支持事务的,同一个时刻的多个查询,由于多版本并发控制(MVCC)的原因,InnoDB 表“应该返回多少行”也是不确定的,所以无法像 MyISAM一样,只维护一个row_count 变量。

如何优化 count(*)?

第一种,近似值

如果你的业务对于统计个数不需要很精确,比如搜索引擎在搜索关键词的时候,给出的搜索结果条数是一个大概值。

这时,我们就可以使用 show table status 或者 explain 命令来表进行估算

第二种,额外表保存计数值

当我们在数据表插入一条记录的同时,将计数表中的计数字段 + 1。也就是说,在新增和删除操作时,我们需要额外维护这个计数表

你可能感兴趣的:(MySQL,数据库,mysql)