麻烦不要再问我count(*)、count(1)、count(id)、count(name)之间的区别了
大家好,我是大都督周瑜,一个在java、python、c++和各种技术源码之间反复横跳的程序员,欢迎关注我的公众号:IT周瑜。
虽然这篇文章会分析MySQL的源码,但是我保证,只要你认认真真的读完,一定能从本质上彻底掌握count(*)、count(1)、count(id)、count(name)之间的区别。
首先,count()函数是MySQL众多聚合函数的一种,MySQL源码中通过Item_sum类来实现各种聚合函数,比如下图的各个类就对应了各种聚合函数的实现,比如Item_sum_count类对应的就是count()聚合函数。
我们都知道count()是用来计数的,因此在Item_sum_count类中定义了一个add()方法,它的具体实现为:
bool Item_sum_count::add()
{
// 这是重点
if (aggr->arg_is_null(false))
return 0;
// 这是重点
count++;
return 0;
}
其中arg_is_null()是用来判断参数是否为null,而count()函数中的入参就是这里的参数,比如count()中的,count(1)中的1,count(id)中的id,count(name)中的name,如果参数为null就不进行count++,不为null才进行count++,而这个count变量就是count()函数最终的返回结果。
也就是说每执行一次count()函数,如果入参不为null,就会对count变量进行加1,那到底要执行多少次count()函数呢?
自然就跟SQL语句所查出的记录数有关系了,查出了10条那就执行10次count()函数,但最终count变量是不是10就不一定,得看传入给count()函数的入参是不是为null。
MySQL层在执行SQL时,会循环调用InnoDB中的row_search_mvcc()函数从索引树上一行一行读取记录,注意每次只读取一行,MySQL层拿到一行数据后,会先进行where条件的过滤,再执行count()函数进行统计。
InnoDB每取到一行数据,不一定会把整行的所有字段都返回给MySQL层,而是按需返回。
比如,如果SQL是:
select count(id) from user_info;
那就只返回当前行的id字段的值给MySQL层,然后MySQL层把该值传给count()函数,进行判空和count++。
如果SQL是:
select count(id) from user_info where name = 'zhouyu';
那就只返回当前行的id和name字段的值给MySQL层,name字段值是用来给MySQL进行where条件过滤的,如果当前行的name字段值符合where条件,才将当前行的id字段值传给count()函数,进行判空和count++。
在row_search_mvcc()函数中比较靠后的位置会调用row_sel_store_mysql_rec()函数,这个函数的作用是把InnoDB的行数据格式转成MySQL层的行数据格式,同时这个函数也会过滤出MySQL层需要的字段,代码为:
for (i = 0; i < prebuilt->n_template; i++) {
const mysql_row_templ_t*templ = &prebuilt->mysql_template[i];
// ...
}
n_template表示MySQL层需要几个字段,mysql_template表示具体需要的字段,比如mysql_template中的col_no属性表示需要表的第几个字段,我的场景中id字段的col_no=0,name字段的col_no=1。
基于此,我们来分析各种count()函数的使用场景。
假如user_info中有3条数据,都不使用where条件,大家只需要知道where条件是用来过滤数据的即可。
先来看count(1)的场景:
select count(1) from user_info;
- MySQL先进行SQL语法解析,由于SQL中并没有使用某个字段,因此n_template为0
- MySQL层调用row_search_mvcc()取出表的第1行记录,不需要返回任何字段给MySQL层
- MySQL层执行count()函数,入参为数字1,1不为null(任何数字都不为null,包括0),所以count++
- MySQL层调用row_search_mvcc()取出表的第2行记录
- 循环执行...
- 最终count为3
再来看count(id)的场景
select count(id) from user_info;
- MySQL先进行SQL语法解析,由于SQL中使用了id字段,因此n_template为1
- MySQL层调用row_search_mvcc()取出表的第1行记录
- 因为SQL中使用了id字段,所以返回第1行记录的id字段的值给MySQL层
- MySQL层执行count()函数,入参为当前记录id字段的值,该值不为null,所以count++
- MySQL层调用row_search_mvcc()取出表的第2行记录
- 循环执行...
- 最终count为3
id字段一般不会为null。
再来看count(name)的场景
select count(name) from user_info;
和count(id)类似,只不过有可能某行数据的name字段为null,则不会进行count++,因此最终的结果不一定为3。
以上是count(1)、count(id)、count(name)的执行流程。
那count(*)呢,在SQL中*号表示获取全部字段,那count(*)也是吗?
先看select *是不是查询所有字段,当我执行:
select * from user_info;
n_template为18,因为我表中确实有18个字段-_-,没有问题。
但是当我执行count(*)时:
select count(*) from user_info;
n_template为0,表示不需要查出字段
为什么呢?
这就跟MySQL的语法规则解析有关的,在sql_yacc.yy中有以下规则:
仔细看,这三个规则分别代表了count(*),count(表达式),count(distinct 表达式)三种语法情况,这里的表达式要么是常量,要么是字段(相当于变量),而且它们对应要执行的函数也不一样。
- count(*)会执行PTI_count_sym(@$),只传了一个参数
- count(表达式)会执行Item_sum_count(@$,$3),传了两个参数,第二个参数$3表示表达式的内容,要么是一个字段,要么是一个常量
但实际上PTI_count_sym(@$)中仍然会执行Item_sum_count(),只不过第二个参数传的是null,表示不使用字段,注意这里不能理解为count(null)。
因此,count(*)中的*不能理解为所有字段,MySQL层并不会把一行记录的所有字段内容都取出来,而是不会取任何字段。
仔细想想也是合理的,对于count()函数而言,没有必要把一行记录的所有字段内容都从磁盘中取出来然后再进行统计。
因此,count(*)和count(1)、count(0)这些本质没有区别,都不会取出具体的字段内容,但是count(id)、count(name)这些就会取出具体的字段内容,并进行判null,不为null才进行count++。
count(id)、count(name)和count(*)和count(1)、count(0)不管是逻辑上,还是性能上都是不一样的,大家平时用的时候要注意。
另外要注意,当我们执行count()时,会遍历表中的每一行的,如果表只有一个聚集索引,那就会进行全部扫描,但是如果表中有多个索引,则会选择占用页数最少的索引,遍历它的叶子节点即可,因为不管是什么字段构建的索引,数据条数都是一样的,比如,聚集索引的叶子节点中有10000条数据,每条数据有5个字段,而另外一个辅助索引的叶子节点中肯定也有10000条数据,只不过每条数据可能只有2个字段,此时辅助索引占用的页数肯定更少,但是条数是一样的,此时count()就会走辅助索引来提高统计速度。
好啦,关于MySQL中的count()函数就分享到这了,有疑问欢迎在评论区讨论.
我是大都督周瑜,一个在java、python、c++和各种技术源码之间反复横跳的程序员,如果觉得好就帮我把这篇文章分享出去,让程序员们更合理的使用count(),让这个世界少一点bug,多一点幸福。
也再次欢迎大家关注我的公众号:IT周瑜,第一时间学习我分享的技术文章,同时里面有我个人的联系方式,我可以免费帮大家优化简历,还可以免费领取我整理的高质量面试题,欢迎大家。
转载自:https://juejin.cn/post/7397410962847039525