面试官:mysql中count(*)和count(1)哪个效率高?

你好, 我是程序猿零壹。

很多同学在面试的时候,经常会被面试官提问的一个问题,那就是在mysql中,使用count(*)和count(1)进行数据行统计哪个效率更高?今天我们就来讨论下这个问题。

我曾经以为count(*)是效率最差的,因为认知上select * from table会读取表中的所有字段,所以凡是带有*字符的语句都会读取表中所有的字段,当时很多博文也这么说。但是当我更深一步理解count()函数的原理后,就被啪啪啪的打脸了。

count()是什么

在mysql中,count()是一个聚合函数,函数的参数可以是字段名,也可以是其他任意的表达式。该函数的作用是统计符合查询条件中的记录中,函数指定的参数不为null的记录有多少个。

假设count()函数的参数是具体的字段名,如下:

select count(column) from table;

这条语句的意思是统计table表中,column字段不为null的记录有多少个。也就是说,如果某一条记录中的column为null,那么这条记录就不会被统计。

假设count()函数的参数是数字1,如下:

select count(1) from table;

这条语句的意思是统计table表中,1这个表达式的不为null的记录有多少个。1是一个数字,它永远都不会是null,所以上面这条语句就相当于是统计table表中有多少个记录。

count()的实现方式

首先要明确的是,在不同的mysql引擎中,count(*)有不同的实现方式。

  • MyISAM 引擎把一个表的总行数存在了磁盘上,因此执行 count(*) 的时候会直接返回这个数,效率很高;
  • 而 InnoDB 引擎就麻烦了,它执行 count(*) 的时候,需要把数据一行一行地从引擎里面读出来,然后累积计数。

使用 MyISAM 引擎时,执行 count 函数只需要 O(1 )复杂度,这是因为每张 MyISAM 的数据表都有一个 meta 信息有存储了row_count值,由表级锁保证一致性,所以直接读取 row_count  值就是 count 函数的执行结果。

而 InnoDB 存储引擎是支持事务的,同一个时刻的多个查询,由于多版本并发控制(MVCC)的原因,InnoDB 表“应该返回多少行”也是不确定的,所以无法像 MyISAM一样,只维护一个 row_count 变量。

注意,在没有任何查询条件下的 count(*),MyISAM 的查询速度要明显快于 InnoDB。而当带上 where 条件语句之后,MyISAM 跟 InnoDB 就没有区别了,它们都需要扫描表来进行记录个数的统计。

那为什么 InnoDB 不跟 MyISAM 一样,也把数字存起来呢?

这是因为即使是在同一个时刻的多个查询,由于多版本并发控制(MVCC)的原因,InnoDB 表“应该返回多少行”也是不确定的。这里用一个算 count(*) 的例子来解释一下。

假设表 t 中现在有 10000 条记录,我们设计了三个用户并行的会话。

  • 会话 A 先启动事务并查询一次表的总行数;
  • 会话 B 启动事务,插入一行后记录后,查询表的总行数;
  • 会话 C 先启动一个单独的语句,插入一行记录后,查询表的总行数。

假设从上到下是按照时间顺序执行的,同一行语句是在同一时刻执行的。

从上图可以看到,在最后一个时刻,三个会话 A、B、C 会同时查询表 t 的总行数,但拿到的结果却不同。这和 InnoDB 的事务设计有关系,可重复读是它默认的隔离级别,在代码上就是通过多版本并发控制,也就是 MVCC 来实现的。每一行记录都要判断自己是否对这个会话可见,因此对于 count(*) 请求来说,InnoDB 只好把数据一行一行地读出依次判断,可见的行才能够用于计算“基于这个查询”的表的总行数。

后面的内容如果没有特别说明,将默认采用InnoDB来作为mysql的搜索引擎。

count(主键字段)的执行过程是怎样的?

在通过count()函数统计有多少个记录时,mysql的server层会维护一个名叫count的变量。server层会循环向InnoDB读取一条记录,如果count()函数指定的参数不为null,那么就将count变量加1,直至符合查询的全部记录读完,就退出循环,最后将count变量的值发送给客户端。

InnoDB是通过B+树来保持记录的,根据索引的类型又分为聚簇索引和二级索引。这两种索引的区别在于,聚簇索引的叶子节点存放的是实际数据,而二级索引的叶子节点存放的是主键值,而不是实际数据。

举个栗子

// id为主键值
select count(id) from table;

如果表里只有主键索引,没有二级索引时,那么InnoDB将循环遍历聚簇索引,将读取到的记录返回给server层,然后读取记录中的id值,判断id的值是否为null,如果不为null,就将count变量加1。

但是,如果table表里有二级索引,那么InnoDB循环遍历的对象就不是聚簇索引,而是二级索引。这是因为聚簇索引存放的是实际数据,二级索引存放的是主键值,在相同数量下的二级索引记录要比聚簇索引记录占用更少的存储空间,所以二级索引树比聚簇索引树小,遍历二级索引的I/O成本就比遍历聚簇索引的I/O成本小,因此优化器优先选择的是二级索引。

count(1)的执行过程是怎样的?

举个栗子

select count(1) from table;

因为count()函数的参数是1,不是字段,不会读取记录中的任何字段的值,而是放一个数字“1”进去,判断是不可能为空的,所以将count变量加1。这时候。我们可以对比出来了,count(1)执行得要比count(主键ID)快,因为从InnoDB返回ID会涉及到解析数据行以及拷贝字段值的操作。

count(*)的执行过程是怎样的?

对于select * 这条语句来说,是查询表中的记录的所有字段值,但是在count(*)中,并不是这个意思。count(*)并不会把所有字段取出来,而是专门做了优化,不取值,count(*)肯定不是null,将count变量加1。count(*)其实可以理解为等于count(0),mysql会将参数 * 转化为参数 0 来进行处理,所以count(*)和count(1)的执行过程是基本一样的,性能上没有什么差异

在mysql5.7的官方手册中有这么一句话:

InnoDB handles SELECT COUNT(*) and SELECT COUNT(1) operations in the same way. There is no performance difference.

翻译:InnoDB以相同的方式处理SELECT COUNT(*)和SELECT COUNT(1)操作,没有性能差异。

而且 MySQL 会对 count(*) 和 count(1) 有个优化,如果有多个二级索引的时候,优化器会使用key_len 最小的二级索引进行扫描。只有当没有二级索引的时候,才会采用主键索引来进行统计。

count(column)的执行过程是怎样的?

举个栗子

# column是表里的某个字段
select count(column) from table;

对于这个查询来说,会采用全表扫描的方式来计数,所以它的执行效率是比较差的。

小结

count(1)、 count(*)、 count(主键字段)在执行的时候,如果表里存在二级索引,优化器就会选择二级索引进行扫描。

所以,如果要执行 count(1)、 count(*)、 count(主键字段) 时,尽量在数据表上建立二级索引,这样优化器会自动采用 key_len 最小的二级索引进行扫描,相比于扫描主键索引效率会高一些。

再来,就是不要使用 count(字段)  来统计记录个数,因为它的效率是最差的,会采用全表扫描的方式来统计。如果你非要统计表中该字段不为 NULL 的记录个数,建议给这个字段建立一个二级索引。

结论

按性能高低进行排序

count(1)≈count(*)>count(主键ID)>count(column)

欢迎关注

最后,欢迎大家关注我的公众号呀 。打开微信搜索程序猿零壹公众号即可关注,希望能与大家共同进步。

  • 3
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值