文章目录

  • 前言
  • count()聚合函数简介
  • 不同存储引擎实现
  • MyISAM
  • Innodb
  • MVCC
  • 具体实现
  • 总结
  • 执行结果
  • 执行效率
  • 使用推荐


前言

在面试的时候或者平时工作的时候,经常会遇到MySQL中的count()函数,本文就是主要学习并了解count()函数中的性能以及效率问题。

count()聚合函数简介

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

比如select count(name) from t_order;这条语句是统计「 t_order 表中,name 字段不为 NULL 的记录」有多少个。也就是说,如果某一条记录中的 name 字段的值为 NULL,则就不会被统计进去。

不同存储引擎实现

在不同的存储引擎中,实现count()函数的方法是不一样的。

MyISAM

MyISAM引擎每张表的总行数是存储在磁盘上,所以当执行 count(*) 时,是直接从磁盘拿到这个值返回,能够快速返回。但是如果加WHERE条件,则该优化就不起作用。

Innodb

虽然Innodb表里也存了一个表行数的概念,但是这个官方给的是预估值,并不能把这个数值用来代替count()的结果。

这里要简单说下MVCC即版本并发控制。因为Innodb是支持事务的。

MVCC

MVCC 在 MySQL InnoDB 中的实现主要是为了提高数据库并发性能,用更好的方式去处理读-写冲突,做到即使有读写冲突时,也能做到不加锁,非阻塞并发读。

就是因为要实现多版本并发控制,所以才导致 InnoDB 引擎不能直接存储表总记录数。因为每个事务获取到的一致性视图都是不一样的,所以返回的数据总记录也是不一致的。

具体实现
  • 只有聚簇索引的情况
    在这种情况下,count(*)count(1)都会根据循环遍历聚簇索引(主键索引),将读取到的记录返回给 server 层,但是不会读取记录中的任何字段的值,因为他们的参数都不可能是空;
    count(主键)也是循环遍历聚簇索引(主键索引),将读取到的记录返回给 server 层,但是会读取主键值并判断是否为空,不为空则count+1,因为多了一步操作,所以效率上略低于上面两种。
    而如果是count(普通列)的话就是全表扫描了,性能最差。
  • 存在聚簇索引和二级索引
    在聚簇索引和二级索引都存在的情况下,因为相同数量的二级索引记录可以比聚簇索引记录占用更少的存储空间,所以二级索引树比聚簇索引树小,这样遍历二级索引的 I/O 成本比遍历聚簇索引的 I/O 成本小,因此「优化器」优先选择的是二级索引。索引count(*)count(1)count(主键)会使用key_len 最小的二级索引进行扫描。只有当没有二级索引的时候,才会采用主键索引来进行统计。但是count(主键)也是会有一次判空操作,所以效率会稍微低点。
    count(普通列)如果这个列是存在二级索引的,那么就遍历这个二级索引,读取主键值进行判空,不为空则count+1,如果不存在二级索引,那么就是全表扫描,性能很差,所以不建议使用这种方法。
    MySQL 5.7 的官方手册中有这么一句话:

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

总结

执行结果

从执行的结果数量来看:

count(1) = count(*) = count(主键) >= count(普通列)

因为count()会过滤到NULL值,而前三个都是不可能为空,所以查询出来的数量一致。如果普通的列字段也是不可为空的,那么数量就和前三者一致,否则的话就会小于前三者。

执行效率

count(1) = count(*) > count(主键) > count(普通列)

因为主键和普通列都有获取数据判断NULL操作,且普通列如果没有索引的情况下还会造成全表扫描。

因为主键和普通列都有获取数据判断NULL操作,且普通列如果没有索引的情况下还会造成全表扫描。

使用推荐

在使用count()函数的时候,推荐直接使用count(*)函数即可,既避免了一些复杂的判断,又能使用到底层执行器的默认优化,最大程度减少麻烦。