MySQL 学习(一):一文了解 COUNT() 函数的使用及优化方式

说明

平时工作最不陌生的就是对数据库的 CRUD 操作,尤其在需要进行分页查询时,要先根据给定参数查询符合条件的记录行数,这里就要使用到 MySQL 的 COUNT() 函数。本篇博文,我将通过 MySQL 的官方文档,来介绍 COUNT() 函数的使用,以及 COUNT(列名)、COUNT(1) 和 COUNT(*) 的区别,还有 MySQL 对 COUNT() 函数进行了哪些优化。

正文

在 MySQL 关于优化的文档中,在 WHERE Clause Optimization 章节,有这样一段话:

COUNT(*) on a single table without a WHERE is retrieved directly from the table information for MyISAM and MEMORY tables. This is also done for any NOT NULL expression when used with only one table.

单表的 COUNT(*) 函数并且没有 WHERE 语句的查询中,对 MyISAM 和 MEMORY 类型的表将直接从表信息中获取结果,同时对于单表的含 NOT NULL 表达式的 COUNT() 函数查询也同样如此。

COUNT(expr)

接下来,再看文档中对 COUNT() 函数的介绍:
MySQL 学习(一):一文了解 COUNT() 函数的使用及优化方式

对于 COUNT(expr) 函数,将会返回一个通过 SELECT 语句检索行的表达式 expr 值非空的记录行数,值为 BIGNIT 类型

如何没有满足表达式的行,将会返回0。

对于 COUNT(*) 函数,与 COUNT(expr) 函数有些区别,该函数会返回所有的行数,不管是否包含空值 NULL。

对于 expr 表达式,我们可以让 expr 表示一个列名或者一个常量。上面提到 COUNT(expr) 将会返回 expr 表达式值为非空的记录行数,因此,当 epxr 表示列名时,COUNT(列名) 将会返回指定列值非空的行数;当表示一个常量时,因为常量肯定不为NULL,所以 COUNT(常量) 将会返回所有的记录行数。我们常用的 COUNT(1) 和 COUNT(*) 具有相同的作用。

COUNT(*) 优化

上面提到,对于 MyISAM 类型的表,不带 WHERE 条件的 COUNT(*) 语句将直接从表信息中获取结果。

但是对于 InnoDB 的表,则无法使用此种方式进行优化。

这是因为 InnoDB 存储引擎支持事务,对于该类型的存储引擎,单独记录行数会带来并发问题。不同的事务可能在相同时间看到不同的结果。因此对于 SELECT COUNT(*) 语句只返回当前事务可见的行数。

但是在 MySQL 8.0.13中,对 InnoDB 表的不带其他子句(如 WHERE 或 GROUP BY)的 SELECT COUNT(*) FROM tbl_name 的查询语句进行了单线程工作负载的性能优化。

As of MySQL 8.0.13, SELECT COUNT(*) FROM tbl_name query performance for InnoDB tables is optimized for single-threaded workloads if there are no extra clauses such as WHERE or GROUP BY.

InnoDB 在处理 SELECT COUNT(*) 语句时,在没有指定索引或者优化器指定其他索引的情况下,将会扫描最小的辅助索引。如果没有辅助索引,则扫描聚簇索引。

同时文档指出,若索引记录值没有全部在缓冲池时,处理 SELECT COUNT(*) 语句将耗费一些时间。但为了可以快速计数,可以创建一个计数表,使得应用根据 Insert 或 Delet 语句来更新这个表的值。然而这种方式不能很好的扩展,比如上千并发事务同时更新该表。如果一个大概的数值可以满足要求,可以使用 SHOW TABLE STATUS 获取记录数值。

以上就是 Mysql 对于 InnoDB 表执行 COUNT(*) 函数的优化介绍,COUNT(1) 语句作用与之相同,那这两个函数有什么区别呢?

COUNT(1) 和 COUNT(*) 的区别

对于 InnoDB 存储引擎,会以相同的方式执行 SELECT COUNT(*) 和 SELECT COUNT(1) 语句,并且两者性能没有任何区别

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

对于 MyISAM 存储引擎,单表的不检索其他列,不带 WHERE 子句的 SELECT COUNT(*) 查询语句,将直接从表信息获取值,同时对于 COUNT(1) ,如果表的第一列定义为非空 NOT NUL,才会进行同样的优化。

For MyISAM tables, COUNT(*) is optimized to return very quickly if the SELECT retrieves from one table, no other columns are retrieved, and there is no WHERE clause.

This optimization only applies to MyISAM tables, because an exact row count is stored for this storage engine and can be accessed very quickly. COUNT(1) is only subject to the same optimization if the first column is defined as NOT NULL.

两个函数采用相同的方式优化,效率相同,那我们应该使用哪个函数获取所有记录行数呢?

对此,阿里的开发手册中做了以下强制规定:

MySQL 学习(一):一文了解 COUNT() 函数的使用及优化方式

看完以上内容,你是否会回答以下问题:

  • COUNT(列名) 和 COUNT(1)、COUNT(*) 有什么区别?
  • MySQL 的不同存储引擎对 COUNT(*) 有什么优化?
  • MySQL 对 COUNT(*) 优化的前提条件是什么?
  • COUNT(1) 和 COUNT(*) 哪个效率更快?与 COUNT(列名) 比呢?
  • 阿里开发手册为什么强制要求使用 COUNT(*)?

总结

MySQL 的 COUNT() 函数有三种使用方式,COUNT(列名) 返回指定列不为空的记录行数,COUNT(1) 和 COUNT(*) 返回所有记录行数。

因为 COUNT(列名) 需要扫描所有行判断记录是否为空,执行效率要比其他两种方式低。

而对于 COUT(1) 和 COUT(*),MySQL 采用了相同的优化方式,InnoDB 优先扫描最小的辅助索引,不存在辅助索引时扫描聚簇索引;MyISAM 则直接从表信息中获取行数值。

这些优化都有相同的前提条件就是 SELECT 语句不能包含其他从句,如 WHERE 或者 GROUP BY。

在查询所有记录行数时,应优先使用 COUNT(*) 语句,因为它是 SQL92 定义的标准统计行数的语法,跟数据库无关。

参考资料

《MySQL 8.0 function_count》

《MySQL的COUNT语句,竟然都能被面试官虐的这么惨!?》