看极客时间的专栏《MySQL实战45讲》,14篇提到了MySQL聚合函数count。对于文中count(*)和count(1)的性能比较有一点争议。我把原文中关键点摘抄出来(更多内容,请购买专栏)。
count(*)的实现方式
你首先要明确的是,在不同的MySQL引擎中,count(*)有不同的实现方式。
- MyISAM引擎把一个表的总行数存在了磁盘上,因此执行count(*)的时候会直接返回这个数,效率很高;
- 而InnoDB引擎就麻烦了,它执行count(*)的时候,需要把数据一行一行地从引擎里面读出来,然后累积计数。
这里需要注意的是,我们在这篇文章里讨论的是没有过滤条件的count(*),如果加了where 条件的话,MyISAM表也是不能返回得这么快的。
在前面的文章中,我们一起分析了为什么要使用InnoDB,因为不论是在事务支持、并发能力还是在数据安全方面,InnoDB都优于MyISAM。我猜你的表也一定是用了InnoDB引擎。这就是当你的记录数越来越多的时候,计算一个表的总行数会越来越慢的原因。
不同的count用法
需要注意的是,下面的讨论还是基于InnoDB引擎的。
这里,首先你要弄清楚count()的语义。count()是一个聚合函数,对于返回的结果集,一行行地判断,如果count函数的参数不是NULL,累计值就加1,否则不加。最后返回累计值。
所以,count(*)、count(主键id)和count(1) 都表示返回满足条件的结果集的总行数;而count(字段),则表示返回满足条件的数据行里面,参数“字段”不为NULL的总个数。
至于分析性能差别的时候,你可以记住这么几个原则:
server层要什么就给什么;
InnoDB只给必要的值;
现在的优化器只优化了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才累加。
也就是前面的第一条原则,server层要什么字段,InnoDB就返回什么字段。
但是count(*)是例外,并不会把全部字段取出来,而是专门做了优化,不取值。count(*)肯定不是null,按行累加。
看到这里,你一定会说,优化器就不能自己判断一下吗,主键id肯定非空啊,为什么不能按照count(*)来处理,多么简单的优化啊。
当然,MySQL专门针对这个语句进行优化,也不是不可以。但是这种需要专门优化的情况太多了,而且MySQL已经优化过count(*)了,你直接使用这种用法就可以了。
所以结论是:按照效率排序的话,count(字段)<count(主键id)<count(1)≈count(*),所以我建议你,尽量使用count(*)。
补充一:count(*)的优化
MySQL document:https://dev.mysql.com/doc/refman/8.0/en/aggregate-functions.html#function_count
As of MySQL 8.0.13,
SELECT COUNT(*) FROM
query performance fortbl_name
InnoDB
tables is optimized for single-threaded workloads if there are no extra clauses such asWHERE
orGROUP BY
.
InnoDB
processesSELECT COUNT(*)
statements by traversing the smallest available secondary index unless an index or optimizer hint directs the optimizer to use a different index. If a secondary index is not present,InnoDB
processesSELECT COUNT(*)
statements by scanning the clustered index.
从MySQL8.0.13开始(对比mysql 5.6的文档,就没有这一段优化的描述:https://dev.mysql.com/doc/refman/5.6/en/aggregate-functions.html#function_count),count(*)的优化去是去遍历统计可用的最小的辅助索引来得到到count(*)的值,仔细想一想,这个辅助索引肯定是not null的。如果没有这样的辅助索引,那就会去统计主键索引,当然性能还是比count(pk)要好,因为不涉及解析主键字段。
此外,官方文档还提到了,如果辅助索引没有全部在缓冲池中,那么count(*)性能肯定受影响,这时候你最好自己去做个统计表,然后在增删的时候去维护这个统计值,当然专栏里面除此之外还提了几个方案。
Processing
SELECT COUNT(*)
statements takes some time if index records are not entirely in the buffer pool. For a faster count, create a counter table and let your application update it according to the inserts and deletes it does. However, this method may not scale well in situations where thousands of concurrent transactions are initiating updates to the same counter table. If an approximate row count is sufficient, useSHOW TABLE STATUS
.
补充二:count(*)和count(1)
MySQL document:https://dev.mysql.com/doc/refman/8.0/en/aggregate-functions.html#function_count
专栏里面提到性能count(*)≈count(1),但是如果按照官方文档所述,其实可以写count(*)=count(1)。下文引用的这一段在MySQL8和MySQL5.6的count文档中都提到了。
InnoDB
handlesSELECT COUNT(*)
andSELECT COUNT(1)
operations in the same way. There is no performance difference.
按照官网文档所述,count(*)和count(1)操作是一样的,没有性能差异。
以上两点,作为该专栏14章节的一个补充。另外,专栏值得购买。