注册/登录

如何避免写出“慢SQL”

数据库 其他数据库
所谓慢SQL,就是执行特别慢的SQL语句。什么样的SQL语句才是慢SQL?多慢才算是慢SQL?对于这类问题,并没有一个非常明确的标准,或者说是界限。但这并不代表区分正常的SQL和慢SQL很困难,在实际的大多数系统中,慢SQL消耗掉的数据库资源,往往是正常SQL的几倍、几十倍,甚至几百倍,所以慢SQL还是非常容易区分的。

所谓慢SQL,就是执行特别慢的SQL语句。什么样的SQL语句才是慢SQL?多慢才算是慢SQL?对于这类问题,并没有一个非常明确的标准,或者说是界限。但这并不代表区分正常的SQL和慢SQL很困难,在实际的大多数系统中,慢SQL消耗掉的数据库资源,往往是正常SQL的几倍、几十倍,甚至几百倍,所以慢SQL还是非常容易区分的。

问题是,我们不能等到系统上线,慢SQL消耗完数据库的资源之后,再找出慢SQL来改进,那样将会造成很多不良影响。那么,怎样才能在开发阶段就尽量避免写出慢SQL呢?

01定量认识MySQL

这说明,慢SQL对数据库的影响,是一个从量变到质变的过程,对“量”的把握,对于开发人员来说是很重要的。一个合格的程序员,需要对数据库的能力有一个定量的认识。

影响MySQL处理能力的因素有很多,比如,服务器的配置、数据库中数据量的大小、MySQL的一些参数配置、数据库的繁忙程度,等等。但是,通常情况下,这些因素对于MySQL的性能和处理能力的影响,大概在一个数量级的范围内,也就是上下几倍的性能差距。所以,我们不需要知道精确的性能数据,只要掌握一个大致的量级,就足够应对实际的开发工作了。

目前,一台普通的MySQL数据库服务器,处理能力的极限大致是,每秒一万条左右的简单SQL。这里的“简单SQL”,指的是类似于主键查询这种不需要遍历很多条记录的SQL语句。根据配置的高低,服务器的处理能力也会有所不同,可能低配的服务器只能达到每秒几千条,高配的服务器则可以达到每秒几万条,所以这里给出的每秒一万条是中位数的经验值。考虑到正常的系统不可能只有简单SQL,所以实际的处理能力还要打很大折扣。

我个人的经验是,一台MySQL数据库服务器,平均每秒执行的SQL数量在几百左右,一般就已经是非常繁忙了。即使看起来CPU利用率和磁盘繁忙程度并不高,我们也需要考虑为数据库“减负”了。

另外一个重要的定量指标是,多慢的SQL才算是慢SQL?这里的“慢”,衡量的单位本来是执行时长,但是对于时长这个指标,我们在编写SQL的时候并不好衡量。因此可以用执行SQL查询时,需要遍历的数据行数来替代时间作为衡量标准,因为查询的执行时长与遍历的数据行数基本上是正相关的。

我们在编写一条查询语句的时候,可以依据所要查询数据表的数据总量估算一下这条查询大致需要遍历多少行数据。如果遍历的行数在百万以内,只要不是每秒都要执行几十上百次的查询,就可以认为该查询是安全的。遍历数据行数达到几百万量级的,查询最快也要花费几秒的时间,这时我们就要仔细考虑有没有优化的办法。遍历行数达到千万量级或以上的,这种SQL就不应该出现在系统中了。当然,我们这里讨论的都是在线交易系统,离线分析类系统另当别论。

遍历行数达到千万量级的SQL,是MySQL查询的一个坎儿。在MySQL中,单个表的数据量,也要尽量控制在一千万条以下,最多不要超过两三千万这个量级。原因很简单,对一个千万量级的表执行查询,加上几个WHERE条件过滤一下,符合条件的数据最多可能是几十万或百万量级的,还是可以接受的。但如果再与其他的表做一个联合查询,遍历的数据量很可能就会超过千万量级了。所以,每个表的数据量最好控制在千万量级以内。

如果数据库中的数据量本身就很多,而且查询业务逻辑确实需要遍历大量数据,应该怎么办呢?

02使用索引避免全表扫描

使用索引,可以有效减少执行查询时遍历数据的行数,从而提高查询的性能。

数据库索引的原理比较简单,一个例子就能说明白。比如,有一个无序的数组,数组中的每个元素都是一个用户对象。如果我们要把所有姓李的用户都找出来,那么比较笨的办法是,用一个循环把数组遍历一遍。

是否还有更好的办法呢?答案是肯定的。比如,我们可以用一个Map(在某些编程语言中是Dictionary)来为数组做一个索引,Key用于保存姓氏,值是所有这个姓氏的用户对象在数组中序号的集合,如图1所示。这样在查找的时候,就不用遍历数组了,只需要先在Map中查找,然后再根据序号直接去数组中获取用户数据即可,这样查找速度就快多了。

图1 基于Map构建的内存索引

