本文为《高性能MySQL》读书笔记
慢查询基础:优化数据访问
查询性能低下最基本的原因是访问的数据太多。对于低效的查询,我们发现通过下面两个步骤来分析总是很有效的:
- 确认应用程序是否在检索大量超过需要的数据。这通常意味着访问了太多的行,但有时候也可能是访问了太多的列。
- 确认MySQL服务器层是否在分析大量超过需要的数据行。
是否向数据库请求了不需要的数据
有些查询会请求超过实际需要的数据,然后这些多余的数据会被应用程序丢弃。这会给MySQL服务器带有额外的负担,并增加网络开销,另外也会消耗应用服务器的CPU和内存资源。
一些典型案例:
MySQL是否在扫描额外的记录
在确定查询只返回需要的数据以后,接下来应该看看查询为了返回数据结果是不是扫描了过多的数据。对于MySQL,最简单的衡量查询开销的三个指标如下:
- 响应时间
- 扫描的行数
- 返回的行数
这三个指标都会记录到MySQL的慢日志中,所以检查慢日志记录是找出扫描行数过多的查询的好办法。
扫描的行数和返回的行数:
扫描过多的行数 则说明该查询找到需要的数据的效率不高,其实也就是索引建的不够优化。
扫描的行数与返回的行数大小相等,是最优的查询。
扫描的行数和访问类型:
是不是选择最优的访问类型来减少扫描的行数。
在评估查询开销的时候,需要考虑一下从表中找到某一行数据的成本。MySQL有好几种访问方式可以查找并返回一行结果。有些访问方式可能需要扫描很多行才能返回一行结果,也有些访问方式可能无须扫描就能返回结果。
在EXPLAIN语句中的Type列反应了访问类型。访问类型有很多种,从全表扫描到索引扫描、范围扫描、唯一索引查询、常数引用等。这里列的这些,速度是从慢到快,扫描的行数也是从小到大。你不需要记住这些访问类型,但需要明白扫描表、扫描索引、范围访问和单值访问的概念。
建立合适的索引 & 只查询需要的列。
重构查询的方式
简化SQL,拆分大SQL,分解关联查询。
MySQL查询执行过程
查询状态
查看当前查询线程状态:SHOW FULL PROCESSLIST
命令。
状态枚举值:
- Sleep:线程正在等待客户端发送新的请求。
- Query:线程正在执行查询或者正在将结果发送给客户端。
- Locked:在MySQL服务器层,该线程正在等待表所。在存储引擎级别实现的锁,例如InnoDB的行锁,并不会提现在线程状态中。
- Analyzing and statistics:线程正在收集存储引擎的统计信息,并生成查询的执行计划。
- Copying to tmp table [on disk]:线程正在执行查询,并将其结果集都复制到一个临时表中,这种状态一般要么是在做GROUP BY操作,要么是文件排序操作,或者是UNION操作。如果这个状态后面还有“on disk”标记,那表示MySQL正在将一个内存临时表存到磁盘上。
- Sorting result:线程正在对结果集进行排序。
- Sending data:线程可能在多个状态之间传送数据,或者在生成结果集,或者在想客户端返回数据。
查询缓存
在解析一个查询语句之前,如果查询缓存是打开的,那么MySQL会优先检查这个查询是否命中查询缓存中的数据。这个检查是通过一个对大小写敏感的哈希查找实现的。查询和缓存中的查询即使只有一个字节不同,那也不会匹配缓存结果,这种情况下查询就会进入下一阶段的处理。
如果当前的查询恰好命中了查询缓存,那么在返回查询结果之前MySQL会检查一次用户权限。这仍然是无须解析查询SQL语句的,因为在查询缓存中已经存放了当前查询需要访问的表信息。如果权限没有问题,MySQL会跳过所有其他阶段,直接存缓存中拿到结果并返回给客户端。这种情况下,查询不会被解析,不用生成执行计划,不会被执行。
查询优化处理
查询的生命周期的下一步是将一个SQL转换为一个执行计划,MySQL再按照这个执行计划和存储引擎进行交互。这个阶段可以分为几个子阶段:解析SQL、预处理、优化SQL执行计划。
语法解析器和预处理:
首先,MySQL通过关键字将SQL语句进行解析,并生成一棵对应的“解析树”。MySQL解析器将使用MySQL语法规则校验和解析查询。例如,它将校验是否使用错误的关键字,或者使用关键字的顺序是否正确等,再或者它还会验证引号是否能前后正确匹配。
预处理器则根据一些MySQL规则进一步检查解析树是否合法,例如,这里讲检查数据表和规则列是否存在,还会解析名字和别名,看看它们是否有歧义。
查询优化器:
一条查询可以有很多种执行方式,最后都返回相同的结果。优化器的作用就是找到这其中最好的执行计划。
mysql的查询优化器是一个非常复杂的部件,它使用了很多优化策略来生成一个最优的执行计划。优化策略可以简单的分为两种,一种是静态优化,一种是动态优化。静态优化可以直接对解析树进行分析,并完成优化。例如,优化器可以通过一些简单的代数变换将where条件转换成另一种等价形势。静态优化不依赖于特别的数值,如where条件中带入的一些常熟等等。静态优化在第一次完成后就一直有效,几遍使用不同的参数重复执行查询也不会发生变化。可以认为这是一种“编译时优化”。
相反动态优化则和查询上下文有关,也可能和很多其他因素有关,例如,where条件中的取值、索引中条目对应的数据行等等。着需要在每次查询的时候重新评估,可以认为这是“运行时的优化”。
下面是一些mysql能够处理的优化类型:
- 重新定义关联表的顺序;
- 将外链接转化成内连接;
- 使用等价变换规则(如 5=5 AND a>5将被改写成a>5);
- 优化count()、min()和max();
- 预估并转化为常数定义式;
- 覆盖索引扫描;
- 子查询优化;
- 提前终止查询;
- 等值传播;
- 对象IN()的比较;
MySQL如何执行关联查询
mysql中的“关联”一次所包涵的意义比一般意义上理解的要更广泛。总的来说,mysql认为任何一个查询都是一个“关联” —— 并不仅仅是一个查询需要到两个表匹配才叫关联,所以在mysql中,每一个查询,每一个片段(包括子查询,甚至基于单表的select)都有可能是关联。
我们根据union查询的例子来理解关联查询。对于union查询,mysql先将一系列的单个查询结果放到一个临时表中,然后再重新读出临时表来完成union查询。在mysql的概念中,每个查询都是一次关联,所以读取结果临时表也是一次关联。
当前mysql关联执行得策略很简单:mysql对任何关联都执行嵌套循环关联操作,即mysql先在一个表中循环取出单条数据,然后再嵌套循环到下一个表寻找匹配的行,一次下去,知道找到所有表中匹配的行为止。然后根据各个表匹配的行,返回查询结果汇总需要的各个列。mysql会尝试在最后一个关联表中找到所有匹配的行,如果最后一个关联表无法找到更更多行以后,mysql返回到上一层次的关联表,看是否能够找到更多的匹配记录,一次类推迭代执行。
按照这样的方式查找第一个表的记录,再嵌套查询下一个关联表,然后回溯到上一个表,在mysql中时通过嵌套循环的方式实现的。 请看下面的例子中的简单查询:
SELECT tbl1.col1 , tbl2.col2 FROM tbl1 INNER JOIN tbl2 USING(col3) where tbl1.col1 in (5,6)
假设MySQL按照查询中的表顺序进行关联操作,我们则可以使用下面的伪代码表示MySQL将如何完成这个查询:
特定类型的查询优化
优化COUNT()查询
COUNT()可以统计某个列值的数量,也可以统计行数。在统计列值时要求列值是非空的(不统计NULL)。如果在COUNT()的括号中指定了列或者列的表达式,则统计的就是这个表达式有值的结果数(而非NULL)。
COUNT()的另一个作用是统计结果集的行数,当MySQL确认括号内的表达式值不可能为空时,实际上就是在统计行数。最简单的就是当我们使用COUNT()的时候,这种情况下通配符 并不会像我们猜想的那样扩展成所有的列,实际上,它会忽略所有的列而直接统计所有的行数。
一种常见的错误就是,在括号内指定了一个列却希望统计结果集的行数。如果希望知道的是结果集的行数,最好使用count(*),这样写意义清晰,性能也会很好。
MYISAM只有当没有任何WHERE条件的COUNT(*)才非常快,因为此时无须实际地计算表的行数。MySQL可以利用存储引擎的特性直接获得这个值。如果MySQL知道某列col不可能为NULL值,那么MySQL内部会将COUNT(col)表达式优化为COUNT(*)。当统计待WHERE子句的结果集行数时,MyISAM与其他数据库引擎没有太大区别。
优化关联查询
- 确保ON或者USING子句中的列上有索引。在创建索引的时候就要考虑到关联的顺序。当表A和表B用列C关联的时候,如果优化器的关联顺序是B、A,那么就不需要在B表的对应列上建上索引。没有用到的索引只会带来额外的负担。一般来说,除非有其他理由,否则只需要在关联顺序中的第二个表的相应列上创建索引。
- 确保任何的GROUP BY和ORDER BY 中的表达式只涉及到一个表中的列,这样MySQL才有可能使用索引来优化这个过程。
优化子查询
尽可能使用关联查询代替子查询。
优化GROUP BY优化
针对以GROUP BY的优化 主要分为有索引和无索引两种情况。
当无法使用索引的时候,GROUP BY使用两种策略来完成分组工作:使用临时表或者文件排序来做分组,其实就是进行一次全表扫描筛选数据形成一个临时表,然后按照GROUP BY 指定的列进行排序。在这个临时表里面,对于每一个group的数据行来说是连续在一起的。完成排序之后,就可以发现所有的GROUPS,并可以执行聚合函数。所以,我们常常在explain后看到“Using temporary; Using filesort”。
如果没有通过 ORDER BY子句显示地指定排序列,当查询使用GROUP BY子句的时候,结果集会自动按照分组的字段进行排序。如果不关心结果集的顺序,而这种默认排序又导致了需要文件排序,则可以使用ORDER BY NULL,让MySQL不再进行文件排序。也可以在GROUP BY子句中直接使用DESC或者ASC关键字,使分组的结果集按需要的方向进行排序。
优化LIMIT分页查询
假设有如下的分页SQL语句:
SELECT * FROM table LIMIT offset , rows ;
这是一条典型的LIMIT语句,常见的使用场景是,某些查询返回的内容特别多,而客户端处理能力有限,希望每次只取一部分结果进行处理。
上述SQL语句的实现机制是:
- 从“table”表中读取offset+rows行记录。
- 抛弃前面的offset行记录,返回后面的rows行记录作为最终的结果。
这种实现机制存在一个弊端:虽然只需要返回rows行记录,但却必须先访问offset行不会用到的记录。对一张数据量很大的表进行查询时,offset值可能非常大,此时limit语句的效率就非常低了。
使用覆盖索引来优化:
尽可能使用索引覆盖扫描,确定需要返回行的主键等,然后再根据需要做一次关联操作再返回所需的列。通常此种优化都是使用子查询来实现。
比如我们有如下的SQL语句:
select * from student where score > 90 limit 1000,10
我们就可以先利用覆盖索引查询速度快的优点先查询出对应分页段内的学号,然后再根据学号去做关联查询,第二步是直接使用主键做关联,也是非常快的。优化后的SQL如下:
select * from student as stu INNER JOIN (select id from student limit 1000,10) as tmp on stu.id = tmp.id;
确定分页起始值,减少扫描行数
我们可以记住上次取数据的位置,然后下次就可以直接从该位置开始扫描数据,然后取指定的长度。假设上次获取到的最后一个学生学号是:20180131200,则我们可以改写成如下SQL:
select * from student stu where stu.id > 20180131200 order by id limit 100 ;
上面的SQL首先使用主键进行排序,因为聚簇索引的特性,所以主键ID在索引树中本身已经有序存储了,所以此处的order by 非常快。然后再使用主键进行筛选 也是非常快的。
优秀网文:mysql大数据量之limit优化