能绕开group by+join的三类组合:组内统计、取每组最优行、补全局指标;核心是用窗口函数替代join,避免扫描和语义风险,但需注意索引、null、排序及高基数场景。

不能完全避免,但能绕开最常踩的三类GROUP BY+JOIN组合:求组内统计值、取每组最优行、补全局聚合指标。核心是窗口函数不压缩行数,所以不用先GROUP BY再LEFT JOIN回原表——那层JOIN就是性能和语义风险的源头。
需要每行带“所属组的SUM/MAX/AVG”时,直接用PARTITION BY
典型错误写法是:SELECT a.*, b.dept_total FROM orders a LEFT JOIN (SELECT dept, SUM(amount) AS dept_total FROM orders GROUP BY dept) b ON a.dept = b.dept。这多一次扫描、多一次哈希JOIN,还可能因NULL dept导致漏数据。
- 正确做法:直接
SUM(amount) OVER(PARTITION BY dept),结果列和原表行数一致,无JOIN开销 -
PARTITION BY字段必须有索引,否则大表下比GROUP BY更慢——窗口函数没法像GROUP BY那样利用哈希聚合提前终止 - 注意NULL值:所有
dept IS NULL的行会被归为同一组,SUM结果会包含它们,可能拉高均值
要取“每组最新/最高/最低的一整行”,别用GROUP BY + MAX(time)再JOIN
常见陷阱是:SELECT o1.* FROM orders o1 JOIN (SELECT order_id, MAX(created_at) AS max_time FROM orders GROUP BY customer_id) o2 ON o1.order_id = o2.order_id AND o1.created_at = o2.max_time。一旦同客户有多条相同created_at,就会返回多行;若created_at有NULL,JOIN直接失效。
- 改用
ROW_NUMBER() OVER(PARTITION BY customer_id ORDER BY created_at DESC, id DESC),然后外层WHERE rn = 1 -
ORDER BY必须显式写全:仅ORDER BY created_at DESC不够,时间相同时需用id等唯一列保序 - 别在WHERE里直接写
ROW_NUMBER() = 1——窗口函数执行晚于WHERE,语法报错
要补全表级统计(如总行数、全表MAX),慎用空OVER()
比如想每行都显示total_count,有人写COUNT(*) OVER()。这在PostgreSQL里会触发隐式全表排序,比单独SELECT COUNT(*)再JOIN还慢。
- 小表或低频查询可接受;生产环境千万级表,优先走两步:
SELECT COUNT(*) FROM t查总数,再用应用层或CTE注入 -
OVER()没PARTITION BY也没ORDER BY时,某些引擎(如旧版MySQL)根本不支持,别硬套 - 如果同时需要组内统计和全表统计,例如
AVG(salary) OVER(PARTITION BY dept)和MAX(salary) OVER(),PostgreSQL可能分别排序两次,实测耗时翻倍
真正难处理的是PARTITION BY字段高基数(如user_id)且无索引,或多个OVER子句的ORDER BY方向冲突。这时候窗口函数不是银弹,得回到索引优化或物化中间结果。










