前言在实际开发过程中,统计一个表的数据量是一个经常遇到的需求。统计数据库表的行数,会用到COUNT(*),COUNT(1)orCOUNT(field),但是表中的记录越来越多,用COUNT(*)会越来越慢。在本文中,我们将分析COUNT的性能。1.COUNT(1),COUNT(*)和COUNT(field)哪个更快?执行效果:COUNT(*)MySQL优化了COUNT(*),COUNT(*)直接扫描主键索引记录,并没有取出所有字段,直接按行累加。COUNT(1)InnoDB引擎遍历整张表,但不取值。服务器层将数字“1”放入返回的每一行并逐行累加。COUNT(field)如果这个“字段”定义为NOTNULL,那么InnoDB引擎会逐行从记录中读取这个字段,server层会判断它不能为NULL,并逐行累加;如果这个“字段”定义允许NULL,那么InnoDB引擎会从记录中逐行读取这个字段,然后取出值进行判断。如果不为NULL,则累加。实验分析本次测试使用的环境:[root@zhyno1~]#cat/etc/system-releaseCentOSLinuxrelease7.9.2009(Core)[root@zhyno1~]#uname-aLinuxzhyno13.10.0-1160.62.1。el7.x86_64#1SMPTueApr516:57:59UTC2022x86_64x86_64x86_64GNU/Linux测试数据库使用(存储引擎使用InnoDB,其他参数默认):(MonJul2509:41:392022)[root@GreatSQL][(none)]>选择版本();+------------+|版本()|+------------+|8.0.25-16|+------------+1rowinset(0.00sec)实验开始:#首先我们创建一个实验表CREATETABLEtest_count(`id`int(10)NOTNULLAUTO_INCREMENTPRIMARYKEY,`name`varchar(20)NOTNULL,`salary`int(1)NOTNULL,KEY`idx_salary`(`salary`))ENGINE=InnoDBDEFAULTCHARSET=utf8;#插入1000W条数据DELIMITER//CREATEPROCEDUREinsert_1000w()BEGINDECLAREiINT;设置我=1;WHILEi<=10000000DOINSERTINTOtest_count(name,salary)VALUES('KAiTO',1);设置i=i+1;ENDWHILE;END//DELIMITER;#执行存储过程调用insert_1000w();接下来,让我们分别进行实验:COUNT(1)耗时4.19秒(SatJul2322:56:042022)[root@GreatSQL][test]>s从test_count中选择计数(1);+------------+|计数(1)|+----------+|10000000|+--------+1行在集合中(4.19秒)COUNT(*)花费了4.16秒(2022年7月23日星期六22:57:41)[root@GreatSQL][test]>selectcount(*)来自测试计数;+--------+|计数(*)|+----------+|10000000|+--------+1行在集合中(4.16秒)COUNT(字段)花费了4.23秒(2022年7月23日星期六22:58:56)[root@GreatSQL][test]>select来自test_count的计数(id);+------------+|计数(id)|+------------+|10000000|+------------+1rowinset(4.23sec)我们可以再次测试执行计划COUNT(*)(SatJul2322:59:162022)[root@GreatSQL][test]>explainselectcount(*)fromtest_count;+----+-------------+------------+----------+--------+-------------+------------+---------+--------+--------+----------+------------+|id|select_type|table|partitions|type|possible_keys|key|key_len|ref|rows|filtered|Extra|+----+------------+------------+------------+--------+--------------+------------+--------+------+--------+----------+------------+|1|简单|测试计数|空|索引|空|idx_薪水|4|空|9980612|100.00|使用索引|+----+------------+------------+------------+-------+----------------+------------+--------+------+--------+----------+------------+1行在集合中,1个警告(0.01秒)(2022年7月23日星期六22:59:48)[root@GreatSQL][测试]>显示警告;+------+------+--------------------------------------------------------------------+|级别|代码|留言|+------+-----+-------------------------------------------------------------------+|注意|1003|/*select#1*/selectcount(0)AS`count(*)`from`test`.`test_count`|+------+------+------------------------------------------------------------------集合中的+1行(0.00秒)COUNT(1)(2022年7月23日星期六23:12:45)[root@GreatSQL][test]>explainselect数(1)fromtest_count;+----+------------+------------+------------+-------+----------------+------------+--------+------+--------+----------+------------+|编号|选择类型|表|分区|类型|可能的键|钥匙|密钥长度|参考|行|过滤|额外|+----+------------+------------+------------+-------+----------------+------------+--------+------+--------+----------+------------+|1|简单|测试计数|空|索引|空|idx_薪水|4|空|9980612|100.00|使用索引|+----+------------+------------+------------+-------+----------------+------------+--------+------+--------+----------+------------+1行在集合中,1个警告(0.00秒)(2022年7月23日星期六23:13:02)[root@GreatSQL][测试]>显示警告;+------+------+--------------------------------------------------------------------+|级别|代码|信息|+--------+------+----------------------------------------------------------------------+|注意|1003|/*select#1*/selectcount(1)AS`count(1)`from`test`.`test_count`|+------+------+------------------------------------------------------------------集合中的+1行(0.00秒)COUNT(字段)(2022年7月23日星期六23:13:14)[root@GreatSQL][test]>explainselect来自测试计数的计数(id);+----+------------+------------+--------------+--------+----------------+------------+---------+------+--------+----------+------------+|编号|选择类型|表|分区|类型|可能的键|钥匙|密钥长度|参考|行|过滤|额外|+----+------------+------------+------------+-------+----------------+------------+--------+------+--------+----------+------------+|1|简单|测试计数|空|索引|空|idx_薪水|4|空|9980612|100.00|使用索引|+----+------------+------------+------------+--------+----------------+------------+--------+------+--------+--------+--------------集合中的1行,1条警告(0.00秒)(2022年7月23日星期六23:13:29)[root@GreatSQL][test]>显示警告;+--------+------+-------------------------------------------------------------------------------------------+|级别|代码|留言|+--------+------+----------------------------------------------------------------------------------------+|注意|1003|/*select#1*/selectcount(`test`.`test_count`.`id`)AS`count(id)`from`test`.`test_count`|+--------+------+----------------------------------------------------------------------------------------+1行set(0.00sec)需要注意的是,如果是COUNT(TueJul2614:01:572022)中的非主键字段[root@GreatSQL][test]>explainselectcount(name)fromtest_countwhereid<100;+----+------------+-----------+----------+--------+----------------+--------+--------+------+------+----------+------------+|编号|选择类型|表|分区|类型|可能的键|钥匙|密钥长度|参考|行|过滤|额外|+----+------------+------------+----------+-------+----------------+--------+---------+------+------+------------+------------+|1|简单|测试计数|空|范围|初级|初级|4|空|99|100.00|----+------------+--------+----------------+---------+--------+-----+-----+---------+------------+1rowinset,1warning(0.00sec)实验结果从上面的实验我们可以得出结论,COUNT(*)和COUNT(1)是最快的,其次是COUNT(id)COUNT(*)被重写了MySQL查询优化器将其设置为COUNT(0),并选择了idx_salary索引。COUNT(1)和COUNT(id)都选择idx_salary索引。实验结论总结:COUNT(*)=COUNT(1)>COUNT(id)MySQL的官方文档也说:InnoDB对SELECTCOUNT(*)和SELECTCOUNT(1)操作的处理是一样的。没有性能差异翻译:InnoDB以相同的方式处理SELECTCOUNT(*)和SELECTCOUNT(1)操作。没有性能差异,所以说明对于COUNT(1)或者COUNT(*),MySQL的优化其实是一模一样的,性能上没有差异。但是推荐使用COUNT(*),因为这是MySQL92定义的统计行数的标准语法。2.COUNT(*)和TABLES_ROWS在InnoDB中,MySQL数据库的每个表占用的空间和表中记录的行数可以打开MySQL的information_schema数据库。库中有一张TABLES表,该表的主要字段有:TABLE_SCHEMA:数据库名TABLE_NAME:表名ENGINE:使用的存储引擎TABLES_ROWS:记录数DATA_LENGTH:数据大小INDEX_LENGTH:索引大小TABLE_ROWS用于显示该表的当前状态有多少行?这个命令执行得非常快。这个TABLE_ROWS可以替换COUNT(*)吗?我们使用TABLES_ROWS查询表记录数(SatJul2323:15:142022)[root@GreatSQL][test]>SELECTTABLE_ROWS->FROMINFORMATION_SCHEMA.TABLES->WHERETABLE_NAME='test_count';+-----------+|TABLE_ROWS|+------------+|9980612|+------------+1rowinset(0.03sec)可以看出记录数不准确,因为InnoDB引擎下的TABLES_ROWS行数只是一个大概估计。3.COUNT(*)是如何实现的?首先,必须清楚MySQL有很多不同的引擎。在不同的引擎中,COUNT(*)有不同的实现方式。本文主要介绍InnoDB引擎。执行过程在InnoDB存储引擎中,COUNT(*)函数首先将表中的数据从内存中读取到内存缓冲区中,然后扫描全表获取行记录数。简单来说就是全表扫描,一个循环解决问题,在循环内:先读取一行,然后判断该行是否包含在COUNT中,在循环内,逐行进行计数。在MyISAM引擎中,一张表的总行数是存储在磁盘上的,所以在执行COUNT(*)时,会直接返回这个数字,效率很高。InnoDB之所以不像MyISAM一样存储数字,是因为即使同时进行多个查询,由于多版本并发控制(MVCC),InnoDB表应该返回多少行是不确定的。并且无论是在事务支持、并发能力还是数据安全性方面,InnoDB都优于MyISAM。尽管如此,InnoDB还是针对COUNT(*)操作进行了优化。InnoDB是一个索引组织表。主键索引树的叶子节点是数据,而普通索引树的叶子节点是主键值。因此,普通索引树比主键索引树小很多。对于COUNT(*)等操作,遍历哪棵索引树得到的结果在逻辑上是相同的。因此,MySQL优化器会寻找最小的树进行遍历。需要注意的是,本文讨论的是没有过滤条件的COUNT(*)。如果加上WHERE条件,MyISAM引擎的表就没法那么快返回了。4、总结COUNT(*)=COUNT(1)>COUNT(id)COUNT函数的用法,主要用于统计表的行数。主要用法有COUNT(*)、COUNT(字段)和COUNT(1)。因为COUNT(*)是SQL92定义的统计行数的标准语法,所以MySQL对它做了很多优化。MyISAM会直接把表的总行数单独记录下来进行COUNT(*)查询,而InnoDB在扫描表时会选择最小的索引来降低成本。这些优化的前提是没有WHERE和GROUP的条件查询。InnoDB中COUNT(*)和COUNT(1)的实现没有区别,效率是一样的,但是COUNT(field)需要判断字段是否不为NULL,所以效率会低一些。因为COUNT(*)是SQL92定义的统计行数的标准语法,效率很高,所以推荐使用COUNT(*)查询表的行数。就像上面COUNT(name)的用例一样,在建表的过程中,需要根据业务需求建立高性能的索引,同时也要注意避免创建不必要的索引.最后,我想多说一点。本文中可能有一些用例不够全面。如有不同意见。
