子查询不能直接实现行转列,必须配合聚合函数(如max/sum)与case when在group by上下文中完成;pivot仅为语法糖,底层仍等价于该模式,mysql 8.0+虽支持但非原生,postgresql需crosstab()扩展。

子查询不能直接做行转列,得靠聚合 + CASE 或 PIVOT
SQL 标准语法里,子查询本身不提供行转列能力。所谓“用子查询行转列”,实际是把 CASE 表达式或聚合逻辑塞进子查询里,再配合外层 GROUP BY 完成透视。常见错误是试图在子查询里直接 SELECT col1, col2, col3 FROM ... 然后指望结果自动横排——这只会返回多行,不是一宽表。
正确做法是:子查询先准备好带分类标识的明细(比如 student_id, subject, score),外层用 MAX(CASE WHEN subject = 'Math' THEN score END) 拆出各科列。
-
CASE必须搭配聚合函数(MAX/SUM等),否则会因GROUP BY报错 - MySQL 8.0+ 支持
PIVOT语法,但仍是语法糖,底层仍等价于CASE + GROUP BY - PostgreSQL 需用
crosstab()扩展函数,不能纯靠子查询实现 - 如果原始数据有重复
(student_id, subject)组合,聚合前必须明确业务规则(取最大?平均?最新?)
列转行必须用 UNION ALL 或 LATERAL,子查询只是载体
列转行本质是把宽表的多个字段“摊开”成多行,标准写法是 UNION ALL 多个子查询,每个查一列。例如把 math_score, eng_score, chem_score 转成 subject 和 score 两列,就得写三个 SELECT student_id, 'Math' AS subject, math_score AS score 并用 UNION ALL 连接。
容易踩的坑:
- 各
SELECT的字段数、类型、顺序必须严格一致,否则UNION ALL直接报错 - MySQL 5.7 不支持
LATERAL,但 8.0+ 可用LATERAL (VALUES (...))更简洁地替代UNION ALL - SQL Server 支持
UNPIVOT,但它要求所有被转列的数据类型完全一致,遇到INT和DECIMAL混用会隐式转换失败 - 子查询若带
WHERE过滤,要放在每个UNION ALL分支里,而不是只在外层加——否则可能漏掉某些列的非空值
嵌套子查询易引发性能问题,尤其在列转行场景
当列转行用多个子查询拼接时(比如每个 SELECT 里都含 (SELECT ... FROM t2 WHERE t2.id = t1.id)),数据库可能对同一主表扫描多次。实测 PostgreSQL 在 10 万行数据上,UNION ALL + 关联子查询比用 LATERAL JOIN 慢 3–5 倍。
优化建议:
- 优先用
LATERAL(PostgreSQL/SQL Server 2016+/MySQL 8.0+)替代独立子查询 - 列转行前,先用
JOIN把所有需要的字段一次性捞出来,再用VALUES或UNION ALL拆行 - 避免在子查询中用
ORDER BY或LIMIT——除非真需要 Top-N,否则徒增排序开销 - 确认执行计划里没有
Subplan或Materialize节点反复出现,那是嵌套子查询失控的信号
NULL 值和数据类型不一致是子查询行/列转中最隐蔽的雷
行转列时,CASE 表达式中某一分支没匹配到数据,结果就是 NULL;列转行时,如果某列本来是 NULL,UNION ALL 后它依然存在,但容易被误认为“数据缺失”。更麻烦的是类型隐式转换:比如把 INT 列和 VARCHAR 列一起 UNION ALL,MySQL 会把整数转成字符串再拼接,可能导致数值比较失效。
- 行转列后检查每列的
COUNT(*)和COUNT(col)是否一致,不一致说明有NULL填充 - 列转行前统一显式转换类型:
CAST(math_score AS DECIMAL(5,2)),别依赖数据库自动推断 - 用
COALESCE包裹列转行中的字段,避免NULL导致后续计算中断(如SUM(score)会跳过NULL,但有时你希望计为 0) - Oracle 用户注意:
DECODE在行转列中不能直接用于聚合上下文,必须套一层MAX(DECODE(...))










