如何优化sql中order By语句

67次阅读
没有评论

共计 4495 个字符,预计需要花费 12 分钟才能阅读完成。

这篇文章主要介绍“如何优化 sql 中 order By 语句”的相关知识,丸趣 TV 小编通过实际案例向大家展示操作过程,操作方法简单快捷,实用性强,希望这篇“如何优化 sql 中 order By 语句”文章能帮助大家解决问题。

在使用数据库进行数据查询时,难免会遇到基于某些字段对查询的结果集进行排序的需求。在 sql 中通常使用 orderby 语句来实现。将需要排序的字段放到 该关键词后,如果有多个字段的话,就用 , 分割。

select * from table t order by t.column1,t.column2;

上面的 sql 表示查询表 table 中数据,然后先按照 column1 排序,如果 column1 相同的话,在按照 column2 排序,排序的方式默认是降序。当然排序方式也是可以指定的。在被排序字段后添加 DESC,ASE, 分别表示降序和升序。

使用该 orderby 可以很方便的实现日常的排序操作。使用的多了,不知道你有没有遇到过这种场景:有时候使用 orderby 后,sql 执行效率非常慢,有时候却比较快,由于整天被 curd 缠身,也没有时间研究,反正就是觉得很神奇。趁这个周末比较闲,就来研究下,mysql 中 orderby 是怎么实现的。

为了方便描述,我们先建立一个数据表 t1,如下:

CREATE TABLE `t1` ( `id` int(11) NOT NULL not null auto_increment,
 `a` int(11) DEFAULT NULL,
 `b` int(11) DEFAULT NULL,
 `c` int(11) DEFAULT NULL,
 PRIMARY KEY (`id`) ,
 KEY `a` (`a`) USING BTREE
) ENGINE=InnoDB;

并插入数据:

insert into t1 (a,b,c) values (1,1,3);
insert into t1 (a,b,c) values (1,4,5);
insert into t1 (a,b,c) values (1,3,3);
insert into t1 (a,b,c) values (1,3,4);
insert into t1 (a,b,c) values (1,2,5);
insert into t1 (a,b,c) values (1,3,6);

为了使索引生效,插入 10000 行 7,7,7,无关数据,数据量少的情况下,会直接全表扫描

insert into t1 (a,b,c) values (7,7,7);

我们现在需要查找 a= 1 的所有记录,然后按照 b 字段进行排序。

查询 sql 为

select a,b,c from t1 where a = 1 order by b limit 2;

为了防止在查询过程中全表扫描,我们在字段 a 上添加了索引。

首先我们先通过语句

explain select a,b,c from t1 where a = 1 order by b lmit 2;

查看 sql 的执行计划,如下所示:

在 extra 中我们可以看到出现了 Using filesort,这个表示 该 sql 执行过程中,执行了排序操作,排序操作在 sort_buffer 中完成,sort_buffer 是 mysql 分配给每个线程的一个内存缓冲区,该缓冲区专门用来完成排序,大小默认是 1M,其大小由变量 sort_buffer_size 进行控制。

mysql 在对 orderby 进行实现时,根据放入到 sort_buffer 中的字段内容不同,进行了两种不同实现方式:全字段排序和 rowid 排序。

全字段排序

首先我们先通过一张图整体看一下 sql 执行过程:

mysql 先根据查询条件确定需要排序的数据集,也就是表中 a= 1 的数据集,即主键 id 从 1 到 6 的这些记录。

整个 sql 的执行的过程如下:

1. 创建并初始化 sort_buffer, 并确定需要放到该缓冲区中的字段,也就是 a,b,c 这三个字段。

2. 从索引树 a 中找到第一个满足 a = 1 的主键 id,也就是 id=1。

3. 回表到 id 索引,取出整行数据,然后从整行数据中,取出 a,b,c 的值,放入到 sort_buffer 中。

4. 从索引 a 中按照顺序找到下一个 a = 1 的主键 id。

5. 重复步骤 3 和步骤 4,直到获取到最后一个 a = 1 的记录,也就是主键 id=5。

6. 此时满足条件 a = 1 的所有记录的 a,b,c 字段,全部读放到了 sort_buffer 中,然后,对这些数据按照 b 的值进行进行排序,排序的方式是快速排序。就是那个面试经常面到的快速排序,时间复杂度为 log2n 的快速排序。

7. 然后从排序后的结果集中取出前 2 行数据。

上面是就是 msql 中 orderby 的执行流程。因为放入到 sort_buffer 中的数据是需要输出的全部字段,所以这种排序被称为全排序。

看到这里不知道你是否会有疑问?如果需要排序的数据量很大的话,sort_buffer 装不下怎么办?

的确,如果 a = 1 的数据行特别多,且需要存放到 sort_buffer 中的字段比较多,可能不止 a,b,c 三个字段,有些业务可能需要输出更多字段。那么默认大小只有 1M 的 sort_buffer 很可能容纳不下。

当 sort_buffer 容纳不下的时候,mysql 会创建一批临时的磁盘文件来辅助排序。默认情况下会创建 12 个临时文件,将需要排序的数据分成 12 份,每一份单独排序,形成 12 个内部数据有序的文件,然后把这 12 个有序文件在合并成一个有序的大文件,最终完成数据的排序。

基于文件的排序,相比基于内存的排序,排序效率要低很多,为了提高排序的效率,应该尽量避免基于文件的排序,要想避免基于文件排序,就需要让 sort_buffer 可以容纳需要排序的数据量。

