CROSS APPLY 的核心价值在于支持左表每行驱动右侧行集计算的语义,使优化器能识别索引 Seek 并生成高效执行计划;标量子查询则因无法显式控制计划、难利用排序索引且易触发全表扫描而性能较差。

CROSS APPLY 不是“比普通子查询快”,而是让某些根本写不出来的逻辑变得可执行——所谓效率高,其实是避免了被迫用低效方式硬凑。
为什么相关子查询在 SELECT 里会拖慢性能
很多人习惯在 SELECT 列表里写标量子查询,比如:
SELECT u.Name, (SELECT TOP 1 o.Amount FROM Orders o WHERE o.UserId = u.Id ORDER BY o.CreatedAt DESC) AS LastOrderAmount FROM Users u
这种写法表面简洁,但 SQL Server 通常无法将它优化为索引 Seek;更常见的是对每个 u.Id 触发一次独立的嵌套循环,且每次都要重新编译/估算,尤其当 Users 行数多、Orders 没有合适索引时,容易退化成多次全表扫描。
- 标量子查询不能显式控制执行计划,优化器常保守选择 Hash 或 Merge Join 回退路径
- 无法利用
TOP+ORDER BY的索引有序性(比如(UserId, CreatedAt DESC)) - 若子查询返回多行,直接报错;而
CROSS APPLY天然支持多行输出
CROSS APPLY 如何让优化器“看懂”你的意图
把上面逻辑改写为:
SELECT u.Name, o.Amount AS LastOrderAmount FROM Users u CROSS APPLY ( SELECT TOP 1 o.Amount FROM Orders o WHERE o.UserId = u.Id ORDER BY o.CreatedAt DESC ) o
这时优化器能明确识别这是「每行驱动一次索引 Seek」,只要 Orders 上有 (UserId, CreatedAt DESC) 复合索引,执行计划里就会出现「Nested Loops + Index Seek」,而不是「Table Scan + Compute Scalar」。
-
CROSS APPLY的右侧子查询被当作一个可内联展开的“行级函数”,不是黑盒标量表达式 - 优化器可以下推过滤条件、复用索引排序、甚至并行化外层扫描
- 如果右侧是 TVF(如
dbo.ParseJson(u.Payload)),只有APPLY能合法引用u.Payload
什么时候换用 CROSS APPLY 反而更慢
不是所有场景都适合。如果你右侧子查询不依赖左表字段,或者本身就很重(比如含聚合+大表 JOIN),强行套 CROSS APPLY 可能适得其反:
- 右侧写成
SELECT * FROM BigTable WHERE 1=0—— 每行都执行一次空结果集,开销白费 - 没建对索引:比如只建了
UserId单列索引,但ORDER BY CreatedAt DESC还是触发排序 - 左侧数据量极大(千万级)且右侧逻辑复杂(如调用 CLR 函数),此时逐行调用反而放大 CPU 压力
真正关键的不是语法选 CROSS APPLY 还是子查询,而是你是否需要「左表每行作为参数,驱动一次右侧行集计算」——这个语义一旦成立,CROSS APPLY 就是唯一合法且最易被优化的选择;其他写法要么报错,要么绕路,性能差只是结果,不是原因。










