count(*)的实现方式
在不同的存储引擎中,无过滤条件的count(*)有不同的实现方式:
- MyISAM引擎:把表的总行数存在磁盘上,因此执行count(*)直接返回这个数,效率很高。
- InnoDB引擎:执行count(*)时,需要把数据一行行从引擎里面读出来,然后累计计数。
为什么InnoDB不和MyISAM一样,把数字存起来呢?
由于多版本并发控制(MVCC)的原因,即使在同一个时刻的多个查询,InnoDB表返回多少行也是不确定的。
假设表t中有10000行记录,有3个会话:
- 会话A先启动事务,并查询一次表的总行数。
- 会话B启动事务,插入一行后,再查询表的总行数。
- 会话C先启动一个单独的插入语句,然后在查询表的总行数。
结果:虽然最后一个时刻,会话A,B,C同时查询表t的总行数,但是拿到的结果都不同。
A返回10000,B返回10002,C返回10001。
A,B,C拿到结果不一样和InnoDB事务设计有关系,可重复读是它默认的隔离级别,在代码上使用多版本并发控制(MVCC)实现,每一行记录都要判断自己是否对这个会话可见,因此对于count(*)请求,InnoDB只好把数据一行一行读出判断对于当前事务是否可见,以此来计算总行数。
InnoDB针对count()也是做了优化的,由于主键索引树的叶子节点是整行数据,而普通索引树的叶子节点是主键索引,所以普通索引树比主键索引树小得多,对于count()这样的操作,MySQL优化器会找到最小的那棵树来遍历,尽可能减少扫描的数据量。
关于show table status命令获取总数
使用show table status命令,会输出一个TABLE_ROWS,这个是采样估算的,官方文档给出误差可能达到40%-50%。
如果让一张表的总数查询变快
- 方法一:数据库保存技术,额外加一张表直接保存某个表的总数,推荐。
- 方法二:用redis保存,但是可能有不一致问题。
count(*)、count(主键id)、count(字段)、count(1)用法区别,性能差别。
首先,count()是一个聚合函数,对于返回的结果集,一行行地判断,如果count()函数的参数不是null,就累计加1,最后返回结果。
取数对比如下:
- count(*)、count(主键id)、count(1)都表示返回满足条件的结果集的总行数。
- count(字段)表示返回满足条件的数据行里,参数"字段"不为null的总行数。
性能:count(*)≈count(1)>count(主键)>count(字段)
性能区分如下:
-
count(主键id):InnoDB会遍历整张表,把每一行的id值都取出来,返回给server层,Server层拿到id后,判断不为空就按行累加。
-
count(1):InnoDB引擎遍历整张表,但不取值,Server层对于返回的每一行,放一个数字"1"进去,判断是不可能为空的,按行累加。
- count(1)执行比count(主键id)快,因为count(主键id)从引擎返回id会涉及到解析数据行,以及拷贝字段值的操作。
-
count(字段):
- 如果这个字段定义为not null,那么需要一行行从记录里面读出这个字段,判断不能为null,按行累加。
- 如果这个字段定义为default null,那么执行的时候,判断有可能是null,还需要把值取出来判断一下,不是null才累加。
-
count(* ):不会把所有字段取出来,专门做了优化,并不会取值,count(*)肯定不是null,按行累加。
【总结】性能:count(*)≈count(1)>count(主键)>count(字段)