MySQL高级篇——排序、分组、分页优化

本文涉及的产品
云数据库 RDS MySQL,集群系列 2核4GB
推荐场景:
搭建个人博客
RDS MySQL Serverless 基础系列,0.5-2RCU 50GB
云数据库 RDS MySQL,高可用系列 2核4GB
简介: 排序优化建议、案例验证、范围查询时索引字段选择、filesort调优、双路排序和单路排序、分组优化、带排序的深分页优化

 导航:

【Java笔记+踩坑汇总】Java基础+JavaWeb+SSM+SpringBoot+SpringCloud+瑞吉外卖/谷粒商城/学成在线+设计模式+面试题汇总+性能调优/架构设计+源码解析

目录

5. 排序优化

5.1 排序优化建议

5.2 测试

5.2.1 案例验证

5.3.2 练习

5.3 范围查询时索引字段选择

5.4 filesort算法

5.4.1 双路排序和单路排序

5.4.2 调优filesort

6. 分组优化

7. 分页查询优化

7.1 深分页查询优化

7.2 带排序的深分页优化


5. 排序优化

5.1 排序优化建议

问题:在 WHERE 条件字段上加索引,但是为什么在 ORDER BY 字段上还要加索引呢?

在MySQL中,支持两种排序方式,分别是 FileSort 和 Index 排序。

  • Index排序:索引排序中,索引可以保证数据的有序性,不需要再进行排序,效率更高,推荐使用
  • FileSort排序:FileSort 排序则一般在内存中进行排序,占用CPU较多。如果待排结果较大,会产生临时文件 I/O 到磁盘进行排序的情况,效率较低。

优化建议:

  • 优化器自动选择排序方式:MySQL支持索引排序和FileSort排序,索引保证记录有序性,性能高,推荐使用。FileSort排序是内存中排序,数据量大时产生临时文件在磁盘里排序,效率低还占用大量CPU。并不是说FileSort一定效率低,一些情况它可能效率高。例如没覆盖索引的左模糊、“不等于”查询,全表扫描效率比索引遍历再回表更高。
  • 要符合最左前缀:where后条件和order by字段创建联合索引,顺序要需要符合最左前缀。例如索引(a,b,c),查询where a=1 order by b,c。
  • 范围查询右边排序索引失效:例如索引(a,b,c),查询where a>1 order by b,c,导致b,c排序不能走索引,需要filesort。
  • 要么全升序要么全降序:排序顺序必须要么全部DESC,要么全部ASC。乱序会导致索引失效。
  • 待排序数量大时会导致索引失效:待排序数据量大约超过一万个,就不走索引走filesort了。建议用limit和where过滤,减少数据量。数据量很大时,索引排序完需要回表查所有数据,性能很差,还不如FileSort在内存中排序效率高。并不是说使用limit一定会走索引排序,关键看的是数据量,数据量过大时优化器会使用FileSort排序。
  • 优先范围字段加索引:当【范围条件】和【group by 或者 order by】的字段出现二选一时,如果过滤的数据足够多,而需要排序的数据并不多时,优先把索引放在范围字段上。这样即使范围查询导致排序索引失效,效率依然比只索引排序字段时候高。如果只能过滤一点点,那就优先索引放到排序字段上。
  • 调优FileSort :无法使用 Index 排序时,需要对 FileSort 方式进行调优。例如增大sort_buffer_size(排序缓冲区大小)和 max_length_for_sort_data(排序数据最大长度)

5.2 测试

5.2.1 案例验证

删除student表和class表中已创建的索引。

# 方式1
DROP INDEX idx_monitor ON class;
DROP INDEX idx_cid ON student;
DROP INDEX idx_age ON student;
DROP INDEX idx_name ON student;
DROP INDEX idx_age_name_classId ON student;
DROP INDEX idx_age_classId_name ON student;
# 方式2:call调用删除函数
call proc_drop_index('atguigudb2','student');
image.gif

以下是否能使用到索引,能否去掉using filesort

不加索引直接走filesort排序:

#索引失败。没有limit
EXPLAIN SELECT SQL_NO_CACHE * FROM student ORDER BY age,classid;

