解密 MySQL:索引—优化数据库性能的关键(二)

2023-11-15 17:10:41 +08:00
 th3ee9ine

摘要:通过上一篇的内容,我们对 MySQL 索引的基础知识有了一个大致的了解,知道了如何创建与使用索引了,但是这些内容还不足以让我们用好索引,接下来我们将继续深入了解索引的其他知识点。

上篇回顾:解密 MySQL:索引—优化数据库性能的关键(一)

一、执行计划

1 、什么是执行计划?

执行计划( Execution Plan )是数据库管理系统( DBMS )在执行一个查询时生成的一种计划或路线图。这个计划描述了 DBMS 如何获取所需数据并执行查询,包括了操作的顺序、访问表的方式、索引的使用,以及连接操作等信息。执行计划是数据库查询优化器生成的,它的目标是使查询尽可能高效地执行,减少资源消耗和响应时间。

执行计划通常以树形结构的形式表示,被称为执行计划树。这颗树包含了一系列执行操作,每个操作都代表了一个步骤,从中获取或处理数据,然后将数据传递给下一个步骤。这些操作可以包括表扫描、索引扫描、连接操作、排序、过滤等等。

数据库查询优化器负责生成最优的执行计划,它会分析查询语句、表结构、索引的可用性等因素,以确定如何最有效地执行查询。生成的执行计划会被 DBMS 执行引擎用于实际执行查询操作。

2 、如何生成和解释 MySQL 查询的执行计划?

在 MySQL 中,您可以使用 EXPLAIN 语句来生成和解释查询的执行计划。

在 MySQL 命令行或查询工具中,输入您要分析的查询语句,然后在查询语句之前加上 EXPLAIN 关键字。例如:


EXPLAIN SELECT column1, column2 FROM your_table WHERE condition;

# 例:
EXPLAIN SELECT * FROM user_info WHERE id > 1;

执行上述 EXPLAIN 语句。MySQL 将返回一个描述查询执行计划的表格,其中包括有关查询的各种信息,如操作顺序、表、索引、连接类型等。

以下是执行计划中常见字段的含义:

这些字段提供了有关查询执行计划的详细信息,可以帮忙我们了解查询是如何执行的,是否使用了索引,连接类型是什么,以及是否存在潜在的性能瓶颈。通过分析这些字段,我们可以更好地优化查询和数据库性能。

我们在后面的内容中,会频繁使用 EXPLAIN 语句,用于验证索引是否被正确使用。

二、最左匹配原则

1 、什么是最左匹配原则

在 MySQL 建立联合索引时会遵守最左前缀匹配原则,即最左优先,在检索数据时从联合索引的最左边开始匹配。

我们在上篇文章 解密 MySQL:索引—优化数据库性能的关键(一) 中,我们已经了解了索引的底层数据结构是一颗 B+树。

由于构建一棵 B+树只能根据一个值来确定索引关系,所以数据库依赖联合索引最左的字段来构建。

举例:创建一个( a,b )的联合索引,那么它的索引树就是下图的样子。

可以看到 a 的值是有顺序的,1 ,1 ,2 ,2 ,3 ,3 ,而 b 的值是没有顺序的 1 ,2 ,1 ,4 ,1 ,2 。

但是我们又可发现 a 在等值的情况下,b 值又是按顺序排列的,但是这种顺序是相对的。

这是因为 MySQL 创建联合索引的规则是首先会对联合索引的最左边第一个字段排序,在第一个字段的排序基础上,然后在对第二个字段进行排序。

所以 b=2 这种查询条件没有办法利用索引。

2 、使用场景分析

场景:user_info 表有一个( a ,b )的索引,下面的 sql 语句是否会走到这个索引:


