作者:小林coding来源:小林coding
当我们对一张数据表中的记录进行统计的时候,习惯都会使用count函数来统计,但是count函数传入的参数有很多种,比如count(1)、count(*)、count(字段)等。
到底哪种效率是最好的呢?是不是count(*)效率最差?
我曾经以为count(*)是效率最差的,因为认知上selete*fromt会读取所有表中的字段,所以凡事带有*字符的就觉得会读取表中所有的字段,当时网上有很多博客也这么说。
但是,当我深入count函数的原理后,被啪啪啪的打脸了!
不多说,发车!
哪种count性能最好?我先直接说结论:
要弄明白这个,我们得要深入count的原理,以下内容基于常用的innodb存储引擎来说明。
count()是什么?count()是一个聚合函数,函数的参数不仅可以是字段名,也可以是其他任意表达式,该函数作用是统计符合查询条件的记录中,函数指定的参数不为NULL的记录有多少个。
假设count()函数的参数是字段名,如下:
selectcount(name)fromt_order;这条语句是统计「t_order表中,name字段不为NULL的记录」有多少个。也就是说,如果某一条记录中的name字段的值为NULL,则就不会被统计进去。
再来假设count()函数的参数是数字1这个表达式,如下:
selectcount(1)fromt_order;这条语句是统计「t_order表中,1这个表达式不为NULL的记录」有多少个。
1这个表达式就是单纯数字,它永远都不是NULL,所以上面这条语句,其实是在统计t_order表中有多少个记录。
count(主键字段)执行过程是怎样的?在通过count函数统计有多少个记录时,MySQL的server层会维护一个名叫count的变量。
server层会循环向InnoDB读取一条记录,如果count函数指定的参数不为NULL,那么就会将变量count加1,直到符合查询的全部记录被读完,就退出循环。最后将count变量的值发送给客户端。
InnoDB是通过B+树来保持记录的,根据索引的类型又分为聚簇索引和二级索引,它们区别在于,聚簇索引的叶子节点存放的是实际数据,而二级索引的叶子节点存放的是主键值,而不是实际数据。
用下面这条语句作为例子:
//id为主键值selectcount(id)fromt_order;如果表里只有主键索引,没有二级索引时,那么,InnoDB循环遍历聚簇索引,将读取到的记录返回给server层,然后读取记录中的id值,就会id值判断是否为NULL,如果不为NULL,就将count变量加1。
但是,如果表里有二级索引时,InnoDB循环遍历的对象就不是聚簇索引,而是二级索引。
这是因为相同数量的二级索引记录可以比聚簇索引记录占用更少的存储空间,所以二级索引树比聚簇索引树小,这样遍历二级索引的I/O成本比遍历聚簇索引的I/O成本小,因此「优化器」优先选择的是二级索引。
count(1)执行过程是怎样的?用下面这条语句作为例子:
selectcount(1)fromt_order;如果表里只有主键索引,没有二级索引时。
那么,InnoDB循环遍历聚簇索引(主键索引),将读取到的记录返回给server层,但是不会读取记录中的任何字段的值,因为count函数的参数是1,不是字段,所以不需要读取记录中的字段值。参数1很明显并不是NULL,因此server层每从InnoDB读取到一条记录,就将count变量加1。
可以看到,count(1)相比count(主键字段)少一个步骤,就是不需要读取记录中的字段值,所以通常会说count(1)执行效率会比count(主键字段)高一点。
但是,如果表里有二级索引时,InnoDB循环遍历的对象就二级索引了。
count(*)执行过程是怎样的?看到*这个字符的时候,是不是大家觉得是读取记录中的所有字段值?
对于selete*这条语句来说是这个意思,但是在count(*)中并不是这个意思。
count(*)其实等于count(0),也就是说,当你使用count(*)时,MySQL会将*参数转化为参数0来处理。
所以,count(*)执行过程跟count(1)执行过程基本一样的,性能没有什么差异。
在MySQL5.7的官方手册中有这么一句话:
InnoDBhandlesSELECTCOUNT(*)andSELECTCOUNT(1)operationsinthesameway.Thereisnoperformancedifference.
翻译:InnoDB以相同的方式处理SELECTCOUNT(*)和SELECTCOUNT(1)操作,没有性能差异。
而且MySQL会对count(*)和count(1)有个优化,如果有多个二级索引的时候,优化器会使用key_len最小的二级索引进行扫描。
只有当没有二级索引的时候,才会采用主键索引来进行统计。
count(字段)执行过程是怎样的?
count(字段)的执行效率相比前面的count(1)、count(*)、count(主键字段)执行效率是最差的。
用下面这条语句作为例子:
//name不是索引,普通字段selectcount(name)fromt_order;对于这个查询来说,会采用全表扫描的方式来计数,所以它的执行效率是比较差的。
小结count(1)、count(*)、count(主键字段)在执行的时候,如果表里存在二级索引,优化器就会选择二级索引进行扫描。
所以,如果要执行count(1)、count(*)、count(主键字段)时,尽量在数据表上建立二级索引,这样优化器会自动采用key_len最小的二级索引进行扫描,相比于扫描主键索引效率会高一些。
再来,就是不要使用count(字段)来统计记录个数,因为它的效率是最差的,会采用全表扫描的方式来统计。如果你非要统计表中该字段不为NULL的记录个数,建议给这个字段建立一个二级索引。
为什么要通过遍历的方式来计数?你可以会好奇,为什么count函数需要通过遍历的方式来统计记录个数?
我前面将的案例都是基于Innodb存储引擎来说明的,但是在MyISAM存储引擎里,执行count函数的方式是不一样的,通常在没有任何查询条件下的count(*),MyISAM的查询速度要明显快于InnoDB。
使用MyISAM引擎时,执行count函数只需要O(1)复杂度,这是因为每张MyISAM的数据表都有一个meta信息有存储了row_count值,由表级锁保证一致性,所以直接读取row_count值就是count函数的执行结果。
而InnoDB存储引擎是支持事务的,同一个时刻的多个查询,由于多版本并发控制(MVCC)的原因,InnoDB表“应该返回多少行”也是不确定的,所以无法像MyISAM一样,只维护一个row_count变量。
举个例子,假设表t_order有条记录,现在有两个会话并行以下语句:
在会话A和会话B的最后一个时刻,同时查表t_order的记录总个数,可以发现,显示的结果是不一样的。所以,在使用InnoDB存储引擎时,就需要扫描表来统计具体的记录。
而当带上where条件语句之后,MyISAM跟InnoDB就没有区别了,它们都需要扫描表来进行记录个数的统计。
如何优化count(*)?如果对一张大表经常用count(*)来做统计,其实是很不好的。
比如下面我这个案例,表t_order共有+万条记录,我也创建了二级索引,但是执行一次selectcount(*)fromt_order要花费差不多5秒!
面对大表的记录统计,我们有没有什么其他更好的办法呢?
第一种,近似值如果你的业务对于统计个数不需要很精确,比如搜索引擎在搜索关键词的时候,给出的搜索结果条数是一个大概值。
这时,我们就可以使用showtablestatus或者explain命令来表进行估算。
执行explain命令效率是很高的,因为它并不会真正的去查询,下图中的rows字段值就是explain命令对表t_order记录的估算值。
第二种,额外表保存计数值
如果是想精确的获取表的记录总数,我们可以将这个计数值保存到单独的一张计数表中。
当我们在数据表插入一条记录的同时,将计数表中的计数字段+1。也就是说,在新增和删除操作时,我们需要额外维护这个计数表。