所以对于 sort_buffer 容纳不下的情况,mysql 进行了优化。就是在排序时候,降低存放到 sort_buffer 中的字段个数。

具体优化方式,就是下面的 rowId 排序

RowId 排序

在全字段排序实现中,排序的过程中,要把需要输出的字段全部放到 sort_buffer 中,当输出的字段比较多的时候,可以放到 sort_buffer 中的数据行就会变少。也就增大了 sort_buffer 无法容纳数据的风险,直至出现基于文件的排序。

rowId 排序对全字段排序的优化手段,主要是减少了放到 sort_buffer 中字段个数。

在 rowId 排序中,只会将需要排序的字段和主键 Id 放到 sort_buffer 中。

select a,b,c from t1 where a = 1 order by b limit 2;

在 rowId 的排序中的执行流程如下:

1. 初始化并创建 sort_buffer,并确认要放入的的字段,id 和 b。

2. 从索引树 a 中找到第一个满足 a = 1 的主键 id,也就是 id=1。

3. 回表主键索引 id,取出整行数据,从整行数据中取出 id 和 b,存入 sort_buffer 中。

4. 从索引 a 中取出下一条满足 a = 1 的 记录的主键 id。

5. 重复步骤 3 和 4,直到最后一个满足 a = 1 的主键 id,也就是 a =6。

6. 对 sort_buffer 中的数据,按照字段 b 排序。

7. 从 sort_buffer 中的有序数据集中,取出前 2 个,因为此时取出的数据只有 id 和 b,要想获取 a 和 c 字段,需要根据 id 字段,回表到主键索引中取出整行数据,从整行数据中获取需要的数据。

根据 rowId 排序的执行步骤,可以发现:相比全字段排序,rowId 排序的实现方式,减少了存放到 sort_buffer 中的数据量,降低了基于文件的外部排序的可能性。

那 rowid 排序有不足的地方吗?肯定有的,要不然全字段排序就没有存在的意义了。rowid 排序不足之处在于,在最后的步骤 7 中,增加了回表的次数,不过这个回表的次数,取决于 limit 后的值,如果返回的结果集比较小的话,回表的次数还是比较小的。

mysql 是如何在全字段排序和 rowId 排序的呢?其实是根据存放的 sort_buffer 中每行字段的长度决定的,如果 mysql 认为每次放到 sort_buffer 中的数据量很大的话,那么就用 rowId 排序实现,否则使用全字段排序。那么多大算大呢?这个大小的阈值有一个变量的值来决定,这个变量就是 max_length_for_sort_data。如果每次放到 sort_buffer 中的数据大小大于该字段值的话,就使用 rowId 排序,否则使用全字段排序。

orderby 的优化

上面讲述了 orderby 的两种排序的方式,以及一些优化策略,优化的目的主要就是避免基于磁盘文件的外部排序。因为基于磁盘文件的排序效率要远低于基于 sort_buffer 的内存排序。

但是当数据量比较大的时候,即使 sort_buffer 比较大,所有数据全部放在内存中排序,sql 的整体执行效率也不高,因为排序这个操作,本身就是比较消耗性能的。

试想,如果基于索引 a 获取到所有 a = 1 的数据,按照字段 b,天然就是有序的,那么就不用执行排序操作,直接取出来的数据,就是符合结果的数据集,那么 sql 的执行效率就会大幅度增长。

其实要实现整个 sql 执行过程中,避免排序操作也不难,只需要创建一个 a 和 b 的联合索引即可。

alter table t1 add index a_b (a,b);

添加 a 和 b 的联合索引后,sql 执行流程就变成了:

1. 从索引树 (a,b) 中找到第一个满足 a = 1 的主键 id,也就是 id=1。

2. 回表到主键索引树,取出整行数据,并从中取出 a,b,c,直接作为结果集的一部分返回。

3. 从索引树 (a,b) 上取出下一个满足 a = 1 的主键 id。

4. 重复步骤 2 和 3,直到找到第二个满足 a = 1 的主键 id, 并回表获取字段 a,b,c。

此时我们可以通过查看 sql 的执行计划,来判断 sql 的执行过程中是否执行了排序操作。

explain select a,b from t1 where a = 1 order by b lmit 2;

通过查看执行计划,我们发现 extra 中已经没有了 using filesort 了,也就是没有执行排序操作了。

其实还可以通过覆盖索引,对该 sql 进一步优化,通过在索引中覆盖字段 c,来避免回表的操作。

alter table t1 add index a_b_c (a,b,c);

添加索引 a_b_c 后,sql 的执行过程如下:

1. 从索引树 (a,b,c) 中找到第一个满足 a = 1 的索引,从中取出 a,b,c。直接作为结果集的一部分直接返回。

2. 从索引 (a,b,c) 中取出下一个,满足 a = 1 的记录作为结果集的一部分。

3. 重复执行步骤 2,直到查到第二个 a = 1 或者不满足 a = 1 的记录。

此时通过查看执行 sql 的的还行计划可以发现 extra 中只有 Using index。

explain select a,b from t1 where a = 1 order by b lmit 2;

关于“如何优化 sql 中 order By 语句”的内容就介绍到这里了,感谢大家的阅读。如果想了解更多行业相关的知识,可以关注丸趣 TV 行业资讯频道,丸趣 TV 小编每天都会为大家更新不同的知识点。

正文完
 
丸趣
版权声明:本站原创文章,由 丸趣 2023-07-13发表,共计4495字。
转载说明:除特殊说明外本站除技术相关以外文章皆由网络搜集发布,转载请注明出处。
评论(没有评论)