1. 先说一个最容易被忽略的问题:MAX() 真的只有“取最大值”这么简单吗
很多同学刚开始学 SQL 的时候,第一个聚合函数接触的往往不是 COUNT() 就是 MAX()。SELECT MAX(price) FROM products,查出一个最高价,完事。看起来确实没什么好讲的。但我在实际开发里发现,MAX() 这个函数真正用好的时候,远不止“找出最大数”这么简单,而用错的时候,造成的困惑又往往非常隐蔽。
前阵子帮同事排查一个线上报表的问题,逻辑很简单:想按用户分组取出每个人最后一次登录时间,SQL 写出来大概是:
sql复制SELECT user_id, MAX(login_time)
FROM user_login_log
GROUP BY user_id;
这段 SQL 本身没问题,但同事拿这个结果去关联 user 表取用户昵称的时候,出来的数据让他怀疑人生——同一批 user_id 居然能关联出多条记录,而且昵称还不一定对。问题出在哪?出在他以为“MAX() 查出了最后一次登录时间对应的整行记录”,但实际上 MAX() 只返回那个时间值,不返回那一行的其他字段。要用最大值所在的完整记录,得换思路。
这其实是对 MAX() 函数最常见的误解之一。所以我想借这篇内容把 MAX() 从语法、分组、窗口函数、数据清洗到性能优化完整捋一遍,也把那些我在实际项目里踩过的坑、排查过的慢 SQL 一并说清楚。这篇文章适合谁?如果你刚学 SQL 不久,可以用它把聚合函数的底子打牢;如果你已经写了几年 SQL,那重点看第 5 节和第 6 节的避坑与优化思路,应该会有帮助。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. MAX() 基础语法与一个很多人没注意过的“输出规则”
2.1 标准语法和最简单的例子
MAX() 是标准 SQL 里的聚合函数,作用是返回某列的最大值。基本语法就一行:
sql复制SELECT MAX(column_name)
FROM table_name
[WHERE condition];
其中 column_name 可以是数值列、字符串列、日期列。它处理的数据类型很宽,但要注意的是:字符串列比较的是字典顺序,日期列比较的是时间先后,只有数值列才是真正数学意义上的“大小”。
举两个例子对比一下。数值列:
sql复制SELECT MAX(price) AS max_price
FROM order_items;
商品订单明细表里最高的一笔单价是多少,返回一行一列。
日期列:
sql复制SELECT MAX(created_at) AS latest_order_time
FROM orders;
订单表里最近一次下单时间。日期时间类型的大小比较就是时间先后,所以 MAX(created_at) 天然等价于“取最新时间”。
2.2 输出规则:聚合函数不配合 GROUP BY 时结果永远只有一行
这一条对新手来说特别容易困惑。在没有 GROUP BY 的情况下,对全表做聚合,结果必然是一行。注意我说的是“必然”——就算表里一行数据都没有,它也会返回一行。
空表的时候:
sql复制SELECT MAX(price) FROM empty_table;
不会报错,也不会返回零行,而是返回一行,那个字段的值是 NULL。这一点很多从 Excel 转过来的人会不适,因为 Excel 里 MAX 空区域返回 0,SQL 里返回 NULL,处理不好就是下游一堆空值报错。
2.3 MAX() 与 WHERE 的执行顺序
MAX() 配合 WHERE 时,执行顺序是先过滤后聚合。数据库引擎执行的时候会先根据 WHERE 条件把数据行筛出来,再对筛选结果做最大值运算。例如:
sql复制SELECT MAX(amount)
FROM payments
WHERE status = 'success';
这个需求语境是:只统计支付成功的订单里最大金额。如果写得不对,把条件放到聚合之后,或者忘了加条件,统计口径就会偏。
一个常见的错误是试图用 WHERE MAX(...) > 1000 筛选分组后的结果,这直接违反 SQL 执行顺序,会报错。聚合之后的条件过滤要用 HAVING,这个后面第 3 节会展开。
2.4 MAX() 对 NULL 的处理
这算是在数据质量参差不齐的生产环境里最实用的小知识点:MAX() 会自动忽略 NULL 值。
比如某张表里有一列 score,三行数据分别是 85、NULL、92,那么 SELECT MAX(score) 返回 92,而不是因为碰见 NULL 就整体变成 NULL。
但如果整列都是 NULL,或者表是空的,那 MAX() 的返回值就是 NULL。也就是说,聚合函数不会把“全 NULL”的结果当成 0 行,它仍然返回一行值 NULL。这在做数据校验的时候需要特别小心,如果下游有非空约束或数值计算,最好用 COALESCE(MAX(column), 0) 包一层兜底。
3. 分组统计里的 MAX():搭配 GROUP BY 才是生产环境的高频用法
3.1 分组求最大值的基本写法
MAX() 最常出现的生产场景是配合 GROUP BY 做分组统计。比如:
- 每个部门里工资最高的员工工资是多少
- 每个商品类目下最近一次上架时间是什么时候
- 每台服务器 CPU 使用率的历史峰值是多少
语法模板是:
sql复制SELECT group_column, MAX(value_column)
FROM table_name
GROUP BY group_column;
以员工表为例:
sql复制SELECT dept_id, MAX(salary) AS max_salary
FROM employee
GROUP BY dept_id
ORDER BY max_salary DESC;
语义是:“按部门分组,计算每个部门的最高工资”。注意一个细节:如果你在 SELECT 里同时写了 dept_id 和 MAX(salary),那么 dept_id 必须出现在 GROUP BY 中。反之,如果 GROUP BY 只写了 dept_id,那么 SELECT 里就只能出现 dept_id 和聚合函数,不能再出现 emp_name、hire_date 这类非分组非聚合列——在 MySQL 关闭 ONLY_FULL_GROUP_BY 模式时这个限制可能被放宽,但得到的结果是随机值,容易埋雷。
3.2 分组后过滤:HAVING 与 MAX() 的组合
聚合之后再过滤不能写在 WHERE 里,要用 HAVING。比如找出平均工资超过 10000 的部门中,部门的最高工资是多少:
sql复制SELECT dept_id, MAX(salary) AS max_salary
FROM employee
GROUP BY dept_id
HAVING AVG(salary) > 10000;
这个语句的完整执行逻辑是:先按 dept_id 分组,然后分别计算每组平均工资和最高工资,再保留平均工资大于 10000 的组。也就是说 HAVING 子句是在分组聚合完成之后才执行的,所以里面可以放心写聚合函数。
3.3 常见业务误用:要“最大值所在的整行”时,MAX() 单独用是做不到的
这是特别经典的一个问题。举个例子,需求是查每个部门里工资最高的员工,并且要展示员工姓名、工资、入职日期。很多人第一反应是:
code复制
