这篇文章是MySQL索引优化的第三部分。前两篇文章可以看:
这篇文章的主要主题是关于优化 MySQL 中的分页和连接。
分页优化
分页查询是指从一个大的结果集中只返回一部分数据,常用于网站的数据展示、浏览等场景。在MySQL中,LIMIT关键字用于实现分页查询,其语法如下:
sql
SELECT * FROM table_name LIMIT offset , row_count;
其中offset指定查询结果的起始位置,并row_count指定查询结果的条数。
要查询表的前10条记录,可以使用以下SQL语句:
sql
SELECT * FROM table_name LIMIT 0 , 10 ;
要查询接下来的10条记录,可以使用以下SQL语句:
sql
SELECT * FROM table_name LIMIT 10 , 10 ;
但是,在处理大型数据集时,分页可能会成为性能瓶颈,尤其是在使用 OFFSET 子句时,它需要在返回指定的记录子集之前扫描整个数据集。
看下面这个例子
vbnet
explain select * from Students order by name limit 100000,10;
该SQL语句看似从表中检索10条记录,但实际上它从表中检索100010条记录,并丢弃前100000条记录以获得所需的10条记录。
因此,当进一步深入结果集时,该查询的性能会下降。
SQL可以优化如下:
vbnet
explain select * from Students t1 join
(select id from Students order by `name` limit 100000, 10) t2
on t1.id = t2.id;
优化思路:首先使用覆盖索引的方法查询10条数据,然后用这10条数据进行连接查询。
这样查询的字段就完全被索引覆盖了
覆盖索引说明:基于MySQL数据结构,主键索引(InnoDB引擎)存储完整的记录,而辅助索引仅存储主键。MySQL 节点通常为 16KB,因此二级索引叶节点可以容纳更多记录。扫描辅助索引比扫描主键索引需要更少的 I/O 操作。
优化前查询时间:
优化后查询时间:
其他优化方法包括:
- 尽可能避免使用子查询,因为子查询会增加查询复杂性和执行时间。相反,使用联接或表联接查询来替换子查询。
- 对需要分页的字段添加索引,提高查询速度。分页时,使用limit和order by语句,并根据排序字段创建索引,提高排序效率。
- 您可以使用缓存机制将分页结果缓存到缓存服务器中,这样可以减少数据库查询的次数。
Join优化
Join操作是常见的数据库操作之一,但也很容易导致性能问题。
Join查询是常见的数据库操作,但连接操作也会遇到性能问题。连接查询可分为内连接、左连接和右连接。
执行操作时有两种情况Join:使用索引字段连接和不使用索引字段连接。
以下是两个表 t1 和 t2 的示例,其中a列有索引,而b列没有索引:
scss
CREATE TABLE t1 (
id INT(11) NOT NULL AUTO_INCREMENT,
a INT(11) DEFAULT NULL,
b INT(11) DEFAULT NULL,
PRIMARY KEY (id),
INDEX idx_a (a)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE t2 (
id INT(11) NOT NULL AUTO_INCREMENT,
a INT(11) DEFAULT NULL,
b INT(11) DEFAULT NULL,
PRIMARY KEY (id),
INDEX `idx_a` (`a`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
INSERT INTO t1 (a, b) VALUES (1,1), (2,2), (3,3), (4,4), (5,5);
INSERT INTO t2 (a, b) VALUES (1,1), (2,2), (3,3), (4,4), (5,5);
表t2的结构与t1完全相同,t1有16743行数据,t2有540行数据。
csharp
explain select * from t1 inner join t2 on t1.a = t2.a;
执行计划分析:
- t2对(540 条记录)执行全表扫描。
- 使用idx_a连接t1和t2
- 检索 t1中满足连接条件的行并将它们与t2 中的数据合并。将结果返回给客户端。
耗时估计:
- 扫描t2表:540次
- 扫描t1表:N次。此过程的时间复杂度大约是O(1)可以使用索引定位的数据的时间复杂度。
- 内存中的比较次数:540 * 16743 = 9041220。
这个过程被称为基于块的块嵌套循环连接(BNL)算法。
该算法的原理可以理解为:将驱动表中的数据加载到连接缓冲区中,然后算法扫描驱动表并将驱动表的每一行与连接缓冲区中的数据进行比较。
使用BNL算法时,如果连接缓冲区不够大,无法容纳驱动表中的所有数据,可以将表分成多个块,并将每个块加载到连接缓冲区中进行比较。
这样可以避免join buffer不足的问题。这种方法称为 Block In-Memory Join 算法,或者 Sort-Merge Join 算法的变体。
当t2表有大量行,例如1000行,但连接缓冲区一次只能容纳800行时,将使用一种称为"分段"方法的策略。前 800 行将被加载到连接缓冲区中,然后算法将扫描 t1 表并执行比较。之后,连接缓冲区将被清除,剩余的 200 行将被加载到缓冲区中。
然后算法将再次扫描 t1 表并执行另一次比较。
换句话说,如果连接缓冲区在两次迭代中无法容纳所有需要的数据,算法将再次扫描 t1 表,并重复该过程,直到处理完所有需要的数据。
JOIN的其他知识点
在JOIN查询中,一般有两种算法:
- NLJ 算法:使用索引字段进行连接。
- BNL 算法:使用非索引字段进行连接。
NLJ算法的性能优于BNL算法。
连接查询的优化方法:
- 在连接列上添加索引可以帮助MySQL选择NLJ算法,该算法通常比BNL算法更快、更高效。
- 在连接查询中使用较小的表来驱动较大的表。
- 使用BNL算法时,如有必要,可以增加内存中的连接缓冲区大小,以避免驱动表的多次扫描。
为什么非索引字段不适合嵌套循环连接 (NLJ) 算法?
这是因为NLJ算法采用磁盘扫描的方式:首先扫描驱动表并检索一行数据。然后,它使用该数据的关联字段在驱动表中查找相应的数据。此过程使用索引字段并且非常快。
但是,如果使用此方法使用非索引字段,则需要全表扫描来查找驱动表中的关联数据,因为索引访问方法不可用。
例如,如果驱动表有100行数据,则驱动表需要扫描100次。如果驱动表包含10,000行数据,那么就会有100*10,000=1,000,000次磁盘I/O操作,速度会非常慢。
MySQL 中 In 和 Exists 关键字的优化。
IN和EXISTS的优化只有一个原则:用数据集较小的表来驱动数据集较大的表。
当B表的数据集小于A表的数据集时,IN 优先于EXISTS。
csharp
select * from A where id in (select id from B)
当A表的数据集小于B表的数据集时,EXISTS优先于IN,即数据集较小的表为外表。
sql
select * from A where exists (select 1 from B where B.id = A.id)
索引设计原则
- 先写代码,然后根据情况创建索引。一般来说,只有代码写好之后,我们才能清楚地判断哪些字段会使用索引。
- 尽量让复合索引尽可能覆盖大部分业务。
一般不建议在表上创建太多索引,因为维护索引也需要性能开销。因此,建议尝试创建1 到3个覆盖业务中SQL查询条件的复合索引。
- 不要在低基数列上创建索引。
- 可以使用部分前缀创建长字符串的索引
对长字段的部分字符串(例如 varchar(255))建立索引可以减少索引使用的磁盘空间。 例如,您只能索引前 20 个字符。但需要注意的是,这可能会导致排序失败,因为只使 用了前 20 个字符,而索引只能保证粗略的顺序。
- 基于慢查询日志优化索引是一项持续的任务。
由于业务需求的迭代,查询条件可能会发生变化。这时就可以根据慢SQL日志对索引进 行持续优化。
- 建议尽可能创建唯一索引。
- 当where条件与ORDER BY查询中存在冲突时,应优先为WHERE条件创建索引。
因为过滤掉数据后,剩下的数据集通常比较小,排序的成本并不高。因此,最好首先使 用where子句上的索引来更快地过滤数据。
如果喜欢这篇文章,点赞支持一下,关注我第一时间查看更多内容!