SQL 分组聚合分析优化技巧

1次阅读

group by字段顺序必须与复合索引最左前缀严格一致才能命中索引;count(*)性能最优且语义清晰;having不能减少中间结果集,应优先用where过滤;非group by列必须聚合或加入group by,否则报错或返回不可靠数据。

SQL 分组聚合分析优化技巧

GROUP BY 字段顺序影响索引命中吗? 影响很大。mysqlpostgresql 都要求 GROUP BY 字段顺序与复合索引的最左前缀严格一致,才能走索引;SQL Server 稍宽松,但乱序仍大概率退化为排序 + 哈希聚合。如果业务查询固定按 status, region 分组,却建了 (region, status) 索引,GROUP BY status, region 就无法利用该索引——优化器会先全表扫描再内存排序,小表不显,大表秒变慢。

  • 索引字段顺序必须和 GROUP BY 列顺序完全一致(含方向,如都 ASC)
  • 若同时有 ORDER BY,优先保证 GROUP BY 顺序,再追加 ORDER BY 字段到索引末尾
  • WHERE 条件字段应放在索引最左侧,再接 GROUP BY 字段(例如:WHERE created_at > '2024-01-01' AND status = 'active',GROUP BY region, category → 推荐索引:(status, created_at, region, category)

用 COUNT(*) 还是 COUNT(1) 或 COUNT(列名)? 在绝大多数场景下,COUNT(*) 是最优解。它由优化器直接识别为“行计数”,不检查字段是否为 NULL;而 COUNT(列名) 必须逐行判断该列是否非空,多一次 NULL 检查开销;COUNT(1) 虽无 NULL 判断,但仍是表达式计算,部分旧版 MySQL 会额外生成常量列,徒增 CPU 开销。

  • COUNT(*) 是标准写法,语义清晰、兼容性好、性能不输任何变体
  • COUNT(列名) 仅在明确需要排除 NULL 行时才用(比如统计有手机号的用户数)
  • 不要迷信“COUNT(1) 比 COUNT(*) 快”——这是过时经验,现代引擎已无实质差异,还可能误导可读性

HAVING 子句提前过滤能减少中间结果集吗? 不能。HAVING 是在分组完成之后才执行的筛选,它对聚合后的结果集做二次过滤,不影响分组过程本身的数据量。真正能减小中间结果的是把条件尽量前移到 WHERE(过滤原始行),或用子查询/CTE 先缩小数据范围再分组。

  • 错误认知:“HAVING status = ‘done’ 会跳过其他 status 的分组” → 实际上所有分组都会先算完,再筛
  • 正确做法:把可下推的过滤写进 WHERE,例如 WHERE status IN ('done', 'pending'),再 GROUP BY status
  • 复杂条件(如需基于聚合值过滤)才必须用 HAVING,比如 HAVING COUNT(*) > 100

聚合字段里混用非 GROUP BY 列会出什么错? MySQL 5.7 默认开启 sql_mode=ONLY_FULL_GROUP_BY 后,直接报错:Expression #2 of select list is not in GROUP BY clause and contains nonaggregated column。即使关掉该模式,结果也极不可靠——数据库会从每组中随机选一行的某个字段值返回,表面跑通,实则数据错乱。

  • 所有出现在 SELECT 中、又没被聚合函数包裹的字段,必须完整出现在 GROUP BY 列表中
  • 常见翻车点:SELECT user_id, MAX(created_at), name FROM orders GROUP BY user_id —— name 没在 GROUP BY 里,也不聚合,非法
  • 安全写法:要么加进 GROUP BYGROUP BY user_id, name),要么用聚合函数(如 MAX(name),但注意语义是否合理)

聚合逻辑一旦涉及业务判断,就很容易在字段归属和执行顺序上绕晕。尤其当多个聚合函数混合、又带 HAVING 和子查询时,执行计划稍有偏差,结果就不是“慢一点”的问题,而是“错一点”。

text=ZqhQzanResources