image.gif

  image.gif

 

加索引后,order by 时不limit导致数据量过大,从而索引失效:

CREATE INDEX idx_age_classid_name ON student(age,classId,name);
#索引失败。没有limit
EXPLAIN SELECT SQL_NO_CACHE * FROM student ORDER BY age,classid;

image.gif

image.gif

#索引成功,key_len为73
EXPLAIN SELECT SQL_NO_CACHE * FROM student ORDER BY age,classid LIMIT 10000;

image.gif

image.gif

过程三:order by 时顺序错误,索引失效

#创建索引age,classid,stuno
#call proc_drop_index('atguigudb2','student');
CREATE INDEX idx_age_classid_stuno ON student (age,classid,stuno);
#索引失效,不符合最左前缀
EXPLAIN SELECT * FROM student ORDER BY classid LIMIT 10;
#索引失效,不符合最左前缀
EXPLAIN SELECT * FROM student ORDER BY classid,name LIMIT 10;
#索引失效,不符合最左前缀
EXPLAIN SELECT * FROM student WHERE classid=1 ORDER BY age,stuno;
#全走索引,虽然不符合最左前缀,但因为查询量小,优化器先排序三个字段,再where找10个返回。
#优化器认为索引比filesort效率高,就用了索引
EXPLAIN SELECT * FROM student WHERE classid=1 ORDER BY age,stuno LIMIT 10;
#索引成功,符合最左前缀
EXPLAIN SELECT * FROM student ORDER BY age,classid,stuno LIMIT 10;
#索引成功,符合最左前缀
EXPLAIN SELECT * FROM student ORDER BY age,classid LIMIT 10;

image.gif

过程四:order by 时规则不一致,索引失效(顺序错,不索引;方向反,不索引)

必须符合最左前缀和“全升序或全降序”

#创建索引age,classid,stuno
CREATE INDEX idx_age_classid_stuno ON student (age,classid,stuno);
#没符合“全升序或全降序”,索引失效
EXPLAIN SELECT * FROM student ORDER BY age DESC,classid ASC LIMIT 10;
#没符合最左前缀,索引失效
EXPLAIN SELECT * FROM student ORDER BY classid DESC,name DESC LIMIT 10;
#没符合“全升序或全降序”,索引失效
EXPLAIN SELECT * FROM student ORDER BY age ASC,classid DESC LIMIT 10;
#符合最左前缀,符合“全升序或全降序”,索引成功
EXPLAIN SELECT * FROM student ORDER BY age DESC,classid DESC LIMIT 10;

image.gif

过程五:limit数据量小时,不满足最左前缀也可能走索引,先排序再where筛选。

CREATE INDEX idx_age_classid_stuno ON student (age,classid,stuno);
CREATE INDEX idx_age_classid_name ON student(age,classId,name);
#都走了索引。
EXPLAIN SELECT * FROM student WHERE age=45 ORDER BY classid LIMIT 10;
#都走了索引。
EXPLAIN SELECT * FROM student WHERE age=45 ORDER BY classid,name;
#都没用索引,不符合最左前缀
EXPLAIN SELECT * FROM student WHERE classid=45 order by age;
#全走了索引。因为limit数据量小,优化器直接先用排序字段索引排序,然后再where筛选10个
EXPLAIN SELECT * FROM student WHERE classid=45 order by age limit 10;

image.gif

范围查找导致索引失效:下面有索引(userDbid,addressDbid,createTime),userDbid,addressDbid走了索引,因为addressDbid是范围查找,导致createTime索引失败。

image.gif

5.3.2 练习

INDEX a_b_c(a,b,c)
order by 能使用索引最左前缀
- ORDER BY a
- ORDER BY a,b
- ORDER BY a,b,c
- ORDER BY a DESC,b DESC,c DESC
如果WHERE使用索引的最左前缀定义为常量,则order by 能使用索引
- WHERE a = const ORDER BY b,c
- WHERE a = const AND b = const ORDER BY c
- WHERE a = const ORDER BY b,c
- WHERE a = const AND b > const ORDER BY b,c
不能使用索引进行排序
- ORDER BY a ASC,b DESC,c DESC /* 排序不一致 */
- WHERE g = const ORDER BY b,c /*丢失a索引*/
- WHERE a = const ORDER BY c /*丢失b索引*/
- WHERE a = const ORDER BY a,d /*d不是索引的一部分*/
- WHERE a in (...) ORDER BY b,c /*对于排序来说,多个相等条件也是范围查询*/