# sql1
SELECT * FROM user_info WHERE a = '张三' AND b = '三年级';
# sql2
SELECT * FROM user_info WHERE b = '三年级' AND a = '张三';
# sql3
SELECT * FROM user_info WHERE b = '三年级' OR a = '张三';
# sql4
SELECT * FROM user_info WHERE a = '张三' AND b LIKE '三%';
# sql5
SELECT * FROM user_info WHERE a = '张三';
# sql6
SELECT * FROM user_info WHERE b = '三年级';
# sql7 ,这里假设 b 字段,为出生日期
SELECT * FROM user_info WHERE a = '张三' AND YEAR(b) = 2000;

3 、创建索引的一些建议

创建联合索引的时候应该注意的问题:

创建联合索引还是多个单列索引:

三、前缀索引

1 、什么是前缀索引?

前缀索引是一种特殊的索引,它只针对索引列的前若干个字符创建索引,而不是整个列。通常用于处理文本类型的数据列。

MySQL 数据库支持在字符串类型的列上创建前缀索引,语法如下:

# 这里 length 表示创建索引的前缀长度。
CREATE INDEX index_name ON table_name (column_name(length));
# 例子
CREATE INDEX idx_title_prefix ON article (title(10));

前缀索引的特点如下:

根据上面的特点,我们可以发现前缀索引更适用于查找变化较小的文本列,如名称、标题等静态信息。需要注意避免前缀值重复太多导致过滤效果不明显。总之,前缀索引是处理文本字段的重要手段,可以有效缩减索引体积,提升文本查找速度。

四、回表( Index Lookups )

在 MySQL 中,回表( Index Lookups )是指在使用非聚簇索引进行查询时,MySQL 需要根据该索引的键值去聚簇索引中查找对应的数据行的过程。

举例来说:

假设有一个包含学号(主键索引)、姓名和年龄的表,并在姓名字段上创建了非聚簇索引。当使用姓名索引进行查询时,MySQL 会首先在该索引中定位到满足条件的记录的主键值,然后再根据这些主键值去聚簇索引(通常是主键索引)中查找对应的数据行。这个额外的查找聚簇索引的过程就是回表。

解密 MySQL:索引—优化数据库性能的关键(一) 文章中,我们了解到在 InnoDB 存储引擎中,非聚簇索引的 data 域存储相应记录主键的值而不是地址。所以导致在利用非聚簇索引进行查询时,需要进行回表。

回表操作可能导致额外的 IO 开销,影响查询性能,特别是当查询的列不包含在非聚簇索引中时,具体造成的影响如下所示:

为了优化查询性能,一般可以使用覆盖索引和索引条件下推来避免回表操作,提高查询效率。

五、覆盖索引

1 、什么是覆盖索引

覆盖索引是指一个索引包含了查询所需的所有列,从而数据库可以直接使用索引返回查询结果,而不需要再去访问实际的数据行。

举例:如果有一个包含列 A 和列 B 的表,并且创建了 (A, B) 的联合索引,如果某个查询仅仅需要使用列 A ,并且 A 已经包含在了索引中,那么查询就会利用到覆盖索引,直接从索引中获取所需的数据,而无需再访问实际数据行。

覆盖索引的优势:

覆盖索引在以下情况下会被使用到:

2 、使用场景分析

假设有一个 user_info 表,其中包括一个联合索引 (a, b) 和一个主键索引 (a)。下面的 sql 语句是否会使用覆盖索引:

# sql1—仅需主键列的查询
SELECT a FROM user_info WHERE a = 100;
# sql2—范围查询涉及联合索引的第一个列
SELECT a FROM user_info WHERE a > 100 AND a < 200;
# sql3—基于联合索引的查询,只选择包含在索引中的列
SELECT a, b FROM user_info WHERE a = 100;
# sql4—按联合索引的第一个列进行排序
SELECT * FROM user_info ORDER BY a;
# sql5—使用聚合函数,仅涉及索引列
SELECT COUNT(a) FROM user_info WHERE a > 50;
# sql6—覆盖索引列满足查询条件
SELECT a, b FROM table_name WHERE a = 100 AND b = 'other_value';
# sql7—查询需要不在索引中的列
SELECT a, c FROM table_name WHERE a = 100;
# sql8—or
SELECT a, b FROM table_name WHERE a = 100 OR b = 'other_value';

