窗口函数不能直接替代子查询,必须先用EXPLAIN ANALYZE确认瓶颈在子查询逻辑而非底层扫描失控,否则在百万行未过滤结果上执行窗口计算反而更慢。

窗口函数能直接替代子查询吗?先看执行计划再动手
MySQL 8.0 引入窗口函数后,很多人第一反应是“把原来的自关联或相关子查询全换成 ROW_NUMBER()、RANK() 或 LAG()”,但实际中常发现改完反而更慢。根本原因是:窗口函数本身不减少数据扫描量,它只是在已扫描结果集上做逻辑计算。如果原始查询 WHERE 条件没走索引、或者 JOIN 导致中间结果集膨胀到百万行,那 OVER (PARTITION BY ... ORDER BY ...) 就是在百万行上排序——这比原SQL里那个小范围子查询还重。
实操建议:
- 先用
EXPLAIN ANALYZE看原SQL的rows和filtered值,确认瓶颈是否真在子查询逻辑本身,还是底层扫描就失控了 - 窗口函数只应在
WHERE过滤后结果集可控(通常 - 避免在窗口函数的
ORDER BY子句里使用非索引字段,否则会触发Using filesort
用 ROW_NUMBER() OVER() 实现分页时为什么比 LIMIT 更慢?
很多同学把老式分页 SELECT * FROM t ORDER BY create_time DESC LIMIT 10000,20 换成 SELECT * FROM (SELECT *, ROW_NUMBER() OVER (ORDER BY create_time DESC) rn FROM t) t1 WHERE rn BETWEEN 10001 AND 10020,结果发现耗时翻倍。这是因为 ROW_NUMBER() 必须对整张表(或整个分区)排序并编号,而 LIMIT offset, size 在 InnoDB 中可通过索引直接跳过前 offset 行——只要 create_time 有索引,前者是 O(N log N),后者接近 O(log N + size)。
正确做法:
- 深分页场景(
offset > 10000)仍用游标分页(WHERE create_time ),不是窗口函数的适用场 - 若必须用窗口函数(如需同时返回总行数),至少把
PARTITION BY加上,限制排序范围,例如按天分区:ROW_NUMBER() OVER (PARTITION BY DATE(create_time) ORDER BY create_time DESC) - 确保
ORDER BY字段上有索引,且该索引能被WHERE条件复用(避免索引失效)
LAG() / LEAD() 替代自连接时,为什么有时查不出数据?
用 LAG(user_id) OVER (ORDER BY id) 获取上一条记录的 user_id,看似简洁,但容易忽略两个关键约束:一是窗口函数按指定顺序排完才计算,二是它只在当前结果集内找“相邻”行——如果 WHERE 过滤后某用户的数据被切成了不连续的块(比如中间有其他用户数据被过滤掉),LAG() 返回的就是隔壁用户的值,甚至 NULL。
面向 CRM 系统的 MySQL 8 数据库模式设计。当用户需要为客户关系管理(CRM)系统设计、评审、优化或生成数据库模式时,应使用此项技能。
典型踩坑点:
- 没加
PARTITION BY user_id就直接LAG(),导致跨用户比较,业务语义错乱 -
ORDER BY字段存在重复值(如多个记录create_time相同),MySQL 不保证相同值之间的相对顺序,LAG()结果不稳定 - 原自连接 SQL 用了
ON t1.id = t2.id - 1这种强物理序依赖,而窗口函数依赖的是逻辑排序序,ID 断号或删除会导致行为不一致
为什么 COUNT(*) OVER() 让执行计划从 index_merge 退化成 ALL?
给聚合查询加 COUNT(*) OVER() 获取总数,本意是避免二次查询,但常导致优化器放弃原有索引策略。因为 OVER() 要求全结果集可见,MySQL 可能被迫放弃使用索引下推(ICP)或索引合并(index_merge),转而走主键聚簇索引全扫,只为确保每行都能参与窗口计算。
可落地的折中方案:
- 用
SELECT COUNT(*) FROM t WHERE ...单独查总数,应用层拼接;虽然多一次网络往返,但比全表扫快一个数量级 - 若必须单SQL返回,把窗口函数拆到派生表里,并显式提示优化器使用索引:
SELECT t.*, cnt.total FROM t JOIN (SELECT COUNT(*) total FROM t WHERE ...) cnt - 对高频统计场景,在业务低峰期用
ANALYZE TABLE更新统计信息,帮优化器更准判断窗口代价
真正难的不是写对窗口函数语法,而是判断它该不该出现在这条SQL里——很多时候,一个 WHERE 条件补全、一个联合索引调整、甚至把 OR 拆成 UNION ALL,比硬套窗口函数更能解决问题。别让新特性变成新负担。










