窗口函数更高效,因其将N次重复扫描压缩为一次扫描+一次排序+一次线性遍历;ROW_NUMBER()查最新记录仅需单次排序标号,LAG()/LEAD()不依赖ID连续,SUM() OVER避免O(N²)子查询扫描。

窗口函数比自连接子查询更高效,根本原因是它把原本需要 N 次重复扫描、排序和临时结构构建的逻辑,压缩成一次扫描 + 一次排序 + 一次线性遍历。
ROW_NUMBER() 替代自连接查“最新记录”时只扫一次表
自连接查每个用户的最新订单,典型写法是 JOIN orders o2 ON o1.user_id = o2.user_id AND o2.order_time > o1.order_time,再加 WHERE o2.order_time IS NULL。这种写法在 EXPLAIN 中常看到 Type: ALL 和 Rows 数值爆炸——外层每行都触发一次内层全表过滤。
-
ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY order_time DESC, id DESC)只需一次按(user_id, order_time DESC, id DESC)排序,然后流式标号 - 必须带
PARTITION BY user_id,否则变成全表排号,失去业务意义 - ORDER BY 中二级排序字段(如
id)不能省:order_time相同时不加它,MySQL 8.0 可能每次返回不同“第 1 名” - 索引要建复合的:
INDEX(user_id, order_time DESC, id),否则排序无法跳过
LAG()/LEAD() 不依赖 ID 连续性,也不触发嵌套扫描
有人用 JOIN t1 ON t2.id = t1.id + 1 查前后行差值,但生产环境 ID 绝对不连续——删过数据、批量插入、分布式主键都会让这个假设崩掉,结果错漏且不可复现。
-
LAG(amount) OVER (PARTITION BY user_id ORDER BY event_time, id)只认逻辑顺序,不依赖物理存储 - 第三个参数填默认值,比如
LAG(amount, 1, 0),避免前一行不存在时返回NULL,污染后续减法计算 - 如果
event_time有重复,ORDER BY必须补二级字段(如id),否则 MySQL 8.0 每次执行可能选不同“前一行” - 漏写
ORDER BY会直接报错:ERROR 3589 (HY000): Window '<unnamed>' requires an ORDER BY clause</unnamed>
SUM() OVER 替代关联子查询做累计求和,避免 O(N²) 扫描
传统写法 (SELECT SUM(amount) FROM orders o2 WHERE o2.date 是典型 O(N²) 模式:N 行数据,就执行 N 次子查询扫描,I/O 压力指数级上升。
- 改用
SUM(amount) OVER (ORDER BY date ROWS UNBOUNDED PRECEDING),MySQL 内部走单趟累积算法 - 必须显式写
ROWS,别省略——RANGE在时间字段上会把同一天所有行全纳入,导致重复累加 - 日期字段含
NULL时,MySQL 默认把NULL排最前,第一行累计值可能异常;建议提前WHERE date IS NOT NULL或用COALESCE(date, '1970-01-01') - 没索引时,
ORDER BY date会强制 filesort;有索引则可跳过,但PARTITION BY字段无索引时仍会拖慢
内存使用差异:窗口函数复用结构,子查询各自建临时表
窗口函数的 Using temporary 是构建一个内存中的窗口框架,整张表的数据只进这个结构一次;而自连接或相关子查询中,每处理外层一行,就重新分配一块临时空间、重建筛选条件、再扫描内表——10 万行外层数据,就可能触发 10 万次临时结构创建。
- MySQL 不会复用这些临时结构,内存峰值直接线性翻倍
- 子查询容易触发磁盘临时表:一旦超限
tmp_table_size(默认 64MB),就会落盘,引入 I/O 延迟和锁竞争 - 窗口函数全程在
sort_buffer和window_frame内存区域流式计算,只要总数据量不超限,就不会写磁盘 - EXPLAIN 中看到
Using filesort和Using temporary同时出现且loops=1,基本就是最优路径;若子查询的loops显著大于 1,内存压力已经实打实地来了
真正容易被忽略的是:窗口函数的性能优势不是靠“少算”,而是靠“不重复建上下文”。PARTITION BY 和 ORDER BY 看似简单,但一旦字段没索引、或排序字段含大量 NULL、或重复值没加二级排序,就立刻退化成全表排序+随机取值——这时候它不比自连接快,甚至更慢。

















