group by 会压缩明细行是因为其本质是聚合操作,将多行合并为单行统计结果;要保留明细并计算分组值,应使用窗口函数如sum() over(partition by x)。

GROUP BY 为什么“丢”了明细行
因为 GROUP BY 的本质是聚合——它把多行压缩成一行,原始的每行数据(比如每笔订单的 order_id、created_at)在结果里不再单独存在。你看到的只是分组后的统计值(如 COUNT(*)、SUM(amount)),不是原始记录本身。
这不是 bug,是设计行为。想保留明细又带分组计算,就得换思路:不走聚合,改用窗口函数。
用 ROW_NUMBER() + 子查询强行“还原”明细
当你要在分组后仍显示每条原始记录,并附带该组的统计值(比如“本用户共下了 5 单”),ROW_NUMBER() 本身不解决聚合问题,但配合子查询或 CTE 可以绕过 GROUP BY 的压缩限制。
- 先用窗口函数算出每组的聚合值,不改变行数
- 再按需过滤或排序,比如只取每组最新一条:
ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY created_at DESC) - 不能直接在
WHERE中用窗口函数结果,必须嵌套一层(CTE 或子查询)
示例:查每个用户的首单时间,同时保留所有订单明细
SELECT user_id, order_id, amount,
MIN(created_at) OVER (PARTITION BY user_id) AS first_order_time
FROM orders;
SUM / COUNT / AVG 等聚合函数加 OVER 就是窗口版
把 SUM(amount) 改成 SUM(amount) OVER (PARTITION BY user_id),结果不再是单行,而是每行都带上“该用户总金额”。这正是替代 GROUP BY 做分组统计但不丢明细的核心操作。
-
COUNT(*) OVER (PARTITION BY x)相当于“每组多少行”,比GROUP BY x+COUNT(*)多保留了原始字段 - 注意
ORDER BY在窗口定义里会触发累积计算(如SUM() OVER (PARTITION BY x ORDER BY dt)),不加才是全组总和 - MySQL 8.0+、PostgreSQL、SQL Server、Oracle 都支持;SQLite 3.25+ 也支持,但旧版不支持
容易忽略的 NULL 和排序陷阱
窗口函数对 NULL 的处理和普通聚合不同:默认 PARTITION BY 会把 NULL 视为同一组,但某些数据库(如 PostgreSQL)允许用 IS NOT DISTINCT FROM 显式控制;更常见的是排序字段含 NULL 导致 ROW_NUMBER() 排序结果不稳定。
- 如果
ORDER BY字段可能为NULL,加上NULLS LAST(PostgreSQL/Oracle)或用COALESCE(dt, '9999-01-01')做兜底 - MySQL 不支持
NULLS LAST,得靠ORDER BY col IS NULL, col - 没写
PARTITION BY时,OVER ()表示整张表为一组,小心误算全局值
真正难的不是写对语法,而是想清楚:你到底要“按组看汇总”,还是“看每条记录的同时知道它所属组的汇总”——后者才是窗口函数不可替代的场景。










