前言:之前实习时候做过一阵子的查询优化,那段时间结合阅读《高性能mysql》,有些心得体会,现在重新总结。围绕着《高性能mysql》,同时加入自己的心得体会。
(一)慢查询的原因
1,是否向数据库请求不必要的数据
(1)查询不需要的数据
简单来说,就是获取全部全部的结果再进行筛选,然后抛弃大部分的数据,其实这些数据可以不用筛选出来的,我们可以通过加limit等方式解决
(2)多表关联返回全部列
这种情况之前在优化时候很常见,很多人都会写出类似下面这样的语句:
SELECT * FROM tab1
INNER JOIN tab2
INNER JOIN tab3
WHERE a = b
这样写将会返回三个表的全部数据列,正确的方式应该是只取需要的。我们每次在看到SELECT * 的时候,都要问问这样是否必要, 有时候是为了扩展性,如果在自己明白代价的情况下,也是可以接受的。
(二)是否扫描额外的记录
我们看到返回的数据是我们需要的,但是应该继续看看返回结果是否扫描了过多的数据,这里有三个指标:响应时间,扫描的行数,返回的行数。我们可以使用explain来查看。这里就不展开了。
(三)重构查询的方式
1,是否可以将一个复杂查询分成多个简单查询
我们写复杂查询的原因是想让数据库尽量一次性完成尽可能多的任务,认为网络通信,查询解析和优化是代价很高的事情。但是现在已经不是这样了。但是这样切分是要合理的,不能把原来的好的查询硬要拆开。接下来将说说拆分的技巧
2,切分查询
接下来举个例子来说明应用场景。我们如果需要每个月定期删除大量的旧数据,如果用一个语句来完成,那么将会锁住大量的数据,系统资源会被耗尽,同时还可能阻塞其他事务,甚至发生死锁。那么,我们就可以进行拆分。
原语句:
DELETE FROM tab WHERE create_time < DATE_SUB(NOW(),INTERVAL 3 MONTH);
拆分为:
row = 0;
do {
row = do_query("DELETE FROM tab WHERE create_time < DATE_SUB(NOW(),INTERVAL 3 MONTH) LIMIT 10000")
}WHILE row > 0
一次性删除一万条,我觉得对于服务器来说很OK,而且每次删除完我们还可以暂停一会。
3,分解关联查询
(这里就全部借鉴这本书了,之前要拆分,但是老大不让,所以没什么实际体会)
首先来看一个例子
SELECT * FROM tag
JOIN tag_post ON tag_post.tag_id=tag.id
JOIN post ON tag_post.id=post.id
WHERE tag.tag='mysql';
可以拆分为
SELECT * FROM tag WHERE tag='mysql';
SELECT * FROM tag_post WHERE tag_id=1234;
SELECT * from post WHERE post.id in (124,456,567,9098,8904);
那么来看看这样的好处
(1)缓存的效率更高。我们很多应用程序可以很方便缓存单笔查询对应的结果对象,例如如果tag被缓存了,那么第一条查询就可以跳过了。对于mysql的查询缓存来说,如果关联中的某个表发生变化,那么就无法使用缓存,而拆分后,如果有某个表很少该表,那么基于该表的查询就可以重复利用查询缓存结果了
(2)执行单个查询可以减少锁的竞争
(3)应用层做关联,可以更容易对数据库进行拆分,更容易做到高性能和扩展性
(4)减少冗余记录的查询。在应用层做关联查询,意味着某条记录只需要查询一次,在数据库做关联查询,那么可能重复查询某一部分数据
(四)优化特定的查询
1,优化COUNT()查询
COUNT()可以拿来统计某个列值的数量,也可以统计行数(列值非空)。
我们知道,在MyISAM中,在没有任何where条件的情况下,COUNT(*)是非常快的,因为此时我们不需要去计算表的行数,如果带上WHERE的花,那么就和其他存储引擎没什么区别了。
(1)简单的优化
我们可以使用COUNT(*)来加速特定的查询,例如查询ID大于5的城市,我们可以这样写:
SELECT COUNT(*) FROM world.City WHERE ID>5
但是,这样写我们可能要扫描很多条数据,于是我们可以换种方式:
SELECT (SELECT COUNT(*) FROM world.City)-COUNT(*) FROM world.City WHERE ID <= 5
(2)复杂优化
通常来说,COUNT都需要扫描大量的行才能获得精确的结果,因此很难优化,因此我们可以使用汇总表,或者增加redis这样的缓存系统,但是我们会发现,快速,精确,实现简单,永远只能满足其中两个。
(五)优化关联查询
(1)确保ON或者USING子句列上有索引,一般来说,除非有其他理由,不然则只需要在关联顺序的第二个表中的相应列创建索引。
(2)确保任何GROUP BY和ORDER BY中的表达式只设计到一个表中的列。这样mysql才有可能使用索引来优化这个过程
(六)优化LIMIT分页
我们在需要分页操作的时候,我们通常会使用LIMIT加上偏移量的办法实现,同时加上合适的ORDER BY子句,如果有相应的索引,那么效果会不错,否则,就需要做大量的文件排序操作。
接下来讲一个很多人都会遇到的情况,我在实习时候优化也发现了,那就是偏移量很大的情况下,例如:LIMIT 10000,20,那么查询了10020条才返回了20条,前面的10000条都被抛弃了,这样的代价很高,那么这时候,要么就限制分页数量,要么就优化大偏移量的性能。优化大偏移量的方法如下:
(1)尽可能使用索引覆盖扫描,而不是查询所有的列,然后根据需要做一次关联操作再返回需要的列。
SELECT film_id,description FROM sakila.film ORDER BY title LIMIT 50,5
如果表很大,最后改成下面的样子
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子句。
(2)有时候可以将LIMIT查询转换为已知位置的查询,让Mysql通过范围扫描获得到对应的结果。例如,如果在一个位置列上有索引,而且预先算出了边界值,上面的查询就可以写为:
SELECT film_id,description FROM sakila.film WHERE position BETWEEN 50 AND 54 ORDER BY position
(3)可以使用书签保存上次记录数据的位置,那么下一次就可以从书签位置开始扫描,这样也可以避免使用OFFSET。但是这种做法是有要求的,例如要求主键是单调递增的。下面看一个例子
SELECT * FROM sakila.rental ORDER BY rental_id DESC LIMIT 20
例如返回的主键是10000到10019的记录,那么下一页查询就可以从10019开始
SELECT * FROM sakila.rental
WHERE rental_id < 10019
ORDER BY rental_id DESC LIMIT 20
(七)优化UNION查询
Mysql总是通过创建和填充临时表的方式来执行UNION查询,因此很多优化策略在UNION中没法很好使用,需要下推到UNION的子查询中,例如直接将这些子句冗余的写一份到各个子查询中。
所以,除非需要消除重复的行,不然一定要使用UNION ALL。使用UNION的时候,Mysql会给临时表添加一个DISTINCT选项,对整个临时表做唯一检查,性能代价很高。
总结:其实还有很多技巧的,这些是我之前常用的,就拿出来分享了,其他的技巧自己暂时也还不熟悉,就先不写进去了。