百度360必应搜狗淘宝本站头条
当前位置:网站首页 > IT技术 > 正文

深度剖析:MySQL聚合函数 count() ,这下彻底搞懂了!

wptr33 2025-03-01 15:45 22 浏览

在实际开发工作中,难免会使用到 MySQL 的 count(expr) 函数进行统计操作,但是,对于count(1)、count(*)、count(常量)、count(主键)、count(非主键)、count(distinct(字段)) 等多个函数,很多开发人员因为缺乏原理性的了解,往往会比较困惑选择哪一种,特别是在加 where 条件时,更加担心会不会造成性能问题,今天我们就来聊聊这些 count(expr) 函数背后的实现原理以及它们的执行效率,帮你胸有成竹的选择合适具体函数。

申明:本文基于 MySQL 8.0.30,数据库引擎为 InnoDB引擎 和 MyISAM引擎;

本文的count(expr)操作都是基于不加 where条件

如果需要mac本地安装 MySQL,参考:macOS M1 源码安装 MySQL8 版本

背景

count(expr) 是 MySQL 的一个聚合函数,函数接收一个 expr 表达式,expr 表达式可以是 NULL、可以是列名,可以是常量,其他任意表达式都可以,它有 2个重要的作用:统计某个列值的数量以及统计表行数。在统计列数量时只统计列值不为 NULL 的总数,统计表总行数时不做限制。 为了更好地展开本文的讲解,需要先创建 user 和 person 两张表,user 表使用 InnoDB 引擎,person 表使用 MyISAM 引擎,表创建完成后,查看它们在磁盘上的文件信息对比,具体对比如下截图:

从上述截图可以看出:

在使用 MyISAM引擎的 person表中,表定义,数据,索引是分三个文件存储,如下:

  • person_365.sdi,存储 person表定义,sdi(Serialized Dictionary Information,序列化字典信息),MySQL 8.0引入,以前的版本是 .frm;
  • person.MYD,存储 person表数据,MYD(MyISAM Data);
  • person.MYI,存储 person表索引,MYI(MyISAM Index);

在使用 InnoDB引擎的 user表中,表定义,数据,索引都存放在一个文件中,如下:

  • user.ibd,ibd(innodb data)

接下来正式分析各个count(expr)操作

count(*)

对于 MyISAM 引擎,会把表的总行数存在了磁盘上(存放在 information_schema 库中的 PARTITIONS 表中),在不加 where 条件时,执行 count( ) 时会直接返回这个总数,因此效率很高,但是在加 where 限定语句的时候 MySQL 需要对全表进行检索从而得出 count 的总数。 而 InnoDB 引擎并没有像 MyISAM 那样把表的总行数存储在磁盘,而是在执行 count( )时,在不加 where 限定语句时,MySQL Server 层需要把数据从引擎里面读出来,然后逐行累加得出总数;如果加了 where 限定语句,需要根据 where 条件从引擎里面筛选出数据,然后累加得出总数。 下图展示了 MyISAM 引擎磁盘存储数据总行数:

为了更好地理解两种引擎中 count( )的差异,我们可以比较count( ) 在 user 表和 person 表 中的执行计划,结果如图:

从上述执行计划的截图可以看出:

  • InnoDB 引擎执行计划显示 rows = 2(全表只有 2 行数据),说明需要扫描全表,Extra 里面的内容是 “Using index”,说明该 count( ) 操作使用了索引(主键索引)。
  • MyISAM 引擎执行计划显示 rows = NULL,说明不需要扫描表,Extra 里面的内容是 “Select tables optimized away”,它的意思是:MyISAM 表以单独的行数存储总数,执行 count 查询时,MySQL 不需要查看任何表行数据,而是将预先计算的行数立即返回。

那么,为什么 InnoDB引擎不能像 MyISAM引擎一样,把表的数据总数存起来,而是需要扫描全表呢?

