第五节 排序分组优化

1、排序

①结论

  • order by 子句需要配合 limit 子句才能让索引生效。
  • 如果涉及多个字段的排序,那么这些字段排序的方向要一致(要升序都升序,要降序都降序),否则索引失效。

②测试基本情况

索引情况:

img019.png

[1]只有 order by 的情况

分析语句:

  1. EXPLAIN SELECT SQL_NO_CACHE * FROM emp ORDER BY age;

分析结果:

img020.png

[2]配合 limit 子句的情况

分析语句:

EXPLAIN  SELECT SQL_NO_CACHE * FROM emp ORDER BY age limit 10;

img021.png

③测试排序方向的影响

删除上例索引:

drop index idx_age on emp;

创建新索引:

create index idx_age_deptId on emp(age, deptId);

分析语句:排序方向一致的情况

EXPLAIN  SELECT SQL_NO_CACHE * FROM emp ORDER BY age desc,deptId desc limit 10;

分析结果:

img022.png

分析语句:排序方向不一致的情况

EXPLAIN  SELECT SQL_NO_CACHE * FROM emp ORDER BY age desc,deptId asc limit 10;

分析结果:

img023.png

2、双路排序和单路排序

①filesort

如果 order by 指定的字段没有建立索引,此时 MySQL 就无法在内存完成排序了,而是执行 filesort——也就是操作硬盘完成排序。

执行 filesort 又分两种不同情况:

  • 双路排序:
    • 特征:两批 I/O
    • 对应的 MySQL 版本:4.1 之前
    • 大致工作机制:
      • 第一次读取硬盘:读取『行指针』以及『order by 子句指定的字段』
      • 排序:对已读取的『order by 子句指定的字段』进行排序
      • 第二次读取硬盘:根据『行指针』读取 select 子句中指定的其他字段
    • I/O方式:随机 I/O
  • 单路排序:
    • 特征:一批 I/O
    • 对应的 MySQL 版本:4.1 之后
    • 大致工作机制:
      • 读取硬盘:读取 select 子句指定的所有列
      • 排序:按照 order by 列在 buffer 中对它们进行排序
    • I/O方式:顺序I/O

②单路排序需要注意的问题

单路排序在内存的缓冲区中执行排序,所以需要更大的内存空间。我们管这个缓冲区叫:sort_buffer。此时需要注意:如果为了排序而取出的数据体积大于 sort_buffer,这就会导致每次只能取 sort_buffer 容量大小的数据。所以这种情况下,数据的加载和排序是分段完成的。在这个过程中,MySQL 会创建临时文件,最后再把每段数据合并到一起。

所以 sort_buffer 容量太小时会导致排序操作产生多次 I/O。单路本来想省一次 I/O 操作,反而导致了大量的 I/O 操作,反而得不偿失。

③单路排序的优化

调整下面的三个参数:

参数名称 参数含义 调整建议
sort_buffer_size 单路排序缓冲区的大小 适当增大
max_length_for_sort_data select 子句要查询的所有字段的总宽度和该参数比较:
大于该参数:使用双路排序
小于等于该参数且排序字段不是TEXT、BLOB类型:使用单路排序
适当增大
select 子句中查询的字段 尽量减少

::: tip

对 sort_buffer_size 的补充说明:

不管用哪种算法,提高这个参数都会提高效率,要根据系统的能力去提高,因为这个参数是针对每个进程(connection)的 1M-8M之间调整。 MySQL5.7,InnoDB 存储引擎默认值是 1048576 字节,1MB。

对 max_length_for_sort_data 的补充说明:

max_length_for_sort_data 不能设的太高,否则数据总容量超出 sort_buffer_size 的概率就增大。明显症状是高的磁盘 I/O 活动和低的处理器使用率。建议设置在 1024-8192 字节之间。

:::

最终目标:

  • 尽量能够基于索引执行排序
  • 如果确实要执行 filesort,那么尽量使用单路排序,而且尽量让单路排序只做一次 I/O。

④select * 会影响排序

在实际业务功能开发过程中,禁止在 select 子句中使用 * 号代表全部字段。如果确实需要查询全部字段,那就把全部字段都写明。其实这个时候更要注意的是:是不是真的要查全部字段。

具体从 SQL 优化的角度来说,select * 会导致我们加载很多没有创建索引的字段到内存中,增加了数据体积超过 sort_buffer_size 的风险。有可能会导致单路排序变成双路排序,性能下降。

3、GROUP BY优化

Group by 分组优化原则如下:

  • group by 先排序再分组,同样遵照最左原则
  • 当无法使用索引列,增大 max_length_for_sort_data 和 sort_buffer_size 参数的设置
  • where 高于 having,能写在 where 限定的条件就不要写在 having 中了

举个例子帮助大家理解:

  • 假设有 100W 条记录待筛选
  • 有一个 X 条件能够过滤掉 90W 条
    • X 条件用在 where 子句:后续 GROUP BY 操作针对 10W 条数据操作
    • X 条件用在 having子句:后续 GROUP BY 操作还是针对原来的 100W 条数据操作

所以在整个 SQL 查询语句中,能够将数据过滤掉的条件在不影响查询结果的前提下都要尽早使用,尽早过滤数据,缩小要操作的数据量,让后续操作减轻负担。

::: tip

关闭 ONLY_FULL_GROUP_BY 模式

  • 查看当前 SQL 模式

mysql> select @@GLOBAL.sql_mode;
+—————————————————————————————————————————————————————————————————————-+
| @@GLOBAL.sql_mode |
+—————————————————————————————————————————————————————————————————————-+
| ONLY_FULL_GROUP_BY,STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION |
+—————————————————————————————————————————————————————————————————————-+
1 row in set (0.00 sec)

  • 关闭 ONLY_FULL_GROUP_BY 模式

修改 /etc/my.cnf 配置文件,在配置文件末尾增加一行:

sql_mode=STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION
  • 重启 MySQL 服务
  • 查看修改完成后的效果

mysql> select @@GLOBAL.sql_mode;
+————————————————————————————————————————————————————————————+
| @@GLOBAL.sql_mode |
+————————————————————————————————————————————————————————————+
| STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION |
+————————————————————————————————————————————————————————————+
1 row in set (0.00 sec)

mysql> use db_hr_sys;
Reading table information for completion of table and column names
You can turn off this feature to get a quicker startup with -A

Database changed
mysql>
mysql> select * from emp group by deptid limit 0,10;
+———-+————+————+———+————+
| id | empno | name | age | deptId |
+———-+————+————+———+————+
| 10893 | 110893 | MJTDjl | 41 | 1 |
| 523 | 100523 | VOizXo | 41 | 2 |
| 7118 | 107118 | VthHLL | 39 | 3 |
| 14788 | 114788 | PTasQX | 37 | 4 |
| 16297 | 116297 | gLpozF | 41 | 5 |
| 7597 | 107597 | ZItShh | 35 | 6 |
| 817 | 100817 | mxzhmN | 31 | 7 |
| 1673 | 101673 | demYgL | 34 | 8 |
| 30032 | 130032 | fYDUPn | 50 | 9 |
| 1169 | 101169 | wjFANm | 49 | 10 |
+———-+————+————+———+————+
10 rows in set (0.53 sec)

:::

上一节 回目录