mysql分组查询性能瓶颈主因是长文本字段导致回表和内存膨胀:未覆盖索引触发整行读取,哈希分组拷贝大字段致临时表落盘,全文扫描加载text加剧io,根本解法是冷热分离建模与覆盖索引。

长文本字段导致分组时强制回表读取整行
只要 GROUP BY 的列没被覆盖索引完全包含,MySQL 就必须回表——而回表会把整行数据(包括 TEXT、JSON 等大字段)从磁盘或 buffer pool 里拉出来。哪怕你只按 user_id 分组,只要 SELECT 或 GROUP BY 涉及的字段不在同一个索引里,大字段照读不误。
常见错误现象:EXPLAIN 显示 type=ref 但实际执行极慢;Handler_read_rnd_next 指标飙升;监控看到 Innodb_buffer_pool_reads 暴涨。
- 即使加了
INDEX(user_id),只要查询里有SELECT user_id, COUNT(*) FROM logs且logs表含content TEXT,仍会触发整行读取 - 用
SELECT user_id, COUNT(*) FROM logs WHERE status = 'error'也一样——WHERE 条件走索引,但 COUNT(*) 需要确认每行是否满足条件,最终仍得访问行数据 - 真正能绕过回表的,只有覆盖索引:比如
CREATE INDEX idx_user_status ON logs (status, user_id),再写SELECT user_id, COUNT(*) FROM logs WHERE status = 'error' GROUP BY user_id
GROUP BY 过程中大字段被反复拷贝进内存哈希表
数据库做哈希分组时,会把分组键值(如 category 字符串)和对应行的原始数据副本一起存进内存哈希桶。TEXT 字段动辄几十 KB,一个桶里存几条就吃掉几 MB 内存,一旦超出 sort_buffer_size 或 tmp_table_size,就会落盘生成磁盘临时表,IO 直接翻倍。
典型表现:SHOW PROCESSLIST 中状态为 Creating tmp table 或 Copying to tmp table,且持续时间长;Created_tmp_disk_tables 计数猛增。
- 别信“用 HASH(category) 替代 category 分组能省内存”——哈希值本身不解决副本问题,且碰撞后结果错,得不偿失
-
GROUP BY TRIM(LOWER(category))看似合理,但没函数索引的话,每次都要计算+复制完整字符串,比裸分组还慢 - PostgreSQL 的流式分组(Stream Aggregate)可绕过哈希表,但前提是
GROUP BY列上有索引且数据已按该列物理排序
统计类查询误触全文扫描 + 大字段加载
像 SELECT COUNT(*) FROM logs WHERE content LIKE '%timeout%' 这种语句,表面看只是计数,实则先全表扫描匹配行,再把每行 content 全部加载出来做子串查找——IO 和 CPU 双重暴击。
更隐蔽的是:就算你加了 FULLTEXT(content),如果没用 MATCH(content) AGAINST('timeout' IN BOOLEAN MODE),而是继续写 LIKE,索引照样失效。
- MySQL 的
FULLTEXT索引只对MATCH ... AGAINST生效,LIKE、REGEXP、SUBSTRING全部无视 - 想按关键词频次统计?必须改写为
SELECT COUNT(*) FROM logs WHERE MATCH(content) AGAINST('+timeout' IN BOOLEAN MODE) - 如果业务真需要精确出现次数(如“abc”在字段中出现几次),别在 SQL 层硬算——提前在写入时用应用层解析并存为结构化字段
冷热分离没做,统计永远拖着大文本跑
报表查的是“哪天、哪个用户、发生了什么错误”,但表结构却把错误详情(detail TEXT)和元数据(user_id, error_code, created_at)混在一张表里。每次 COUNT/GROUP BY 都得把几万条日志的完整文本拖进内存,纯属浪费。
这不是优化技巧问题,是数据建模缺陷。IO 高的根因,往往藏在 CREATE TABLE 语句里。
- 正确做法:拆出
log_summary表(含id,user_id,error_code,created_date,detail_id),所有常规统计都在这张表上跑 -
log_detail表只存id和content TEXT,按月分区 +ROW_FORMAT=COMPRESSED,仅用于溯源查看 - 如果暂时不能改表,至少给
log_summary加联合索引:INDEX idx_stat (created_date, error_code, user_id),让 COUNT 基本走索引扫描











