在数据分析与报表统计中,条件求平均值是一个非常高频的需求。比如统计每个部门的平均薪资时只计算在职员工、计算商品平均售价时只统计销量大于100的记录、求平均分时排除缺考学生等。很多初学者习惯把这些逻辑写到WHERE条件里,结果发现原本应该参与分组的行被整行过滤掉了,导致统计口径出错。本文将系统讲解SQL SELECT中实现条件求平均的几种方法,包括最经典的AVG配合CASE WHEN写法、SQL标准中的FILTER子句,以及不同场景下的选择策略。

方法一:AVG 结合 CASE WHEN 实现条件平均
这是兼容性最好、应用最广泛的写法。AVG函数在计算时会自动忽略NULL值,而CASE WHEN可以灵活地把不符合条件的行返回为NULL,符合条件的行返回实际数值,两者组合就实现了条件求平均。核心思路是:不让不满足条件的行参与运算,而不是把它们从结果集中删除。
基本语法如下:
SELECT
AVG(CASE WHEN salary > 5000 THEN salary END) AS high_avg_salary,
AVG(CASE WHEN salary <= 5000 THEN salary END) AS low_avg_salary
FROM employees;
注意上面CASE WHEN没有写ELSE分支,此时不满足WHEN条件的行会默认返回NULL,而AVG会忽略这些NULL,这正是我们需要的效果。如果写成ELSE 0,所有行都会参与运算,平均值会被严重拉低,这是新手最容易踩的坑。
这种写法最大的优势在于可以在一次查询中同时输出多个口径的平均值。上面的例子在同一条SELECT语句里分别计算了高薪和低薪两组的平均薪资,如果用WHERE过滤则需要写两条查询或者用UNION拼接,效率明显更低。此外,CASE WHEN内部还可以嵌套AND、OR组合多个条件,例如:
-- 统计各部门2024年入职且在职员工的平均薪资
SELECT
department_id,
AVG(CASE WHEN status = 1 AND hire_date >= '2024-01-01' THEN salary END) AS avg_salary_2024
FROM employees
GROUP BY department_id;
需要提醒的是,如果某个分组内所有行都不满足条件,AVG的结果会是NULL而不是0。如果业务上需要显示为0,可以用COALESCE函数处理:COALESCE(AVG(...), 0)。
方法二:使用 FILTER 子句的简洁写法
FILTER是SQL标准引入的聚合修饰子句,PostgreSQL、SQLite较新版本、DuckDB等数据库都支持,语法比CASE WHEN更加直观清晰。它直接在聚合函数后面声明过滤条件,只有满足条件的行才参与该聚合计算。
写法如下:
SELECT
department_id,
AVG(salary) FILTER (WHERE status = 1) AS avg_active_salary,
AVG(salary) FILTER (WHERE status = 0) AS avg_inactive_salary
FROM employees
GROUP BY department_id;
与CASE WHEN相比,FILTER的可读性明显更好,条件紧跟聚合函数,一眼就能看出这列统计的是什么口径。而且在PostgreSQL等实现中,FILTER在执行计划层面可以直接跳过不满足条件的行,某些场景下性能略优。
不过FILTER的短板是兼容性。MySQL、SQL Server、Oracle目前都不支持这个语法。如果项目需要跨数据库运行,或者用的是主流商业数据库,仍然推荐使用CASE WHEN写法以保证可移植性。在写法选择上有一个简单的判断原则:确认数据库支持且追求可读性时用FILTER,需要兼容多种数据库时用CASE WHEN。
方法三:WHERE 过滤与子查询的适用边界
当整个查询只统计一个口径的平均值时,直接在WHERE中加条件是最简单直接的方式:
SELECT department_id, AVG(salary) AS avg_salary FROM employees WHERE status = 1 GROUP BY department_id;
但WHERE的作用域是整行数据,一旦同一查询中还要统计其他口径,或者SELECT列表里还有别的聚合列(比如统计全部员工的计数),WHERE就无能为力了。因为WHERE把行过滤掉之后,其他聚合函数也无法再访问这些行。这时必须回到CASE WHEN或FILTER方案。
还有一种思路是使用标量子查询或JOIN派生表,把不同口径分别查询再关联:
SELECT
d.dept_name,
a.avg_active,
b.avg_all
FROM departments d
LEFT JOIN (
SELECT department_id, AVG(salary) AS avg_active
FROM employees WHERE status = 1
GROUP BY department_id
) a ON d.department_id = a.department_id
LEFT JOIN (
SELECT department_id, AVG(salary) AS avg_all
FROM employees
GROUP BY department_id
) b ON d.department_id = b.department_id;
这种写法逻辑清晰、每个子查询职责单一,在复杂报表中维护性不错。但代价是需要对同一张表扫描多次,数据量大时性能开销明显。如果条件不多,优先考虑单次扫描的CASE WHEN方案。
常见陷阱与最佳实践
第一个陷阱是NULL与0的混淆。AVG本身忽略NULL,但COUNT和SUM的行为需要额外留意,混合使用多个聚合函数时务必想清楚每一列的统计口径。第二个陷阱是分组维度与条件的关系:条件只影响聚合列,不影响分组的行数,GROUP BY产出的分组数量由分组字段决定,这一点与WHERE过滤有本质区别。
第三个陷阱是除零与空集问题。条件过严可能导致某些分组内没有任何满足条件的行,AVG返回NULL。前端展示时建议统一用COALESCE兜底。第四个陷阱是浮点精度,AVG返回的可能是浮点数,涉及金额时建议显式ROUND到指定小数位,例如ROUND(AVG(CASE WHEN ... THEN salary END), 2)。
总结一下选择建议:单一口径统计用WHERE最简单;同一查询多口径统计优先CASE WHEN,PostgreSQL环境可以用FILTER提升可读性;超复杂的报表可以用子查询拆分逻辑但要注意扫描次数。掌握这几种写法后,条件求平均的需求基本都能游刃有余地解决。