目录
- 向数据库请求不需要的数据
- 查询不需要的记录
- 总是返回全部的列
- MySQL扫描了额外的行
- 扫描的行数和返回的行数
- 行访问类型
- 也要注意extra列的信息
- 优化扫描行数过多的建议
- 重构查询方式
- 一个复杂的查询还是多个简单的查询
- 切分查询
- 常用的查询技巧
- 使用内连接而不是外连接
- 优化关联查询
- 优化子查询
- 优化COUNT()、MIN()和MAX()
- 提前终止查询
- 多个OR条件转成IN()查询
- 排序优化
- 优化union查询
- 优化LIMIT分页
一般谈论性能优化是指查询性能的优化,导致查询慢的原因主要是访问的数据太多了。大部分性能低下的查询都可以通过减少访问的数据量的方法进行优化,可以从以下两个方面考虑减少访问的数据量:
- 应用程序向MySQL请求了不必要的数据,如太多的行或列
- MySQL服务器层在分析大量超过需要的数据行(数据能在存储引擎层的索引中完成筛选是最优的)
向数据库请求不需要的数据
查询不需要的记录
应用程序只需要数据库中前10条数据,却向数据库请求所有的数据,然后丢弃前10条之后的数据。这样会造成极大的资源浪费,要养成使用limit关键字限制返回的记录数。
总是返回全部的列
查询语句总是用select *
的方式,MySQL对这种查询无法应用覆盖索引的优化,因为没有索引会将表中所有的字段都覆盖。但是这种方式也有优点,可以简化开发。
MySQL扫描了额外的行
explain关键字可以分析SQL语句是否高效,从输出的结果可以大致看出MySQL访问行的类型、数量以及一些额外的操作。
扫描的行数和返回的行数
理想情况下扫描的行数和返回的行数应该是一致的,但实际情况中这种“美事”并不多。例如在做一个关联查询时,服务器必须要扫描多行才能生成结果集中的一行。扫描的行数对返回的行数的比率通常很小,一般在1:1和10:1之间,不过有时候这个值也可能非常非常大
行访问类型
在评估查询开销的时候,需要考虑一下从表中找到某一行数据的成本。MySQL有好几种访问方式可以查找并返回一行结果。有些访问方式可能需要扫描很多行才能返回一行结果,也有些访问方式可能无须扫描就能返回结果。
在EXPLAIN语句中的type列反应了访问类型。访问类型有很多种,从全表扫描到索引扫描、范围扫描、唯一索引查询、常数引用等。这里列的这些,速度是从慢到快,扫描的行数也是从小到大。你不需要记住这些访问类型,但需要明白扫描表、扫描索引、范围访问和单值访问的概念。
也要注意extra列的信息
如extra列中的“Using Where”
表示MySQL将通过WHERE条件来筛选存储引擎返回的记录。
一般MySQL能够使用如下三种方式应用WHERE条件,从好到坏依次为:
- 在索引中使用WHERE条件来过滤不匹配的记录。这是在存储引擎层完成的。
- 使用索引覆盖扫描(在Extra列中出现了Using index)来返回记录,直接从索引中过滤不需要的记录并返回命中的结果。这是在MySQL服务器层完成的,但无须再回表查询记录。
- 从数据表中返回数据,然后过滤不满足条件的记录(在Extra列中出现Using Where)。这在MySQL服务器层完成,MySQL需要先从数据表读出记录然后过滤。
优化扫描行数过多的建议
如果发现查询需要扫描大量的数据但只返回少数的行,那么通常可以尝试下面的技巧去优化它:
- 使用索引覆盖扫描,把所有需要用的列都放到索引中,这样存储引擎无须回表获取对应行就可以返回结果了(在前面的章节中我们已经讨论过了)。
- 改变库表结构。例如使用单独的汇总表(这是我们在第4章中讨论的办法)。
- 重写这个复杂的查询,让MySQL优化器能够以更优化的方式执行这个查询
重构查询方式
在优化有问题的查询时,目标应该是找到一个更优的方法获得实际需要的结果——而不一定总是需要从MySQL获取一模一样的结果集。
一个复杂的查询还是多个简单的查询
在互联网发展初期,网络通信的时间成本很高,往往让数据库做更多的事,减少查询次数。现在网络带宽已经远远好于之前了,单次网络通信可以在毫秒级完成,对于复杂的查询可以拆分成多个简单的查询,在应用程序中完成业务逻辑的处理。
当然不一定所有的复杂查询都要拆分,要考虑成本。拆分成多个简单查询会增加开发的成本,这需要在实际中作出衡量。
以一个关联查询为例
mysql> SELECT * FROM tag-> JOIN tag_post ON tag_post.tag_id=tag.id-> JOIN post ON tag_post.post_id=post.id-> WHERE tag.tag='mysql';
可以分解成下面这些查询来代替:
mysql> SELECT * FROM tag WHERE tag='mysql';
mysql> SELECT * FROM tag_post WHERE tag_id=1234;
mysql> SELECT * FROM post WHERE post.id in (123,456,567,9098,8904);
这样做会有以下的优点:
- 让缓存更高效。应用程序可以缓存tag_id和post.id对应的数据,当下一次查询时就可以访问缓存无需查数据库了。另外对MySQL查询缓存也友好,因为表结构是很少会改变的。
- 将查询分解后,执行单个查询可以减少锁的竞争
- 查询本身效率也可能会有所提升。使用IN()代替关联查询,可以让MySQL按照ID顺序进行查询,这可能比随机的关联要更高效。
- 可以减少冗余记录的查询。这样做相当于在应用中实现了哈希关联,而不是使用MySQL的嵌套循环关联。
切分查询
有时候对于一个大查询我们需要“分而治之”,将大查询切分成小查询,每个查询功能完全一样,只完成一小部分,每次只返回一小部分查询结果。
删除旧的数据就是一个很好的例子。定期地清除大量数据时,如果用一个大的语句一次性完成的话,则可能需要一次锁住很多数据、占满整个事务日志、耗尽系统资源、阻塞很多小的但重要的查询。将一个大的DELETE语句切分成多个较小的查询可以尽可能小地影响MySQL性能,同时还可以减少MySQL复制的延迟。
一次删除一万行数据一般来说是一个比较高效而且对服务器影响也最小的做法(如果是事务型引擎,很多时候小事务能够更高效)。同时,需要注意的是,如果每次删除数据后,都暂停一会儿再做下一次删除,这样也可以将服务器上原本一次性的压力分散到一个很长的时间段中,就可以大大降低对服务器的影响,还可以大大减少删除时锁的持有时间。
常用的查询技巧
使用内连接而不是外连接
在联表查询时外连接会返回两张表的所有记录,未匹配的记录在其他字段会以null显示。在实际应用中往往是以其中一张表的记录为准做匹配,如果以外连接的方式实现这样的查询结果,会使MySQL执行额外的关联匹配操作,造成资源浪费。
优化关联查询
- 确保ON或者USING子句中的列上有索引。在创建索引的时候就要考虑到关联的顺序。当表A和表B用列c关联的时候,如果优化器的关联顺序是B、A,那么就不需要在B表的对应列上建上索引。
- 确保任何的GROUP BY和ORDER BY中的表达式只涉及到一个表中的列,这样MySQL才有可能使用索引来优化这个过程
优化子查询
关于子查询优化我们给出的最重要的优化建议就是尽可能使用关联查询代替,至少当前的MySQL版本需要这样。“尽可能使用关联”并不是绝对的,如果使用的是MySQL 5.6或更新的版本或者MariaDB,那么就可以直接忽略关于子查询的这些建议了。
优化COUNT()、MIN()和MAX()
要找到某一列的最小值,只需要查询对应B-Tree索引最左端的记录,MySQL可以直接获取索引的第一行记录。同理最大值和统计总数也是类似的。所以可以通过为对应的列建立索引实现优化。
提前终止查询
在发现已经满足查询需求的时候,MySQL总是能够立刻终止查询。一个典型的例子就是当使用了LIMIT子句的时候。
除此之外,MySQL还有几类情况也会提前终止查询
发现了一个不成立的条件
mysql> EXPLAIN SELECT film.film_id FROM sakila.film WHERE film_id = -1;
发现某些特殊的条件
mysql> SELECT film.film_id-> FROM sakila.film -> LEFT OUTER JOIN sakila.film_actor USING(film_id) -> WHERE film_actor.film_id IS NULL;
这个查询将会过滤掉所有有演员的电影。每一部电影可能会有很多的演员,但是上面的查询一旦找到任何一个,就会停止并立刻判断下一部电影,因为只要有一名演员,那么WHERE条件则会过滤掉这类电影。类似这种“不同值/不存在”的优化一般可用于DISTINCT、NOT EXIST()或者LEFT JOIN类型的查询。
多个OR条件转成IN()查询
MySQL将IN()列表中的数据先进行排序,然后通过二分查找的方式来确定列表中的值是否满足条件,这是一个O(log n)复杂度的操作,等价地转换成OR查询的复杂度为O(n),对于IN()列表中有大量取值的时候,MySQL的处理速度将会更快
排序优化
无论如何排序都是一个成本很高的操作,所以从性能角度考虑,应尽可能避免排序或者尽可能避免对大量数据进行排序。在查询的结果不需要排序时可以使用order by null让MySQL不对结果集排序。
当不能使用索引生成排序结果的时候,MySQL需要自己进行排序,如果数据量小则在内存中进行,如果数据量大则需要使用磁盘,不过MySQL将这个过程统一称为文件排序(filesort),即使完全是内存排序不需要任何磁盘文件时也是如此
优化union查询
MySQL总是通过创建并填充临时表的方式来执行UNION查询。
除非确实需要服务器消除重复的行,否则就一定要使用UNION ALL,这一点很重要。如果没有ALL关键字,MySQL会给临时表加上DISTINCT选项,这会导致对整个临时表的数据做唯一性检查。这样做的代价非常高。
事实上,MySQL总是将结果放入临时表,然后再读出,再返回给客户端。
优化LIMIT分页
我们通常会使用LIMIT加上偏移量的办法实现,同时加上合适的ORDER BY子句。如果有对应的索引,通常效率会不错,否则,MySQL需要做大量的文件排序操作。
在偏移量非常大的时候,例如可能是LIMIT 1000,20这样的查询,这时MySQL需要查询10 020条记录然后只返回最后20条,前面10 000条记录都将被抛弃,这样的代价非常高。
优化此类分页查询的一个最简单的办法就是尽可能地使用索引覆盖扫描,而不是查询所有的列。然后根据需要做一次关联操作再返回所需的列。对于偏移量很大的时候,这样做的效率会提升非常大。考虑下面的查询:
mysql> SELECT film_id, description FROM sakila.film ORDER BY title LIMIT 50, 5;
如果这个表非常大,那么这个查询最好改写成下面的样子:
mysql> SELECT film.film_id, film.description-> FROM sakila.film -> INNER JOIN ( -> SELECT film_id FROM sakila.film-> ORDER BY title LIMIT 50, 5 -> ) AS lim USING(film_id);
这里的“延迟关联”将大大提升查询效率,它让MySQL扫描尽可能少的页面,获取需要访问的记录后再根据关联列回原表查询需要的所有列。
LIMIT和OFFSET的问题,其实是OFFSET的问题,它会导致MySQL扫描大量不需要的行然后再抛弃掉。如果可以使用书签记录上次取数据的位置,那么下次就可以直接从该书签记录的位置开始扫描,这样就可以避免使用OFFSET。
mysql> SELECT * FROM sakila.rental-> WHERE rental_id < 16030 -> ORDER BY rental_id DESC LIMIT 20;