大家好,我是小林。我们统计数据表的记录时,习惯使用count函数来统计,但是count函数传入的参数有很多种,比如count(1)、count(*)、count(field)),ETC。。哪种效率最好?count(*)是不是效率最差?之前一直觉得count(*)是效率最差的,因为认知上select*fromt会读取表中的所有字段,所以凡是带*字符的感觉都会读取表中的所有字段。当时网上很多博客也是这么说的。然而,当我深入到count函数的原理时,却被打脸了!话不多说,开始吧!哪个计数的性能最好?哪个计数的性能最好?直接说结论吧:要搞清楚这个,还得深入count的原理,下面的内容以常用的InnoDB存储引擎来说明。什么是计数()?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服务器层会维护一个名为count的变量。服务器层会循环从InnoDB中读取一条记录。如果count函数指定的参数不为NULL,那么变量count就会加1,直到读取完所有匹配查询的记录,然后退出循环。最后将计数变量的值发送给客户端。InnoDB通过B+树来保存记录。根据索引的类型,分为聚簇索引和二级索引。它们的区别在于聚集索引的叶子节点存储的是实际数据,而二级索引的叶子节点存储的是主键值,不是实际数据。以下面的语句为例://id是主键值selectcount(id)fromt_order;如果表中只有主键索引,没有二级索引,那么InnoDB会循环聚簇索引,将读取的记录返回给server层,然后读取记录中的id值,判断id值是否为无效的。如果它不为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(*)的执行过程是怎样的?当你看到*这个字符的时候,你是不是认为你在读取记录中的所有字段值?这是语句select*的意思,但是在count(*)中不是这个意思。count(*)实际上等于count(0),也就是说,当你使用count(*)时,MySQL会把*参数转换成参数0进行处理。因此,count(*)的执行过程与count(1)的执行过程基本相同,性能上没有区别。在MySQL5.7的官方手册中,有这样一句话:InnoDBhandlesSELECTCOUNT(*)andSELECTCOUNT(1)operationsinthesameway。没有性能差异。翻译:InnoDB以相同的方式处理SELECTCOUNT(*)和SELECTCOUNT(1)操作,没有性能差异。此外,MySQL会优化count(*)和count(1)。如果有多个二级索引,优化器会使用key_len最小的二级索引进行扫描。只有在没有二级索引的情况下,才会使用主键索引进行统计。count(field)的执行过程是怎样的?count(field)的执行效率相比前面的count(1)、count(*)、count(primarykeyfield)是最差的。以如下语句为例://name不是索引,公共字段selectcount(name)fromt_order;对于这个查询,它会使用全表扫描的方式来统计,所以它的执行效率比较差。总结在执行count(1)、count(*)、count(主键字段)时,如果表中存在二级索引,优化器会选择二级索引进行扫描。因此,如果要执行count(1)、count(*)、count(主键字段),尽量在数据表上创建二级索引,这样优化器会自动使用key_len最小的二级索引对于scanning,相比Scanningtheprimarykeyindex会更有效率。其次不要用count(field)来统计记录条数,因为它的效率是最差的,会用全表扫描的方式来统计。如果要统计表中该字段不为NULL的记录条数,建议为该字段创建二级索引。为什么要通过遍历来计数?你可能会好奇,为什么count函数需要遍历来统计记录的条数?前面我描述的案例都是基于Innodb存储引擎,但是在MyISAM存储引擎中,count函数的执行方式是不同的。通常,对于不带任何查询条件的count(*),MyISAM的查询速度明显快于InnoDB。使用MyISAM引擎时,count函数的执行只需要O(1)的复杂度。这是因为每张MyISAM数据表都有一个元信息存储row_count值,表级锁保证了一致性,所以直接读取row_count值就是执行count函数的结果。InnoDB存储引擎支持事务。同时进行多个查询,由于多版本并发控制(MVCC),InnoDB表的“应该返回多少行”也是不确定的,所以不可能像MyISAM那样只维护一个。row_count变量。例如,假设表t_order有100条记录,现在有两个会话并行运行如下语句:在会话A和会话B的最后时刻,同时查看表t_order的记录总数,它可以发现显示的结果不一样。因此,在使用InnoDB存储引擎时,需要对表进行扫描,统计特定的记录。有了where条件语句,MyISAM和InnoDB就没有区别了。他们都需要扫描表来计算记录的数量。如何优化count(*)?如果经常在大表上使用count(*)进行统计,其实是很不好的。例如,在我下面的例子中,表t_order共有1200万条以上的记录。我也创建了一个二级索引,但是执行一个selectcount(*)fromt_order大约需要5秒!面对一个大表的记录统计,我们还有什么更好的办法吗?第一个是近似值。如果您的业务不需要非常精确的统计数量,例如搜索引擎搜索关键字时,给出的搜索结果数量是一个近似值。这时候我们可以使用showtablestatus或者explain命令来计算表。执行explain命令非常高效,因为它实际上并没有查询。下图中rows字段的值是表t_order记录的explain命令的预估值。二是将计数值保存在extra表中。如果我们想准确的获取表中记录的总数,可以将这个计数值保存在一个单独的计数表中。当我们在数据表中插入一条记录时,我们会将count表中的count字段加1。也就是说,在进行增删操作时,我们需要额外维护这张计数表。
