在 SQL 数据分析中,分组查询(GROUP BY) 是核心技能之一。它能将数据按照指定字段归类,结合聚合函数(COUNT、SUM、AVG 等)实现 “按类别统计”,比如统计各部门人数、各商品销售额、不同性别用户数等。本文将从基础到进阶,全面讲解分组数据的使用方法、常见坑点和最佳实践。
数据分组的本质是 “分类汇总”:将数据表中具有相同特征的行归为一组,然后对每组数据执行聚合计算。
如下图:根据用户性别进行分组,然后计算每个分组的平均值,计算用户的平均年龄。

适用场景:统计各维度的汇总数据(如按地区统计订单数、按月份统计销售额等)
数据分组的核心关键字:GROUP BY(创建分组)、HAVING(过滤分组)
注意:分组需要配合聚合函数(COUNT、SUM、AVG、MAX、MIN),用于计算每组的统计值,不然分组意义不大。例如:
> select gender FROM `user` group by gender gender| ------+ | 男 | 女 | 3 row(s) fetched.
实际上,我们可能需要计算平均年龄,如下:
> select gender, avg(age) as avg_gender FROM `user` group by gender gender|avg_gender| ------+----------+ | 32.0000| 男 | 28.0000| 女 | 27.8000| 3 row(s) fetched.
创建分组
在 SQL 语言中,创建分组使用 group by 关键字,基本语法如下:
SELECT 分组字段, 聚合函数(字段) FROM 表名 [WHERE 行过滤条件] GROUP BY 分组字段;
注意:
(1)GROUP BY 后接1 个或多个分组字段,字段需与 SELECT 中 “非聚合字段” 一一对应,例如:
select gender,age from user group by gender,age;
如果 SELECT 中 “非聚合字段” 和分组字段不匹配,会抛出错误:
> select gender,name from user group by gender,age SQL Error [1055] [42000]: Expression #2 of SELECT list is not in GROUP BY clause and contains nonaggregated column 'sql_demo.user.name' which is not functionally dependent on columns in GROUP BY clause; this is incompatible with sql_mode=only_full_group_by
但是,允许分组字段不出现在 SELECT 中 “非聚合字段” ,例如:
> select gender from user group by gender,age gender| ------+ | 男 | 男 | 男 | 男 | 女 | 女 | 女 | 女 | 女 | 10 row(s) fetched.
(2)聚合函数仅作用于分组后的 “每组内部” 的数据。例如:
> select gender, sum(age) as sum_age from user group by gender gender|sum_age| ------+-------+ | 32| 男 | 112| 女 | 139| 3 row(s) fetched.
上面 SQL 语句中,sum() 函数仅仅作用域对应的分组,共三个分组,分别是性别为 ”男“、”女“ 和 NULL。
单字段分组顾名思义就是使用一个字段进行分组,例如统计不同性别的用户数量:
> SELECT gender, -- 分组字段 COUNT(*) AS user_count -- 聚合函数:统计每组行数 FROM user GROUP BY gender -- 基于 gender 进行分组 gender|user_count| ------+----------+ | 1| 男 | 4| 女 | 5| 3 row(s) fetched.
多字段分组
多字段分组即根据多个字段进行分组,如根据用户性别和年龄分组,如下:
> SELECT gender, -- 第一个分组字段 age, -- 第二个分组字段 COUNT(*) AS user_count FROM user GROUP BY gender, age gender|age|user_count| ------+---+----------+ | 32| 1| 男 | 24| 1| 男 | 25| 1| 男 | 28| 1| 男 | 35| 1| 女 | 22| 1| 女 | 27| 1| 女 | 29| 1| 女 | 30| 1| 女 | 31| 1| 10 row(s) fetched.
分组的常见问题
多表关联时,若分组字段在多个表中重名,必须指定表名 / 别名,例如:关联 user 表和 order 表,统计用户的订单数量,此时 user_id 字段在 user 表和 order 表均存在
-- 错误示例:user_id 来源不明确 > select user_id, count(*) order_count from user u join `order` o on o.user_id=u.user_id group by user_id SQL Error [1052] [23000]: Column 'user_id' in field list is ambiguous -- 正确示例:指定表别名,u.user_id 指定采用 user 表中的 user_id 字段进行分组 > select u.user_id, count(*) order_count from user u join `order` o on o.user_id=u.user_id group by u.user_id user_id|order_count| -------+-----------+ 1| 3| 2| 3| 3| 4| 4| 3| 5| 4| 6| 3| 6 row(s) fetched.
注意,后续章节将介绍多表关联。
非聚合字段必须出现在GROUP BY中,否则会报错。例如:
-- 错误示例:SELECT中的age未在GROUP BY中,且非聚合字段 > SELECT gender, age, COUNT(*) FROM user GROUP BY gender SQL Error [1055] [42000]: Expression #2 of SELECT list is not in GROUP BY clause and contains nonaggregated column 'sql_demo.user.age' which is not functionally dependent on columns in GROUP BY clause; this is incompatible with sql_mode=only_full_group_by -- 正确示例:要么将age加入GROUP BY,要么对age使用聚合函数 > SELECT gender, AVG(age) AS avg_age, COUNT(*) FROM user GROUP BY gender gender|avg_age|COUNT(*)| ------+-------+--------+ |32.0000| 1| 男 |28.0000| 4| 女 |27.8000| 5| 3 row(s) fetched.
WHERE 用于过滤行,HAVING 用于过滤分组,注意:只有满足 HAVING 条件的分组才会被保留。
下面是 WHERE 和 HAVING 关键字的区别:
关键字 | 作用对象 | 执行时机 | 能否使用聚合函数 |
WHERE | 行 | 分组前过滤 | 不能 |
HAVING | 分组 | 分组后过滤 | 能 |
根据性别 ”gender“ 进行分组,过滤掉分组中用户数小于 2 的分组。如下:
-- 未过滤的分组数据 > select gender, count(*) as count_user from user group by gender gender|count_user| ------+----------+ | 1| 男 | 4| 女 | 5| 3 row(s) fetched. -- 过滤分组用户数小于2的分组 > select gender, count(*) as count_user from user group by gender having count_user>=2 gender|count_user| ------+----------+ 男 | 4| 女 | 5| 2 row(s) fetched.
需求:统计 “年龄≥28 岁” 的用户中,用户数≥2 的部门
> select gender, count(*) as count_user from user where gender is not null -- 先过滤:仅保留性别不为NULL的用户 group by gender having count_user>=2 -- 再过滤:仅保留用户数≥的用户 gender|count_user| ------+----------+ 男 | 4| 女 | 5| 2 row(s) fetched.
GROUP BY 仅负责分组,不保证分组结果的顺序。如果需对分组结果排序,必须使用 ORDER BY 子句。
按 “用户数降序” 统计用户的数量,如下:
> select gender, count(*) as count_user from user where gender is not null -- 先过滤:仅保留性别不为NULL的用户 group by gender having count_user>=2 -- 再过滤:仅保留用户数≥的用户 order by count_user desc -- 根据用户数降序排序 gender|count_user| ------+----------+ 女 | 5| 男 | 4| 2 row(s) fetched.
先按性别升序,再按用户数降序统计用户,例如:
> select gender, count(*) as count_user from user where gender is not null -- 先过滤:仅保留性别不为NULL的用户 group by gender having count_user>=2 -- 再过滤:仅保留用户数≥的用户 order by gender asc, count_user desc gender|count_user| ------+----------+ 男 | 4| 女 | 5| 2 row(s) fetched.
注意:GROUP BY 与 ORDER BY 的坑
(1)不要依赖 GROUP BY 的默认排序:不同数据库(MySQL、PostgreSQL、Oracle)的分组默认顺序可能不同;
(2)排序字段可使用别名:ORDER BY user_count 等价于 ORDER BY COUNT(*),推荐用别名提升可读性。
写 SQL 时我们习惯按 SELECT → FROM → WHERE → GROUP BY → HAVING → ORDER BY 书写,但数据库的执行顺序完全不同,这是理解分组查询的关键。官方执行顺序
(1)FROM:确定查询的基础表;
(2)WHERE:过滤表中的行;
(3)GROUP BY:对过滤后的行分组;
(4)HAVING:过滤分组后的结果;
(5)SELECT:筛选最终要返回的字段(包括聚合计算);
(6)ORDER BY:对最终结果排序;
(7)LIMIT:限制返回的行数(可选);
示例验证:
> SELECT gender AS dept, -- 步骤5:别名生效 COUNT(*) AS user_count FROM user -- 步骤1 WHERE age > 20 -- 步骤2 GROUP BY gender -- 步骤3 HAVING user_count >= 2 -- 步骤4:注意!这里不能用别名(部分数据库支持,但不推荐) ORDER BY user_count DESC dept|user_count| ----+----------+ 女 | 5| 男 | 4| 2 row(s) fetched.
注意,HAVING 执行在 SELECT 之前,因此HAVING 中不能直接使用 SELECT 的字段别名(MySQL 例外,但不推荐),应使用原始聚合函数:
-- 正确写法 HAVING COUNT(*) ≥ 2; -- 错误写法(部分数据库报错) HAVING user_count ≥ 2;
完整 SQL 语句如下:
> SELECT gender AS dept, -- 步骤5:别名生效 COUNT(*) AS user_count FROM user -- 步骤1 WHERE age > 20 -- 步骤2 GROUP BY gender -- 步骤3 HAVING COUNT(*) >= 2 -- 步骤4:注意!这里不能用别名(部分数据库支持,但不推荐) ORDER BY user_count DESC dept|user_count| ----+----------+ 女 | 5| 男 | 4| 2 row(s) fetched.