在关系型数据库里,统计不同地区的平均工资是典型的分组聚合需求。核心思路是利用GROUP BY把具有相同地区值的记录划分到同一个桶中,再对每个桶使用AVG函数计算薪资的平均值。这种方式比在应用层先查出全量数据再自己写循环求平均要高效得多,因为分组和聚合都在数据库引擎内部以集合运算完成,既能减少网络传输,也能借助索引加速。

基础语法与可运行示例
最基础的写法是在SELECT子句中同时列出分组列和聚合函数。假设我们有一张employee表,其中包含region(地区)和salary(月薪)两个字段。下面的语句会按region分组,并算出每个地区的不重复平均薪资。
需要注意,出现在SELECT中但没有被聚合的列,必须全部写在GROUP BY后面,否则标准SQL会报错。某些数据库如MySQL在宽松模式下允许非聚合列出现在SELECT里,但结果不可控,生产环境应严格遵循标准写法。另外,AVG默认忽略NULL值,如果某员工salary为NULL,他不会参与该地区的平均计算,但也不会从分组中消失。
SELECT
region,
AVG(salary) AS avg_salary,
COUNT(*) AS emp_count
FROM employee
GROUP BY region
ORDER BY avg_salary DESC;
上面这段代码中,COUNT(*)统计的是每个地区的总行数,包括salary为NULL的人;如果想只看有薪资的人数,可写成COUNT(salary)。ORDER BY放在最后,用于对计算结果排序,数据库通常会在分组聚合完成后再执行排序。
处理空地区与多维度拆解
真实业务里,region字段可能存在NULL,代表地区信息缺失。GROUP BY会把所有NULL归为同一组,输出一行region为NULL的平均工资,这容易让读报表的人困惑。我们可以用COALESCE把NULL转成明确标记,使结果更清晰。
此外,有时不仅要按大区,还要按城市或部门交叉统计。这时GROUP BY后面可以写多个列,数据库会按照列的顺序做复合分组。例如先按region再按city,就能得到每个大区下面各个城市的平均工资。也可以结合CASE WHEN做自定义分区,比如把低于某值的薪资单独归类,但那样AVG仍是针对原值,只是分组标签变了。
SELECT
COALESCE(region, '未知地区') AS region_label,
city,
AVG(salary) AS avg_salary
FROM employee
GROUP BY region, city
ORDER BY region_label, avg_salary DESC;
在多列分组时,索引设计很关键。如果经常在region、city上做GROUP BY,建立联合索引(region, city)能让数据库用松散索引扫描快速得到分组边界,避免临时表落盘。没有合适索引时,数据量一大就会触发filesort或外部排序,响应明显变慢。
常见错误与性能优化思路
新手常犯的一类错误是把WHERE和HAVING混淆。WHERE在分组前过滤行,比如只想统计2023年入职的员工,应写WHERE hire_date >= '2023-01-01';而HAVING用于过滤分组后的结果,例如只保留平均薪资大于一万的地区,要写HAVING AVG(salary) > 10000。把聚合条件放进WHERE会导致语法错误,因为WHERE执行时分组还没发生。
另一个性能误区是认为AVG一定比SUM/COUNT慢。实际上在大多数引擎中,AVG只是SUM除以COUNT的语法糖,并不会多扫一遍数据。真正的瓶颈往往是分组字段基数过高或缺少索引。对于超大型表,可考虑先按地区汇总到物化视图,或者只用必要列避免宽表读取。如下示例展示了HAVING的正确用法。
SELECT
region,
AVG(salary) AS avg_salary
FROM employee
WHERE status = 1
GROUP BY region
HAVING AVG(salary) > 10000;
最后补充一点,如果地区名称本身不规范,比如“北京”和“北京市”被当成两个值,GROUP BY会识别出两组。此时应先在ETL阶段做字典映射统一,或者在SQL里用UPDATE清理维度表,否则统计出的平均工资会出现人为分裂,误导管理决策。通过上面这些写法与注意点,就能用AVG加GROUP BY稳定准确地算出不同地区平均工资。