gitbook/后端存储实战课/docs/211555.md
2022-09-03 22:05:03 +08:00

93 lines
9.7 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 09 | 怎么能避免写出慢SQL
你好,我是李玥。
通过上节课的案例我们知道一个慢SQL就可以直接让MySQL瘫痪。今天这节课我们一起看一下怎么才能避免写出危害数据库的慢SQL。
所谓慢SQL就是执行特别慢的SQL语句。什么样的SQL语句是慢SQL多慢才算是慢SQL并没有一个非常明确的标准或者说是界限。但并不是说我们就很难区分正常的SQL和慢SQL在大多数实际的系统中慢SQL消耗掉的数据库资源往往是正常SQL的几倍、几十倍甚至几百倍所以还是非常容易区分的。
但问题是我们不能等着系统上线慢SQL吃光数据库资源之后再找出慢SQL来改进那样就晚了。那么怎样才能在开发阶段尽量避免写出慢SQL呢
## 定量认识MySQL
我们回顾一下上节课的案例那个系统第一次全站宕机发生在圣诞节平安夜故障之前的一段时间系统并没有更新过版本这个时候其实慢SQL已经存在了直到平安夜那天访问量的峰值比平时增加一些正是增加的这部分访问量引发了数据库的雪崩。
这说明,**慢SQL对数据库的影响是一个量变到质变的过程对“量”的把握就很重要**。作为一个合格的程序员,你需要对数据库的能力,有一个定量的认识。
影响MySQL处理能力的因素很多比如服务器的配置、数据库中的数据量大小、MySQL的一些参数配置、数据库的繁忙程度等等。但是通常情况下这些因素对于MySQL性能和处理能力影响范围大概在几倍的性能差距。所以我们不需要精确的性能数据只要掌握一个大致的量级就足够指导我们的开发工作了。
一台MySQL数据库大致处理能力的极限是每秒一万条左右的简单SQL这里的“简单SQL”指的是类似于主键查询这种不需要遍历很多条记录的SQL。根据服务器的配置高低可能低端的服务器只能达到每秒几千条高端的服务器可以达到每秒钟几万条所以这里给出的一万TPS是中位数的经验值。考虑到正常的系统不可能只有简单SQL所以实际的TPS还要打很多折扣。
我的经验数据一般一台MySQL服务器平均每秒钟执行的SQL数量在几百左右就已经是非常繁忙了即使看起来CPU利用率和磁盘繁忙程度没那么高你也需要考虑给数据库“减负”了。
另外一个重要的定量指标是到底多慢的SQL才算慢SQL。这里面这个“慢”衡量的单位本来是执行时长但是时长这个东西我们在编写SQL的时候并不好去衡量。那我们可以用执行SQL查询时需要遍历的数据行数替代时间作为衡量标准因为查询的执行时长基本上是和遍历的数据行数正相关的。
你在编写一条查询语句的时候,可以依据你要查询数据表的数据总量,估算一下这条查询大致需要遍历多少行数据。如果遍历行数在百万以内的,只要不是每秒钟都要执行几十上百次的频繁查询,可以认为是安全的。遍历数据行数在几百万的,查询时间最少也要几秒钟,你就要仔细考虑有没有优化的办法。遍历行数达到千万量级和以上的,我只能告诉你,这种查询就不应该出现在你的系统中。当然我们这里说的都是在线交易系统,离线分析类系统另说。
遍历行数在千万左右是MySQL查询的一个坎儿。MySQL中单个表数据量也要尽量控制在一千万条以下最多不要超过二三千万这个量级。原因也很好理解对一个千万级别的表执行查询加上几个WHERE条件过滤一下符合条件的数据最多可能在几十万或者百万量级这还可以接受。但如果再和其他的表做一个联合查询遍历的数据量很可能就超过千万级别了。所以每个表的数据量最好小于千万级别。
如果数据库中的数据量就是很多,而且查询业务逻辑就需要遍历大量数据怎么办?
## 使用索引避免全表扫描
使用索引可以有效地减少执行查询时遍历数据的行数,提高查询性能。
数据库索引的原理也很简单,我举个例子你就明白了。比如说,有一个无序的数组,数组的每个元素都是一个用户对象。如果说我们要把所有姓李的用户找出来。比较笨的办法是,用一个循环把数组遍历一遍。
有没有更好的办法很多办法是吧比如说我们用一个Map(在有些编程语言中是Dictionary)来给数组做一个索引Key保存姓氏值是所有这个姓氏的用户对象在数组中序号的集合。这样再查找的时候就不用去遍历数组先在Map中查找然后再直接用序号去数组中拿用户数据这样查找速度就快多了。
这个例子对应到数据库中存放用户数据的数组就是表我们构建的Map就是索引。实际上数据库的索引和编程语言中的Map或者Dictionary它们的数据结构都是差不多的基本上就是各种B树和HASH表。
绝大多数情况下,我们编写的查询语句,都应该使用索引,避免去遍历整张表,也就是通常说的,避免全表扫描。你在每次开发新功能,需要给数据库增加一个新的查询时,都要评估一下,是不是有索引可以支撑新的查询语句,如果有必要的话,需要新建索引来支持新增的查询。
但是,增加索引付出的代价是,会降低数据插入、删除和更新的性能。这个也很好理解,增加了索引,在数据变化的时候,不仅要变更数据表里的数据,还要去变更每个索引。所以,对于更新频繁并且对更新性能要求较高的表,可以尽量少建索引。而对于查询较多更新较少的表,可以根据查询的业务逻辑,适当多建一些索引。
怎么写SQL能更好地使用索引查询效率更高这是一门手艺需要丰富的经验不是通过一节课的学习能练成的。但是我们是有方法可以评估写出来的SQL的查询性能怎么样是不是一个潜在的“慢SQL”。
逻辑不是很复杂的单表查询我们可能还可以分析出来查询会使用哪个索引。但如果是比较复杂的多表联合查询我们单看SQL语句本身就很难分析出查询到底会命中哪些索引会遍历多少行数据。MySQL和大部分数据库都提供一个帮助我们分析查询功能执行计划。
## 分析SQL执行计划
在MySQL中使用执行计划也非常简单只要在你的SQL语句前面加上**EXPLAIN**关键字,然后执行这个查询语句就可以了。
举个例子说明比如有一个用户表包含用户ID、姓名、部门编号和状态这几个字段
![](https://static001.geekbang.org/resource/image/43/48/437d6d3fb610431cfb9044781a8faa48.png)
我们希望查询某个二级部门下的所有人查询条件就是部门代号以00028开头的所有人。下面这两个SQL他们的查询结果是一样的都满足要求但是哪个查询性能更好呢
```
SELECT * FROM user WHERE left(department_code, 5) = '00028';
SELECT * FROM user WHERE department_code LIKE '00028%';
```
我们分别查看一下这两个SQL的执行计划
![](https://static001.geekbang.org/resource/image/4b/74/4b50e4e1192714379ff3a4697d02a774.png)
我带你一起来分析一下这两个SQL的执行计划。首先来看rows这一列rows的含义就是MySQL预估执行这个SQL可能会遍历的数据行数。第一个SQL遍历了四千多行这就是整个User表的数据条数第二个SQL只有8行这8行其实就是符合条件的8条记录。显然第二个SQL查询性能要远远好于第一个SQL。
为什么第一个SQL需要全表扫描第二个SQL只遍历了很少的行数呢注意看type这一列这一列表示这个查询的访问类型。ALL代表全表扫描这是最差的情况。range代表使用了索引在索引中进行范围查找因为第二个SQL语句的WHERE中有一个LIKE的查询条件。如果直接命中索引type这一列显示的是index。如果使用了索引可以在key这一列中看到实际上使用了哪个索引。
通过对比这两个SQL的执行计划就可以看出来第二个SQL虽然使用了普遍认为低效的LIKE查询条件但是仍然可以用到索引的范围查找遍历数据的行数远远少于第一个SQL查询性能更好。
## 小结
在开发阶段衡量一个SQL查询语句查询性能的手段是估计执行SQL时需要遍历的数据行数。遍历行数在百万以内可以认为是安全的SQL百万到千万这个量级则需要仔细评估和优化千万级别以上则是非常危险的。为了减少慢SQL的可能性每个数据表的行数最好控制在千万以内。
索引可以显著减少查询遍历数据的数量所以提升SQL查询性能最有效的方式就是让查询尽可能多的命中索引但索引也是一把双刃剑它在提升查询性能的同时也会降低数据更新的性能。
对于复杂的查询最好使用SQL执行计划事先对查询做一个分析。在SQL执行计划的结果中可以看到查询预估的遍历行数命中了哪些索引。执行计划也可以很好地帮助你优化你的查询语句。
## 思考题
课后请你想一下在讲解SQL执行计划那个例子中的第一个SQL为什么没有使用索引呢
```
SELECT * FROM user WHERE left(department_code, 5) = '00028';
```
欢迎你在留言区与我讨论,如果你觉得今天学到的知识对你有帮助,也欢迎把它分享给你的朋友。