《高性能MySQL》- 查询性能优化

原创
02/16 23:44
阅读数 137

二、查询性能优化

2.1 优化数据访问

2.1.1 只查询需要的列

2.1.2 只查询需要的行

  1. 响应时间

  2. 扫描行数和返回的行数

  3. 扫描行数和访问类型

    如果扫描行数远远大于返回行数,优化方法:

    • 使用覆盖索引
    • 改变表结构。使用汇总表
    • 重写复杂SQL

2.2 重构查询方式

2.2.1 一个复杂查询还是多个简单查询

  • 连表数据重复很多时,减少冗余记录查询
  • 可以使用缓存
  • 可以使用异步查询
  • 可以支持应用层分库分表

2.2.2 切分查询

使用分治思想,切分大查询为小查询,然后归并。在DML语句可以减少长事务对连接的持有时间,减少锁冲突

2.2.3 分解关联查询

  • 连表数据重复很多时,减少冗余记录查询
  • 可以使用缓存
  • 可以使用异步查询
  • 可以支持应用层分库分表
  • 可能提升性能。比如IN按ID顺序查询,比连表随机查找更快
  • 相当于使用了哈希索引,而不是嵌套循环查询

2.3 查询优化器局限

循环优化器不是每次都是最优结果。

2.3.1 关联子查询优化

  • 一般建议使用左外连接来替代子查询
  • 当返回结果只有一个表的某些列时,关联子查询会更好

不过每个具体的案例会各有不同,有时候子查询写法也会快些。例如,当返回结果中只有一个表中的某些列的时候。听起来,这种情况对于关联查询效率也会很好。具体情况具体分析,例如下面的关联,我们希望返回所有包含同一个演员参演的电影,因为一个电影会有很多演员参演,所以可能会返回一些重复的记录:

mysql>SELECT film.film_id FROM sakila.film
->		INNER JOIN sakila.film_actor USING(film_id);

我们需要使用DISTINCT和GROUP BY来移除重复的记录:

mysql>SELECT DISTINCT film.film_id FROM sakila.film
->		INNER JOIN sakila.film_actor USING(fi1m_id);

但是,回头看看这个查询,到底这个查询返回的结果集意义是什么?至少这样的写法会让 SQL 的意义很不明显。如果使用EXISTS则很容易表达“包含同一个参演演员”的逻辑,而且不需要使用DISTINCT和 GROUP BY,也不会产生重复的结果集,我们知道一旦使用了DISTINCT和GROUP BY,那么在查询的执行过程中,通常需要产生临时中间表。下面我们用子查询的写法替换上面的关联:

mysql> SELECT film_id FROM sakila.film

-> 	WHERE EXISTS(SELECT * FROM sakila.film_actor
->	WHERE film.film_id = film_actor.fi1m_id);

EXISTS和关联性能对比查询

查询 每秒查询数结果(QPS)
INNER J0IN 185 QPS
EXISTS子查询 325 QPS

2.3.2 UNION的限制

有时, MySQL无法将限制条件从外层“下推”到内存。

比如UNION各个子句只取部分结果

(SELECT 
 	first_name, last_name 
FROM 
	sakila.actor
ORDER BY 
	last_name)
UNION ALL
(SELECT 
	first_name, last_name
FROM 
 	sakila.customer
ORDER BY last_name)
LIMIT 20;

	

可以优化为

(SELECT 
 	first_name, last_name 
FROM 
	sakila.actor
ORDER BY 
	last_name
LIMIT 20)
UNION ALL
(SELECT 
	first_name, last_name
FROM 
 	sakila.customer
ORDER BY last_name
LIMIT 20)
LIMIT 20;

2.3.3 索引合并优化

当WHERE子句包含多个复杂条件的时候,MySQL能够访问单个表的多个索引以合并和交叉过滤的方式来丁诶需要查找的行。

2.3.4 等值传递

2.3.5 并行执行

