原文链接:https://www.percona.com/blog/count-vs-countcol-in-mysql/ 
如果我们观察一下大家是如何使用 COUNT(*) 和 COUNT(col) 的,大多数人都认为这两个操作是等价的,使用哪个只是个人的喜好不同;其实恰恰相反,这两个函数操作在查询性能甚至查询结果上是有很大的差异的。此外,我们还发现在 InnoDB 和 MyISAM 引擎上执行也存在很大差异。
注意:所有的测试都是在 MySQL 的 8.0.30 版本上完成的,在后台,我运行了每一个查询三到五次,以确保所有这些查询操作都完全在缓冲池(对于 InnoDB)或文件系统(对于 MyISAM)缓存中完成。
InnoDB 引擎中 COUNT 函数 让我们观察一下 InnoDB 引擎的以下一系列示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 CREATE  TABLE  count_innodb (  id int (10 ) unsigned NOT  NULL  AUTO_INCREMENT,   val_with_nulls int (11 ) default  NULL ,   val_no_null int (10 ) unsigned NOT  NULL ,   PRIMARY  KEY idx (id) ) ENGINE= InnoDB DEFAULT  CHARSET= latin1; (mysql) >  select  count (* ) from  count_innodb; + |  count (* ) | + |  10000000  | + 1  row  in  set  (0.38  sec)(mysql) >  select  count (val_no_null) from  count_innodb; + |  count (val_no_null) | + |            10000000  | + 1  row  in  set  (0.38  sec)
在 InnoDB 存储引擎中,我们可以发现,通过 COUNT(*) 和 COUNT(val_no_null) 操作来获取表的行数是需要一定时间的,在后面的内容我们还会提到,在获取 COUNT(*) 的结果方面,MyISAM 引擎比 InnoDB 引擎要快得多。
为什么我们不能缓存实际的行数呢?InnoDB 引擎并没有在内部保存表行数,因为在并发的事务场景下,在同一时刻执行的结果可能是不同的。因此,SELECT COUNT(*) 语句仅统计当前事务可见的行。顺便说一句,我们可以使用 MySQL 的 information_schema 立即获得有关表的大致行数:
1 2 3 4 5 6 7 (mysql) >   select  table_rows from  information_schema.tables where  table_name= 'count_innodb' ; + |  TABLE_ROWS | + |     9980586  | + 1  row  in  set  (0.00  sec)
正如我们所看到的,这个执行结果并不是准确的。但是在有些场景下粗略的统计结果就足够了。
下面我们来看一下 COUNT(val_with_nulls) 操作:
1 2 3 4 5 6 7 (mysql) >   select  count (val_with_nulls) from  count_innodb; + |  count (val_with_nulls) | + |                9990001  | + 1  row  in  set  (2.14  sec)
在这里我们看到,COUNT(*) 与 COUNT(val_with_nulls) 的执行结果存在差异。
为什么会这样呢?因为 val_with_nulls 字段被没有被定义为 NOT NULL,所以这个字段上存在一部分 NULL 值,MySQL 必须要执行全表扫描找到这些 NULL 值的行并在结果中排除掉,所以第二个查询执行结果不同。
因此 COUNT(*) 和 COUNT(col) 查询不仅可能具有显着的性能差异,而且还会提出不同的问题。
下面我们来看下面一系列查询,我们将对比一下,在带有 WHERE 条件的查询中,InnoDB 引擎是如何处理 COUNT(*), COUNT(val_no_null), COUNT(val_with_nulls) 的:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 (mysql) >   select  count (* ) from  count_innodb where  id< 1000000 ; + |  count (* ) | + |    980000  | + 1  row  in  set  (0.30  sec)(mysql) >  explain select  count (* ) from  count_innodb where  id< 1000000 \G * * * * * * * * * * * * * * * * * * * * * * * * * * *  1.  row  * * * * * * * * * * * * * * * * * * * * * * * * * * *   select_type: SIMPLE         table : count_innodb          type: range  possible_keys: PRIMARY            key: PRIMARY           rows : 1955802       filtered: 100.00          Extra: Using  where ; Using  index (mysql) >   select  count (val_no_null) from  count_innodb where  id< 1000000 ; + |  count (val_no_null) | + |              980000  | + 1  row  in  set  (0.33  sec)(mysql) >   explain select  count (val_no_null) from  count_innodb where  id< 1000000 \G * * * * * * * * * * * * * * * * * * * * * * * * * * *  1.  row  * * * * * * * * * * * * * * * * * * * * * * * * * * *   select_type: SIMPLE         table : count_innodb          type: range  possible_keys: PRIMARY            key: PRIMARY           rows : 2013804       filtered: 100.00          Extra: Using  where  
我们可以看到在这两种情况下,查询的性能都是相同的,并且只有 10% 的差异;如果我们使用 EXPLAIN 更进一步观察 COUNT(*) 查询的执行计划,就会注意到执行计划中使用了索引。这意味着 MySQL 只需要使用索引,而不用获取其余表数据,就可能足以获得一张大表的行数。
我们可能希望使用已有索引的列来加快对大型表的查询。
那 COUNT(val_with_nulls) 会给我们带来什么惊喜吗?让我们看一下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 (mysql) >  select  count (val_with_nulls) from  count_innodb where  id< 1000000 ; + |  count (val_with_nulls) | + |                 970001  | + 1  row  in  set  (0.33  sec)(mysql) >  explain select  count (val_with_nulls) from  count_innodb where  id< 1000000 \G * * * * * * * * * * * * * * * * * * * * * * * * * * *  1.  row  * * * * * * * * * * * * * * * * * * * * * * * * * * *            id: 1    select_type: SIMPLE         table : count_innodb    partitions: NULL           type: range  possible_keys: PRIMARY            key: PRIMARY        key_len: 4            ref : NULL           rows : 1955802       filtered: 100.00          Extra: Using  where  1  row  in  set , 1  warning (0.00  sec)
没有任何惊喜;我们可以看到,在所有 COUNT(*)、COUNT(val_no_null) 和 COUNT(val_with_nulls) 查询中,性能都相当均匀。
MyISAM 引擎中 COUNT 函数 下面我们来看一下在 MyISAM 引擎中的 COUNT() 函数操作:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 CREATE  TABLE  count_myisam (  id int (10 ) unsigned NOT  NULL ,   val_with_nulls int (11 ) default  NULL ,   val_no_null int (10 ) unsigned NOT  NULL ,   KEY idx (id) ) ENGINE= MyISAM DEFAULT  CHARSET= utf8mb4 COLLATE = utf8mb4_0900_ai_ci; (mysql) >  select  count (* ) from  count_myisam; + |  count (* ) | + |  10000000  | + 1  row  in  set  (0.00  sec)(mysql) >  select  count (val_no_null) from  count_myisam; + |  count (val_no_null) | + |            10000000  | + 1  row  in  set  (0.00  sec)
我们看到了飞快的执行速度!
由于这是一个 MyISAM 引擎表,在引擎内部缓存了表的行数,这就是 MyISAM 引擎的工作方式;这就是为什么它可以立即返回 COUNT(*) 和 COUNT(val_no_null) 的查询结果。
请注意引擎之间的区别:InnoDB 是一个事务存储引擎,MyISAM 是一个非事务存储引擎。
1 2 3 4 5 6 7 (mysql) >  select  count (val_with_nulls) from  count_myisam; + |  count (val_with_nulls) | + |                9990001  | + 1  row  in  set  (14.18  sec)
但是当涉及到 MyISAM 表的 COUNT(val_with_nulls) 时,我们可以看到比 InnoDB 慢了 7 倍;多么巨大的差异。此外,我们可以看到 COUNT(val_with_nulls) 的相同行为,因为 NULL 值显然不会被考虑。 在这种情况下,MySQL 优化器做得很好,只有在需要时才进行全表扫描,因为列值可能为 NULL。
现在,让我们尝试使用 WHERE 子句对 MyISAM 表进行更多查询:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 (mysql) >   select  count (* ) from  count_myisam where  id< 1000000 ; + |  count (* ) | + |   1001237  | + 1  row  in  set  (0.41  sec)(mysql) >   explain select  count (* ) from  count_myisam where  id< 1000000  \\G * * * * * * * * * * * * * * * * * * * * * * * * * * *  1.  row  * * * * * * * * * * * * * * * * * * * * * * * * * * *   select_type: SIMPLE         table : count_myisam          type: range  possible_keys: idx           key: idx          rows : 1041561       filtered: 100.00          Extra: Using  where ; Using  index (mysql) >  select  count (val_no_null) from  count_myisam where  id< 1000000 ; + |  count (val_no_null) | + |             1001237  | + 1  row  in  set  (2.55  sec)(mysql) >   explain select  count (val_no_null) from  count_myisam where  id< 1000000 \\G * * * * * * * * * * * * * * * * * * * * * * * * * * *  1.  row  * * * * * * * * * * * * * * * * * * * * * * * * * * *   select_type: SIMPLE         table : count_myisam          type: range  possible_keys: idx           key: idx          rows : 1041561       filtered: 100.00          Extra: Using  index condition ; Using  MRR (mysql) >   select  count (val_with_nulls) from  count_myisam where  id< 1000000 ; + |  count (val_with_nulls) | + |                1000281  | + 1  row  in  set  (2.55  sec)(mysql) >   explain select  count (val_with_nulls) from  count_myisam where  id< 1000000 \\G * * * * * * * * * * * * * * * * * * * * * * * * * * *  1.  row  * * * * * * * * * * * * * * * * * * * * * * * * * * *   select_type: SIMPLE         table : count_myisam          type: range  possible_keys: idx           key: idx          rows : 1041561       filtered: 100.00          Extra: Using  index condition ; Using  MRR 
正如我们所看到的,即使是带有 WHERE 条件的查询,COUNT(*) 和 COUNT(col) 的性能也会有很大不同。事实上,这个示例显示了 5 倍的性能差异,因为所有数据都在内存中命中(对于目前场景,由于是使用 MyISAM 引擎,数据缓存发生在文件系统缓存级别)。如果 IO 工作负载很大,在这种情况下,我们可以看到甚至 100 倍的性能差异。
COUNT(*) 查询能够使用覆盖索引,而 COUNT(col) 是不行的。当然,我们可以将索引扩展为 (id, val_with_nulls),再次查询就可以使用覆盖索引;但只有在无法更改查询语句(例如:它是第三方应用程序)或列名出于某种原因出现在查询中,并且需要计算非 NULL 值的行数时,我才会使用此解决方法。
值得注意的是在这种情况下,MySQL 优化器在优化查询方面做得不够好。我们可能会注意到 (val_with_nulls) 列不为空,因此 COUNT(val_with_null) 与 COUNT(*) 查询结果相同;因此可以使用覆盖索引进行查询操作,但是它不会,在这种情况下,两个查询都必须执行行读取。
我认为原文这里有误,正确的内容应该是:值得注意的是在这种情况下,MySQL 优化器在优化查询方面做得不够好。我们可能会注意到 (val_no_null) 列不为空,因此 COUNT(val_no_null) 与 COUNT(*) 查询结果相同;因此可以使用覆盖索引进行查询操作,但是它不会,在这种情况下,两个查询都必须执行行读取。 
原文内容:It is worth to note in this case, MySQL Optimizer does not do a good job of optimizing the query. One could notice (val_with_nulls) column is not null, so COUNT(val_with_nulls) is the same as COUNT(*), and so the query could be run as an index-covered query. It does not, and both queries have to perform row reads in this case.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 (mysql) >   alter  table  count_myisam drop  key idx, add  key idx (id,val_with_nulls); Query OK, 10000000  rows  affected (1  min 38.71  sec) Records: 10000000   Duplicates: 0   Warnings: 0  (mysql) >   select  count (val_with_nulls) from  count_myisam where  id< 1000000 ; + |  count (val_with_nulls) | + |                1000281  | + 1  row  in  set  (0.42  sec)(mysql) >   select  count (* ) from  count_myisam where  id< 1000000 ; + |  count (* ) | + |   1000762  | + 1  row  in  set  (0.56  sec)
正如我们看到的,与没有索引的 COUNT(val_with_nulls) 相比,扩展索引有助于提高 COUNT(val_with_nulls) 查询空值的性能,大约有 7 倍提升。但是,我们也发现 COUNT(*) 变慢了大约 0.6 倍,这可能是因为在这种情况下索引长度大约扩大为原来两倍。
最后,我想消除一些关于 COUNT(0) 和 COUNT(1) 的错觉。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 (mysql) >   select  count (1 ) from  count_innodb where  id< 1000000 ; + |  count (1 ) | + |    980000  | + 1  row  in  set  (0.30  sec)(mysql) >   select  count (0 ) from  count_innodb where  id< 1000000 ; + |  count (0 ) | + |    980000  | + 1  row  in  set  (0.30  sec)(mysql) >  explain select  count (1 ) from  count_innodb where  id< 1000000 \G * * * * * * * * * * * * * * * * * * * * * * * * * * *  1.  row  * * * * * * * * * * * * * * * * * * * * * * * * * * *   select_type: SIMPLE         table : count_innodb          type: range  possible_keys: PRIMARY            key: PRIMARY           rows : 1955802       filtered: 100.00          Extra: Using  where ; Using  index (mysql) >   explain select  count (0 ) from  count_innodb where  id< 1000000 \G * * * * * * * * * * * * * * * * * * * * * * * * * * *  1.  row  * * * * * * * * * * * * * * * * * * * * * * * * * * *   select_type: SIMPLE         table : count_innodb          type: range  possible_keys: PRIMARY            key: PRIMARY           rows : 1955802       filtered: 100.00          Extra: Using  where ; Using  index 
正如我们所看到的,实际查询的性能和 EXPLAIN 查询计划的结果都是相同的;在 COUNT() 函数的括号内放什么数字并不重要,它可以是我们想要的任何数字,并且根据性能和查询的实际输出结果来看,它与 COUNT(*) 完全等价。