MySQL Execution Plan--COUNT相关测试
COUNT全表记录
在MySQL中,相同的SQL不同的存储引擎执行计划不同:
对于MyISAM引擎,由于使用表锁进行并发控制,同一时间点多个并发线程执行相同查询获得的结果相同,且MyISAM存储引擎专门存储表总记录数,因此使用COUNT(*)查询全表记录时能直接返回。
而对于InnoDB存储引擎,由于使用MVCC和行锁进行并发控制,同一时间点多个并发线程执行相同查询获得的结果存在差异(每个回话的READVIEW不同),且没有专门存储表总记录数,因此每次查询都需要扫描全表或扫描某个索引的全部记录。
For transactional storage engines such as InnoDB, storing an exact row count is problematic. Multiple transactions may be occurring at the same time, each of which may affect the count.
InnoDB does not keep an internal count of rows in a table because concurrent transactions might “see” different numbers of rows at the same time. Consequently, SELECT COUNT(*) statements only count rows visible to the current transaction.
Prior to MySQL 5.7.18, InnoDB processes SELECT COUNT(*) statements by scanning the clustered index. As of MySQL 5.7.18, InnoDB processes SELECT COUNT(*) statements by traversing the smallest available secondary index unless an index or optimizer hint directs the optimizer to use a different index. If a secondary index is not present, the clustered index is scanned.
Processing SELECT COUNT(*) statements takes some time if index records are not entirely in the buffer pool. For a faster count, create a counter table and let your application update it according to the inserts and deletes it does. However, this method may not scale well in situations where thousands of concurrent transactions are initiating updates to the same counter table. If an approximate row count is sufficient, use SHOW TABLE STATUS.
InnoDB handles SELECT COUNT(*) and SELECT COUNT(1) operations in the same way. There is no performance difference.
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。
现有测试表TB101:
CREATE TABLE `tb101` ( `ID` int(11) NOT NULL AUTO_INCREMENT, `C1` int(11) NOT NULL, `C2` int(11) DEFAULT NULL, PRIMARY KEY (`ID`) ) ENGINE=MyISAM AUTO_INCREMENT=140001 DEFAULT CHARSET=utf8
对于没有WHERE条件的COUNT(*)/COUNT(1)/COUNT(ID)/COUNT(C1)的执行计划为:
mysql> EXPLAIN SELECT COUNT(*) FROM TB101 \G
*************************** 1. row ***************************
id: 1
select_type: SIMPLE
table: NULL
partitions: NULL
type: NULL
possible_keys: NULL
key: NULL
key_len: NULL
ref: NULL
rows: NULL
filtered: NULL
Extra: Select tables optimized away
1 row in set, 1 warning (0.00 sec)
对于没有WHERE条件的COUNT(C2)的执行计划为:
mysql> EXPLAIN SELECT COUNT(C2) FROM TB101 \G *************************** 1. row *************************** id: 1 select_type: SIMPLE table: TB101 partitions: NULL type: ALL possible_keys: NULL key: NULL key_len: NULL ref: NULL rows: 140000 filtered: 100.00 Extra: NULL 1 row in set, 1 warning (0.00 sec)
可以发现,对于MyISAM存储引擎,在没有WHERE条件下,如果C1列为NOT NULL,则可以将COUNT(C1)与COUNT(*)和COUNT(1)做相同的处理。
针对上面的测试,对于InnoDB存储引擎,在没有WHERE条件下:
1、ID列为NOT NULL主键,COUNT(ID)和COUNT(1)或COUNT(*)的执行计划相同,返回结果相同。 2、C1列为NOT NULL,COUNT(C1)和COUNT(1)或COUNT(*)的执行结果相同,但执行计划不同。
COUNT(expr)异同
1、COUNT(1)和COUNT(*)等价,两者在执行计划和执行效率上完全相同。
个人推荐使用COUNT(1)替换COUNT(*),原因是简单直观, 他人推荐使用COUNT(1),原因是符合SQL92标准,阿里巴巴Java开发手册推荐。
2、COUNT(*)和COUNT(C1)不一定等价,两者执行计划和执行结果会存在差异。
COUNT(*):执行返回满足WHERE条件的行数,不考虑NULL值问题
COUNT(C1): 执行返回满足WHERE条件且C1不等于NULL的行数,不统计C1等于NULL的行。
换种理解思路:
对于MyISAM引擎表和InnoDB引擎表,无论是显式主键还是因此ROWID,都要求非空唯一,每行记录都肯定存在一个不为NULL的列(列组),因此计算COUNT(*)时不需要考虑NULL值问题。
一个有趣的扩展,如果C1为NOT NULL,那么COUNT(C1)与COUNT(1)的返回结果相同,那么MySQL会对此进行优化么?
现有测试表结果如下:
CREATE TABLE `tb01` ( `ID` int(11) NOT NULL AUTO_INCREMENT, `C1` int(11) NOT NULL, `C2` int(11) NOT NULL, PRIMARY KEY (`ID`), KEY `IDX_C2` (`C2`) ) ENGINE=InnoDB AUTO_INCREMENT=16384 DEFAULT CHARSET=utf8
查看COUNT(*)和COUNT(C1)的执行计划:
mysql> EXPLAIN SELECT COUNT(*) FROM TB01 WHERE C2<100 \G *************************** 1. row *************************** id: 1 select_type: SIMPLE table: TB01 partitions: NULL type: range possible_keys: IDX_C2 key: IDX_C2 key_len: 4 ref: NULL rows: 99 filtered: 100.00 Extra: Using where; Using index 1 row in set, 1 warning (0.00 sec) mysql> EXPLAIN SELECT COUNT(C1) FROM TB01 WHERE C2<100 \G *************************** 1. row *************************** id: 1 select_type: SIMPLE table: TB01 partitions: NULL type: range possible_keys: IDX_C2 key: IDX_C2 key_len: 4 ref: NULL rows: 99 filtered: 100.00 Extra: Using index condition 1 row in set, 1 warning (0.00 sec)
从上面执行计划可以发现,在处理COUNT(*)时,仅需要使用IDX_C2即可完成查询,因此Extra为Using index,而在处理COUNT(C1)时,需要使用IDX_C2进行过滤后再执行回表查询,因此Extra为Using index condition。
针对上面的测试,MyISAM存储引擎和InnoDB存储引擎的测试结果相同。
COUNT(DISTINC ...)操作
MySQL官网解释为:
COUNT(DISTINCT expr,[expr...]) Returns a count of the number of rows with different non-NULL expr values. In MySQL, you can obtain the number of distinct expression combinations that do not contain NULL by giving a list of expressions. In standard SQL, you would have to do a concatenation of all expressions inside COUNT(DISTINCT ...).
在MySQL中允许执行:
SELECT COUNT(DISTINCT ID,C1) FROM TB02;
但不允许执行:
SELECT COUNT(ID,C1) FROM TB02;
总结
1、对于InnoDB和MyISAM存储引擎,COUNT(1)和COUNT(*)在任何场景下都等价,执行性能和执行计划相同。 2、在查询全表记录(没有WHERE条件)时,对于MyISAM存储引擎,存储引擎存储表总记录数,无需扫描数据因此查询可以很快返回,对于InnoDB存储引擎,需要扫描全表或某个索引的全部记录因此查询可能比较耗时。 3、对于MyISAM存储引擎,在没有WHERE条件情况下,如果列C1为NOT NULL,那么COUNT(C1)和COUNT(*)执行操作相同。 4、对于InnoDB存储引擎,如果列C1为主键,那么COUNT(C1)和COUNT(*)执行计划和执行效率相同,如果C1为NOT NULL,那么COUNT(C1)和COUNT(*)执行计划和执行效率不一定相同,只有在查询使用C1列上索引时才可能相同。
参考链接:
https://dev.mysql.com/doc/refman/8.0/en/group-by-functions.html#function_count
https://dev.mysql.com/doc/refman/5.7/en/create-index.html
https://mp.weixin.qq.com/s/IOHvtel2KLNi-Ol4UBivbQ