高性能MySQL——查询性能优化

在数据库设计中,如果查询写得很糟糕,即使库表结构设计再合理,索引再合理也无法实现高性能。

1、优化数据访问

查询性能低下最基本的原因是访问的数据太多。对于低效的查询,通过以下两步来分析总是很有效:

  1. 确认应用程序是否在检索大量超过需要的数据。这通常意味着访问了太多行或者太多列;
  2. 确认MySQL服务器层是否在分析大量超过需要的数据行。

  是否向数据库请求了不需要的数据

有些查询会请求超过实际需要的数据,然后被应用程序丢弃,这会增加MySQL服务器的额外负担,也会增加网络开销。

典型案例:

  1. 查询不需要的记录。例如:查询表中所有数据,只显示了前10条。最简单有效的办法——在这样的查询后面加上LIMIT;
  2. 多表关联时返回全部列。例如:SETECT * FROM actor INNER JOIN film_actor USING(actor_id) ...; 改进:SELCET actor.* FROM actor INNER JOIN film_actor USING(actor_id) ...;只取出需要的列;
  3. 总是取出全部列。取出全部列,会让优化器无法优化器无法完成索引覆盖扫描这类优化,还会为服务器带来额外的I/O、CPU和内存消耗。因此一些DBA甚至严格禁止SELECT *这种写法;
  4. 重复查询相同的数据。

  MySQL是否扫描额外的记录

对于MySQL,最简单的衡量查询开销的三个指标:响应时间;扫描行数;返回行数。

一般MySQL能够使用三种方式应用where条件,从优到劣依次为:

  1. 在索引中使用where条件来过滤不匹配的记录,这在存储引擎层完成;
  2. 使用索引覆盖扫描来返回记录,直接从索引中过滤不需要的记录,并返回命中结果。这在MySQL服务器层完成,但无需回表查询;
  3. 从数据表中返回数据,然后过滤不满足条件的记录。这在MySQL服务器层完成,需要从数据表读出所有记录然后过滤。

如果发现查询需要扫描大量数据,但是只返回少量数据时,可以尝试如下方法去优化它:

  1. 使用索引覆盖扫描,把所有需要用到的列都放到索引中,这样存储引擎无需回表获取对应行数据;
  2. 改变库表结构。例如:使用单独的汇总表;
  3. 重写次复杂查询,让MySQL优化器能够以更优的方式去执行它。

2、重构查询的方式

  1. 一个复杂查询还是多个简单查询。设计查询的时候,是否需要将一个复杂查询分成多个简单查询,这是一个需要考虑的重要问题;
  2. 切分查询——将一个大查询切分成多个小查询,每个查询完全一样,每次只返回一部分结果。例如:一次需要删除1000w条数据,如果切分为每次删除1w条,分多次删除,这样会大大减轻MySQL服务器的负担。DELETE FROM messages WHERE ...;
  3. 分解关联查询——用分解关联查询的方式重构查询有如下优势:
    • 让缓存的效率更高。许多引用程序可以方便的缓存单表查询对应的结果对象;
    • 将查询分解后,执行单个查询可以减少锁的争用;
    • 在应用层做关联,可以更容易对数据库做拆分,更容易做到高性能和可扩展;
    • 查询本身效率也可能会有所提升。例如:使用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选项,这会导致对临时表的数据做唯一性检查。

 

posted @ 2018-01-17 17:06  西门吹-牛  阅读(230)  评论(0编辑  收藏  举报