高性能MySQL——查询性能优化
在数据库设计中,如果查询写得很糟糕,即使库表结构设计再合理,索引再合理也无法实现高性能。
1、优化数据访问
查询性能低下最基本的原因是访问的数据太多。对于低效的查询,通过以下两步来分析总是很有效:
- 确认应用程序是否在检索大量超过需要的数据。这通常意味着访问了太多行或者太多列;
- 确认MySQL服务器层是否在分析大量超过需要的数据行。
是否向数据库请求了不需要的数据
有些查询会请求超过实际需要的数据,然后被应用程序丢弃,这会增加MySQL服务器的额外负担,也会增加网络开销。
典型案例:
- 查询不需要的记录。例如:查询表中所有数据,只显示了前10条。最简单有效的办法——在这样的查询后面加上LIMIT;
- 多表关联时返回全部列。例如:SETECT * FROM actor INNER JOIN film_actor USING(actor_id) ...; 改进:SELCET actor.* FROM actor INNER JOIN film_actor USING(actor_id) ...;只取出需要的列;
- 总是取出全部列。取出全部列,会让优化器无法优化器无法完成索引覆盖扫描这类优化,还会为服务器带来额外的I/O、CPU和内存消耗。因此一些DBA甚至严格禁止SELECT *这种写法;
- 重复查询相同的数据。
MySQL是否扫描额外的记录
对于MySQL,最简单的衡量查询开销的三个指标:响应时间;扫描行数;返回行数。
一般MySQL能够使用三种方式应用where条件,从优到劣依次为:
- 在索引中使用where条件来过滤不匹配的记录,这在存储引擎层完成;
- 使用索引覆盖扫描来返回记录,直接从索引中过滤不需要的记录,并返回命中结果。这在MySQL服务器层完成,但无需回表查询;
- 从数据表中返回数据,然后过滤不满足条件的记录。这在MySQL服务器层完成,需要从数据表读出所有记录然后过滤。
如果发现查询需要扫描大量数据,但是只返回少量数据时,可以尝试如下方法去优化它:
- 使用索引覆盖扫描,把所有需要用到的列都放到索引中,这样存储引擎无需回表获取对应行数据;
- 改变库表结构。例如:使用单独的汇总表;
- 重写次复杂查询,让MySQL优化器能够以更优的方式去执行它。
2、重构查询的方式
- 一个复杂查询还是多个简单查询。设计查询的时候,是否需要将一个复杂查询分成多个简单查询,这是一个需要考虑的重要问题;
- 切分查询——将一个大查询切分成多个小查询,每个查询完全一样,每次只返回一部分结果。例如:一次需要删除1000w条数据,如果切分为每次删除1w条,分多次删除,这样会大大减轻MySQL服务器的负担。DELETE FROM messages WHERE ...;
- 分解关联查询——用分解关联查询的方式重构查询有如下优势:
- 让缓存的效率更高。许多引用程序可以方便的缓存单表查询对应的结果对象;
- 将查询分解后,执行单个查询可以减少锁的争用;
- 在应用层做关联,可以更容易对数据库做拆分,更容易做到高性能和可扩展;
- 查询本身效率也可能会有所提升。例如:使用IN()代替关联查询,可以让MySQL按照ID顺序进行查询,这可能比随机关联更高效;
- 可以减少冗余记录的查询。
3、查询执行的基础(略)
4、MySQL查询优化器的局限性
关联子查询
例如:SELECT * FROM film WHERE film_id IN(SELECT film_id FROM film_actor WHERE actor_id = 1); MySQL优化器优化后:SELECT * FROM film WHERE EXISTS(SELECT film_id FROM film_actor WHERE actor_id = 1 AND film_actor.film_id = film.film_id);
从优化器优化后的结果可以看出,MySQL会将相关外层表压到子查询中,此时子查询需要根据“film_id”来关联外部表film,因为需要“film_id”字段,MySQL认为无法执行子查询,所以会对film表执行全表扫描,如此性能就非常低了。
优化方案:通过关联查询来改写关联子查询。例如:SELECT film.* FROM film INNER JOIN film_actor USING(film_id) WHERE actor_id = 1;
UNION的限制
有时候MySQL不能将限制条件从外层下到内层。例如:希望UNION的各个子句能够根据LIMIT只取部分结果集,或希望先排序再合并结果集,就需要在UNION的各个子句分别使用这些子句。
如:(SELECT first_name, last_name FROM actor ) UNION (SELECT first_name, last_name FROM customer) ORDER BY last_name LIMIT 20;
优化方案:(SELECT first_name, last_name FROM actor ORDER BY last_name LIMIT 20) UNION (SELECT first_name, last_name FROM customer ORDER BY last_name LIMIT 20) LIMIT 20;
松散索引扫描
由于历史原因,MySQL不支持松散索引扫描,也就无法按照不连续的方式扫描一个索引。
例如:存在索引key(a, b);执行查询SELECT * FROM table WHERE b BETWEEN 2 AND 5; 因为索引的前导字段是列a,但查询中只指定了b,顾MySQL无法使用这个索引;
优化方案:给前导列加上可能的常数值。SELECT * FROM table WHERE a IN (2, 3, 5, ...) AND b BETWEEN 2 AND 5;
在同一张表上查询和更新
MySQL不允许对同一张表同时进行查询和更新。可以通过生成临时表的形式来绕过此限制。
例如:UPDATE tbl AS outer_tbl SET cnt = (SELECT count(*) FROM tbl AS inner_tbl WHERE inner_tbl.type = outer_tbl.type);
优化方案:UPDATE tbl INNER JOIN(SELECT type, count(*) AS cnt FROM tbl GROUP BY type) SET tbl.cnt = der.cnt;
5、查询优化器的提示
如果对优化器选择的执行计划不满意,可以使用优化器提供的几个提示(hint)来控制最终执行计划。具体用法可查阅MySQL官方手册。
6、优化特定类型的查询
优化count()查询
count()的作用:count()是一个特殊的聚合函数,有两种非常不同的用法:统计某列值的数量;统计行数。在统计列值时,要求列值时非空的(不统计null)。如果count()括号中时某列或某列的表达式,则统计的是这个表达式有值的结果数。
优化关联查询
- 确保ON或者USING子句中的列上有索引;
- 确保任何GROUP BY 和 ORDER BY 中的表达式只涉及到一个表中的列,这样MySQL才有可能利用索引来优化此过程;
- 当升级MySQL的时候需要注意:关联语法、运算符优先级等其他可能会发生变化的地方。
优化子查询
子查询尽可能使用关联查询代替。但如果时MySQL5.6或更新版本或MariaDB,可忽略关于子查询的这些优化建议。
优化GROUP BY 和DISTINCT
优化LIMIT分页
优化UNION查询
MySQL总是通过创建并填充临时表的方式来执行UNION查询。因此很多优化策略在UNION中都没法很好的使用,经常需要手动将WHERE、LIMIT、GROUP BY 等子句下推到UNION的各个子查询中。
注意:除非确实需要服务器消除重复行,否则一定要使用UNION All。因为如果没有ALL关键字,MySQL会给临时表加上DISTINCT选项,这会导致对临时表的数据做唯一性检查。