使用覆盖索引的要点总结:

六、ICP (Index Condition Pushdown)

1 、什么是 ICP ( Index Condition Pushdown )?

索引条件下推( Index Condition Pushdown ,ICP )是一种数据库查询优化技术,其主要作用是在查询执行过程中利用索引预先过滤数据,减少读取的数据量。

通过 解密 MySQL:一条 SQL 语句的执行过程 文章,我们可以很容易的想到,索引条件下推是在 sql 执行的优化阶段进行的。但是想弄明白索引条件下推是什么,我们还需要了解一下,MySQL 的组成,具体如下图所示:

从上图我们可以看出 MySQL 从上至下分为以下几层:

MySQL 服务层负责 SQL 语法解析、触发器、视图、内置函数、binlog 、生成执行计划等,并调用存储引擎层去执行数据的存储和检索。“索引下推”的“下”其实就是指将部分上层(服务层)负责的事情,交给了下层(存储引擎)去处理。

其大致执行流程如下:

这样可以避免全表扫描,锁定需要读取的数据范围,有效利用索引提升查询效率。

优点总结如下:

2 、场景举例

有一张 products 表,如下所示:

CREATE TABLE products (
    id int(10) primary key,
    name varchar(100),
    price int(10),
    cate varchar(50),
    sold int(10)
);

CREATE INDEX idx_name ON products(name);
CREATE INDEX idx_cate_sold ON products(cate, sold);

假设我们需要执行下面这条 sql:

select * from products
    where name like 'iphone%' 
      and cate = 'phone'
      and sold > 10000;

不使用 ICP 的情况,执行计划如下所示:

通过执行计划,我们可以发现 MySQL 进行了全表扫描 all rows 和 Where 条件过滤。

使用 ICP 的情况,执行计划如下所示:

通过执行计划,我们可以发现 MySQL 利用索引 idx_name 和 idx_cate_sold 分别过滤,记录数降低,效率提高了。

所以正确使用索引条件下推可以显著优化查询计划,减少无效记录访问。

3 、支持索引下推的 MySQL 版本和存储引擎

MySQL 中支持索引条件下推(ICP)的版本和存储引擎如下:

版本方面:

所以建议使用 MySQL 5.6 及以上版本,可以利用 ICP 带来的查询性能提升。

存储引擎方面:

InnoDB 是 MySQL 的默认存储引擎,因此大多数情况下,只要使用 MySQL 5.6+ 版本与 InnoDB 引擎组合,就可以直接使用 ICP 。MyISAM 不支持 ICP 也是其逐步被 InnoDB 取代的一个重要原因之一。

七、MRR (Multi-Range Read)

1 、什么是 MRR

在 MySQL ( 5.6 及更高版本,且使用 InnoDB 存储引擎) 中的 MRR 指的是 Multi-Range Read ,即多范围读取。MRR 是一种优化查询的技术,它可以在读取多个索引范围时减少磁盘 I/O 和 CPU 消耗。

通常情况下,在执行查询时,MySQL 会遍历整个索引树,以找到所有匹配的行。但是,对于大型数据集,这种方式可能会导致性能下降,因为它需要大量的磁盘 I/O 和 CPU 资源。

MRR 通过将索引分成多个范围并在内存中缓存结果来避免这种情况。在使用 MRR 时,MySQL 会尝试将查询范围分成多个不重叠的部分,并使用范围扫描技术来查找每个部分中的匹配行。这种方式可以有效地减少磁盘 I/O 和 CPU 消耗,从而提高查询性能。

