MySQL 的 count (*) 到底慢在哪?5 种计数方案对比
做后端开发的小伙伴可能很多都被大表的行数统计这么一个简单的查询语句折磨过。例如线上一张千万级的业务表,做分页的时候要统计总条数,一个count(*)扔过去,等了十几秒都没返回,直接超时,非常影响用户体验。很多人都说count(*)性能差,但是它到底差在哪?有没有替代方案呢?今天我们就从底层原理聊到方案选型,把这个问题彻底说清楚。
count ( * ) 为啥这么慢?
要搞懂这个问题,首先得区分 MySQL 的存储引擎,现在大家用的基本都是 InnoDB,我们就拿它和已经快被淘汰的 MyISAM 对比来讲。
MyISAM 引擎非常简单粗暴,直接把表的总行数存在磁盘的元数据里,不带 where 条件的count(*)直接返回这个数,是 O (1) 操作,速度快到飞起来。不过现在生产环境基本在见不到 MyISAM 的影子了,毕竟不支持事务,崩溃容易丢数据,根本满足不了线上业务的可靠性要求。
现在基本上清一色的全是用 InnoDB引擎,但是对于千万级的大表执行count( * ) 时慢的真是不想说。为什么InnoDB就不能像MyISAM搞一个表的行数直接存起来查呢? InnoDB 之所以不存这个总行数,核心原因就是它支持MVCC(多版本并发控制),默认的隔离级别是RR(可重复读)。这么说吧,同一个时刻的多个并发事务,因为各自的 Read View 不同,能看到的行数据是不一样的,InnoDB 根本没办法存一个统一的总行数给所有事务用,所以每次执行count(*)的时候,只能一行行把数据读出来,判断对当前事务可见的才会被计入总数。
你是不是以为逐行扫就是扫全表?实际上,InnoDB 的优化器会自动选择最小的那棵索引树来遍历。比如你的表有个普通二级索引,它的叶子节点只存主键值,比存整行数据的主键索引小很多,扫描起来速度会快很多。所以你要是发现count(*)特别慢,可以先看看是不是没有合适的二级索引可以用。
大家不知道的是count(*)本身已经是 MySQL 优化过的最优写法了,可能网上有些会说count(1)更快,但实际上两者性能几乎没有区别的。我们可以把常用的 count 写法的性能排个序:count(可空字段) < count(非空字段) = count(主键id) < count(1) ≈ count(*)
原理大概如下:
count(字段)需要把字段值取出来判断是否为 NULL,额外多了很多操作;count(主键id)需要解析行数据取出 id,多了拷贝字段的开销;count(1)不需要取值,server 层直接按行累加;count(*)是专门做了优化的,不取值直接按行累加,性能最高。
5 种计数方案全对比,性能差别巨大
搞懂了count(*)慢的底层原因,我们来聊聊实际业务中常用的 5 种计数方案,从准确率、实时性、适用场景三个维度全方面对比,你可以根据自己的业务需求直接选。
第1种:直接用 count ( * )
- 准确率:100%,完全符合当前事务的可见性规则,结果绝对准确。
- 实时性:实时计算,但是大表场景下性能极差,千万级表可能需要几秒甚至十几秒才能返回结果,表越大耗时越长。
- 适用场景:对计数准确性要求极高的场景,比如财务对账、交易数据核对,或者表数据量很小(比如十万级以内)的普通业务场景。如果是带 where 条件的 count (* ),一定要记得给过滤字段加联合索引,能让性能提升好几倍。
当然了,要是你的表已经到了千万甚至亿级别,除非是离线对账,否则千万别在线上接口直接用count(*),用户大概率会等得直接飙脏话。
第 2种:用 explain 估算
- 准确率:非常低,官方文档说误差最高能到 40%-50%,因为是基于页采样估算出来的行数,频繁增删的表误差会更大。
- 实时性:毫秒级返回,不需要实际执行查询,只是读取优化器的统计信息。
- 适用场景:完全不需要准确结果的场景,比如后台运营想大概看下某张表的数据量级,或者做初步的容量评估的时候用。用起来也很简单,执行
explain select * from 表名,结果里的 rows 字段就是估算的行数。
执行explain analysis select * from 表名会实际执行,并更新优化器的统计数据。如果统计数据和实际差距比较大时可以通过这种方式更新统计数据。
第3种:缓存计数(Redis)
- 准确率:逻辑上不精确,因为 Redis 和 MySQL 是两个独立的数据源,并发场景下很容易出现数据不一致,而且 Redis 异常重启的时候还可能丢失最近的更新操作。
- 实时性:读写都是毫秒级,性能极高
- 适用场景:读多写少、允许少量数据误差的场景,比如博客的浏览量、商品的收藏数、社区的帖子数这类场景。就算差个几条几十条,用户根本感知不到,完全可以接受。
这里要提一下不一致的问题,比如你先更新 MySQL 插入一条数据,还没来得及更新 Redis,这时候有请求来读计数,拿到的就是旧值。你也可以用分布式锁或者延迟双删来降低不一致的概率,但没办法完全避免。要是 Redis 挂了也没关系,重启之后全表扫一次count(*)重新同步数据就行,这种异常场景毕竟很少见。
第4种:汇总表
- 准确率:100%,只要你维护得当,结果完全准确
- 实时性:取决于你的更新方式,如果是和业务数据在同一个事务里同步更新,就是完全实时的;如果是定时跑批更新,就是准实时的
- 适用场景:对准确性要求高、读多写少的场景,比如电商的订单总数量、会员总数量这类统计场景。
实现起来很简单,单独建一张计数汇总表,比如CREATE TABLE table_count (table_name varchar(50) primary key, cnt bigint default 0),每次往业务表插入 / 删除数据的时候,在同一个事务里更新汇总表的计数就行,利用 InnoDB 的事务特性就能保证数据一致性。其实汇总表还有个进阶玩法,就是按业务维度做分层汇总,比如按天、按地区统计订单量,后续查多维度的计数也会更快一些。
不过这个方案也有缺点,就是高并发写的场景下,汇总表的行锁会成为瓶颈,所以不适合写操作特别频繁的表。
方案 5:information_schema 查询
- 准确率:误差在 30% 左右,和 explain 的估算值是同一个来源,都是 InnoDB 采样统计的元数据
- 实时性:准实时,元数据的更新有一定延迟,频繁增删的表数据会更旧
- 适用场景:DBA 快速排查问题的时候用,比如想大概看下库里所有表的大小,不需要精确结果,执行
select TABLE_ROWS from information_schema.TABLES where TABLE_NAME='你的表名'就能拿到结果,速度非常快。
如何选择最合适?
选型没有最优,只有最合适,一定要结合业务尝尽来选择,任何方案都有优缺点,所以只能结合场景做取舍。下面是整理了一个选型对比表,你可以直接对着自己的业务场景选:
| 计数方案 | 准确率 | 实时性 | 性能 | 适用场景 |
|---|---|---|---|---|
| 直接 count (*) | 100% | 实时 | 大表极差 | 对账等要求绝对准确、数据量小的场景 |
| explain 估算 | 50%-60% | 准实时 | 极高 | 粗略估算数据量级的场景 |
| Redis 缓存 | 90%+ | 实时 | 极高 | 读多写少、允许少量误差的 C 端场景 |
| 汇总表 | 100% | 实时 / 准实时 | 极高 | 要求准确、读多写少的 B 端统计场景 |
| information_schema | 60%-70% | 准实时 | 极高 | DBA 快速排查问题的场景 |
做技术选型适合自己业务的才是最好的。比如你做 C 端的浏览量统计,非要强求 100% 准确用汇总表,反而会因为锁冲突拖垮整个系统,此时就完全没必要。比如我们经常看到内容创作者平台创作者统计数据,基本上是不显示实时的,而是第二天显示前一天的,这就是取舍后的结果。通常来说实时数据肯定感受更好,但是完全没必要。当然了,这种数据量通常巨大,不会是简单的差个库就统计好了,通常是大数据跑批计算的,这个有机会后面再讲,如果你想了解大数据这些的话可以给我留言。
你们平时做计数统计都用的哪种方案呢?有没有踩过什么相关的坑?欢迎在评论区留言。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐

所有评论(0)