最近在公司优化了几个慢查询接口的性能,并且总结了一些经验分享给大家,希望对你有所帮助。我们使用的数据库是Mysql8,使用的存储引擎是Innodb。这个优化除了优化索引之外,更多的是优化count(*)。一般情况下,分页接口一般会查询数据库两次,第一次是获取具体数据,第二次是获取记录总行数,然后对结果进行综合后返回结果。比如查询具体数据的sql是这样的:selectid,namefromuserlimit1,20;它没有性能问题。但是另外一个sql是用count(*)来查询总行数的,例如:selectcount(*)fromuser;性能不佳。为什么会这样?1count(*)为什么性能差?在Mysql中,count(*)的作用是统计表中记录的总行数。count(*)的性能与存储引擎直接相关。并非所有存储引擎的count(*)性能都很差。Mysql中使用最多的存储引擎是:innodb和myisam。在myisam中,总行数会被保存到磁盘中。使用count(*)时,只需要返回那条数据,不需要额外的计算,执行效率非常高。InnoDB不同,因为它支持事务,并且有MVCC(即多版本并发控制)的存在。在同一时间点的不同事务中,同一条查询SQL返回的行数可能是不确定的。innodb在使用count(*)时,需要从存储引擎逐行读取数据,然后累加,执行效率很低。表的数据量小还好,一旦表的数据量大了,innodb存储引擎使用count(*)统计时性能会很差。2如何优化count(*)性能?由上可知,既然count(*)存在性能问题,那我们该如何优化呢?我们可以从以下几个方面入手。2.1添加redis缓存对于简单的count(*),比如统计总访问次数或者总访问次数,我们可以直接使用redis缓存接口,不需要实时统计。当用户打开指定页面时,可以在缓存中每次设置count=count+1。当用户第一次访问该页面时,redis中的count值被设置为1,以后每次用户访问该页面时,count都会加1,最后重新设置到redis中。这样在需要显示数量的地方,可以从redis中查到计数值返回。这种场景下,不需要使用数据埋点表中的count(*)个实时统计数据,性能会得到很大的提升。但是在高并发的情况下,缓存和数据库之间可能会出现数据不一致的情况。但是对于统计总访问次数或者总访问次数的业务场景,数据的准确率不高,数据不一致是可以容忍的。2.2添加二级缓存对于一些业务场景,新增数据很少,大部分是统计操作,查询条件比较多。这时候用传统的count(*)实时统计数据,性能肯定不会好。如果页面可以使用id、name、status、time、source等一个或多个条件来统计品牌数量。这种情况下,用户的组合条件很多,加个联合索引也没用。用户可以选择一个或多个查询条件。有时联合索引会失效,只能添加索引以满足最常用的条件。即有的组合条件可以索引,有的组合条件不能索引。如何优化这些无法索引的场景?答:使用二级缓存。二级缓存其实就是内存缓存。我们可以使用caffine或者guava来实现二级缓存的功能。目前SpringBoot已经集成了caffine,使用起来非常方便。在需要增加二级缓存的查询方法中使用@Cacheable注解即可。@Cacheable(value="brand",,keyGenerator="cacheKeyGenerator")publicBrandModelgetBrand(Condition条件){returngetBrandByCondition(条件);然后自定义cacheKeyGenerator来指定缓存key。公共类CacheKeyGenerator实现KeyGenerator{@OverridepublicObjectgenerate(Objecttarget,Methodmethod,Object...params){returntarget.getClass().getSimpleName()+UNDERLINE+method.getName()+","+StringUtils。arrayToDelimitedString(参数,“,”);}}这个键是由各种条件组成的。这样通过组合条件查询品牌数据后,结果会缓存在内存中,过期时间设置为5分钟。之后,当用户在5分钟内使用相同条件再次查询数据时,可以直接从二级缓存中取出数据,直接返回。这样可以极大的促进count(*)的查询效率。但是如果使用了二级缓存,在不同的服务器上数据可能会不一样。我们需要根据实际的业务场景来选择,并不能适用于所有的业务场景。2.3多线程执行不知道大家有没有做过这样的需求:统计有效订单数和无效订单数。这种情况一般需要写两条SQL。统计有效订单的SQL如下:selectcount(*)fromorderwherestatus=1;统计无效订单的SQL如下:selectcount(*)fromorderwherestatus=0;但是如果在一个接口中,同步执行这两个SQL的效率会很低。这时候可以改成一条sql:selectcount(*),statusfromordergroupbystatus;使用groupby关键字对相同状态的数量进行分组统计,只会生成两条记录,一条记录为有效订单数,另一条记录为无效订单数量。但是有个问题:status字段只有1和0两个值,重复度很高,辨别度很低。不能使用索引,会扫描全表,效率不高。还有其他解决方案吗?答:使用多线程。我们可以通过CompleteFuture使用两个线程异步调用统计有效订单的sql和统计无效订单的sql,最后汇总数据,这样可以提高查询接口的性能。2.4在减少join表的情况下,使用count(*)实时统计总数。但是如果表本身的数据量不大,但是join的表太多,count(*)的效率也可能会受到影响。例如查询商品信息时,需要根据商品名称、单位、品牌、分类等信息查询数据。这时候写一条SQL语句就可以查到想要的数据,例如:selectcount(*)fromproductpinnerjoinunituonp.unit_id=u.idinnerjoinbrandbonp.brand_id=b.idinner加入类别conp.category_id=c.idwherep.name='testproduct'andu.id=123andb.id=124andc.id=125;使用产品表加入单位、品牌和类别表面。其实这些查询条件都可以查询到product表中的数据,不需要join额外的表。我们可以把sql改成这样:selectcount(*)fromproductwherename='testproduct'andunit_id=123andbrand_id=124andcategory_id=125;count(*)的时候只查产品单表,去掉多余的表join可以提高很多查询效率。2.5改用ClickHouse有时候jointable太多了,要去掉多余的join是不可能的。我应该怎么办?比如上面的例子,在查询商品信息的时候,需要根据商品名称、单位名称、品牌名称、分类名称等信息来查询数据。此时无法根据产品单表查询数据。需要join:三个表:unit,brand,category。这个时候怎么优化?A:数据可以保存到ClickHouse。ClickHouse是一个基于列存储的数据库,不支持事务,查询性能非常高。它声称可以查询超过十亿条数据并可以在几秒钟内返回。为了避免业务代码的嵌入,可以使用Canal来监控Mysql的binlog日志。产品表新增数据时,需要同时查询单位、品牌、分类数据,生成新的结果集,保存在ClickHouse中。查询数据时,从ClickHouse查询,这样使用count(*)的查询效率可以提高N倍。特别提醒:在使用ClickHouse时,不要过于频繁地添加数据,尽量批量插入数据。其实如果查询条件比较多的话,并不是特别适合用ClickHouse。这时候可以换成ElasticSearch,但是和Mysql一样有深度分页的问题。3count各种用法的性能比较说到count(*),就要说说count家族的其他成员了,比如:count(1)、count(id)、count(普通索引列)、count(未添加索引列)。那么有什么区别呢?count(*):不做任何处理,获取所有行的数据,行数加1。count(1):会获取所有行的数据,每行有一个固定值1,也就是行数加1。count(id):id表示主键,需要解析出从所有行的数据中提取id字段,其中id不能为NULL,行数加1。count(普通索引列):需要从所有行的数据中解析出普通索引列,然后判断是否为NULL。如果不为NULL,则行数+1。count(unindexedcolumns):扫描全表获取所有数据,分析中的unindexedcolumns,然后判断是否为NULL。如果不为NULL,则行数+1。因此,最终的count性能从高到低为:count(*)≈count(1)>count(id)>count(普通索引列)>count(非索引列)所以,其实count(*)是最快的.惊喜吗,惊喜吗?不要将它与select*混淆
