在SQL的聚合查询场景中,使用GROUP BY进行分组聚合时,查询的SELECT子句只能包含分组字段和聚合函数,非分组列无法直接被选中,这是SQL标准对GROUP BY语法的硬性约束。如果强行将非分组列加入SELECT子句,部分数据库会直接报错,部分数据库虽然不报错但返回的结果是不确定的,无法满足业务需求。

GROUP BY保留非分组列的常见方案
子查询关联法
这种方案的核心是先把聚合结果通过GROUP BY查询出来作为子查询,再将子查询和原表通过分组字段进行关联,从而拿到非分组列的数据。假设我们有一张订单表order_info,结构如下:
-- 订单表结构
CREATE TABLE order_info (
order_id INT PRIMARY KEY,
user_id INT,
goods_name VARCHAR(50),
order_amount DECIMAL(10,2),
order_date DATE
);
现在需要查询每个用户的最新订单信息,包括用户ID、订单金额、订单日期、商品名称,其中商品名称是非分组列。使用子查询关联的写法如下:
SELECT
o.user_id,
o.order_amount,
o.order_date,
o.goods_name
FROM order_info o
INNER JOIN (
-- 子查询先拿到每个用户的最新订单日期
SELECT user_id, MAX(order_date) AS latest_date
FROM order_info
GROUP BY user_id
) t ON o.user_id = t.user_id AND o.order_date = t.latest_date;
这种方案的优点是兼容性好,所有支持标准SQL的数据库都可以使用,缺点是如果关联字段没有唯一约束,可能会出现重复数据,而且多表关联会增加查询的执行成本。
窗口函数法
窗口函数可以在不改变原表行数的情况下,对数据进行分组聚合计算,聚合结果会作为新列添加到每一行数据中,因此天然支持保留非分组列。同样查询每个用户的最新订单信息,使用窗口函数的写法如下:
SELECT
user_id,
order_amount,
order_date,
goods_name
FROM (
SELECT
*,
-- 按用户分组,按订单日期倒序排序,给每行打序号
ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY order_date DESC) AS rn
FROM order_info
) t
WHERE rn = 1;
这里使用ROW_NUMBER()窗口函数,按照user_id分组,按照order_date降序排序,序号为1的就是每个用户的最新订单,外层查询筛选序号为1的行即可得到结果,所有原表的列都可以正常保留。
窗口函数与GROUP BY的选型对比
两种方案各有适用场景,我们可以从以下几个维度进行对比:
| 对比维度 | GROUP BY+子查询 | 窗口函数 |
|---|---|---|
| 兼容性 | 所有数据库支持 | MySQL8.0+、PostgreSQL、SQL Server等主流新版本支持,旧版本数据库不支持 |
| 非分组列保留能力 | 需要额外关联,间接保留 | 原生支持,直接保留 |
| 执行效率 | 多表关联,数据量大时效率较低 | 单次扫描计算,数据量大时效率更高 |
| 适用场景 | 只需要聚合结果,或者数据库不支持窗口函数 | 需要保留非分组列,且数据库支持窗口函数 |
注意事项
- 使用子查询关联时,要确保关联条件能够唯一匹配到目标行,否则会出现重复数据,必要时可以加上order_id等唯一字段作为关联条件。
- 窗口函数中的排序规则要符合业务需求,比如查询最新订单用DESC排序,查询最早订单用ASC排序。
- 如果只需要聚合后的统计结果,不需要保留非分组列,优先使用GROUP BY,执行效率会略高于窗口函数。
在实际开发中,我们可以根据数据库的版本、业务需求、数据量大小来选择合适的方案,既满足功能需求,也保证查询的执行效率。