CROSS APPLY 用于解决 JOIN 无法处理的逐行动态计算问题,如调用函数、拆分字符串或取 TOP N;它支持右表依赖左表字段,而 JOIN 的 ON 条件仅支持静态列比对。

CROSS APPLY 不是用来“替代” JOIN 的,它解决的是 JOIN 根本写不出来的问题——比如右侧要逐行调用函数、解析 JSON、取 TOP N 或拆分字符串。
为什么不能直接用 INNER JOIN 拆分逗号分隔字段?
常见错误是试图在 WHERE 或子查询中直接引用左表字段来调用 STRING_SPLIT:
WHERE tag IN (SELECT value FROM STRING_SPLIT(t.tags, ','))
这条语句在 t.tags 来自左表时会报错:The multi-part identifier "t.tags" could not be bound. 因为子查询无法感知外部作用域的列。JOIN 的 ON 条件只允许静态列比对,不支持“每行传参”。
真正能动态把 t.tags 作为参数传进去的,只有 CROSS APPLY 或 OUTER APPLY。
-
STRING_SPLIT返回的是表,不是值,必须配合APPLY才能被当“右表”用 - 漏掉别名(如
AS s)会触发Incorrect syntax near '(' - 右侧不能是裸子查询,必须是可执行的表值表达式(函数调用、带
SELECT的括号表达式等)
如何用 CROSS APPLY + STRING_SPLIT 正确拆分并保留顺序?
默认 STRING_SPLIT 不保证顺序,'a,b,c' 可能返回 c/a/b。SQL Server 2022+ 支持第三个参数 enable_ordinal = 1,启用后返回 ordinal 列:
SELECT t.id, t.name, s.value, s.ordinal FROM products t CROSS APPLY STRING_SPLIT(t.tags, ',', 1) AS s ORDER BY t.id, s.ordinal;
注意:不加 ORDER BY s.ordinal,排序就无效;旧版本(2016–2019)没有 ordinal,只能靠业务逻辑补救,比如提前存好索引位,或改用数字辅助表手动拆。
- 不加第三个参数时,
ORDER BY s.value没意义,结果仍是无序的 - 启用
ordinal后,必须显式在ORDER BY中引用它,才能还原原始位置 - 记得用
TRIM(s.value)和NULLIF(TRIM(s.value), '')处理空格和纯空字符串
CROSS APPLY 真正快的关键:避免全表窗口计算
当你要“为每个用户取最新一条订单”,用 INNER JOIN 强行改写,常会写出这种低效写法:
FROM Users u INNER JOIN ( SELECT *, ROW_NUMBER() OVER (PARTITION BY UserId ORDER BY CreatedAt DESC) rn FROM Orders ) o ON o.UserId = u.Id AND o.rn = 1
这会先对整个 Orders 表做窗口计算,再过滤——千万级数据下极易慢成瓶颈。
而等价的 CROSS APPLY 写法:
FROM Users u CROSS APPLY ( SELECT TOP 1 * FROM Orders o WHERE o.UserId = u.Id ORDER BY CreatedAt DESC )
只要 Orders(UserId, CreatedAt DESC) 有复合索引,每次都是索引 Seek,总耗时 ≈ 左表行数 × 单次 Seek 时间。
- “快几倍”不是
CROSS APPLY本身快,而是它让原本必须扫全表的逻辑,降维成多次高效索引查找 - 性能收益完全依赖右侧表达式是否可优化;如果右侧没索引、或写成无条件
SELECT *,CROSS APPLY一样慢 - 它本质是能力边界扩展:让你能合法写出“每行独立执行一次子查询”的语义
最容易被忽略的一点:CROSS APPLY 的执行计划里,右侧逻辑是嵌套循环(Nested Loops)驱动的——这意味着左侧结果集越大、右侧越重,整体开销越线性放大。别在没过滤的百万行表上直接 CROSS APPLY STRING_SPLIT,先 WHERE 缩小左侧行数,再拆。










