不能直接用WHERE排除MAX/MIN,因WHERE执行早于聚合计算;需用ROW_NUMBER()窗口函数正反序编号后筛选中间值,并嵌套子查询或CTE才能安全聚合。

为什么不能直接用 WHERE 排除 MAX/MIN?
因为 WHERE 执行早于 SELECT,而 MAX() 和 MIN() 是聚合结果,必须在 GROUP BY 之后才能算出。你写 WHERE score NOT IN (SELECT MAX(score), MIN(score) FROM t) 会报错——NOT IN 后面不能跟两个标量子查询,语法不合法;即使拆成两个 AND 条件,也会导致全组匹配剔除(比如有 3 个并列最高分,全被干掉),业务上往往只需要各删一个。
ROW_NUMBER() 是唯一可靠方案
必须用窗口函数给每行打序号,再筛掉“每个分组里排第一(最小)和排最后(最大)”的记录。关键点是:ROW_NUMBER() 比 RANK() 或 DENSE_RANK() 更合适,因为它强制生成唯一序号,避免并列时无法精准定位首尾。
- 先正序编号:
ROW_NUMBER() OVER (PARTITION BY class_id ORDER BY score ASC) AS rnum_asc - 再倒序编号:
ROW_NUMBER() OVER (PARTITION BY class_id ORDER BY score DESC) AS rnum_desc - 保留中间部分:
WHERE rnum_asc > 1 AND rnum_desc > 1
这样哪怕成绩完全相同,也能确保每组至少留一个中间值(除非该组只有 2 行,那剔除后为空)。
嵌套一层再聚合才安全
窗口函数只能出现在 SELECT 子句,不能直接在 WHERE 或 HAVING 中引用别名。所以必须把带窗口函数的查询包进子查询或 CTE,再在外层做 AVG() 或其他聚合。
SELECT class_id, AVG(score) AS avg_without_extremes
FROM (
SELECT *,
ROW_NUMBER() OVER (PARTITION BY class_id ORDER BY score ASC) AS rnum_asc,
ROW_NUMBER() OVER (PARTITION BY class_id ORDER BY score DESC) AS rnum_desc
FROM scores
) t
WHERE rnum_asc > 1 AND rnum_desc > 1
GROUP BY class_id;
漏掉这层嵌套,就容易写出 SELECT ..., ROW_NUMBER() ... AS rn WHERE rn > 1 这种必然报错的语句。
MySQL 8.0+ 和 PostgreSQL 可用 CTE 提高可读性
CTE 不改变逻辑,但能把步骤拆开,方便调试和复用。注意 CTE 中不能加 ORDER BY(除非配合 LIMIT),排序只应在窗口定义里写。
WITH ranked AS (
SELECT *,
ROW_NUMBER() OVER (PARTITION BY class_id ORDER BY score ASC) AS rnum_asc,
ROW_NUMBER() OVER (PARTITION BY class_id ORDER BY score DESC) AS rnum_desc
FROM scores
)
SELECT class_id, AVG(score)
FROM ranked
WHERE rnum_asc > 1 AND rnum_desc > 1
GROUP BY class_id;
真正容易被忽略的是:当某组只有 1 行或 2 行时,rnum_asc > 1 AND rnum_desc > 1 会让整组数据消失,AVG 返回 NULL——如果业务要求“不足 3 行则不剔除”,就得额外加 COUNT(*) OVER (PARTITION BY class_id) 判断后再分支处理。

