MySQL无法使用多核特性来并行执行查询。

可以在应用层使用分治归并来实现

2.3. 哈希关联

2.3.7 松散索引扫描

MySQL不支持松散索引扫描,无法按照不连续的方式扫描一个索引

松散索引扫描

2.3.8 最大值和最小值优化

对于MIN()和MAX()查询,MySQL的优化有时候不生效。

mysq1>SELECT MIN(actor_id) FROM sakila.actor NHERE first_name = 'PENELOPE';

应该溢出MIN()使用LIMIT来重写

mysql>SELECT actor_id FROM sakila.actor USE INDEX(PRIMARY)
->WHERE first_name = 'PENELOPE’ LIMIT 1;

2.3.9 在同一个表上查询和更新

MySQL不支持对同一张表上同时进行查询和更新。

mysql>UPDATE tb1 AS outer_tbl
->		SET cnt = (
->			SELECT count(*) FROMtb1 AS inner_tbl 
    		WHERE inner_tbi.type = outer_tbl.type
-);
ERROR 1093'(HYooo): You can't specify target table 'outer_tbl' for update in FROM clause

改写成


mysql>UPDATE tbl
->	INNER JOIN(
->		SELECT type,count(*) AS cnt
->		FROM tbl
->		GROUP BY type
->	) As der USING(type)
->	SET tbl.cnt = der.cnt;

2.4 查询优化器的提示(hint)

如果对优化器选择的执行计划不满意,可以使用优化器提供的几个提示(hint)来控制最终的执行计划

2.4.1 HIGH_PRIORITY和LOW_PRIORITY

这个提示告诉MySQL,当多个语句同时访问某一个表的时候,哪些语句的优先级相对高些、哪些语句的优先级相对低些。

2.4.2 DELAYED

这个提示对INSERTREPLACE有效。

MySQL会将使用该提示的语句立即返回给客户端,并将插入的行数据放入到缓冲区,然后在表空闲时批量将数据写入。日志系统使用这样的提示非常有效,或者是其他需要写人大量数据但是客户端却不需要等待单条语句完成I/O的应用。

这个用法有一些限制:并不是所有的存储引擎都支持这样的做法,并且该提示会导致函数LAST_INSERT_ID()无法正常工作。

2.4.3 STRAIGHT_JOIN

这个提示可以放置在SELECT语句的SELECT关键字之后,也可以放置在任何两个关联表的名字之间。

第一个用法是让查询中所有的表按照在语句中出现的顺序进行关联。

第二个用法则是固定其前后两个表的关联顺序。

2.4.4 SQL_SMALL_RESULT和SQL_BIG_RESULT

这两个提示只对SELECT语句有效。它们告诉优化器对GROUPBY或者DISTINCT查询如何使用临时表及排序。SQL_SMALL_RESULT告诉优化器结果集会很小,可以将结果集放在内存中的索引临时表,以避免排序操作。如果是SQL_BIG_RESULT,则告诉优化器结果集可能会非常大,建议使用磁盘临时表做排序操作。

2.4.5 SQL_BUFFER_RESULT

这个提示告诉优化器将查询结果放入到一个临时表,然后尽可能快地释放表锁。这和前面提到的由客户端缓存结果不同。当你没法使用客户端缓存的时候,使用服务器端的缓存通常很有效。带来的好处是无须在客户端上消耗太多的内存,还可以尽可能快地释放对应的表锁。代价是,服务器端将需要更多的内存。

2.4.6 SQL_CACHE和SQL_NO_CACHE

这个提示告诉MySQL这个结果集是否应该缓存在查询缓存中,下一章我们将详细介绍如何使用。

2.4.7 SQL_CALC_FOUND_ROWS

