CASE子查询比直接JOIN更慢,是因为相关子查询无法物化或下推,导致每行独立执行N次扫描;改写为LEFT JOIN可合并多次查询为一次扫描,并通过GROUP BY+CASE聚合结果,避免重复IO和索引失效。

为什么CASE子查询比直接JOIN更慢?
当CASE WHEN EXISTS(...)或CASE WHEN (SELECT ...)出现在SELECT列表里,数据库必须为每一行执行一次子查询——哪怕关联字段有索引。PostgreSQL、Oracle、MySQL都如此。这不是“写法错”,而是执行模型决定的:相关子查询无法提前物化,也不能被优化器下推到JOIN阶段,导致N次独立扫描。
CASE子查询改写为LEFT JOIN的实操要点
核心思路是把多次子查询合并成一次JOIN,用GROUP BY + CASE聚合结果。关键不是“能不能改”,而是“怎么改不漏数据、不错行”:
- 所有子查询必须关联同一张表(如都查
t2),否则需拆成多个JOIN或用CTE预处理 -
LEFT JOIN必须用ON条件精确匹配,不能把过滤逻辑(如status='completed')放在WHERE里,否则会过滤掉主表空匹配行 - 聚合函数(如
SUM、COUNT)必须包裹CASE表达式,且ELSE 0或ELSE NULL要明确——NULL参与SUM会被忽略,0则计入 - 主表
GROUP BY字段必须包含所有非聚合列,否则报错(如PostgreSQL严格模式)
示例(原低效写法):
SELECT u.id, CASE WHEN EXISTS(SELECT 1 FROM orders o WHERE o.user_id = u.id AND o.status = 'completed') THEN 1 ELSE 0 END AS has_completed, CASE WHEN EXISTS(SELECT 1 FROM orders o WHERE o.user_id = u.id AND o.status = 'pending') THEN 1 ELSE 0 END AS has_pending FROM users u;
优化后:
SELECT u.id, MAX(CASE WHEN o.status = 'completed' THEN 1 ELSE 0 END) AS has_completed, MAX(CASE WHEN o.status = 'pending' THEN 1 ELSE 0 END) AS has_pending FROM users u LEFT JOIN orders o ON u.id = o.user_id GROUP BY u.id;
EXISTS子查询在CASE里失效的典型表现
看执行计划时,如果出现以下特征,基本确认CASE WHEN EXISTS没走索引下推:
-
has_variable = 0(达梦/Oracle执行计划中标识变量未下推) - 子查询步骤显示
SSCN(索引全扫)而非SSEK(索引范围查找) - 逻辑读远高于主表行数 × 子查询平均扫描页数(比如主表64行,总逻辑读3667,说明子查询反复扫表)
此时强行加/*+ USE_NL(t2) */等hint通常无效——优化器根本没法把外部行值注入子查询执行路径。唯一可靠解法就是剥离子查询,转为JOIN。
PostgreSQL 12+ 特别注意:JIT对CASE无加速作用
文档说JIT能加速CASE WHEN计算,但仅限于**已加载到内存的标量值运算**,比如CASE WHEN x > 100 THEN y*1.2 ELSE y END。它不加速子查询执行、不减少IO、不改变执行计划结构。所以别指望开jit = on就能救活CASE WHEN (SELECT ...)——该慢还是慢。
真正省时间的地方,永远在减少表扫描次数和避免重复IO,而不是让单次扫描算得更快。


















