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

GROUP BY 后字段必须出现在 SELECT 或聚合函数里
很多初学者写 SELECT name, COUNT(*) FROM users GROUP BY city 会报错,因为 name 既没参与分组也没被聚合。数据库(如 postgresql、mysql 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自动剪枝,比函数处理快一个数量级
复杂点在于:聚合逻辑一旦嵌套函数或关联子查询,优化器基本放弃索引选择。宁可多存一列,也不要临时算。