严格来说,这并不是一个优化器提示。它不会告诉优化器任何关于执行计划的东西。它会让MySQL返回的结果集包含更多的信息。查询中加上该提示 MySQL会计算除去LIMIT子句后这个查询要返回的结果集的总数,而实际上只返回LIMIT要求的结果集。可以通过函数FOUND_ROW()获得这个值。(参阅后面的“SQL_CALC_FOUND_ROWS优化”部分,了解下为什么不应该使用该提示。)

2.4.8 USE INDEX、IGNORE INDEX和FORCE INDEX

这几个提示会告诉优化器使用或者不使用哪些索引来查询记录(例如,在决定关联·顺序的时候使用哪个索引)。在MySQL 5.0和更早的版本,这些提示并不会影响到优化器选择哪个索引进行排序和分组,在MyQL 5.1和之后的版本可以通过新增选项FOR ORDER BYFOR GROUP BY来指定是否对排序和分组有效。 FORCE INDEXUSE INDEX基本相同,除了一点:FORCE INDEX会告诉优化器全表扫描的成本会远远高于索引扫描,哪怕实际上该索引用处不大。当发现优化器选择了错误的索引,或者因为某些原因(比如在不使用ORDER BY的时候希望结果有序)要使用另一个索引时,可以使用该提示。在前面关于如何使用LIMIT高效地获取最小值的案例中,已经演示过这种用法。

2.5 优化特定类型的查询

2.5.1 优化COUNT()查询

COUNT()是一个特殊的函数,有两种非常不同的作用:它可以统计某个列值的数量,也可以统计行数。在统计列值时要求列值是非空的(不统计NULL)。

一个容易产生的误解就是:MyISAMCOUNT()函数总是非常快,不过这是有前提条件的,即只有没有任何wHERE条件的COUNT(*)才非常快,因为此时无须实际地去计算表的行数。

  • 使用近似值:有时候某些业务场景并不要求完全精确的COUNT值,此时可以用近似值来代替。
  • 更复杂的优化:通常来说,COUNT()都需要扫描大量的行(意味着要访问大量数据)才能获得精确的结果,因此是很难优化的。除了前面的方法,在MySQL层面还能做的就只有索引覆盖扫描了。如果这还不够,就需要考虑修改应用的架构,可以增加汇总表

2.5.2 优化关联查询

  • 确保ON或者USING子句中的列上有索引。
  • 确保任何的GROUP BY和ORDER BY中的表达式只涉及到一个表中的列,这样MySQL才有可能使用索引来优化这个过程。

2.5.3 优化子查询

关于子查询优化我们给出的最重要的优化建议就是尽可能使用关联查询代替

2.5.4 优化GROUP BY和 DISTINCT

在很多场景下,MySQL都使用同样的办法优化这两种查询,事实上,MySQL优化器会在内部处理的时候相互转化这两类查询。

如果需要对关联查询做分组(GROUP BY),并且是按照查找表中的某个列进行分组,那么通常采用查找表的标识列分组的效率会比其他列更高。

如果没有通过ORDER BY子句显式地指定排序列,当查询使用GROUP BY子句的时候,结果集会自动按照分组的字段进行排序。如果不关心结果集的顺序,而这种默认排序又导致了需要文件排序,则可以使用ORDER BY NULL,让 MySQL不再进行文件排序。也可以在GROUP BY子句中直接使用DESC或者ASC关键字,使分组的结果集按需要的方向排序。

2.5.5 优化LIMIT分页

优化此类分页查询的一个最简单的办法就是尽可能地使用索引覆盖扫描,而不是查询所有的列。然后根据需要做一次关联操作再返回所需的列。对于偏移量很大的时候,这样做的效率会提升非常大。考虑下面的查询:

mysq1> SELECT film_id,description FRON sakila.film ORDER BY title LIMIT 50, 5

优化为

mysql>SELECT film.film_id,film.description
-		FROM sakila.film
->		INNER JOIN (
->			SELECT film_id FROMsakila.film
->			ORDER BY title LIHIT 50,5
->			) AS lim USING(film_id);

