应优先使用sum(case when)和count(case when)进行条件聚合,一次扫描完成多指标统计,避免多次子查询导致的性能损耗;需注意null处理、类型对齐及数据库兼容性。

用SUM(CASE WHEN)和COUNT(CASE WHEN)一次性算多个指标
当你需要在同一个查询里统计“总订单数”“已支付订单数”“退款订单数”三类数据,却写了三条子查询或三个独立SELECT,就属于典型可优化场景。条件聚合不是炫技,是让数据库只扫一遍表,把不同条件的计数/求和全塞进一行结果里。
-
COUNT(CASE WHEN status = 'paid' THEN 1 END)比COUNT(*) FILTER (WHERE status = 'paid')兼容性更广,MySQL、SQL Server、Oracle 都支持;PostgreSQL 用户可选后者,但注意它不支持占位符(如WHERE status IN (?)) -
ELSE 0和ELSE NULL效果不同:COUNT()忽略NULL,所以COUNT(CASE WHEN ... THEN 1 ELSE NULL END)才是安全写法;SUM()则建议用ELSE 0,避免NULL使整列变NULL - 别写
COUNT(CASE WHEN status = 'paid' THEN status END)——万一 status 是 NULL,这个表达式就返回 NULL,而 COUNT 会跳过它,导致漏计;显式写THEN 1或THEN order_id更可靠
GROUP BY + 条件聚合 vs 多个子查询:性能差在哪?
三个子查询意味着数据库至少三次全表扫描(或三次索引查找),每次都要构造临时结果集、传输、再合并。而条件聚合在一次扫描中完成所有分支判断,I/O 和 CPU 开销直接砍掉三分之二以上。
- 子查询版本容易触发 MySQL 的“相关子查询误判”,尤其当外层有
GROUP BY时,优化器可能对每组重复执行子查询——实测万级数据下从 0.3 秒拖到 8 秒 - 如果子查询含
GROUP BY或ORDER BY,还可能禁用物化(materialization),退化为嵌套循环,这时 JOIN 改写比条件聚合更有效 - 条件聚合无法替代“查某字段是否存在”的逻辑(比如
WHERE id IN (SELECT ...)),那得用EXISTS或JOIN,不是聚合能解决的
FILTER 子句能完全替代 CASE WHEN 吗?
不能。FILTER 是 PostgreSQL 特有语法糖,仅用于聚合函数后加条件,且只接受常量或列引用,不支持表达式或参数化值。
- 正确:
COUNT(*) FILTER (WHERE status = 'completed')、AVG(amount) FILTER (WHERE amount > 0) - 错误:
COUNT(*) FILTER (WHERE status IN (?))(占位符不被接受)、SUM(amount) FILTER (WHERE EXTRACT(YEAR FROM created_at) = 2024)(表达式不允许) - 全不匹配时返回
NULL,必须用COALESCE(COUNT(*) FILTER (...), 0)兜底,否则前端可能报空值异常
什么时候该放弃条件聚合,改用其他方案?
条件聚合适合“同维度、多口径”的统计,比如按地区算销售额、退货额、毛利额。一旦涉及跨维度关联或存在性判断,硬套就会绕远路。
- 要查“每个用户最近一笔订单时间”,不能用
MAX(created_at) FILTER (WHERE ...)解决——因为 FILTER 不支持窗口语义,得用ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY created_at DESC) - 子查询返回的是单值(如
SELECT MAX(updated_at) FROM config),且不依赖外层行,直接用标量子查询更清晰,没必要塞进 CASE - UNION 多表后再聚合,必须用子查询包裹(
SELECT ... FROM (SELECT ... UNION ALL SELECT ...) AS t GROUP BY ...),这时条件聚合只能作用于内层,不能跨 UNION 结果做全局条件统计
ELSE NULL 写错位置,结果就全偏了。











