(1)背景:
电商场景下非常普遍的商品评论系统的SQL优化,这个商品评论的数据量非常大,拥有多大十亿级的评论数据,所以当时对这个评论数据库,是做了分库分表的,基本上分完库和表过后,
单表的评论数据在百万级别。
每一个商品的所有评论都是放在一个库的一张表里的,这样可以确保作为用户在分页查询一个商品的评论时,一般都是直接从一个库的一张表里执行分页查询语句就可以了,好,现在
开始讲案例了。
在电商网站里,有一些热门的商品,可能销量多大上百万,商品的评论可能多达几十万条。然后,有一些用户,可能喜欢看商品评论,就喜欢不停的对某个热门商品的评论不断的进行分页,
一页页的翻,有时候还会用上分页跳转功能,直接输入自己要跳到第几页去。
所以这个时候,就会涉及一个问题,针对一个商品几十万评论的深分页问题,先看看一个经过简化后的评论表进行分页查询的SQL语句。
SELECT FROM comments WHERE product_id=’xx’ and is_good_comment=’1’ ORDER BY id desc LIMIT 100000,20
这个SQL意思就是,用户选择查询某个商品的评论,因此必须限定product_id,同时还选了只看好评,所以 is_good_comment 也要限定一下。接着他要看第5001页评论,那么此时limit的
offset就会是(5001-1)20,其中20就是每一页的数量,此时其实offset就是100000,所以limit 100000,20 对这个评论表最核心的索引就是 index_product_id,所以对上述SQL语句,正常情况下
肯定会走这个索引的,也就是会通过index_product_id索引,根据product_id=’xx’这个条件从表里先筛选出来这个表里指定商品的评论数据。
接下来第二步,当然是按照is_good_commnet='1'条件,筛选出这个商品评论数据里的所有好评,但是问题来了,这个index_product_id的索引数据里,并没有is_good_comment这个字段。
所以此时只能进行回表。也就是说,对这个商品的每一条评论都要进行一次回表操作,回到聚簇索引里根据id找到那条数据,取出is_good_comment字段的值,接着对is_good_comment=’1’条件做一个比对,
筛选符合条件的数据。
那么假设这个商品的评论有几十万条,岂不是要做几十万次回表操作?虽然每次回表都是根据id在聚簇索引里快速查找的,但还是架不住每条数据都回表。接着对于筛选完毕的所有符合
WHERE product_id=’xx’ and is_good_comment=’1’ 条件的数据,假设有十多万条,接着就是按照id做一个倒序排序,此时还得基于临时磁盘文件进行倒序排序,又得耗时很久。排序完毕就得基于
limit 100000,20 获取第5001页的20条数据,最后返回。
这个过程,因为几十万次回表查询,还有十多万数据的磁盘文件排序,所以发现这条SQL语句基本要跑1秒~2秒。
(2)优化思路:
对于这个案例,我们通常采用如下方式改造分页查询语句:
改造前:
SELECT FROM comments WHERE product_id=’xx’ and is_good_comment=’1’ ORDER BY id desc LIMIT 100000,20
改造后:
SELECT FROM comments a,
(SELECT id FROM comments WHERE product_id=’xx’ and is_good_comment=’1’ ORDER BY id desc LIMIT 100000,20) b where a.id=b.id
上面改造后SQL语句的执行计划就会彻底改变他的执行方式,他通常会先执行括号里的子查询,子查询反而会使用PRIMARY聚簇索引,按照聚簇索引的id值的倒序方向进行扫描,扫描过程中就把符合
WHERE product_id=’xx’ and is_good_comment=’1’ 条件的数据给筛选出来。比如这里就筛选了十万多条数据,并不需要把符合条件的数据都找到,因为limit后跟的是100000,20 ,理论上,只要有
100000+20条符合条件的数据,而且是按照id排序的,此时就可以执行根据limit 100000,20 提取到5001页的这20条数据了。
接着会看到执行计划里 针对子查询的结果集,一个临时表,
所以针对这个场景,反而是优化成这种方式执行分页,会更加合适一些,他只有一个扫描聚簇索引筛选符合你分页所有数据的成本,分页越深,扫描数据越多,分页深度越浅,扫描数据越少,
然后再做一页20条数据的20次回表查询就可以了。
当做了这个分页优化之后,发现这个分页语句一下子执行时间降低到了几百毫秒,此时就达到了优化目的。
(3)总结:
SQL调优实际上是没有银弹的,比如对于上个案例来说,按顺序扫描聚簇索引方案可能会因为找不到数据导致亿级数据量的全表扫描,所以对于第二个案例而言,必须得根据二级索引去查找。
对于本次案例而言,因为前提是做了分库分表,评论表单数据一般在一百万左右,所以首先即使是一个商品没有评论,有全表扫描,也不会像扫描上亿数据那么慢。其次,如果你根据product_id
的二级索引查询,返回可能会出现几十万次回表查询,所以二级索引查找方式反而不合适,而按照聚合索引顺序扫描的方式更加合适。
(4)知识点: 现在有一个线上场景,页深度原因执行花了好几秒,SQL语句如下:
select * from A where xxx order by id group by product_id limit 100000,20
优化点1:可以给product_id加索引这样group by product_id 时不需要内存临时表
优化点2:limit深度分页可以优化为子查询 先查id,再根据id大于指定值后limit 20
优化点3:order by id 可以去掉无效的,默认就是按照id进行排序的。
问题点:关于为什么不对product_id和is_good_comment 做一个联合索引?
解答:
首先是is_good_comment字段表示好评,差评,这个用户可能会经常修改这个字段,如果频繁修改这个字段会导致索引树必须做出相应调整维持索引树的顺序,所以不合适做索引,
即频繁修改的字段不适合做索引,另外如果区别度低的但是经常查询的字段是可以做索引的,这里主要是这个字段经常修改。