这是因为 InnoDB引擎可以支持事务,默认的隔离级别是 Repeatable Read(可重复读,指的是一个事务执行过程无法看到其它事务未提交的数据),而可重复读又是通过多版本并发控制(MVCC)来实现的,MVCC更直白的表述是:一行记录在不同的事务中表现的结果值是不一样的,呈现出一行记录多种版本数据。 关于 MVCC可以参照下面的图来理解:

为了更好地说明 InnoDB 引擎为什么不把数据总行数存磁盘,这里通过一个案例进行解析:假设有两个事务 sessionA,sessionB

  • sessionA 先启动一个事务,然后 select count(*) from user 统计总行数;
  • sessionB 也启动事务,先执行一次 select count( ) from user,然后插入一行数据,再 select count( ) from user,统计总行数;

执行顺序流以及截图如下:

sessionA

sessionB

#开启事务 begin;


select count(*) from user;



select count(*) from user;


#插入一条数据 insert into user(id,age) values(3,30);

select count(*) from user;

select count(*) from user;

通过运行结果截图,我们可以看出,sessionB 新增一条记录后,在未 commit 提交事务前,sessionA 的 count( ) 操作并没有把这条数据统计进去,符合可重复读隔离级别的要求,假如 InnoDB 也像 MyISAM 一样把行的总数存在磁盘上,那么 sessionA 和 sessionB 的 count( ) 结果值相同,也就是说 sessionA 和 sessionB 最后一次 count(*) 的结果值都是 3,这显然就违背了可重复读隔离级别的要求。

所以,通过该案例的分析也刚好验证了上述 explain count(*) 的执行计划,InnoDB引擎需要全表扫描。

有人说,在使用 InnoDB 引擎的 user表上执行 “show table status” 指令,结果中的 ROWS 字段就是表的总行数,快捷方便,效率高。那么,可行吗?

方法是否有效,我们还是用事实说话,我们可以执行”show table status”指令,执行顺序流和结果截图如下:

sessionA

sessionB

#开启事务 begin;

#开启事务 begin;

select count(*) from user;



select count(*) from user;


#插入一条数据 insert into user(id,age) values(3,30);

select count(*) from user;

select count(*) from user;

show table status\G

show table status\G

通过运行结果截图可以看出,sessionA的 count(*)结果和 “show table status”指令结果中的 ROWS值相等,但是在 sessionB中两个值就不一样,因此说,通过 “show table status”来统计总数,结果值是不准确的。

按照 MySQL官方的说法: “show table status”命令显示行数的误差率在 40% ~ 50%。

show table status 查询的是系统 information_schema 库中的 TABLES 表,关于表字段可以参考官方文档:TABLES 官方文档

需要说明的是:尽管 InnoDB引擎的 count( * )操作需要扫描全表,但是 MySQL还是有做过优化处理,具体优化如下:

因为 InnoDB引擎采用的是聚簇索引机制,主键索引的叶子节点存放了数据,而普通索引的叶子节点存放的是主键值。因此,不管遍历哪一棵索引树,count(*)的结果都是一致的,所以,MySQL优化器会找到最小的那棵索引树进行遍历,这样尽管扫描的行数没有减少,但是针对每行记录获取的数据量减少了,因此性能就提升了。

有了上述对 count( * )的讲解,我们分析和理解其他几种 count(expr)操作就会轻松很多,在 InnoDB引擎中,count(expr) 是一个聚合函数,对于引擎返回的结果集,MySQL Server会逐行判断,count(参数)函数最终就是统计”参数不是 NULL”的总数作为结果值。

count(主键)

count(主键)的执行逻辑为:InnoDB引擎会遍历整张表,然后取出每一行的主键值并返回给 MySQL Server层,Server层拿到引擎的结果值后,统计主键总数量。

count(1)