下面我们把这个例子对应到数据库中,存放用户数据的数组就是表,我们构建的Map就是索引。实际上,数据库索引的数据结构与编程语言中的Map或Dictionary的结构差不多,基本上都是各种B树和哈希表。

绝大多数情况下,我们编写的查询语句,都应该使用索引,以避免遍历整张表,也就是通常所说的,避免全表扫描。在开发新功能时,每当需要为数据库增加一个新的查询时,我们都要事先评估一下,是否可以由索引支撑新的查询语句,如果有必要,则需要新建索引,以支持新增的查询。

但是,增加索引需要付出的代价是,会降低数据插入、删除和更新的性能。这一点也很好理解,增加了索引之后,当数据发生变化的时候,不仅要变更数据表里的数据,还要变更各个索引。所以,对于更新频繁并且对更新性能要求较高的表,可以尽量少建索引。而对于查询较多、更新较少的表,可以根据查询的业务逻辑,适当多建一些索引。

那么,如何写SQL才能更好地利用索引,使查询效率更高呢?这是一门技艺,需要有丰富的经验,不是学习完本文的内容就能练成的(推荐阅读《电商存储系统实战:架构设计与海量数据处理》)。但是,对于SQL的查询性能,我们还是有方法评估其是否为一个潜在的“慢SQL”的。

对于逻辑不是很复杂的单表查询,我们可能还可以分析出查询会使用哪个索引。但如果是比较复杂的多表联合查询,单看SQL语句本身,我们将很难分析出查询到底会使用哪些索引,会遍历多少行数据。MySQL和大部分数据库都提供了一个可用于分析查询的功能,即执行计划。

03分析SQL执行计划

在MySQL中使用执行计划非常简单,只要在SQL语句前面加上EXPLAIN关键字,然后执行这个查询语句就可以了。

下面就来举例说明,比如,有这样一个用户表,包含用户ID、姓名、部门编号和状态这几个字段,如图2所示。

图2 用户表示例

我们希望查询某个二级部门下的所有人,查询条件是,部门代号以00028开头的所有人。下面这两个SQL语句的查询结果是一样的,都满足要求。那么,哪个查询语句的性能更好呢?

1SELECT * FROM user WHERE left(department_code, 5) = '00028';
2
3SELECT * FROM user WHERE department_code LIKE '00028%';

我们分别查看一下这两个SQL语句的执行计划,如图3所示。

图3 两个SQL语句的执行计划

下面就来分析一下这两个SQL语句的执行计划。首先来看rows这一列,rows列的含义是,MySQL预估执行这个SQL可能会遍历的数据行数。第一个SQL遍历了4534行,即整个User表的数据条数;第二个SQL只有8行,这8行其实就是符合条件的8条记录。显然,第二个SQL的查询性能要远高于第一个SQL。

为什么第一个SQL需要全表扫描,而第二个SQL只需要遍历很少的行数呢?注意看type这一列,type列表示这个查询的访问类型。ALL代表全表扫描,这是性能最差的情况。range代表使用了索引,表示只在索引中进行范围查找,这是因为SQL语句的WHERE条件中有一个LIKE的查询限制。如果直接使用了索引,则type列显示的是index,并且可以在key列中看到实际上使用的是哪个索引。

通过对比这两个SQL的执行计划,我们可以看到,第二个SQL虽然使用了公认为低效的LIKE查询条件,但是由于用到了索引的范围查找,因此遍历数据的行数远远少于第一个SQL,查询性能更好。

04小结

在开发阶段,衡量一个SQL语句查询性能的手段是,预估执行SQL时需要遍历的数据行数。如果遍历行数在百万量级以内,则可以认为是安全的SQL;百万到千万这个量级,则需要仔细评估和优化;千万量级以上则是非常危险的。为了降低写出慢SQL的可能性,每个数据表的行数最好控制在千万量级以内。

索引可以显著减少查询遍历数据的数量,所以提升SQL查询性能最有效的方式是,让查询尽可能多地使用索引。但是,索引也是一把双刃剑,其在提升查询性能的同时,也会降低数据更新的性能。

对于复杂的查询,最好使用SQL执行计划,事先对查询做一个分析。从SQL执行计划的结果中,我们可以看到查询预估的遍历行数,以及其会使用哪些索引。执行计划也可以很好地帮助大家对查询语句进行优化。

关于作者:李玥,美团基础技术部高级技术专家,极客时间《后端存储实战课》《消息队列高手课》等专栏作者。曾在当当网、京东零售等公司任职。从事互联网电商行业基础架构领域的架构设计和研发工作多年,曾多次参与双十一和618电商大促。专注于分布式存储、云原生架构下的服务治理、分布式消息和实时计算等技术领域,致力于推进基础架构技术的创新与开源。

本文摘编自《电商存储系统实战:架构设计与海量数据处理》,经出版方授权发布。(ISBN:9787111697411)转载请保留文章出处。


责任编辑:武晓燕 数仓宝贝库
点赞
收藏