最直接做法是用row_number()按user_id分组、order_amount降序编号后取rn=1;必须写partition by user_id和order by order_amount desc,且需在子查询/cte中使用;若仅需字段值可用first_value()但须显式指定窗口帧;并列时改用rank();索引建议(user_id, order_amount desc)。

用 ROW_NUMBER() 按用户分组排序取首行
最直接的做法是给每个用户的订单按金额降序编号,再筛选出编号为 1 的记录。注意必须用 ROW_NUMBER()(不是 RANK() 或 DENSE_RANK()),否则遇到同金额订单会返回多行,无法保证“每个用户只取一条”。
常见错误是漏写 PARTITION BY user_id,导致全表排序;或把 ORDER BY order_amount DESC 写成 ASC,结果拿到最低单。
-
SELECT *必须和ROW_NUMBER()在同一层子查询或 CTE 中,不能在窗口函数外直接WHERE rn = 1 - 如果只要最高单的金额,用
MAX(order_amount) OVER (PARTITION BY user_id)更轻量;但要整行数据(比如订单号、时间),就必须用ROW_NUMBER() - 多个订单金额相同时,
ROW_NUMBER()会任意选一个,若需稳定结果,建议追加次要排序字段,如ORDER BY order_amount DESC, created_at DESC
用 FIRST_VALUE() 直接提取字段值
如果只需要最高订单的某个字段(比如金额、订单号),FIRST_VALUE() 是更简洁的选择。它不改变行数,适合在已有查询中补列。
容易忽略的是:默认窗口帧是 ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW,会导致结果不一致。必须显式指定完整范围,否则可能拿不到真正的全局最大值。
- 正确写法:
FIRST_VALUE(order_id) OVER (PARTITION BY user_id ORDER BY order_amount DESC ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) -
FIRST_VALUE()对NULL敏感,如果最高金额订单的某字段为NULL,它就返回NULL,不会跳过 —— 这和ROW_NUMBER()+ 过滤的语义不同 - 不能用
FIRST_VALUE()替代整行提取,它一次只能取一个字段
处理并列最高单:用 RANK() 配合去重逻辑
当业务明确要求“所有并列最高订单都保留”,就不能用 ROW_NUMBER()。此时 RANK() 是更准确的语义表达:相同金额订单获得相同排名,且跳过后续名次。
但要注意,RANK() 返回的是排名值,不是布尔标记。直接 WHERE rank_num = 1 可以,但别误用 QUALIFY(仅 Snowflake/BigQuery 支持),MySQL 8.0+ 和 PostgreSQL 需靠子查询。
- PostgreSQL / MySQL 8.0+:用子查询包裹后加
WHERE rank_num = 1 - 如果还要排除测试订单(如
order_id LIKE 'TEST%'),过滤条件必须放在窗口函数之前(即子查询的WHERE),否则排名计算会包含无效数据 -
RANK()和DENSE_RANK()在并列时行为一致,区别只在后续排名是否跳号,此处无实质影响
性能与索引建议:窗口函数依赖排序效率
窗口函数本身不触发额外扫描,但 ORDER BY 子句会强制排序。如果用户量大、订单表没合适索引,性能会断崖下跌。
典型慢查询表现是执行计划里出现 WindowAgg 节点伴随高 Sort 成本。这不是语法问题,而是数据组织问题。
- 最优索引:复合索引
(user_id, order_amount DESC),可覆盖分区和排序需求 - 如果经常按时间查“最近一次最高单”,考虑
(user_id, created_at DESC, order_amount DESC),兼顾时间过滤与金额排序 - 避免在
ORDER BY中使用函数或表达式(如ABS(order_amount)),会导致索引失效
PARTITION BY 和 WHERE 的边界稍一错位,结果就全偏了。这类逻辑务必在 WHERE 先筛干净,再进窗口计算。