count(1)的执行逻辑为:InnoDB引擎遍历整张表,但是不会取数据值,MySQL Server层对于引擎返回的每一行记录都放置一个数字“1”,最终再统计包含 1的总行数。

所以,count(1)操作要比 count(主键)快。因为count(主键)需要从引擎返回主键值,过程中会涉及到数据行的解析,字段值的拷贝等I/O操作。

count(非主键字段)

count(非主键字段)操作有些特殊,我们先看一张截图:

如上述截图:

InnoDB引擎的 user表中最开始有 3条数据,然后执行 insert into user(id,age) values (4,NULL); 插入一条 age=NULL的数据,各种count(expr)的结果为:

  • count(*) = 4;
  • count(1) = 4;
  • count(id) = 4;
  • count(age) = 3;

MyISAM引擎的 person表中最开始有 3条数据,然后执行 insert into person(id,name) values (4,NULL); 插入一条 name=NULL的数据,各种count(expr)的结果为:

  • count(*) = 4;
  • count(1) = 4;
  • count(id) = 4;
  • count(name) = 3;

从结果可以发现,不管是使用了InnoDB 引擎的user,还是使用了 MyISAM引擎的 person表,通过 count(非主键)方式进行统计时,表的总行数就会比其他几种 count(expr)方式少 1 条,为什么呢?

这是因为:count(非主键) 只统计非主键字段值不为NULL的总数。

源码分析

从上面的理论分析后,我们再从源码角度看下 MySQL 是如何实现 count(expr) 的,MySQL的源码是 C++实现的。count() 函数的实现源码是 item_sum.cc 文件,在 server 端维护了一个 count 变量来记录 count(expr) 的结果值,源码截图如下:

接着看下 arg_is_null() 函数的源码实现,截图如下:

其中 item[i] 表示的就是 count(expr) 函数中 expr 表达式的内容,可以通过调试看下 item[i] 的值是什么。

count(*) 调试结果如下:

上述截图可以看出,当expr=* 时,在MySQL源码中会把的值当做 longlong 类型的0 处理,因此 count() 就转换成 count(0) 了。

count(1) 调试结果如下:

在MySQL源码中会把count(1)中的expr=1直接当做 longlong 类型的1 处理,解释了 count(1),也就很好理解 count(数字常量)这些统计操作了。

count(字段名) 调试结果如下:

通过截图可以看到,当 expr=字段名,对应的类型是Item_field,代表一个字段。

温馨提示:因为历史原因,C++中的 longint相当于Java中的int,longlong其实相当于Java中的long。

最后将InnoDB count(expr)整个过程笼统地归纳为下图:

总结

count(expr)函数的参数 expr可以是任意表达式,该函数用于统计在符合搜索条件的记录总数;

通过上面的对比和分析,我们可以得出 count(expr)函数按照执行效率从低到高依次排序为:

count(非主键字段) < count(主键) < count(1) ≈ count( * )

因此,count(1) 或者 count( * )的效率最高。对于这两种方式的选择,建议尽量使用 count( * ),因为 MySQL优化器会选择最小的索引树进行统计,我们把这个优化的问题交给 MySQL优化器去解决。

count( * )、count(主键)、count(1) 都是返回满足条件的结果总行数;而 COUNT( *) 它返回检索到的行数,无论它们是否包含 NULL值,count(非主键字段),统计”非主键字段”不为 NULL 的总行数。

在生产中,如果对数据不要求特别精确,可以使用 “show table status” 方式获取;

如果没有匹配的行,则 count(expr)返回 0。COUNT(NULL)返回 0;

对于 InnoDB 引擎,如果表只有一个主键索引,没有任何二级索引的情况下,那么 count( ) 和 count(1) 都是通过通过主键索引来统计行数。如果表有二级索引,则 count( ) 和 count(1) MySQL优化器会选择最小的索引树进行统计。

相关推荐

oracle数据导入导出_oracle数据导入导出工具

