在开发中,我们一定经常遇到需要统计表总记录的时候,在 select count(?) from t 这样的查询语句里面,count(*)、count(主键 id)、count(字段) 和 count(1) 该怎么选择呢?
阿里巴巴开发规约其中MySQL规约中,有4条都在规范count的用法,我们查看其中的一条:
【强制】不要使用count(列名)或count(常量)来替代count(*), count(*)就是SQL92定义的标准统计行数的语法,跟数据库无关,跟NULL和非NULL无关。
说明:count(*)会统计值为NULL的行,而count(列名)不会统计此列为NULL值的行。
那么今天我们就来聊聊这几种不同的统计方式是怎么实现的,他们之间的性能差别是什么。
count()函数
首先我们需要了解count() 的语义。
count()是一个聚合函数,对于返回的结果集,一行行的判断,如果count函数的参数不是null,累计值就加1,否则不加。最后返回累计值。如果没有则返回0。
count(*)、count(主键 id) 和 count(1) 都表示返回满足条件的结果集的总行数;而 count(字段),则表示返回满足条件的数据行里面,参数“字段”不为null的总个数。
接下来我们具体分析每种统计方式的性能差别。
count(*)
我们在分析一条SQL语句的实现时,一定不能脱离存储引擎,在不同的MySQL存储引擎中,count(*)有不同的实现。
- MyISAM引擎把一个表的总行数存在了磁盘上,所以在执行count(*)的时候直接返回磁盘上的这个数就可以,效率很高。
- InnoDB引擎是需要一行一行地从引擎中读出数据然后累计计数。
这个时候你是不是在想,MySQL建表时的存储引擎已经默认为InnoDB了,而且很多MySQL相关的书籍中也提到不是特别场景的必要,就使用InnoDB。
但是我们在使用count(*)做统计的时候随着表记录的增加效率会越来越慢,InnoDB怎么就不能跟MyISAM一样也把总记录数提前存储起来呢?
这里主要的一个原因是InnoDB是支持事务的,由于MVCC的原因,InnoDB表在某个时刻应该返回多少行是不确定的,它必须根据当前的事务隔离级别判断某一个记录对于当前事务是否可见。
虽然InnoDB是需要一行一行的统计,但是MySQL也是做了优化的。通过前面索引文章介绍的知识我们了解到,InnoDB分为主键索引树和普通索引树,普通索引树的叶子节点是主键值而不是行记录。所以,普通索引树要比主键索引树小很多,所以count(*)在遍历索引树的时候会找最小的那棵树来遍历。
在保证逻辑正确的前提下,尽量减少扫描的数据量,是数据库系统设计的通用法则之一。
count(*)也并不会把全部字段取出来,而是做了优化,不取值。
count(*)的参数肯定不是null,按行累加之后返回结果。
count(主键 id)
count(主键 id) ,InnoDB引擎会遍历整张表,把每一行的id值取出来,传给server层。server层拿到引擎给的id值后,判断是不可能为空的,就按行累加。
count(1)
count(1),InnoDB引擎遍历整张表,但是不取值。server层对于返回的每一行存放一个数字1,判断是不可能为空过的,按行累加。
count(字段)
而对于count(字段)字段来说,我们需要分两种情况分析参数字段。
如果这个参数字段是不允许为null(not null)的,那么InnoDB引擎则一行行地从记录里面读取这个字段,判断不能为null,则按行累加。
如果这个参数字段是允许为null的,那么执行的时候,判断到可能是null的时候还要把值取出来再判断一次,不是null才累加。
小结
count(字段)和count(主键 id)因为需要从引擎中返回值,会涉及到解析数据行以及拷贝字段的操作,性能上差于其他两种。
而count(字段)还需要判断字段参数是否为null,多了判断的操作,性能上会差于count(主键 id)。
count(1)和count(*)都不会涉及到取值,性能上差别不大。
所以结论就是,按照查询效率排名结果如下:
count(字段)<count(主键 id)<count(1)≈count(*)。
按照开发规范,建议尽量使用count(*)。
我是勾勾,认真生活,快乐工作!愿你每天开心!