mysql中COUNT、SUM等聚合函数的优化方法

COUNT() 比 COUNT(字段) 快,因MySQL对前者有优化:可直接读取行数缓存,而后者需逐行判NULL并全表扫描;COUNT(1)与COUNT()等效,但COUNT(id)会过滤NULL值。

为什么 COUNT(*) 比 COUNT(字段) 快得多

MySQL 对 COUNT(*) 有专门优化:在 InnoDB 中,如果没加 WHERE 条件且表无虚拟列、无全文索引,优化器可能直接读取聚簇索引的行数缓存(如 INFORMATION_SCHEMA.TABLES 中的 TABLE_ROWS 值),甚至跳过全表扫描。而 COUNT(字段) 必须逐行判断该字段是否为 NULL,无法跳过数据页读取。

  • COUNT(*) 替代 COUNT(id)COUNT(1) —— 三者语义不同:COUNT(1)COUNT(*) 行为一致,但 COUNT(id) 会过滤掉 id IS NULL 的行
  • 避免在大表上执行 COUNT(字段) 且该字段无索引、允许 NULL —— 这会强制回表或全扫描二级索引
  • 若业务真需统计非空值数量,优先给该字段建 NOT NULL 约束 + 索引,让优化器有机会走覆盖索引

SUM/AVG 在没有索引时为什么慢得离谱

SUM()AVG() 必须遍历所有匹配行并累加/计数,若过滤条件无法命中索引,就会触发全表扫描。更隐蔽的问题是:即使 WHERE 条件走了索引,只要 SELECT 列不在索引中,仍要回表读取数值字段 —— 回表放大 I/O 开销,尤其在高并发下容易成为瓶颈。

  • 对高频聚合字段(如 amountscore)建立覆盖索引,例如:
    CREATE INDEX idx_user_status_amount ON orders (status, amount);
    这样 SELECT SUM(amount) FROM orders WHERE status = 'paid' 可完全走索引,无需回表
  • 避免在聚合函数中使用表达式,如 SUM(price * quantity) —— 无法利用索引,且计算开销翻倍;考虑新增持久化计算列并为其建索引
  • 注意 AVG() 实际等价于 SUM() / COUNT(),但 MySQL 不会自动拆解优化;若分母可能为 0,记得用 NULLIF() 防止除零错误:COALESCE(SUM(amount) / NULLIF(COUNT(*), 0), 0)

用近似值替代精确 COUNT 的真实场景

很多业务场景其实不需要精确总数,比如后台分页显示“共约 24.5 万条”,或监控大盘展示趋势。硬查 COUNT(*) 在千万级表上可能耗时数秒,而采样估算可在毫秒级返回。

  • 启用 innodb_stats_method = 'nulls_unequal' 并调大 innodb_stats_persistent_sample_pages(默认 20),让统计信息更准,提升执行计划可靠性
  • SELECT TABLE_ROWS FROM INFORMATION_SCHEMA.TABLES 查预估行数——仅适用于无频繁删改的静态表,且该值不实时更新
  • 对超大表做分桶采样:
    SELECT CEIL(COUNT(*) * 1000 / 10) FROM t_sample TABLESAMPLE SYSTEM (0.1);
    注意 TABLESAMPLE 是 MySQL 8.0.

    23+ 才支持,且 SYSTEM 模式按页采样,结果偏差可控

GROUP BY + 聚合导致临时表和文件排序的典型诱因

GROUP BY 字段未被索引覆盖,或聚合字段与分组字段不在同一索引中,MySQL 很可能创建内部临时表(Using temporary; Using filesort),严重拖慢响应。

  • 确保 GROUP BY 字段是联合索引最左前缀,且聚合字段包含在该索引中(覆盖索引),例如:
    SELECT category, SUM(sales) FROM products GROUP BY category;
    对应索引:INDEX idx_category_sales (category, sales)
  • 避免在 GROUP BY 中使用函数或表达式,如 GROUP BY DATE(created_at) —— 会导致索引失效;改用生成列 + 索引
  • 检查 tmp_table_sizemax_heap_table_size 是否足够大,防止内存临时表被强制落盘成磁盘临时表(性能暴跌)

实际线上优化中最容易被忽略的一点:聚合查询的执行计划里出现 Using where; Using index for group-by 才算真正走覆盖索引;只要看到 Using temporary,就说明当前索引设计没对上聚合模式,别只盯着单字段索引。