image.gif

 

5.3 范围查询时索引字段选择

  1. mysql自动选择最优的方案:两个索引同时存在,mysql自动选择最优的方案。(对于这个例子,mysql选择 idx_age_stuno_name)。但是, 随着数据量的变化,选择的索引也会随之变化的。
  2. 过滤比例高时优先过滤字段加索引:当【范围条件】和【group by 或者 order by】的字段出现二选一时,优先观察条件字段的过滤数量,如果过滤的数据足够多,而需要排序的数据并不多时,优先把索引放在范围字段上。反之,亦然。

ORDER BY子句,尽量使用Index方式排序,避免使用FileSort方式排序。

执行案例前先清除student上的索引,只留主键:

DROP INDEX idx_age ON student;
DROP INDEX idx_age_classid_stuno ON student;
DROP INDEX idx_age_classid_name ON student;
#或者
call proc_drop_index('atguigudb2','student');
image.gif

场景:查询年龄为30岁的,且学生编号小于101000的学生,按用户名称排序

EXPLAIN SELECT SQL_NO_CACHE * FROM student WHERE age = 30 AND stuno <101000 ORDER BY NAME ;

image.gif

image.gif

查询结果如下:

mysql> SELECT SQL_NO_CACHE * FROM student WHERE age = 30 AND stuno <101000 ORDER BY NAME;
+---------+--------+--------+------+---------+
| id      | stuno  |  name  | age  | classId |
+---------+--------+--------+------+---------+
| 922     | 100923 | elTLXD | 30   | 249     |
| 3723263 | 100412 | hKcjLb | 30   | 59      |
| 3724152 | 100827 | iHLJmh | 30   | 387     |
| 3724030 | 100776 | LgxWoD | 30   | 253     |
| 30      | 100031 | LZMOIa | 30   | 97      |
| 3722887 | 100237 | QzbJdx | 30   | 440     |
| 609     | 100610 | vbRimN | 30   | 481     |
| 139     | 100140 | ZqFbuR | 30   | 351     |
+---------+--------+--------+------+---------+
8 rows in set, 1 warning (3.16 sec)

image.gif

结论:type 是 ALL,即最坏的情况。Extra 里还出现了 Using filesort,也是最坏的情况。优化是必须的。

方案一: 为了去掉filesort我们创建索引,查询效率高了一点

#创建新索引
CREATE INDEX idx_age_name ON student(age,NAME);
EXPLAIN SELECT SQL_NO_CACHE * FROM student WHERE age = 30 AND stuno <101000 ORDER BY NAME;

image.gif

只有age走了索引:

image.gif

这样我们优化掉了 using filesort

查询结果如下:

image.gif

方案二:尽量让where的过滤条件和排序使用上索引,发现查询效率更高

建一个三个字段的组合索引,发现using filesort依然存在:

DROP INDEX idx_age_name ON student;
CREATE INDEX idx_age_stuno_name ON student (age,stuno,NAME);
EXPLAIN SELECT SQL_NO_CACHE * FROM student WHERE age = 30 AND stuno <101000 ORDER BY NAME;

image.gif

age和stuno走了索引:

image.gif

我们发现using filesort依然存在,所以name并没有用到索引,而且type还是range光看名字其实并不美好。原因是,因为stuno是一个范围过滤,所以索引后面的字段不会在使用索引了 。

结果如下:

mysql> SELECT SQL_NO_CACHE * FROM student
-> WHERE age = 30 AND stuno <101000 ORDER BY NAME ;
+-----+--------+--------+------+---------+
| id | stuno | name | age | classId |
+-----+--------+--------+------+---------+
| 167 | 100168 | AClxEF | 30 | 319 |
| 323 | 100324 | bwbTpQ | 30 | 654 |
| 651 | 100652 | DRwIac | 30 | 997 |
| 517 | 100518 | HNSYqJ | 30 | 256 |
| 344 | 100345 | JuepiX | 30 | 329 |
| 905 | 100906 | JuWALd | 30 | 892 |
| 574 | 100575 | kbyqjX | 30 | 260 |
| 703 | 100704 | KJbprS | 30 | 594 |
| 723 | 100724 | OTdJkY | 30 | 236 |
| 656 | 100657 | Pfgqmj | 30 | 600 |
| 982 | 100983 | qywLqw | 30 | 837 |
| 468 | 100469 | sLEKQW | 30 | 346 |
| 988 | 100989 | UBYqJl | 30 | 457 |
| 173 | 100174 | UltkTN | 30 | 830 |
| 332 | 100333 | YjWiZw | 30 | 824 |
+-----+--------+--------+------+---------+
15 rows in set, 1 warning (0.00 sec)

image.gif

结果竟然是,filesort运行速度比索引还快,而且快了很多,几乎一瞬间就出现了结果。

原因:

所有的排序都是在条件过滤之后才执行的。所以,如果条件过滤掉大部分数据的话,剩下几百几千条数据进行排序其实并不是很消耗性能,即使索引优化了排序,但实际提升性能很有限。相对的 stuno<101000 这个条件,如果没有用到索引的话,要对几万条的数据进行扫描,这是非常消耗性能的,所以索引放在这个字段上性价比最高,是最优选择

结论:

  1. 两个索引同时存在,mysql自动选择最优的方案。(对于这个例子,mysql选择 idx_age_stuno_name)。但是, 随着数据量的变化,选择的索引也会随之变化的
  2. 当【范围条件】和【group by 或者 order by】的字段出现二选一时,优先观察条件字段的过滤数量,如果过滤的数据足够多,而需要排序的数据并不多时,优先把索引放在范围字段上。反之,亦然。

思考:这里我们使用如下索引,是否可行?

DROP INDEX idx_age_stuno_name ON student;
CREATE INDEX idx_age_stuno ON student(age,stuno);
image.gif

当然可以。

5.4 filesort算法

5.4.1 双路排序和单路排序

排序的字段若不在索引列上,则filesort会有两种算法:双路排序和单路排序

双路排序 (慢)

  • MySQL 4.1之前是使用双路排序 ,字面意思就是两次扫描磁盘,最终得到数据, 读取行指针和 order by列 ,对他们进行排序,然后扫描已经排序好的列表,按照列表中的值重新从列表中读取对应的数据输出
  • 从磁盘取排序字段,在buffer进行排序,再从磁盘取其他字段 。

取一批数据,要对磁盘进行两次扫描,众所周知,IO是很耗时的,所以在mysql4.1之后,出现了第二种 改进的算法,就是单路排序。

单路排序 (快)

从磁盘读取查询需要的 所有列 ,按照order by列在buffer对它们进行排序,然后扫描排序后的列表进行输出, 它的效率更快一些,避免了第二次读取数据。并且把随机IO变成了顺序IO,但是它会使用更多的空间, 因为它把每一行都保存在内存中了。

结论及引申出的问题

  • 由于单路是后出的,总体而言好过双路
  • 但是用单路有问题
  • 在sort_buffer中,单路要比多路多占用很多空间,因为单路是把所有字段都取出,所以有可能取出的数据的总大小超出了sort_buffer的容量,导致每次只能取sort_buffer容量大小的数据,进行排序(创建tmp文件,多路合并),排完再取sort_buffer容量大小,再排...从而多次I/O。
  • 单路本来想省一次I/O操作,反而导致了大量的I/O操作,反而得不偿失。

5.4.2 调优filesort

1. 尝试提高 sort_buffer_size

image.gif

2. 尝试提高 max_length_for_sort_data

SHOW VARIABLES LIKE '%max_length_for_sort_data%';
#默认1924字节

image.gif

提高这个参数,会增加用改进算法的概率。但是如果设的太高,数据总容量超出sort buffer size的概率就增大,明显症状是高的磁盘IO活动和低的处理器使用率。如果需要返回的列的总长度大于max_length_for_sort data,使用双路算法,否则使用单路算法.1024-8192字节之间调整

