MySQL的count(*)执行慢的解决方案和不同count的比较
业务中经常需要获取一个表的行数,但随着数据表不断增大,你会发现一条count(*)语句执行的效率越来越让人难以忍受。
聊到这里,有必要介绍一下count的实现方式
- 在MyISAM中,MyISAM直接记录了表的行数,执行count(*)会直接返回这个数字,因此效率很高。
- 在InnoDB中,它会先把数据逐行读取出来,再累计计数,执行效率受数据量的影响。
那么为什么InnoDB不能向MyISAM一样维护一个表行数呢?
主要的原因是InnoDB所支持事务的多版本并发控制(MVCC),即使同一时刻执行同一语句也可能返回不同结果,维护一个count并没有什么意义。此外查询语句如果包含Where,那么即使MyISAM引擎也是需要花时间查找计数的。
那么如何解决count语句的效率问题就变成了一个实际问题,解决这个问题的核心是使用缓存来减少执行查询语句,对于不同类型的数据我通常会使用下面几种不同的解决方案:
1.实时性不是那么高的数据
对于不需要实时获取准确的数据,一个很自然的想法就是缓存+有效期。程序第一次获取行数的时候先从缓存里面找,如果找不到再去执行count语句,然后把结果写入带有生命周期的缓存中。这个方案可以有效的规避count效率慢的问题,对于不同实效性要求的数据也可以很容易的设置不同的缓存周期,但面对要求实时准确的数据就无能为力了。
2.要求实时准确的行数
这一类数据的解决思路是,对数据表进行增删操作时,同步对缓存进行操作,这样就可以确保缓存中的行数永远都是最新的。
这个解决方案也存其他在问题,比如我们使用redis缓存,如果某一时刻redis异常重启,如果在这个过程中数据表有新增数据,那么redis中的缓存数就会与实际行数存在差异,当然可以在启动redis时重新执行count语句来同步准确数字。
即使redis不是异常重启,redis缓存的行数也可能与数据库中的不对。设想用户一个查询行数的请求恰好在MySQL新增数据和redis缓存+1的中间,这时候用户获取的行数就会比真实的数据量少1。
3.绝对准确一致的数据
上面说了第2种解决方案其实也不是绝对准确的,那么对于要求绝对准确的数据该怎么处理?思路其实还是和缓存一样,我们可以用MySQL单独建一张表来维护count,相比于缓存,这种解决方案可以充分利用MySQL的事务,可以确保对于任意时刻count和真实数量保持一致,用户每次查询数量时访问这张计数表即可。
再来聊聊count(*), count(id), count(1)的性能区别
对于count(*),优化器专门优化count(*)的语义为“取行数”,其他“显而易见”的优化并没有做
对于count(主键id)来说,InnoDB引擎会遍历整张表,把每一行的id值都取出来,返回给server层。server层拿到id后,判断是不可能为空的,就按行累加。
对于count(1)来说,InnoDB引擎遍历整张表,但不取值。server层对于返回的每一行,放一个数字“1”进去,判断是不可能为空的,按行累加。
单看这两个用法的差别的话,你能对比出来,count(1)执行得要比count(主键id)快。因为从引擎返回id会涉及到解析数据行,以及拷贝字段值的操作。
对于count(字段)来说:
- 如果这个“字段”是定义为not null的话,一行行地从记录里面读出这个字段,判断不能为null,按行累加;
- 如果这个“字段”定义允许为null,那么执行的时候,判断到有可能是null,还要把值取出来再判断一下,不是null才累加。
按照效率排序的话,count(字段)<count(主键id)<count(1)≈count(*),因此尽量使用count(*)。