参考:小林coding
count的定义
count()是一个聚合函数,它的作用是统计符合查询条件的记录中,函数指定的参数不为NULL的记录有多少个。参数不仅可以是字段名,也可以是表达式。
举个例子:
select count(name) from tb_user;
这条语句的作用是,统计tb_user中,有多少条记录的name字段不为NULL。
如果是下面这个呢?
select count(1) from tb_user;
根据定义来说,统计tb_user中,有多少条记录的1这个表达式不为NULL。1这个表达式永远都不为NULL,所以它其实是在统计tb_user中有多少条记录。
count()原理
原理解析均以InnoDB存储引擎为例。
调用count函数时,MySQL会在Server层维护一个变量count,初始值为0。Server层会循环向存储引擎层读取记录,每次读取一条记录,就判断count函数执行的参数是否为NULL,如果不为NULL则加1,直到符合查询条件的记录被读取完毕,就结束循环,最后将count值返回给客户端。
count(主键字段)原理
以下面这条语句为例:
select count(id) from tb_user;
如果表里只有主键索引,没有二级索引的话,存储引擎会遍历聚簇索引的叶子节点,将读取到的记录返回给server层,server层再去判断主键是否为NULL,不为NULL则将count变量加1。
如果表里有二级索引,存储引擎就会选择遍历二级索引的叶子节点。这是因为二级索引的叶子节点中也存储了主键,并且二级索引占用的存储空间更小,磁盘I/O的成本更小,因此在当前这个案例下,优化器会优先选择二级索引。
当然,不是说count(主键字段)一定会选择二级索引,如果还有其他的查询条件,比如where 主键字段 = xx这种,优化器就会选择使用聚簇索引,所以需要具体情况具体分析。
count(1)原理
如果表里只有主键索引,没有二级索引的话,存储引擎会遍历聚簇索引的叶子节点,将读取到的记录返回给server层,但是server层不会去判断任何字段是否为NULL,因为显然1不为NULL,因此每次读取到就将count值加1即可。 除此之外,count(1)和count(主键字段)就没有差别了,原理同上。
显然,因为少了一个判断的步骤,通常count(1)会比count(主键字段)的效率更高。
count(*)原理
在MySQL中,count()等同于count(0),所以count()的原理和效率和count(1)是一样的。count(字段)的原理基本也是一致的,会根据具体情况去选择使用哪个索引,一般来说,会使用长度(key_len)最短,并且包含了所需字段的索引。
性能比较
count(1) ≈ count(*) > count(主键) ≥ count(字段)
至于最后一个为什么是≥,显然在最差的情况下,就是count(主键)和count(字段),表中只有主键索引,或者有一个长度最短的二级索引包含了该字段,这种情况下count(主键)的性能与count(字段)基本一致,剩下的大部分情况下,count(主键)的性能均好于count(字段),当然,还是那句话,具体情况具体分析。
为什么InnoDB的count()函数要通过遍历来计数,而不是直接存储一个count值在表的元数据中?
因为MVCC的存在,所以即使是同一时刻、同一条sql语句,不同的事务调用count()得到的结果也不一定是一样的。
比如在可重复读隔离级别下,t_order有100条记录,接下来两个事务并行执行以下语句:
在最后一个时刻,因为事务B的事务id大于等于事务A的ReadView中的max_trx_id,所以事务B插入的这条记录对事务A是不可见的,所以事务A执行count()的结果仍然为100,这条记录是事务B自己插入的,所以对事务B可见,所以事务B执行count()的结果为101。如果这部分不懂的话,可以查看我的另一篇文章《MySQL事务详解》。
如何优化count()?
在上面的讲解中,我们可以知道,可以通过创建合适的二级索引,以及使用count(1)或count(*)对count()进行优化。但是如果在遇到非常大的数据量时,即使使用了这些优化手段,count的性能还是会非常的差。
如果我们不需要精确的count值,那我们只需要使用explain命令即可,它会将一个估算的行数值放在rows字段中返回。
如果频繁地需要查询精确的count值的话,就额外维护一张计数表,在插入或者删除时将计数表加1或者减1。