COUNT(CASE WHEN condition THEN 1 END) 是统计每组内满足条件记录数的正确写法,因 COUNT 忽略 NULL;WHERE 用于分组前过滤,HAVING 用于分组后筛选;需注意 NULL 处理、索引优化及数据库兼容性。

GROUP BY + COUNT() 是最直接的解法
想统计每组里满足某个条件的记录数,COUNT() 配合 GROUP BY 是标准做法。但注意:COUNT(*) 统计的是组内所有行,而你要的是“满足条件的行数”,所以得用条件过滤——不是在 WHERE 里全局过滤(那会先筛掉不满足的组),而是用 COUNT() 内部结合 CASE 或布尔表达式。
- MySQL 8.0+ 和 PostgreSQL 支持直接写
COUNT(condition),但其实它等价于COUNT(CASE WHEN condition THEN 1 END),因为COUNT()只统计非 NULL 值 - SQL Server 不支持
COUNT(布尔表达式),必须显式写COUNT(CASE WHEN ... THEN 1 END) - 别用
SUM(IF(condition, 1, 0))(MySQL)或SUM(CASE WHEN ... THEN 1 ELSE 0 END)——虽然结果一样,但语义上COUNT更准确,且部分引擎对COUNT有优化
WHERE 和 HAVING 的边界必须分清
WHERE 过滤的是“进入分组前的原始行”,HAVING 过滤的是“分组聚合后的结果”。比如要查“每个部门中薪资 > 5000 的员工人数 ≥ 3 的部门”,就得先算出各组的计数,再用 HAVING 筛:
SELECT dept_id, COUNT(*) AS high_paid_count FROM employees WHERE salary > 5000 -- 先筛出高薪员工(注意:这里不是你要的“组内条件计数”,只是预过滤) GROUP BY dept_id HAVING COUNT(*) >= 3;
但如果需求是“每个部门里年龄 > 30 的员工数”,就不能把 age > 30 放 WHERE,否则年龄 ≤ 30 的员工整条记录被丢弃,导致该部门总人数变少、甚至整个部门消失。正确写法是把条件放进聚合函数里:
SELECT dept_id,
COUNT(CASE WHEN age > 30 THEN 1 END) AS senior_count
FROM employees
GROUP BY dept_id;NULL 值会让 COUNT(CASE ...) 出现意外结果
COUNT() 忽略 NULL,所以 CASE WHEN condition THEN 1 ELSE NULL END 是安全的;但若写成 ELSE 0,就会把 0 当作有效值计入——结果变成组内总行数,而不是满足条件的行数。
- 错误写法:
COUNT(CASE WHEN status = 'active' THEN 1 ELSE 0 END)→ 所有行都计数 - 正确写法:
COUNT(CASE WHEN status = 'active' THEN 1 END)→ 只计 active 行 - 更清晰的替代:
SUM(CASE WHEN status = 'active' THEN 1 ELSE 0 END),语义明确,且不会因漏写ELSE导致逻辑错乱
性能敏感时注意索引是否生效
带 CASE 的聚合本身无法利用普通 B-tree 索引加速计算,但如果你的“条件”字段本身有索引,数据库仍可能用索引快速定位相关行(尤其当条件能大幅减少扫描量时)。不过,真正影响性能的是 GROUP BY 字段是否被索引覆盖:
- 如果
GROUP BY dept_id,且dept_id无索引,全表扫描 + 排序分组开销很大 - 复合索引如
(dept_id, age)对GROUP BY dept_id+WHERE age > 30效果更好 - PostgreSQL 中可考虑生成式列 + 索引:
ALTER TABLE employees ADD COLUMN is_senior BOOLEAN GENERATED ALWAYS AS (age > 30) STORED,然后在is_senior上建索引
实际执行前务必看 EXPLAIN,别假设优化器一定聪明。

















