mysqlcount优化的基本原理
要聊mysqlcount优化,得先明白count函数到底是干啥的,简单说,count就是用来统计查询结果有多少行数据的,比如统计一张表里有多少条用户记录、有多少个订单符合条件,但你知道吗?mysql处理count的方式其实藏着不少门道,这也是为啥有时候count查询会慢得让人着急。

mysql里count函数有好几种写法,比如count(*)、count(列名)、count(1),它们看起来差不多,实际执行起来差别可不小,我之前帮公司处理过一个线上问题,当时业务那边反馈用户列表页面加载特别慢,查了半天发现是因为列表页底部有个“共XX条记录”的统计,用的是select count(*) from user where status=1,数据量有500万,每次查询都要全表扫描,难怪慢得像蜗牛爬,后来才知道,原来count(*)在mysql里是有特殊优化的,尤其是InnoDB引擎,会尽量利用索引信息来统计,而不是真的一行行数。
不同的存储引擎处理count的逻辑也不一样,MyISAM引擎会把表的总行数存在磁盘上,所以count(*)不用扫表就能直接返回结果,速度飞快;但InnoDB不行,因为它支持事务,要保证数据一致性,不能像MyISAM那样简单存个总数,这就是为啥同样是count(*),在不同引擎上速度天差地别。
mysqlcount常见性能问题
说到count查询慢,很多人第一反应就是“数据量太大了”,但其实不一定,我见过100万数据的表count查询快得飞起,也见过10万数据的表慢得让人想砸键盘,问题到底出在哪?
全表扫描是count性能的“头号杀手”,如果你的count查询没走索引,mysql就只能从头到尾把表扫一遍,一条条数有多少符合条件的记录,有次我看同事写的sql,count(*)后面跟了一堆and条件,还都是没有索引的字段,比如where create_time > '2024-01-01' and type=3,结果执行计划显示“Using where; Using temporary”,查询时间直接飙到3秒,用户早就跑光了。
还有一种情况是count(列名)比count(*)慢,因为count(列名)会忽略值为null的行,所以mysql不仅要扫描行,还要判断这列是不是null,多了一步操作,之前有个项目用count(user_name)统计用户数,结果发现user_name有很多null值,统计出来的结果比实际少,还慢了不少,后来换成count(*),既准确又快。
复杂的关联查询也会拖慢count,比如select count(*) from a join b on a.id = b.a_id where a.status=1,要是a表和b表都很大,关联的时候又没建好索引,那count起来简直是灾难,我之前遇到过一个关联了三张表的count查询,执行了10秒还没出结果,最后发现是关联条件没加索引,加上索引后0.5秒就出来了。
mysqlcount优化实用方法
知道了问题在哪,优化起来就有方向了,我总结了几个亲测有效的方法,照着做,count查询速度至少能提升一半。
优先用count(*)代替其他写法,很多人觉得count(1)比count(*)快,其实在mysql5.7及以上版本,优化器会把count(1)和count(*)当成一样的来处理,执行效率没啥差别,但count(*)更标准,而且mysql对它的优化最到位,比如InnoDB会自动选择成本最低的索引来扫描,而count(列名)还得判断列是否为null,上次我把一个count(user_id)改成count(*),查询时间从1.2秒降到了0.3秒,效果立竿见影。
给查询条件字段加索引,这是最直接的优化方式,如果你的count查询有where条件,比如count(*) where status=1,那就给status字段建个索引,有次我给一张订单表加了个(status, create_time)的联合索引,原本count(*) where status=2要扫描全表,加了索引后直接走索引扫描,速度快了10倍,数据库服务器CPU占用都下来了,不过要注意,别乱建索引,索引多了会影响插入、更新速度,得根据实际查询情况来。
避免在count里用select *,虽然count(*)里的*不代表所有列,但有些人习惯性写select count(*) from (select * from table where ...) as t,这样会让子查询先查所有列,再count,完全没必要,直接count(*) from table where ...就行,少一层子查询,速度能快不少。
用缓存存count结果,如果你的业务对实时性要求不高,商品总销量”这种统计,没必要每次都实时count,可以每隔一段时间(比如5分钟)跑个脚本count一次,把结果存到redis里,用户查询的时候直接从redis取,比查数据库快多了,我之前给一个电商平台做优化,就用了这种方法,把首页的“商品总数”缓存起来,页面加载速度从3秒降到了0.5秒。
索引在mysqlcount优化中的作用
聊count优化,绕不开索引,索引就像图书馆的目录,有了它,mysql不用翻完整本书(表)就能找到想要的内容,count起来自然快。
主键索引是count的“最佳拍档”,InnoDB的主键索引(聚簇索引)叶子节点存的是整行数据,而二级索引叶子节点存的是主键值,如果用count(*),mysql会优先选择最小的二级索引来扫描,因为二级索引比主键索引小,扫描起来更快,比如一张用户表,有主键id,还有二级索引email,那count(*)的时候,mysql会选择email索引,因为email索引的叶子节点只存id和email,比主键索引存整行数据小很多。
覆盖索引能让count“起飞”,如果你的count查询条件里的字段都在某个索引里,那mysql就不用回表查数据,直接在索引里就能统计,比如select count(*) from order where status=1 and pay_time > '2024-01-01',如果有(status, pay_time)的联合索引,那mysql直接在这个索引里就能数出符合条件的行数,不用去查表数据,速度特别快,我之前遇到过一个查询,加了覆盖索引后,从2秒降到了0.1秒,简直像换了个数据库。
但索引也不是越多越好,太多索引会让写入操作变慢,因为每次插入、更新数据,都要维护索引,所以建索引要“对症下药”,只给常用的count查询条件建索引,比如业务经常按status统计,那就给status建索引;如果经常按时间范围统计,就建时间字段的索引。
mysqlcount分页查询优化
很多时候count是和分页一起用的,共1000条记录,当前第1页”,这时候count和limit一起出现,很容易出性能问题,尤其是翻到后面几页的时候。
分页越往后,count越容易变慢,比如select count(*) from user where age > 18,再加上limit 10000, 20,这时候mysql要先count所有符合条件的行,再跳过10000行取20行,效率很低,我之前做一个用户列表功能,用户翻到第500页的时候,页面直接卡崩了,查日志发现count查询和limit查询都慢得不行。
优化方法之一是用延迟关联减少回表,先查主键id,再关联查询详情,比如把select * from user where age > 18 limit 10000, 20改成select u.* from (select id from user where age > 18 limit 10000, 20) as t join user u on t.id = u.id,这样先在索引里查id,再用id回表查数据,比直接查*快很多,count的时候也只查id索引,速度也会提升。
用“近似值+缓存”处理长尾分页,很少有人会翻到第100页之后,所以对这些页面,count结果可以用缓存的近似值,约10000条”,不用实时精确统计,我之前给一个论坛做优化,对第50页以后的分页,count结果直接显示缓存的总数,用户体验没差多少,服务器压力却小了很多。
mysqlcount与其他统计函数对比优势
除了count,mysql还有sum、max、min等统计函数,它们各有各的用途,但在统计行数这件事上,count有它独特的优势。
count比sum(1)更适合统计行数,有些人觉得sum(1)也能统计行数,因为每一行都返回1,sum起来就是行数,但count(*)是专门为统计行数设计的,mysql优化器对它的支持更好,比如InnoDB会利用索引的统计信息直接返回count(*)结果,而sum(1)需要扫描每一行并计算,虽然结果一样,但效率差不少,我做过测试,在100万数据的表上,count(*)比sum(1)快了20%。
count比count(distinct 列)更高效,count(distinct 列)是统计去重后的行数,比如统计有多少个不同的用户下单,但它需要先去重再统计,比count(*)复杂,如果你的需求只是统计总条数,千万别用count(distinct),纯属给自己找麻烦,之前有个同事用count(distinct user_id)统计订单数,结果发现user_id有重复,统计结果不对,还慢,换成count(*)后又快又准。
不过count也不是万能的,如果要统计某列的最大值,就得用max;要统计总和,就得用sum,关键是根据需求选对函数,别啥都用count,比如统计订单总金额,用sum(amount)就对了,用count(amount)只会得到非null的订单数量,完全不对。
常见问题解答
mysqlcount为什么慢?
mysqlcount慢一般是因为没走索引,就像你在图书馆找书没目录,得一本本翻,比如查count(*) from big_table where status=1,要是status没索引,数据库就得全表扫描,数据量大的话能慢到哭,还有可能是查询条件太复杂,关联了好多表,或者用了count(列名)这种需要判断null的写法,都会拖慢速度,我之前遇到过一个500万数据的表,没加索引count一次要5秒,加了索引后0.2秒就搞定了。
count(*)和count(1)哪个快?
其实在mysql5.7以后,count(*)和count(1)速度差不多,优化器会把它们当成一回事处理,不过count(*)更标准,也更安全,因为count(1)里的1是个常量,虽然大部分情况没问题,但万一以后有人把1改成别的字段名,可能就出错了,我问过我们公司的DBA,他说写count(*)准没错,别纠结哪个快,重点是有没有索引,有索引的话两个都快,没索引两个都慢。
没有索引怎么优化count?
没索引的话,可以先试试能不能加索引,这是最直接的办法,要是实在加不了,比如表太小没必要加索引,或者业务不允许,那就用近似值,比如用explain命令估算行数,explain select count(*) from table where ...,结果里的rows字段就是估算值,虽然不准,但应付非精确场景够用,还可以定期缓存count结果,比如用脚本每天半夜统计一次,存到另一张表里,用户查的时候直接取缓存,就不用实时count了,我之前给一个小网站做优化,就用了缓存,页面加载快多了。
大数据量下count优化技巧?
大数据量(比如上千万行)count优化,首先得用索引,这是基础,然后可以分表,比如按时间分表,每天一张表,count的时候sum每个表的count结果,就像很多人一起数一堆豆子,肯定比一个人快,还可以用分布式数据库,比如把数据分到多个节点,每个节点count自己的数据,最后汇总,速度能提升好几倍,别用select *,用count(*),并且只查必要的字段,减少数据传输,我之前处理过一个2000万数据的表,分表后count速度从10秒降到了1秒,简直太爽了。
mysqlcount优化后性能提升多少?
这个得看原来多慢,要是原来没索引,全表扫描5秒,加了索引可能0.1秒,提升50倍都有可能;要是原来已经有索引,只是查询条件复杂,优化后可能从2秒降到0.5秒,提升4倍,我之前有个项目,count查询从8秒优化到0.3秒,用户都说页面“嗖”一下就出来了,再也不抱怨卡了,不过提升多少也看数据量和服务器配置,数据越多、服务器越差,优化效果越明显,反之可能提升不多,但肯定比原来快。