窗口函数能替代关联子查询,因其在单次扫描中完成分组内排序、累计、排名等计算,避免每行触发子查询;如用ROW_NUMBER()取各用户最新订单、LAG()查上一笔记录、SUM()OVER()算占比,均比传统JOIN+子查询更高效。

为什么窗口函数能替代关联子查询
因为窗口函数在单次扫描中就能完成分组内排序、累计、排名等计算,避免了对主表每行都触发一次子查询的开销。传统 SELECT ... (SELECT ... FROM t2 WHERE t2.id = t1.id) 在 MySQL 8.0 之前几乎无法优化,而窗口函数如 ROW_NUMBER()、RANK()、SUM() OVER () 直接基于当前结果集分区计算,执行计划里不会出现 “DEPENDENT SUBQUERY”。
用 ROW_NUMBER() 替代“取每个分组最新一条”的子查询
典型场景:查每个用户最近一次订单(按 created_at 排序)。传统写法常配合 JOIN + 子查询或 GROUP BY + MAX() 再回表,易出错且慢。
推荐写法:
SELECT user_id, order_id, created_at
FROM (
SELECT user_id, order_id, created_at,
ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY created_at DESC) AS rn
FROM orders
) t
WHERE rn = 1;
注意点:
-
PARTITION BY user_id是关键,漏掉就变成全局编号 -
ORDER BY created_at DESC必须明确,否则rn=1结果不可控 - 若存在并列(同用户同时间多单),
ROW_NUMBER()强制唯一编号,RANK()会并列+跳号,DENSE_RANK()并列不跳号——按业务选
用 LAG() / LEAD() 替代自连接查“上一笔/下一笔”
比如统计每个用户连续登录天数,传统做法是 JOIN orders o1 ON o1.user_id = o2.user_id AND o1.created_at = DATE_SUB(o2.created_at, INTERVAL 1 DAY),索引难利用,性能差。
改用 LAG() 更直接:
面向 CRM 系统的 MySQL 8 数据库模式设计。当用户需要为客户关系管理(CRM)系统设计、评审、优化或生成数据库模式时,应使用此项技能。
SELECT user_id, created_at,
DATEDIFF(created_at, LAG(created_at) OVER (PARTITION BY user_id ORDER BY created_at)) AS gap_days
FROM login_logs;
常见陷阱:
-
LAG()默认返回NULL(首行无前值),需用COALESCE(LAG(...), '1970-01-01')避免计算中断 - 排序字段必须含唯一性因子(如加
id),否则相同created_at下窗口顺序不确定,LAG()结果可能每次不同 - MySQL 8.0 不支持
LAG(..., n, default)的第三个参数(默认值),只能靠COALESCE包一层
聚合类窗口函数替代 GROUP BY + 关联求均值/占比
例如:查每笔订单金额占其用户总消费的比例。传统写法要先 GROUP BY user_id 算总额,再 JOIN 回原表——两趟扫描。
窗口函数一行解决:
SELECT order_id, user_id, amount,
amount / SUM(amount) OVER (PARTITION BY user_id) AS ratio
FROM orders;
性能与语义优势明显,但要注意:
-
SUM() OVER ()和SUM() GROUP BY的 NULL 处理一致:忽略 NULL 值;但如果整组全为 NULL,结果为 NULL(不是 0) - 不能在
WHERE或HAVING中直接引用窗口函数别名(如WHERE ratio > 0.5),必须嵌套一层子查询或使用 CTE - 如果需要动态范围(如“最近 7 天累计”),得用
ROWS BETWEEN 6 PRECEDING AND CURRENT ROW,而非简单PARTITION BY
窗口函数不是万能的,比如涉及跨表复杂条件过滤时仍需关联;但只要逻辑落在“单表内分组计算”,优先考虑窗口函数——它省掉的不只是代码行数,更是执行计划里的 nested loop 和临时表。










