必须用 row_number() 标记每组内升序和降序位置,过滤 rn_asc > 1 and rn_desc > 1 才能精准剔除一个最高分和一个最低分;组内少于3条时 avg() 返回 null,需用 having count(*) >= 3 或 case 处理。

用窗口函数排序后跳过首尾行
直接在 GROUP BY 后用 AVG() 无法跳过极值,必须先标记每组内的排名。核心思路是:对每组数据按分数升序编号,再排除 ROW_NUMBER() = 1(最低)和最大编号(最高)。注意,ROW_NUMBER() 和 RANK() 行为不同——前者严格递增,后者会并列跳号,这里必须用 ROW_NUMBER() 才能精准剔除“一个最低”和“一个最高”,哪怕存在重复分。
实操建议:
- 用
ROW_NUMBER() OVER (PARTITION BY group_col ORDER BY score)标记升序位置 - 同时用
ROW_NUMBER() OVER (PARTITION BY group_col ORDER BY score DESC)标记降序位置(或用COUNT(*) OVER (PARTITION BY group_col)算总数再减) - 外层过滤掉两个方向上都是首尾的行:
rn_asc > 1 AND rn_desc > 1
处理组内少于3条记录的边界情况
如果某组只有1或2条记录,剔除首尾后可能无数据,AVG() 返回 NULL。这不是bug,而是符合数学定义——没数可平均。但业务上常需明确提示,比如返回 0 或报错。
常见错误现象:AVG() 结果为空,前端展示成“-”或直接消失,排查时才发现是组太小被清空了。
实操建议:
- 加
HAVING COUNT(*) >= 3在分组前筛掉无效组(若允许丢弃该组) - 用
CASE WHEN COUNT(*) 显式控制输出 - MySQL 8.0+ 可结合
IFNULL(AVG(...), 0),但注意 0 可能与真实均值混淆
兼容 MySQL 5.7 / PostgreSQL / SQL Server 的写法差异
窗口函数在 MySQL 5.7 不可用,PostgreSQL 和 SQL Server 2017+ 支持完整语法,但旧版 SQL Server 需用 TOP + 子查询模拟。跨数据库时最稳妥的是两层子查询法:先算每组 min/max,再 JOIN 掉极值行。
性能影响:窗口函数通常比多层子查询快;但若表极大且未在 (group_col, score) 上建联合索引,两者都会慢。
实操建议:
- MySQL 5.7 必须用自连接或相关子查询,例如:
WHERE score NOT IN (SELECT MIN(score), MAX(score) FROM t AS t2 WHERE t2.group_col = t.group_col)—— 注意NOT IN遇NULL整行失效,要加score IS NOT NULL - PostgreSQL 可直接用
ROW_NUMBER(),且支持FILTER子句:AVG(score) FILTER (WHERE rn_asc > 1 AND rn_desc > 1) - SQL Server 建议用 CTE +
ROW_NUMBER(),避免嵌套过深
当最高/最低分有多个时怎么选?
ROW_NUMBER() 每次只踢一个最高、一个最低,不管有多少并列。这是多数场景想要的行为(比如评委打分,去掉一个最高分、一个最低分),不是去掉所有极值。
容易踩的坑:误用 DENSE_RANK() 或 RANK(),导致多个相同最高分全被剔除,剩下数据过少甚至为空。
实操建议:
- 确认业务规则——是“去掉一个最高一个最低”,还是“去掉所有最高分和所有最低分”
- 前者用
ROW_NUMBER(),后者改用score > (SELECT MIN(score) ...) AND score - 若用
ROW_NUMBER()且需稳定排序(避免同分时每次结果不同),务必在ORDER BY中加入唯一字段,如:ORDER BY score, id
ROW_NUMBER() 的排序稳定性、极值并列的语义、以及数据库版本限制,这三个点最容易漏掉。











