窗口函数必须排序,无索引则强制落临时表:SQL Server出现Sort节点+tempdb警告、Spark显示Spill、MySQL触发filesort,均因底层需全量排序且内存不足时溢出磁盘。

窗口函数必须排序,没索引就只能落临时表
SQL Server、Spark SQL 等引擎执行 ROW_NUMBER()、SUM() OVER (ORDER BY ...) 时,底层必须对数据排序。如果 OVER 子句里的 ORDER BY 列没有对应索引,数据库无法流式读取+计算,只能把整块分区数据先读进内存排序——内存不够就写到 tempdb(SQL Server)或本地磁盘(Spark),也就是 Spill。
- SQL Server 执行计划里出现红色 Sort 节点 + “Warnings: Operator used tempdb” 就是明确信号
- Spark UI 的 Stage 页面里看到 “Spill (memory)” 或 “Spill (disk)” 字样,且数值 > 100MB,说明
ExternalAppendOnlyUnsafeRowArray已频繁溢出 - MySQL 8.0 虽支持窗口函数,但若
ORDER BY列无索引,仍会触发Using temporary; Using filesort,本质也是磁盘排序
低基数 PARTITION BY 让单个窗口吃光内存
窗口函数不是按行算,而是按“分区”维护状态。如果 PARTITION BY status(只有 'active'、'inactive'、'pending' 3 个值),整个千万行大表就被压成 3 个巨分区——每个分区的数据全得缓存在内存里做排序和累计计算。
- 用
SELECT COUNT(*) FROM t GROUP BY status查看分布:若某组 > 200 万行,基本注定 Spill - 换成高基数字段组合,比如
PARTITION BY user_id % 100, status或PARTITION BY YEAR(order_time), user_id,能立刻摊薄单分区压力 - SQL Server 2019 对这种倾斜的内存授予估算不准,即使加了
OPTION (RECOMPILE)也难救,必须从分区逻辑上拆解
Spark 中两个缓冲阈值不协同,Spill 变成高频抖动
Spark 默认用 ExternalAppendOnlyUnsafeRowArray 缓存窗口数据,但它有两个关键开关:spark.sql.windowExec.buffer.in.memory.threshold 和 spark.sql.windowExec.buffer.spill.threshold。只调一个等于白干。
-
spark.sql.windowExec.buffer.in.memory.threshold设太小(如 512)→ 每 512 行就触发一次 spill → 磁盘 IO 暴增,任务卡在 writeLocalFile -
spark.sql.windowExec.buffer.spill.threshold保持默认(Integer.MAX_VALUE)→ spill 后的UnsafeExternalSorter继续往内存里攒数据,直到 OOM 才落盘 - 实操推荐配对:设为
8192和30000,既避免过早 spill,又防止排序器内存失控
嵌套窗口或重复 OVER 子句,计算量翻倍还不自知
写成这样:SUM(x) OVER (PARTITION BY a ORDER BY b), AVG(x) OVER (PARTITION BY a ORDER BY b), COUNT(*) OVER (PARTITION BY a ORDER BY b),引擎不会复用排序结果——每个函数都独立走一遍分区+排序+扫描,相当于执行三次窗口逻辑。
- 改用 CTE 预先算好排序键:
WITH sorted AS (SELECT *, ROW_NUMBER() OVER (PARTITION BY a ORDER BY b) AS rn FROM t),再在外部 SELECT 里直接引用rn或做聚合 - SQL Server 中可配合
OPTION (USE HINT('ENABLE_QUERY_OPTIMIZER_HOTFIXES'))激活窗口复用优化(2019 CU12+) - 注意:MySQL 8.0 和早期 Hive 不支持该优化,必须手动拆解
EXPLAIN EXTENDED 或 Spark UI 的 “Shuffle Read Size” 比看错误日志更有用。

















