MySQL count()详解

0 阅读4分钟

参考:小林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条记录,接下来两个事务并行执行以下语句:

image.png

在最后一个时刻,因为事务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。