查询性能优化
怎么样算查询性能比较好?响应时间短(获取查询数据速度快)
优化数据访问
查询性能低下最基本的原因是访问的数据太多。大部分性能低下的查询都可以通过减少访问的数据量的方式进行优化。
对于低效的查询,我们发现通过下面两个步骤来分析总是很有效:
- 确认应用程序是否在检索大量超过需要的数据。这通常意味着访问了太多行,但有时候也可能是访问了太多的列。
- 确认MySQL服务器层是否在分析大量超过需要的数据行。
是否向数据库请求了不需要的数据
比较容易犯的错误情景:
- 查询不需要的记录。一个常见的错误是误以为MySQL会只返回需要的数据,实际上MySQL却是先返回全部结果集再进行计算。最简单的解决方法是在查询后面加上LIMIT。(假分页)(查询数据多余)
- 多表关联时返回全部列。(SELECT * FROM a join b on ... 这种情况下会查询出表a 和表b的全部列数据.)(查询数据多余)
- 总是取出全部列。(SELECT * ...)(查询数据多余)
- 重复查询相同的数据。(一般是初次查询时把数据缓存起来,这种数据特点一般是:频率高,数据量小)(查询次数多余)
MySQL是否在扫描额外的记录
一般MySQL能够使用如下三种方式应用WHERE 条件,从好到坏依次为:
- 在索引中使用WHERE条件来过滤不匹配的记录。这是在存储引擎层完成的。
- 使用索引覆盖扫描(在EXTRA 列中出现了Using index)来返回记录,直接从索引中过滤不需要的记录并返回命中的结果。这是在MySQL服务器层完成的,但无须再回表查询记录。
- 从数据表中返回数据,然后过滤不满足条件的记录(在EXTRA 列中出现Using where)。这在MySQL服务器层完成,MySQL需要先从数据表读取记录然后过滤。
如果发现查询需要扫描大量的数据但只返回少数的行(例如:SELECT id,COUNT(*) FROM person;这个查询需要读取几千行数据,但是仅返回200行。没有什么索引能够让这样的查询减少需要扫描的行数)
那么通常可以尝试下面的技巧去优化它:
- 使用索引覆盖扫描,把所有需要用到的列都放到索引中
- 改变库表结构。例如使用单独的汇总表
- 重写这个复杂的查询,让MySQL优化器能够以更优化的方式执行这个查询。
优化特定类型的查询
优化count查询
COUNT()作用:它可以统计某个列值的数量,也可以统计行数。如果希望知道的是结果集的行数,最好使用COUNT(*),它会忽略所有列而直接统计行 数。
有时候可以使用MyISAM在COUNT(*)全表非常快的这个特性,来加速一些特定条件的COUNT()的查询。
例如,查询ID大于5的城市。
SELECT COUNT(*) FROM City WHERE ID > 5;
利用MyISAM在COUNT(*)全表非常快的特性,可以先查找ID小于5的城市数,然后用总城市数减去小于5的城市数得到同样的结果。即:
SELECT (SELECT COUNT(*) FROM City) - COUNT(*) FROM City WHERE ID <=5;
使用近似值
有时候有些业务并不要求完全精确的COUNT值,此时可以使用近似值来代替。EXPALIN出来的优化器估算的行数就是一个不错的近似值,执行EXPLAIN并不需要真正的去执行查询,所以成本很低。
优化关联查询
- 确保ON或者USING子句中的列上有索引。当表A和表B用列c关联的时候,如果优化器的关联顺序是B、A,(如何确认优化器的关联顺序),那么就不需要在B表的对应列上建索引。例如:查询语句为:SELECT .. FROM B JOIN A USING(c),关联顺序为B、A,此时查询应该是这样的:b_rows = SELECT ... FROM B; SELECT ... FROM A WHERE A.c = b_rows.c; 一般来说:除非有其他理由,否则只需要在关联顺序中的第二个表的相应列上创建索引。
- 确保任何的GROUP BY和ORDER BY中的表达式只涉及到一个表中的列。这样MYSQL才有可能使用索引来优化这个过程。
优化子查询
尽可能的使用关联查询代替。(这种是说IN()里面是一个表达式而不是常量)
优化limit分页(索引章节有提到)
SELECT film_id,description FROM film ORDER BY title limit 10000,5
如果表很大,使用“延迟关联”可以大大提升查询效率
SELECT film_id, film.description FROM film JOIN ( SELECT film_id FROM film ORDER BY title LIMIT 10000,5 ) AS lim USING (film_id);
它可以让MYSQL扫描尽可能少的页面,获取需要访问的记录后再根据关联列回原表查询需要的所有列。
有时候也可以将LIMIT查询转换为已知位置的查询,让MYSQL通过范围扫描获得到对应的结果。如果在一个位置列上有索引,并且预先计算出了边界值,上面的查询可以改写为:
SELECT film_id,description FROM film WHERE position BETWEEN 10000 AND 10004 ORDER BY position;-- 确认BETWEEN的边界[]
优化UNION查询
除非确实需要服务器消除重复的行,否则就一定要使用UNION ALL,如果没有ALL关键字,MySQL会给临时表加上SIATINCT 选项,这会导致对整个临时表的数据做唯一性检查。这样做的代价非常高。
使用用户自定义变量
使用自定义变量可以更加清晰的梳理sql语句。可以用来存储一些复杂查询的中间变量值。
具体语法:
SET @min_actor := (SELECT MIN(act_id) FROM actor); SELECT .. FROM actor WHERE act_id > @min_actor;
使用自定义变量的限制:
- 无法查询缓存(无关紧要)
- 不能在使用常量或者标识符的地方使用自定义变量,例如 表名、列名和LIMIT子句中
- 用户自定义变量的生命周期是在一个连接中有效,不能用它们来作连接间的通信
- 不能显示地声明自定义变量的类型。如果希望变量是整数类型,那么初始化的时候就赋值为0,如果希望是浮点类型则赋值为0.0,如果希望是字符处则赋值为‘’,用户自定义变量的类型在赋值的时候会改变。
- 赋值符号 := 的优先级非常低,所以需要注意,赋值表达式应该使用明确的括号。
- 使用未定义变量不会产生任何语法错误。
优化排名顺序
使用自定义变量的一个重要特性就是可以在给一个变量赋值的同时使用这个变量。用户自定义的变量具有"左值"特性。
例子1:行号功能(在SQL SERVER 中会使用)
SET @rownum := 0; SELECT actor_id,@rownum := @rownum + 1 AS rownum FROM actor LIMIT 3;
例子2:获取演过最多电影的前10位演员,然后根据他们的出演电影次数做一个排名,如果出演的电影数量一样,则排名相同。(这种有逻辑处理的查询,一般会使用到自定义变量,也可以放在应用程序中处理逻辑。)
SELECT actor_id, COUNT(*) AS cnt FROM film_actor GROUP BY actor_id ORDER BY cnt DESC LIMIT 10;
现在把排名加上去,如果演员参演电影相同,则排名应该相同。这里使用三个变量:一个用来记录当前排名(rank),一个用来记录前一个演员的排名(prev_cnt),一个用来记录当前演员参演的电影数量(curr_cnt)。
SET @curr_cnt := 0 ,@prev_cnt := 0 ,@rank := 0; SELECT actor_id, @curr_cnt := COUNT(*) AS cnt, @rank := IF (prev_cnt <> @curr_cnt,@rank + 1 ,@rank) AS rank, @prev_cnt := @curr_cnt AS dummy FROM ( SELECT actor_id, COUNT(*) AS cnt FROM film_actor GROUP BY actor_id ORDER BY cnt DESC LIMIT 10 ) AS der;
查询结果:
避免重复查询刚刚更新的数据
例子1:在更新行的同时获取该行的数据
一种方式是用两条SQL语句,如下:
UPDATE t1 SET lastUpdated = NOW() WHERE id = 1; SELECT lastUpdated FROM t1 WHERE id = 1;
使用自定义变量查询,如下:
UPDATE t1 SET lastUpdated = NOW() WHERE id = 1 AND @now := NOW(); SELECT @now;
这里第二条查询不涉及任何数据表,速度会比较快
编写偷懒的UNION
例子:假设需要编写一个UNION查询,其第一个子查询作为分支条件先执行,如果找到了匹配的行,则跳过第二个分支。(比如查询的时候会在两个地方查找同一个用户——一个主用户表,一个长时间不活跃的用户表。)
SELECT id FROM users WHERE id = 123 UNION ALL SELECT id FROM users_archived WHERE id = 123;
这条语句虽然可以正常工作,但是不管在users表中是否查到了数据,都会在users_archived中再查找一次。改进后的语句如下:
SELECT GREATEST(@found := -1,id) AS id,'users' AS which_tbl FROM users WHERE id = 1 UNION ALL SELECT id,'users_archived' FROM users_archived WHERE id = 1 AND @found IS NULL UNION ALL SELECT 1,'reset' FROM DUAL WHERE (@found := NULL) IS NOT NULL;