mysql> SELECT * FROM sakila.rental
->		ORDER BY rental_id DESC LIMIT 20;

改写成

mysql> SELECT * FROM sakila.rental
-		WHERE rental_id <16030.
-		ORDER BY rental_id DESC LIMIT 20;

2.5.6 优化SQL_CALC_FOUND_ROWS

分页的时候,另一个常用的技巧是在LIMIT语句中加上 SQL_CALC_FOUND_RONS提示(hint),这样就可以获得去掉LIMIT以后满足条件的行数,因此可以作为分页的总数。看起来,MySQL做了一些非常“高深”的优化,像是通过某种方法预测了总行数。但实际上,MySQL只有在扫描了所有满足条件的行以后,才会知道行数,所以加上这个提示以后,不管是否需要,MySQL都会扫描所有满足条件的行,然后再抛弃掉不需要的行,而不是在满足LIMIT的行数后就终止扫描。所以该提示的代价可能非常高。

  • 一个更好的设计是将具体的页数换成“下一页”按钮,假设每页显示20条记录,那么我们每次查询时都是用LIMIT返回21条记录并只显示20条,如果第21条存在,那么我们就显示“下一页”按钮,否则就说明没有更多的数据,也就无须显示“下一页”按钮了。

  • 有时候也可以考虑使用EXPLAIN的结果中的rows列的值来作为结果集总数的近似值.

2.5.7 优化UNION查询

MySQL总是通过创建并填充临时表的方式来执行UNION查询。因此很多优化策略在UNION查询中都没法很好地使用。经常需要手工地将wHERE、LIMIT、ORDER BY等子句“下推”到UNION的各个子查询中,以便优化器可以充分利用这些条件进行优化(例如,直接将这些子句冗余地写一份到各个子查询)。

除非确实需要服务器消除重复的行,否则就一定要使用UNION ALL。如果没有ALL关键字,MySQL会给临时表加上DISTINCT选项,这会导致对整个临时表的数据做唯一性检查。这样做的代价非常高。即使有ALL关键字,MySQL仍然会使用临时表存储结果。事实上,MySQL总是将结果放入临时表,然后再读出,再返回给客户端。

2.5.8 使用用户自定义变量

用户自定义变量是一个用来存储内容的临时容器,在连接MySQL 的整个过程中都存在。

我们不能使用用户自定义变量:

  • 使用自定义变量的查询,无法使用查询缓存。
  • 不能在使用常量或者标识符的地方使用自定义变量,例如表名.列名和LIMIT子句中。用户自定义变量的生命周期是在一个连接中有效,所以不能用它们来做连接间的通信。
  • 如果使用连接池或者持久化连接,自定义变量可能让看起来毫无关系的代码发生交互(如果是这样,通常是代码bug或者连接池bug,这类情况确实可能发生)。在5.0之前的版本,是大小写敏感的,所以要注意代码在不同MySQL版本间的兼容性问题。
  • 不能显式地声明自定义变量的类型。确定未定义变量的具体类型的时机在不同MySQL版本中也可能不一样。如果你希望变量是整数类型,那么最好在初始化的时候就赋值为0,如果希望是浮点型则赋值为0.0,如果希望是字符串则赋值为",用户自定义变量的类型在赋值的时候会改变。MySQL的用户自定义变量是一个动态类型。
  • MySQL优化器在某些场景下可能会将这些变量优化掉,这可能导致代码不按预想的方式运行。
  • 赋值的顺序和赋值的时间点并不总是固定的,这依赖于优化器的决定。实际情况可能很让人困惑,后面我们将看到这一点。
  • 赋值符号:=的优先级非常低,所以需要注意,赋值表达式应该使用明确的括号。
  • 使用未定义变量不会产生任何语法错误,如果没有意识到这一点,非常容易犯错。
展开阅读全文
打赏
0
0 收藏
分享
加载中
更多评论
打赏
0 评论
0 收藏
0
分享
返回顶部
顶部