GROUP BY卡在千万级日志上主因是全表扫描加内存排序;需建最左前缀索引、避免函数干扰、统一时区并考虑预计算log_date字段。

GROUP BY 为什么卡在千万级日志上
不是 GROUP BY 本身慢,是它默认触发全表扫描 + 内存排序。日志表没索引、字段没过滤、聚合键太宽(比如用 user_agent 或完整 url),MySQL 就得把几千万行全拉进临时表再分组,OOM 或磁盘临时表都可能发生。
实操建议:
- 先确认
EXPLAIN输出里type是ALL还是range;如果是ALL,说明没走索引 - 聚合字段必须有单列索引或联合索引的最左前缀,比如按
status和date分组,建索引要写成INDEX idx_status_date (status, date),反过来无效 - 避免在
GROUP BY中用函数,像GROUP BY DATE(created_at)会失效索引;改用范围查询 + 分组:WHERE created_at >= '2024-01-01' AND created_at
count(*) vs count(1) vs count(字段) 在日志统计里的实际差异
三者执行计划可能完全不同。日志表常含大量 NULL 值(比如 user_id 字段缺失),count(user_id) 会跳过所有 NULL 行,而 count(*) 统计所有行 —— 结果差几倍很常见。
实操建议:
- 统计“总请求数”用
count(*);统计“成功请求中带用户ID的数量”才用count(user_id) -
count(1)和count(*)在 InnoDB 下几乎无差别,别迷信“1更快”,优化器都转成count(*) - 如果字段允许
NULL且你没意识到,count(字段)会悄悄漏数据,查count(*) - count(字段)能快速发现空值比例
用 WITH ROLLUP 实现多维下钻却爆内存
WITH ROLLUP 看似方便,但会在结果集里自动补出所有层级汇总行(比如按 region, service, status 分组时,会额外生成 region+service 小计、region 总计、全局总计),数据量呈指数增长,千万行原始数据可能产出上百万汇总行。
实操建议:
- 只对真正需要的维度组合建模,别一上来就
GROUP BY a,b,c,d WITH ROLLUP - 用
HAVING过滤掉低频组合(如HAVING COUNT(*) > 100),减少中间结果 - 更稳的做法是拆成多个独立查询:先查大区汇总,再按需查某区下的服务明细,用应用层拼接,可控性强
时间字段分组不准:凌晨零点数据被切到两天
日志时间用 DATETIME 存但没统一时区,或者直接用 UNIX_TIMESTAMP 存整数,在 GROUP BY DATE(created_at) 时,服务器时区和写入时区不一致,会导致 00:00–00:59 的日志被算进前一天。
实操建议:
- 查之前先确认
SELECT @@time_zone, @@system_time_zone,和日志写入时区是否一致 - 避免依赖服务器本地时区,统一转成 UTC 后分组:
GROUP BY DATE(CONVERT_TZ(created_at, '+08:00', '+00:00')) - 更彻底的方案是日志入库前就按天切分字段,加一列
log_date DATE AS (DATE(created_at)) STORED,并在此列建索引
聚合不是加个 GROUP BY 就完事,关键在数据分布、索引覆盖、时区对齐这三点。少一个,查一次就卡一次。










