最稳妥的做法是COUNT(DISTINCT CASE WHEN ... THEN x END)。它在聚合内部完成条件过滤、去重和计数,兼容主流数据库,避免WHERE提前删行导致分组数据丢失或子查询逻辑错误。

GROUP BY 里怎么数“满足条件的去重值”
直接说结论:COUNT(DISTINCT CASE WHEN ... THEN x END) 是最稳妥的做法。它把条件过滤、去重、计数三步压进一个聚合表达式,语义清晰,各数据库(MySQL 5.7+、PostgreSQL、SQL Server、Oracle)都支持。
常见错误是先 WHERE 再 GROUP BY —— 这会整个删掉不满足条件的行,导致分组丢失其他字段的完整数据;也有人想用子查询套 DISTINCT,结果要么报错(如 MySQL 5.7 的 ONLY_FULL_GROUP_BY),要么逻辑错乱(比如漏统计空值或重复计数)。
-
CASE WHEN status = 'active' THEN user_id END:注意ELSE省略即为NULL,而COUNT自动忽略NULL,正好符合“只数满足条件的”需求 - 如果字段本身可能为
NULL(比如user_id允许为空),且你不想把它算进去,这个写法天然安全;但如果你需要把NULL当作一个有效值去重,就得显式写ELSE 'null_marker' - 别用
SUM(CASE WHEN ... THEN 1 ELSE 0 END)替代——它不带DISTINCT,纯属计数行数,不是去重计数
MySQL 8.0+ 和 PostgreSQL 用 FILTER 能更干净
这两个数据库支持聚合函数的 FILTER 子句,语义更直白,也避免了 CASE 表达式里隐含的 NULL 处理歧义。
比如统计每个部门里不同邮箱域名的数量(只看已验证邮箱):
SELECT dept, COUNT(DISTINCT email_domain) FILTER (WHERE verified = true) FROM users GROUP BY dept;
对比传统写法:
SELECT dept, COUNT(DISTINCT CASE WHEN verified = true THEN email_domain END) FROM users GROUP BY dept;
两者结果一致,但 FILTER 更易读、不易手误漏写 THEN 后的字段;不过要注意:SQLite 不支持,老版本 MySQL(
别在 COUNT(DISTINCT ...) 里塞复杂表达式
比如 COUNT(DISTINCT CONCAT(first_name, '-', last_name)) 看似能拼出唯一标识,但容易踩坑:
- 如果
first_name或last_name是NULL,整个CONCAT返回NULL,被COUNT忽略——你可能以为人没来,其实是名字不全被吃了 - 拼接符
'-'若恰好出现在某人姓或名里(比如first_name = 'Jean-Paul'),会导致两个不同组合撞成同一个字符串 - 性能上,每行都要执行字符串拼接再哈希去重,比直接对
id或email去重慢不少,尤其数据量大时
真要拼字段去重,优先考虑用 (first_name, last_name) 这样的行构造器(PostgreSQL 支持,MySQL 8.0+ 在某些上下文也支持),语义明确且 NULL 安全。
WHERE 和 HAVING 混用时,DISTINCT 的作用域容易搞反
例如想查“至少有 3 个不同用户状态的部门”,有人写:
SELECT dept
FROM users
WHERE status IN ('active', 'pending')
GROUP BY dept
HAVING COUNT(DISTINCT status) >= 3;
这永远返回空——因为 WHERE 已经把 status 锁死在两个值,COUNT(DISTINCT status) 最大只能是 2。
正确做法是去掉 WHERE,把条件挪进聚合内部:
SELECT dept
FROM users
GROUP BY dept
HAVING COUNT(DISTINCT CASE WHEN status IN ('active', 'pending', 'blocked') THEN status END) >= 3;
关键点在于:条件逻辑必须落在聚合函数内部,才能保证“按原始分组全量计算 + 局部过滤”,而不是先砍数据再统计。
这种嵌套层级一多,括号和 END 就容易漏,建议写完立刻用简单测试数据跑一遍,重点看 NULL 分组和边界条件是否符合预期。










