如何提升MySQL千万级大表Count(*)查询效率?借助Redis或元数据表实现实时统计?
- 内容介绍
- 文章标签
- 相关推荐
本文共计870个文字,预计阅读时间需要4分钟。
由于InnoDB引擎下默认使用COUNT(*)走全表扫描(即使有主键),不进行二级索引优化,也无法利用查询缓存(MySQL 8.0已移除),当表行数超过1000万、QPS较高时,单次COUNT(*)操作可能耗时2-5秒,甚至更容易阻塞其他DML操作。
用 Redis 做实时计数的实操要点
适合写多读少、允许短暂误差(秒级)的场景,比如「当前在线用户数」「文章总浏览量」。关键不是存总数,而是控制好增减时机和原子性。
-
INCRBY和DECRBY必须和业务 SQL 在同一事务中触发 —— 但 Redis 本身不参与 MySQL 事务,所以实际要靠应用层双写 + 补偿:先更新 MySQL,再INCRBY;失败则记录日志,由后台任务重试 - 不要用
GET+SET模拟原子增减,会丢数据;必须用INCRBY - 为防 Redis 宕机导致计数丢失,建议每天凌晨用
SELECT COUNT(*) FROM t全量校准一次,写入SET t:count:backup作为兜底值 - 如果业务有「软删除」,记得在删除逻辑里同步
DECRBY 1;硬删除则通常已含该逻辑
用元数据表替代 COUNT(*) 的落地方式
比 Redis 更可靠,适合要求强一致的场景,比如「会员总人数」「待审核订单数」。本质是把聚合结果物化成一行一列的普通表。
- 建表示例:
CREATE TABLE t_count (name VARCHAR(64) PRIMARY KEY, cnt BIGINT NOT NULL DEFAULT 0); 插入初始值:
INSERT INTO t_count VALUES ('user_total', 0); - 所有 INSERT/DELETE 都必须配套
UPDATE t_count SET cnt = cnt + 1 WHERE name = 'user_total'(或 -1),推荐封装进存储过程或 ORM hook - 避免在高并发写入点直接
UPDATE ... SET cnt = cnt + 1—— 会引发行锁争用;改用INSERT INTO t_count VALUES ('user_total', 1) ON DUPLICATE KEY UPDATE cnt = cnt + 1,利用唯一索引+插入冲突机制降低锁粒度 - 如果业务有批量导入,别循环执行 N 次
UPDATE,应先算出增量 Δ,再单条UPDATE t_count SET cnt = cnt + ?
哪些情况千万别硬上这两种方案
当查询带 WHERE 条件(如 COUNT(*) WHERE status = 1 AND created_at > '2024-01-01'),Redis 和元数据表都无能为力 —— 它们只适合全表或固定条件的计数。此时更务实的做法是:
- 给高频过滤字段加组合索引,让
COUNT(*)走覆盖索引扫描(例如INDEX(status, created_at)) - 用近似统计:
SELECT table_rows FROM information_schema.tables WHERE table_name = 't' AND table_schema = 'db',误差可能达 10%~20%,但毫秒级返回 - 对超大范围分页计数,直接放弃精确总数,改用「加载更多」模式,避免
LIMIT offset, size导致的深度偏移问题
真实线上环境里,最常被忽略的是「软删除字段未索引」和「元数据表没加 FOR UPDATE 或乐观锁」—— 这两类小疏漏,足以让计数在高峰期悄悄漂移。
本文共计870个文字,预计阅读时间需要4分钟。
由于InnoDB引擎下默认使用COUNT(*)走全表扫描(即使有主键),不进行二级索引优化,也无法利用查询缓存(MySQL 8.0已移除),当表行数超过1000万、QPS较高时,单次COUNT(*)操作可能耗时2-5秒,甚至更容易阻塞其他DML操作。
用 Redis 做实时计数的实操要点
适合写多读少、允许短暂误差(秒级)的场景,比如「当前在线用户数」「文章总浏览量」。关键不是存总数,而是控制好增减时机和原子性。
-
INCRBY和DECRBY必须和业务 SQL 在同一事务中触发 —— 但 Redis 本身不参与 MySQL 事务,所以实际要靠应用层双写 + 补偿:先更新 MySQL,再INCRBY;失败则记录日志,由后台任务重试 - 不要用
GET+SET模拟原子增减,会丢数据;必须用INCRBY - 为防 Redis 宕机导致计数丢失,建议每天凌晨用
SELECT COUNT(*) FROM t全量校准一次,写入SET t:count:backup作为兜底值 - 如果业务有「软删除」,记得在删除逻辑里同步
DECRBY 1;硬删除则通常已含该逻辑
用元数据表替代 COUNT(*) 的落地方式
比 Redis 更可靠,适合要求强一致的场景,比如「会员总人数」「待审核订单数」。本质是把聚合结果物化成一行一列的普通表。
- 建表示例:
CREATE TABLE t_count (name VARCHAR(64) PRIMARY KEY, cnt BIGINT NOT NULL DEFAULT 0); 插入初始值:
INSERT INTO t_count VALUES ('user_total', 0); - 所有 INSERT/DELETE 都必须配套
UPDATE t_count SET cnt = cnt + 1 WHERE name = 'user_total'(或 -1),推荐封装进存储过程或 ORM hook - 避免在高并发写入点直接
UPDATE ... SET cnt = cnt + 1—— 会引发行锁争用;改用INSERT INTO t_count VALUES ('user_total', 1) ON DUPLICATE KEY UPDATE cnt = cnt + 1,利用唯一索引+插入冲突机制降低锁粒度 - 如果业务有批量导入,别循环执行 N 次
UPDATE,应先算出增量 Δ,再单条UPDATE t_count SET cnt = cnt + ?
哪些情况千万别硬上这两种方案
当查询带 WHERE 条件(如 COUNT(*) WHERE status = 1 AND created_at > '2024-01-01'),Redis 和元数据表都无能为力 —— 它们只适合全表或固定条件的计数。此时更务实的做法是:
- 给高频过滤字段加组合索引,让
COUNT(*)走覆盖索引扫描(例如INDEX(status, created_at)) - 用近似统计:
SELECT table_rows FROM information_schema.tables WHERE table_name = 't' AND table_schema = 'db',误差可能达 10%~20%,但毫秒级返回 - 对超大范围分页计数,直接放弃精确总数,改用「加载更多」模式,避免
LIMIT offset, size导致的深度偏移问题
真实线上环境里,最常被忽略的是「软删除字段未索引」和「元数据表没加 FOR UPDATE 或乐观锁」—— 这两类小疏漏,足以让计数在高峰期悄悄漂移。

