SQL Server 的 CROSS APPLY 在 PostgreSQL 中用 LATERAL 模拟,在 MySQL 8.0+ 中也支持 LATERAL(需嵌套派生表规避 LIMIT 报错),MySQL 5.7 则需用聚合或窗口函数替代;核心是保证右侧子查询逐行依赖左侧并独立执行。

SQL Server 的 CROSS APPLY 在 PostgreSQL/MySQL 怎么写
没有直接等价语法,但可以用 LATERAL(PostgreSQL)或派生表 + 显式连接(MySQL)模拟行为。核心是:把右侧“依赖左侧结果”的子查询跑起来,且每行独立执行一次。
常见错误现象:SELECT * FROM t1, (SELECT TOP 1 * FROM t2 WHERE t2.id = t1.ref_id ORDER BY score DESC) s 这种写法在 MySQL 8.0 前会报错;PostgreSQL 不支持逗号隐式 LATERAL,漏写 LATERAL 关键字就变成普通 JOIN,结果错得离谱。
- PostgreSQL 必须显式写
LATERAL,例如:SELECT * FROM orders o CROSS JOIN LATERAL (SELECT * FROM order_items i WHERE i.order_id = o.id LIMIT 1) first_item - MySQL 8.0+ 支持
LATERAL,但需注意:子查询里不能用ORDER BY ... LIMIT直接嵌套(会报错),得包一层派生表:(SELECT * FROM (SELECT * FROM order_items WHERE order_id = o.id ORDER BY score DESC LIMIT 1) AS _) - MySQL 5.7 及更早只能用关联子查询 +
JOIN模拟,但无法天然保证“每行触发一次”,得靠GROUP BY或窗口函数兜底,逻辑易绕
OUTER APPLY 对应的 PostgreSQL/MySQL 写法
本质是左连接 + 右侧可为空的“按行执行子查询”,LATERAL 配合 LEFT JOIN 即可实现,不是 LEFT JOIN LATERAL 这个语法本身有特殊含义,而是语义组合的结果。
使用场景:查用户列表,每人附带其最新一条订单(没订单也要显示用户)——这正是 OUTER APPLY 的典型用例。
- PostgreSQL 正确写法:
SELECT * FROM users u LEFT JOIN LATERAL (SELECT * FROM orders o WHERE o.user_id = u.id ORDER BY created_at DESC LIMIT 1) latest_order ON true - MySQL 8.0+ 类似,但
ON true要换成ON 1或直接省略(部分版本要求显式条件) - 容易踩的坑:漏掉
LEFT JOIN的ON条件(哪怕写ON true),会导致变成内连接;另外,LATERAL子查询里若引用了外层未定义的列,错误信息不直观,建议先单独测试子查询
为什么不能直接用普通子查询或 JOIN 替代
因为 APPLY 的关键特征是“相关性”和“逐行求值”:右侧子查询可以引用左侧当前行的任意列,并且对每一行都重新执行。普通 JOIN 或非 LATERAL 子查询做不到这点。
性能影响明显:LATERAL 子查询在 PostgreSQL 中会被优化器尝试展开或物化,但若子查询含 LIMIT / ORDER BY,大概率走 Nested Loop,数据量大时比预想慢得多;MySQL 的 LATERAL 实现尚不成熟,某些嵌套层级下会强制物化中间结果,内存占用陡增。
- 替代方案中,用窗口函数(如
ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY created_at DESC))预计算再过滤,通常比 LATERAL 更快,尤其当“取最新一条”这类需求固定时 - PostgreSQL 中
LATERAL支持索引下推,确保子查询里被引用的外层列上有索引(比如orders(user_id, created_at)复合索引),否则性能断崖下跌 - MySQL 5.7 用户若硬要模拟,常见写法是先用变量模拟序号,但跨行顺序不可靠,遇到并发或优化器改写极易出错
MySQL 5.7 和 PostgreSQL 兼容写法的取舍点
如果代码要同时跑在 MySQL 5.7 和 PostgreSQL 上,别碰 LATERAL —— 它俩语法不兼容,且 MySQL 5.7 根本不认。得回归通用 SQL 模式。
最稳的路径是放弃“逐行子查询”思路,改用聚合或窗口函数预处理。虽然多一步,但可读性和兼容性高得多。
- 例如“每个用户的最新订单”:先用
SELECT user_id, MAX(created_at) AS max_time FROM orders GROUP BY user_id得到时间戳,再连回原表取完整记录 —— 这招在所有版本都通 - PostgreSQL 可用
DISTINCT ON (user_id) ORDER BY user_id, created_at DESC,但 MySQL 没这语法,所以不推荐作为跨库方案 - 真正难搞的是子查询里含复杂逻辑(比如调用自定义函数、动态表名、递归查询),这种场景几乎无法写出真正兼容的 SQL,得在应用层拆分处理
跨数据库时,“看起来像 APPLY”的写法往往掩盖了执行计划差异,上线后才发现某条查询在 MySQL 上全表扫描,在 PostgreSQL 上却走了索引 —— 这类细节比语法转换更值得花时间盯住。