3. Order by 时select * 是一个大忌。最好只Query需要的字段。

  • 当Query的字段大小总和小于max_ength_for_sort_data,而且排序字段不是TEXTBLOB 类型时,会用改进后的算法--单路排序,否则用老算法——多路排序。
  • 两种算法的数据都有可能超出sort_bufer_size的容量,超出之后,会创建tmp文件进行合并排序,导致多次I/0,但是用单路排序算法的风险会更大一些,所以要 提高sort_buffer_size。

6. 分组优化

  • 跟排序优化近似:group by 使用索引的原则几乎跟order by一致 ,group by 即使没有过滤条件用到索引,也可以直接使用索引。
  • 最左前缀:group by 先排序再分组,遵照索引建的最佳左前缀法则
  • 调优FileSort:当无法使用索引列,增大 max_length_for_sort_data 和 sort_buffer_size 参数的设置
  • where效率高于having,能写在where限定的条件就不要写在having中了。where是分组前过滤,having是分组后过滤。
  • 尽量不排序分组、节省cpu:减少使用order by,和业务沟通能不排序就不排序,或将排序放到程序端去做。Order by、group by、distinct这些语句较为耗费CPU,数据库的CPU资源是极其宝贵的。
  • 使用limit:包含了order by、group by、distinct这些查询的语句,where条件过滤出来的结果集请保持在1000行 以内,否则SQL会很慢。

7. 分页查询优化

7.1 深分页查询优化

一般分页查询时,通过创建覆盖索引能够比较好地提高性能。

目前问题: offset非常大时,需要查询大量无用的数据量再分页,性能差。

一个常见又非常头疼的问题就是limit 2000000,10此时需要MySQL排序前200000010 记录,仅仅返回2000000~2000010 的记录,其他记录丢弃,查询排序的代价常大。并且select *需要回表,更耗费时间。

EXPLAIN SELECT * FROM student LIMIT 2000000,10;

image.gif

image.gif

主键自增的表:直接查范围之后的10个数据。可以把Limit 查询转换成某个位置的查询 。

EXPLAIN SELECT * FROM student WHERE id > 2000000 LIMIT 10;

image.gif

image.gif

主键不自增的表:当前表内连接排序截取后的主键表,连接字段是主键。

EXPLAIN SELECT * FROM student t,(SELECT id FROM student ORDER BY id LIMIT 2000000,10) a WHERE t.id = a.id;

image.gif

也可以用子查询,子查询优化成关联查询。

image.gif

7.2 带排序的深分页优化

优化前: 查询根据age逆序排列的深分页

EXPLAIN SELECT * FROM student order by age desc LIMIT 2000000,10;

image.gif

优化方案一: 优化思路跟之前一样,内连接字段是id

EXPLAIN SELECT * FROM student t1,(SELECT id FROM student ORDER BY age desc LIMIT 2000000,10) t2 WHERE t1.id=t2.id

image.gif

优化方案二:如果是顺序翻页,可以得到上一页最后一条记录x,那么目标页码的所有记录id都比x.id小(因为逆序,且排序依据其实是age,id),目标页码的所有记录age都比x.age小或等于。

EXPLAIN SELECT * FROM student WHERE id<#{x.id} AND age<=#{x.age} ORDER BY age DESC LIMIT 10;

image.gif