简单说:MRR 通过把「随机磁盘读」,转化为「顺序磁盘读」,从而提高了索引查询的性能。需要注意的是,MRR 适用于某些类型的查询,例如使用等于、大于、小于等操作符的查询。对于使用 LIKE 操作符的模糊查询,则不适合使用 MRR 。

2 、MRR 开启与关闭

通过参数 optimizer_switch 的标记来控制是否使用 MRR 。

当设置 mrr=on 时,表示启用 MRR 优化。mrr_cost_based 表示是否通过 cost base 基于成本的方式来启用 MRR 。

如果选择 mrr=on,mrr_cost_based=off 则表示总是开启 MRR 优化,参数 read_rnd_buffer_size 用来控制键值缓冲区的大小。

默认情况下:mrr=on,mrr_cost_based=on

3 、原理分析

在不使用 MRR 时,优化器需要根据二级索引返回的记录来进行回表,这个过程一般会有较多的随机 I/O 。

使用 MRR 时,SQL 语句的执行过程是这样的:

通过上述过程,优化器将二级索引随机的 I/O 进行排序,转化为主键的有序排列,从而实现了随机 I/O 到顺序 I/O 的转化,提升性能。

八、BNL ( Block Nested-Loop )与 BKA ( Batched Key Access )

1 、BNL

MySQL 5.5 版本前,MySQL 本身只支持一种表间关联方式,就是嵌套循环( Nested-Loop )。如果关联表的数据量很大,则 join 关联的执行时间会非常长。

在 5.5 版本中,MySQL 通过引入 BNL ( Block Nested-Loop Join )算法来优化嵌套执行。BNL 将外层循环的行/结果集存入到 join buffer ,然后每次遍历被驱动表都与 join buffer 中的数据进行比较,以此来减少全表扫描的次数。

例如,下面这个语句:

select * 
from t1 straight_join t2 
    on t1.a = t2.b;

假设字段 b 上是没有建立索引的。这时候,被驱动表上没有可用的索引,流程如下所示:

虽然 BNL 算法是全表扫描,但是是在内存中进行的判断操作,速度上会快很多。

join_buffer 的大小是由参数 join_buffer_size 设定的,默认值是 256k 。如果放不下表 t1 的所有数据话,则会进行分批处理,流程如下所示:

虽然分成多次放入 join_buffer ,但是判断等值条件的次数还是不变的。

2 、BKA

MySQL 在 5.6 版本开始增加了提高表 join 性能的算法:Batched Key Access (BKA),即批量索引访问。

BKA 其实就等价于 MRR + INLJ ( Index Nested-Loops Join ,基于索引的嵌套循环联接)。

使用 BKA 的表的 JOIN 过程如下:

可以看出,BKA 将有序主建投递到存储引擎是通过 MRR 的接口的调用来实现的,所以 BKA 依赖 MRR 。

BNL 和 BKA 都是批量的提交一部分行给被 join 的表,从而减少访问的次数,那么它们有什么区别呢?

参考资料

原文链接:解密 MySQL:索引—优化数据库性能的关键(二)

微信公众号:啊杰在拱趴

求关注!!!!

1351 次点击
所在节点    MySQL
6 条回复
th3ee9ine
2023-11-15 17:11:41 +08:00
新人公众号,求关注!!!
微信公众号:啊杰在拱趴
chenduke
2023-11-15 20:03:38 +08:00
在这个群普及基础的编程知识咋会有人看哦,这里都是一群大佬划水摸鱼的地方。
th3ee9ine
2023-11-16 11:25:38 +08:00
捞一下
siweipancc
2023-11-16 13:42:46 +08:00
比起这个我还是喜欢 pc 上那种博客,俗话说常看常新,基础知识不一定只能在新手时期看
siweipancc
2023-11-16 13:43:57 +08:00
给你个收藏,什么时候发第三节?
th3ee9ine
2023-11-16 14:32:22 +08:00
@siweipancc 一周左右吧,下一章可能是其他内容了,不是索引了。

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/992179

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX