SQL 优化

345 阅读11分钟

系统的访问速度瓶颈一般都是数据库,比如我们引入 Redis 作为分布式缓存或者本地缓存的目的都是为了提高系统的访问速度。Redis 是一个基于内存的 NoSQL 数据库,而本地缓存则是基于 JVM 的内存。

之所以比 MySQL 快,是因为 MySQL 的数据都是存放在硬盘中的,众所周知,硬盘的速度和内存的速度相差 10 万倍,这也就是为什么要引入缓存来提升系统的性能的原因所在。

但是我们的数据是持久化在 MySQL 中的,也就是数据的一致性是以 MySQL 为核心的,所以 SQL 的优化对于我们来说是非常有必要的,MySQL 架构中 Server 层的优化器就是来搞 MySQL 优化的。

整体大纲

image.png

1、索引

如果没有索引,这就意味着我们查询的时间复杂度为:O(N) ,如果在数据量大的时候这种级别的时间复杂度对于我们来说是无法接受的,所以我们很容易想到建立索引来减少时间复杂度。

MySQL 的索引数据结构是基于 B+ 树的,查询的时间复杂度为:O(logN) 级别,建立索引后查询的时间复杂度大大降低,所以我们可以针对我们的 SQL 语句来建立合适的索引。但是,如何建立索引是我们必须要考虑的问题。

因为索引的维护是需要很大代价的,并且索引也是占空间的,所以索引如何建立也是需要我们迎合 MySQL 的特性来慎重考虑。

1.1 建立索引

1.1.1 索引不要包含选择性过低字段

选择性过低,这就意味着只能建立过滤少部分数据,这种情况是完全没必要建立索引的。

什么叫选择性过低呢?比如说某个字段是性别,这就意味着它的值只有男、女、未知;再比如说某个字段是逻辑删除,这就意味着它的值要么为 0,要么为 1。

select * from `my_table` where a = "xx" and b = 1

组合索引:(a, b)。

b 为逻辑删除字段,值为 0 或 1。这种情况下,只需建立 a 索引即可

1.1.2 选择性高的字段前置或者单独建立索引

select * from `my_table` where a = "xx" and b = "xx" and c = "xx"

组合索引:(a, b, c)。

假如选择性 c > b > a,在保证业务的基础上,组合索引建立的顺序可以改为:(c, b,a)。

因为如果排在前面的索引选择性高的话,意味着能过滤的数据就更多,这样的话访问磁盘 IO 次数就能大大减少。

还有一种情况,假如 c 字段的索引选择性特别高,比如唯一索引啊,这样的话就可以为 c 单独建立索引。

以上必须要在保证业务的基础上!

1.1.3 尽量使用覆盖索引

select `c` from `my_table` where a = "xx" and b = "xx"

组合索引:(a, b)。

如果 a 和 b 过滤完还有大量数据,这就意味着得回表,时间消耗就上来了。

像这种情况我们可以建立覆盖索引:(a, b, c)。这样的话就可以避免回表了。

1.2 使用索引

索引是建好了,但是这并不意味着你真的能用上,有些时候因为你的 SQL 语句不符合 MySQL 的规则,会导致索引失效;还有一种情况就是因为 MySQL 会对你的 SQL 语句进行优化,然后生成一个执行计划,因为它内部的某些规则分析,可能会导致 MySQL 不选择你建立的索引,也就是选错索引。

1.2.1 最左匹配截断

select * from `my_table` where b = "xx"
select * from `my_table` order by b

组合索引:(a, b)。

上面这两条 SQL 语句都用不上组合索引,原因就是因为 MySQL 索引的数据结构是采用 B+ 树,根据 B+ 树的特性,只有组合索引的前 N 个索引或者字符串索引的前 N 个值才能用上索引,这也就是最左前缀原则

如果要想用上组合索引(a, b),必须要带上 a,b 可以不带。

组合索引的匹配规则是从左往右匹配,无论是作为过滤条件还是排序条件都要遵循这个原则。

还有一种情况:

select * from `my_talbe` where b = xx order by a

这种情况下也是走不了组合索引(a,b),因为是先筛选,where 这里索引就已经失效了。

1.2.2 隐式转换

字段:a,varchar。

select * from `my_table` where a = 11232

注意:11232 是个整型。

索引:(a)。

这种情况下索引会失效,因为字段类型不匹配。

注意:在 MySQL 中,字符串和数字做比较的话,是将字符串转换成数字。

再来看另一条 SQL 语句:

select * from `my_table` where a = "123"

注意:这里的 a 字段是 int,"123" 是字符串。

假设建立了索引(a),思考是否会走索引?

答案是会。

1.2.3 in + order by 导致排序失效

select * from `my_table` where a in (1, 2) order by b

组合索引:(a, b)。

解决方式:

  • 如果 a 的过滤性不高,可以将 a 和 b 在组合索引中的位置对换,也就是 (b, a)。注意,虽然这里不满足最左前缀原则,但是还是可以用上这个索引的,原因就是 MySQL 的优化器。这样就可以减少回表。
  • 如果 a 的过滤性高,那么维护当前的索引即可,剩余不多的数据通过 filesort 进行排序。
  • 如果存在大量数据,并且经过 b 过滤后还是存在大量数据,建立通过别的数据存储实现,比如 ElasticSearch。

另外 SQL 建议调整为只查询 id(或者其他已经在索引中的字段),再根据 id 去查对应的数据。可以促使 SQL 走覆盖索引进一步优化、也可以促使 MySQL 底层在进行 filesort 使用更优的排序算法。

1.2.4 范围查询阻断组合索引

select * from `my_table` where a > 10 and b = 1;

组合索引:(a, b)。

这种情况下可以调整索引顺序,也就是(b, a)。

1.2.5 后缀匹配不能走索引

select * from where a = "%name";

索引:(a)。

这种情况索引会失效,还是最左前缀原则。前缀匹配 name% 可以走索引,但是后缀匹配 %name 走不了索引。因为没有办法根据 B+ 树的特性进行查找,只能走全表扫描。

《阿里巴巴 Java 开发手册》有一句话:后缀匹配从浏览器层面解决。(有点忘记那句话了)

1.2.6 or 查询导致失效

select * from `my_table` where a = "xx" or b = "xx";

组合索引:(a, b)。

这种情况是走不了索引的。可以将 a 和 b 字段分别建立索引,利用 MySQL 的 index merge(索引合并)进行优化。本质就是两个字段分别走自己的索引查出对应的数据,再将数据进行合并。

1.2.7 使用函数查询或者运算

select * from `my_table` where month(a) = 7;
select * from `my_table` where a + 1 = 7; 

索引:(a)。

这种情况不会走索引。需要手动改写 SQL。

第一条 SQL 的改写就复杂一点,可以通过范围查询。

第二条 SQL 可以改写为 select * from my_table where a = 7 - 1;

1.2.8 不等于、不包含

select * from `my_table` where a = 1 and b not in (1, 2);
select * from `my_table` where a = 1 and b != 1;

组合索引:(a, b)。

这种情况索引会失效,但是会用到 ICP,用 explain 查询执行计划会显示用到了索引。

ICP:索引下推。

1.2.9 选择性过低,直接走全表

选择性过低会导致索引失效。

由于通过二级索引查询后还有回表查询的开销,如果通过该字段只能过滤少量的数据,整体上还不如直接查询数据表的性能,则MySQL会放弃这个索引,直接使用全表扫描。底层会根据表大小、IO块大小、行数等信息进行评估决定。

1.2.10 asc 和 desc 混用

select * from `my_table` where a = "xx" order by b asc c desc;

组合索引:(a, b,c)。

desc 和 asc 混用时会导致索引失效,不建议混用。

2、编写 SQL

2.1 深分页

select * from `my_table` where a = "xx" limit 1000, 10;

索引:(a)。

MySQL limit 不会传递到引擎层,只是在服务层进行数据过滤。查询数据时,先由引擎层通过索引过滤出一批数据(索引过滤),然后服务层进行二次过滤(非索引过滤)。

所以如果深分页,会导致大量的无效回表(前1000条进行了回表,实际上只需要1000-1010的数据),因此优化的方式就是避免深分页带来的额外回表。

解决方式:

(1)避免深分页

很简单,就是使用条件。例如 select * from my_table where id > 上次的 id 值 and a = "xx" limit 100。这样的话,即使回表也只是回 100 条,可以接受。

(2)覆盖索引

如果业务需要的字段比较少,可以通过保证 SQL 查询的字段和查询条件都在索引上,避免回表。

(3)延迟关联

select * from `my_table` as t1, (select id from `my_table` where a = "xx" limit 1000, 100) as t2 where t1.id = t2.id;

通过延迟关联,通过编写完全基于索引查询数据的 SQL,再根据 id 查询详细的字段数据。

2.2 order by id

select * from `my_table` where a = "xx" order by id;

MySQL INNODB 二级索引最终叶子结点引用的都是主键 id,因此我们可以利用这个点去使用 id 排序。

3、慢 SQL 分析

可以通过 explain 关键字来查询 SQL 语句的执行计划,判断是否走了我们想要的流程,是否用上了我们预想的索引。

注:在 MySQL 架构中,优化器就是用来生成执行计划的,然后交给执行器执行。

执行字段描述:

image.png

extra 字段常见值:

image.png

4、常用 SQL 优化技巧

前提说明:一切都要在满足业务需求的前提下进行 SQL 优化。

4.1 避免使用 select *

如果业务并没有用到所有字段,冗余的字段传输也是一个问题,同样可以联想到 DO、DTO、VO,这种都是一样的思想。

同时如果使用了 select *,即使 SQL 走了索引,但是还会有回表的开销。

4.2 小表驱动大表

也就是说用小表的数据集驱动大表的数据集。

可以用 in 关键字。

(1)如果 sql 语句中包含了 in 关键字,则它会优先执行 in 里面的子查询语句,然后再执行 in 外面的语句。如果 in 里面的数据量很少,作为条件查询速度更快。

(2)如果 sql 语句中包含了 exists 关键字,它优先执行 exists 左边的语句(即主查询语句)。然后把它作为条件,去跟右边的语句匹配。如果匹配上,则可以查询出数据。如果匹配不上,数据就被过滤掉了。

总结:

  • in 适用于左边大表,右边小表。
  • exists 适用于左边小表,右边大表。

4.3 批量操作

不要一条一条的插入数据,要批量插入,但批量插入的数据每次不要超过 500 条。

4.4 用连接查询代替子查询

MySQL 中如果需要从两张以上的表中查询出数据的话,一般有两种实现方式:子查询连接查询

子查询语句的优点是简单,结构化,如果涉及的表数量不多的话。

但缺点是mysql执行子查询时,需要创建临时表,查询完毕后,需要再删除这些临时表,有一些额外的性能消耗。

4.5 join 的表不宜过多

根据《阿里巴巴 Java 开发手册》的规定,join 表的数量不应该超过 3 个。

如果 join 太多,MySQL 在选择索引的时候会非常复杂,很容易选错索引。

4.6 控制索引的数量

索引能够显著的提升查询 SQL 的性能,但索引数量并非越多越好。

索引的维护是需要很大的代价的,索引的空间开销也不低。

《阿里巴巴 Java 开发手册》规定:单表的索引数量应该尽量控制在5个以内,并且单个索引中的字段数不超过5个。

参考链接

没内鬼,来点干货!SQL优化和诊断

那些年我们一起优化的SQL

聊聊sql优化的15个小技巧

后端程序员必备:SQL高性能优化指南!35+条优化建议立马GET!

面试题:在日常工作中怎么做MySQL优化的?

为什么大家都说SELECT * 效率低

MySQL 性能优化神器 Explain 使用分析