相关实践学习
如何在云端创建MySQL数据库
开始实验后,系统会自动创建一台自建MySQL的 源数据库 ECS 实例和一台 目标数据库 RDS。
全面了解阿里云能为你做什么
阿里云在全球各地部署高效节能的绿色数据中心,利用清洁计算为万物互联的新世界提供源源不断的能源动力,目前开服的区域包括中国(华北、华东、华南、香港)、新加坡、美国(美东、美西)、欧洲、中东、澳大利亚、日本。目前阿里云的产品涵盖弹性计算、数据库、存储与CDN、分析与搜索、云通信、网络、管理与监控、应用服务、互联网中间件、移动服务、视频服务等。通过本课程,来了解阿里云能够为你的业务带来哪些帮助 &nbsp; &nbsp; 相关的阿里云产品:云服务器ECS 云服务器 ECS(Elastic Compute Service)是一种弹性可伸缩的计算服务,助您降低 IT 成本,提升运维效率,使您更专注于核心业务创新。产品详情: https://www.aliyun.com/product/ecs
相关文章
|
5天前
|
SQL 关系型数据库 MySQL
MySQL慢查询优化、索引优化、以及表等优化详解
本文详细介绍了MySQL优化方案,包括索引优化、SQL慢查询优化和数据库表优化,帮助提升数据库性能。关注【mikechen的互联网架构】,10年+BAT架构经验倾囊相授。
MySQL慢查询优化、索引优化、以及表等优化详解
|
10天前
|
缓存 监控 关系型数据库
如何优化MySQL查询速度?
如何优化MySQL查询速度?【10月更文挑战第31天】
32 3
|
15天前
|
SQL NoSQL 关系型数据库
2024Mysql And Redis基础与进阶操作系列(5)作者——LJS[含MySQL DQL基本查询:select;简单、排序、分组、聚合、分组、分页等详解步骤及常见报错问题所对应的解决方法]
MySQL DQL基本查询:select;简单、排序、分组、聚合、分组、分页、INSERT INTO SELECT / FROM查询结合精例等详解步骤及常见报错问题所对应的解决方法
|
12天前
|
缓存 关系型数据库 MySQL
如何优化 MySQL 数据库的性能?
【10月更文挑战第28天】
34 1
|
13天前
|
监控 关系型数据库 MySQL
数据库优化:MySQL索引策略与查询性能调优实战
【10月更文挑战第27天】本文深入探讨了MySQL的索引策略和查询性能调优技巧。通过介绍B-Tree索引、哈希索引和全文索引等不同类型,以及如何创建和维护索引,结合实战案例分析查询执行计划,帮助读者掌握提升查询性能的方法。定期优化索引和调整查询语句是提高数据库性能的关键。
70 1
|
14天前
|
监控 关系型数据库 MySQL
数据库优化:MySQL索引策略与查询性能调优实战
【10月更文挑战第26天】数据库作为现代应用系统的核心组件,其性能优化至关重要。本文主要探讨MySQL的索引策略与查询性能调优。通过合理创建索引(如B-Tree、复合索引)和优化查询语句(如使用EXPLAIN、优化分页查询),可以显著提升数据库的响应速度和稳定性。实践中还需定期审查慢查询日志,持续优化性能。
45 0
|
6天前
|
SQL 关系型数据库 MySQL
go语言数据库中mysql驱动安装
【11月更文挑战第2天】
20 4
|
4天前
|
SQL 关系型数据库 MySQL
12 PHP配置数据库MySQL
路老师分享了PHP操作MySQL数据库的方法,包括安装并连接MySQL服务器、选择数据库、执行SQL语句(如插入、更新、删除和查询),以及将结果集返回到数组。通过具体示例代码,详细介绍了每一步的操作流程,帮助读者快速入门PHP与MySQL的交互。
13 1
|
30天前
|
存储 关系型数据库 MySQL
Mysql(4)—数据库索引
数据库索引是用于提高数据检索效率的数据结构,类似于书籍中的索引。它允许用户快速找到数据,而无需扫描整个表。MySQL中的索引可以显著提升查询速度,使数据库操作更加高效。索引的发展经历了从无索引、简单索引到B-树、哈希索引、位图索引、全文索引等多个阶段。
60 3
Mysql(4)—数据库索引
|
15天前
|
关系型数据库 MySQL Linux
在 CentOS 7 中通过编译源码方式安装 MySQL 数据库的详细步骤,包括准备工作、下载源码、编译安装、配置 MySQL 服务、登录设置等。
本文介绍了在 CentOS 7 中通过编译源码方式安装 MySQL 数据库的详细步骤,包括准备工作、下载源码、编译安装、配置 MySQL 服务、登录设置等。同时,文章还对比了编译源码安装与使用 RPM 包安装的优缺点,帮助读者根据需求选择最合适的方法。通过具体案例,展示了编译源码安装的灵活性和定制性。
59 2