count(*)在千万级表中卡住是因为innodb不缓存精确行数,每次执行都需全索引扫描,i/o和cpu消耗随数据量线性增长;有效汇总表需按查询模式设计、原子化字段、组合主键、少建索引,并通过异步更新、批量补数、定期校验保障准确性与性能。

为什么COUNT(*)在千万级表里卡住不动
因为InnoDB不缓存精确行数,每次COUNT(*)都得走聚簇索引或二级索引全扫描——数据量越大,I/O和CPU消耗越线性增长。哪怕加了索引,COUNT(*)也几乎无法跳过数据页遍历,EXPLAIN里rows字段常显示全表估算值,但实际执行就是硬扫。
汇总表不是“多建一张表”那么简单
真正有效的汇总表要匹配你的查询模式,而不是凭空设计。比如业务常查“每日订单金额总和+笔数”,就该建一张按order_date分组的汇总表;如果还要按user_id维度看,就得考虑复合粒度或冗余存储。
- 字段必须是原子化、不可再拆分的统计口径(如
date不能是datetime,否则无法按天聚合) - 主键推荐用统计维度组合(如
PRIMARY KEY (stat_date, region_id)),避免随机写入热点 - 不要在汇总表上建过多索引——它本身已高度有序,额外索引反而拖慢写入
- 用
INSERT ... ON DUPLICATE KEY UPDATE或REPLACE INTO做幂等更新,防止重复统计
如何让汇总逻辑不拖垮线上业务
核心是把计算从“查询时”挪到“数据写入后”或“低峰期”,且必须隔离读写路径。直接在业务事务里同步更新汇总表,等于把OLTP压力转嫁给聚合逻辑,极易锁表或超时。
- 写入侧:用MySQL的
AFTER INSERT/AFTER UPDATE触发器捕获变更,异步写入汇总表(注意触发器不能含复杂逻辑或远程调用) - 补数侧:对历史数据用
INSERT INTO summary_table SELECT ... GROUP BY一次性填充,别用循环 - 校验侧:定期跑
SELECT COUNT(*) FROM detail_tablevsSELECT SUM(cnt) FROM summary_table比对,发现偏差立刻告警 - 读取侧:应用层直连汇总表,禁止在汇总表上加
WHERE create_time > NOW() - INTERVAL 1 DAY这类动态条件——它会破坏索引有效性
容易被忽略的三个坑
汇总表最大的风险不是建不好,而是用错场景、维护断档、口径漂移。
-
count(*)结果不准:如果明细表有软删除(is_deleted=1),但汇总逻辑没过滤,统计值就虚高——必须严格对齐业务定义的“有效数据”范围 - 分区失效:汇总表按月分区后,忘了在
INSERT语句里显式指定PARTITION(p202604),新数据全落到默认分区,查询性能归零 - 时区陷阱:应用写入
stat_date用的是UTC时间,但报表系统按本地时区解析,导致“今天”的数据永远差8小时——所有时间字段必须统一时区并标注