关于oracle的数据导入导出,这个功能的使用场景,一般是换服务环境,把原先的oracle数据导入到另外一台oracle数据库,或者导出备份使用。只不过oracle的导入导出命令不好记忆,稍稍有点复杂...

继续学习Python中的while true/break语句

上次讲到if语句的用法,大家在微信公众号问了小编很多问题,那么小编在这几种解决一下,1.else和elif是子模块,不能单独使用2.一个if语句中可以包括很多个elif语句,但结尾只能有一个else解...

python continue和break的区别_python中break语句和continue语句的区别

python中循环语句经常会使用continue和break,那么这2者的区别是?continue是跳出本次循环,进行下一次循环;break是跳出整个循环;例如:...

简单学Python——关键字6——break和continue

Python退出循环,有break语句和continue语句两种实现方式。break语句和continue语句的区别:break语句作用是终止循环。continue语句作用是跳出本轮循环,继续下一次循...

2-1,0基础学Python之 break退出循环、 continue继续循环 多重循

用for循环或者while循环时,如果要在循环体内直接退出循环,可以使用break语句。比如计算1至100的整数和,我们用while来实现:sum=0x=1whileTrue...

Python 中 break 和 continue 傻傻分不清

大家好啊,我是大田。今天分享一下break和continue在代码中的执行效果是什么,进一步区分出二者的区别。一、continue例1:当小明3岁时不打印年龄,其余年龄正常循环打印。可以看...

python中的流程控制语句:continue、break 和 return使用方法

Python中,continue、break和return是控制流程的关键语句,用于在循环或函数中提前退出或跳过某些操作。它们的用途和区别如下:1.continue(跳过当前循环的剩余部分,进...

L017:continue和break - 教程文案

continue和break在Python中,continue和break是用于控制循环(如for和while)执行流程的关键字,它们的作用如下:1.continue:跳过当前迭代,...

作为前端开发者,你都经历过怎样的面试?

已经裸辞1个月了,最近开始投简历找工作,遇到各种各样的面试,今天分享一下。其实在职的时候也做过面试官,面试官时,感觉自己问的问题很难区分候选人的能力,最好的办法就是看看候选人的github上的代码仓库...

面试被问 const 是否不可变?这样回答才显功底

作为前端开发者,我在学习ES6特性时,总被const的"善变"搞得一头雾水——为什么用const声明的数组还能push元素?为什么基本类型赋值就会报错?直到翻遍MDN文档、对着内存图反...

2023金九银十必看前端面试题!2w字精品!

导文2023金九银十必看前端面试题!金九银十黄金期来了想要跳槽的小伙伴快来看啊CSS1.请解释CSS的盒模型是什么,并描述其组成部分。答案:CSS的盒模型是用于布局和定位元素的概念。它由内容区域...

前端面试总结_前端面试题整理

记得当时大二的时候,看到实验室的学长学姐忙于各种春招,有些收获了大厂offer,有些还在苦苦面试,其实那时候的心里还蛮忐忑的,不知道自己大三的时候会是什么样的一个水平,所以从19年的寒假放完,大二下学...

由浅入深,66条JavaScript面试知识点(七)

作者:JakeZhang转发链接:https://juejin.im/post/5ef8377f6fb9a07e693a6061目录由浅入深,66条JavaScript面试知识点(一)由浅入深,66...

2024前端面试真题之—VUE篇_前端面试题vue2020及答案

添加图片注释,不超过140字(可选)1.vue的生命周期有哪些及每个生命周期做了什么?beforeCreate是newVue()之后触发的第一个钩子,在当前阶段data、methods、com...

今年最常见的前端面试题,你会做几道?

在面试或招聘前端开发人员时,期望、现实和需求之间总是存在着巨大差距。面试其实是一个交流想法的地方,挑战人们的思考方式,并客观地分析给定的问题。可以通过面试了解人们如何做出决策,了解一个人对技术和解决问...