SQL 聚合函数高效使用技巧

1次阅读

group by后字段必须出现在select聚合函数中,否则报错;where在聚合前过滤行,having在聚合后过滤组;count(*)最快最安全,count(col)仅统计非NULL值且可能全表扫描;避免在group by中使用函数导致索引失效。

SQL 聚合函数高效使用技巧

GROUP BY 后字段必须出现在 SELECT 或聚合函数

很多初学者写 SELECT name, COUNT(*) FROM users GROUP BY city 会报错,因为 name 既没参与分组也没被聚合。数据库(如 postgresqlmysql 8.0+ 严格模式)会直接拒绝这种语义模糊的查询。

常见错误现象:column "name" must appear in the GROUP BY clause or be used in an aggregate function

  • 如果真要查每个城市的某个人名,用 MAX(name)MIN(name) —— 它们不保证是“第一条”,但稳定且快
  • 想取分组内任意一条记录的完整字段?别硬塞进 GROUP BY,改用窗口函数或子查询更安全
  • MySQL 5.7 默认允许“隐式分组”(sql_mode 不含 ONLY_FULL_GROUP_BY),但这不是标准行为,迁移到其他库时大概率崩

WHERE 和 HAVING 别混用:过滤时机决定性能

WHERE 在聚合前筛行,HAVING 在聚合后筛组。用错一个,结果可能对,但性能差几倍。

使用场景举例:查“订单总额超 1 万的城市”

  • ✅ 正确:SELECT city, SUM(amount) AS total FROM orders WHERE status = 'paid' GROUP BY city HAVING total > 10000 —— 先筛已支付单,再聚合,最后过滤组
  • ❌ 错误:SELECT city, SUM(amount) AS total FROM orders GROUP BY city HAVING total > 10000 AND status = 'paid' —— status 已不存在于分组结果中,语法报错
  • ⚠️ 更隐蔽的错:SELECT city, SUM(amount) FROM orders GROUP BY city HAVING SUM(amount) > 10000 看似没问题,但如果加了 WHERE created_at > '2024-01-01' 能减少 90% 行数,就该加——早筛永远比晚筛省资源

COUNT(*)、COUNT(1)、COUNT(col) 性能差异真实存在

别信“三者完全一样”的老说法。实际执行计划和存储引擎行为会影响结果。

关键区别:

  • COUNT(*):统计行数,优化器通常走索引或元数据(如 InnoDB 的聚簇索引遍历),最快也最安全
  • COUNT(1):和 COUNT(*) 在绝大多数引擎里等价,但某些旧版本 MySQL 会多做一次常量计算,没必要用
  • COUNT(col):只统计 col IS NOT NULL 的行,如果该列允许 NULL,结果就和前两者不同;且无法利用覆盖索引(除非索引包含该列且非空约束强)
  • 特别注意:在有大量 NULL 值的列上用 COUNT(col),可能触发全表扫描,而 COUNT(*) 可能走二级索引甚至更快

避免在聚合字段上套函数导致索引失效

比如 SELECT date(created_at), COUNT(*) FROM logs GROUP BY DATE(created_at),看着合理,但 DATE() 会让 created_at 索引完全失效。

替代方案更高效:

  • 用范围分组代替函数:GROUP BY created_at::DATE(PostgreSQL)或预计算日期字段并建索引
  • 如果必须按天聚合,建生成列(MySQL 5.7+ / PG 12+):ALTER table logs ADD COLUMN day DATE AS (DATE(created_at)) STORED,再给 day 加索引
  • 时间范围大时,考虑分区表(按月/年分区),GROUP BY 自动剪枝,比函数处理快一个数量级

复杂点在于:聚合逻辑一旦嵌套函数或关联子查询,优化器基本放弃索引选择。宁可多存一列,也不要临时算。

text=ZqhQzanResources