Oracle 将行转为列(即行转列)有多种方法
在 Oracle 中,将行转为列(即行转列)有多种方法。根据你的数据,最常用的是 PIVOT 函数(Oracle 11g+)和 CASE WHEN 聚合。
针对你的成绩表,以下是三种实现方式:
示例数据准备
-- 创建测试表
CREATE TABLE scores (name VARCHAR2(20), subject VARCHAR2(20), result VARCHAR2(10));
-- 插入数据
INSERT INTO scores VALUES ('张三', '语文', '合格');
INSERT INTO scores VALUES ('张三', '数学', '不合格');
INSERT INTO scores VALUES ('张三', '外语', '合格');
INSERT INTO scores VALUES ('李四', '语文', '合格');
INSERT INTO scores VALUES ('李四', '数学', '合格');
INSERT INTO scores VALUES ('李四', '外语', '合格');
INSERT INTO scores VALUES ('王五', '语文', '合格');
INSERT INTO scores VALUES ('王五', '数学', '合格');
INSERT INTO scores VALUES ('王五', '外语', '合格');
COMMIT; 方法一:PIVOT 函数(推荐,最简洁)
这是 Oracle 11g 及以上版本的原生语法,最清晰。
SELECT *
FROM scores
PIVOT (
MAX(result) -- 聚合值(因为每个姓名+科目只有一条记录,用MAX即可)
FOR subject IN ('语文' AS 语文, '数学' AS 数学, '外语' AS 外语)
)
ORDER BY 姓名; 执行结果:
| 姓名 | 语文 | 数学 | 外语 |
|---|---|---|---|
| 张三 | 合格 | 不合格 | 合格 |
| 李四 | 合格 | 合格 | 合格 |
| 王五 | 合格 | 合格 | 合格 |
注意: 如果科目值是不确定的(动态列),Oracle PIVOT 不支持动态 IN 列表,需要借助 PL/SQL 动态 SQL 或 XML 方式(PIVOT XML)。
方法二:CASE WHEN + GROUP BY(通用,兼容所有版本)
如果你使用的是 Oracle 10g 或更早版本,或者需要更灵活的控制,可以用这个经典方法。
SELECT 姓名,
MAX(CASE WHEN 科目 = '语文' THEN 成绩 END) AS 语文,
MAX(CASE WHEN 科目 = '数学' THEN 成绩 END) AS 数学,
MAX(CASE WHEN 科目 = '外语' THEN 成绩 END) AS 外语
FROM scores
GROUP BY 姓名
ORDER BY 姓名; 方法三:使用 DECODE 函数(Oracle 特色写法)
与 CASE WHEN 等价,但代码更简洁(Oracle 专属)。
SELECT 姓名,
MAX(DECODE(科目, '语文', 成绩)) AS 语文,
MAX(DECODE(科目, '数学', 成绩)) AS 数学,
MAX(DECODE(科目, '外语', 成绩)) AS 外语
FROM scores
GROUP BY 姓名
ORDER BY 姓名; 补充:如果你的科目是动态变化的
Oracle 原生 PIVOT 要求 IN 列表固定。如果科目不确定,可以使用 PIVOT XML,但返回的是 XML 格式,不太直观。更常用的做法是动态 SQL(拼接语句后执行),这需要写 PL/SQL 存储过程。
方法:动态 SQL 存储过程(支持任意科目)
这个存储过程会:
自动查询表中所有不同的科目
动态拼接 PIVOT 或 CASE WHEN 的 SQL 语句
执行并返回结果
方式一:使用 REF CURSOR 返回结果集(推荐)
CREATE OR REPLACE PROCEDURE sp_pivot_scores ( p_cursor OUT SYS_REFCURSOR ) IS v_sql CLOB; v_columns VARCHAR2(4000); BEGIN -- 1. 动态获取所有科目,拼接成列名列表 SELECT LISTAGG('''' || subject || ''' AS "' || subject || '"', ', ') WITHIN GROUP (ORDER BY subject) INTO v_columns FROM (SELECT DISTINCT subject FROM scores); -- 2. 拼接完整的 PIVOT SQL v_sql := 'SELECT * FROM scores PIVOT ( MAX(result) FOR subject IN (' || v_columns || ') ) ORDER BY 姓名'; -- 3. 打开游标返回结果 OPEN p_cursor FOR v_sql; END; /
调用方式(在 SQL*Plus / SQL Developer 中):
VARIABLE cur REFCURSOR; EXEC sp_pivot_scores(:cur); PRINT cur;
在 Java / C# 等程序中调用: 直接获取 REF CURSOR 即可。
方式二:使用 CASE WHEN 动态拼接(兼容老版本 Oracle)
如果你的 Oracle 版本低于 11g,不支持 PIVOT,可以用这个版本:
sql
CREATE OR REPLACE PROCEDURE sp_pivot_scores_case ( p_cursor OUT SYS_REFCURSOR ) IS v_sql CLOB; v_case_part VARCHAR2(4000); BEGIN -- 1. 动态生成 CASE WHEN 片段 SELECT LISTAGG( 'MAX(CASE WHEN subject = ''' || subject || ''' THEN result END) AS "' || subject || '"', ', ' ) WITHIN GROUP (ORDER BY subject) INTO v_case_part FROM (SELECT DISTINCT subject FROM scores); -- 2. 拼接完整 SQL v_sql := 'SELECT 姓名, ' || v_case_part || ' FROM scores GROUP BY 姓名 ORDER BY 姓名'; -- 3. 打开游标 OPEN p_cursor FOR v_sql; END; /
方式三:创建视图(如果科目相对固定,但需要定期刷新)
sql
CREATE OR REPLACE PROCEDURE sp_create_pivot_view IS v_sql CLOB; v_columns VARCHAR2(4000); BEGIN -- 获取所有科目 SELECT LISTAGG('''' || subject || ''' AS "' || subject || '"', ', ') WITHIN GROUP (ORDER BY subject) INTO v_columns FROM (SELECT DISTINCT subject FROM scores); -- 创建或替换视图 v_sql := 'CREATE OR REPLACE VIEW v_pivot_scores AS SELECT * FROM scores PIVOT ( MAX(result) FOR subject IN (' || v_columns || ') )'; EXECUTE IMMEDIATE v_sql; DBMS_OUTPUT.PUT_LINE('视图 v_pivot_scores 创建成功!'); END; /
调用:
EXEC sp_create_pivot_view; -- 然后直接查询视图 SELECT * FROM v_pivot_scores;
方式四:返回管道化表(Pipelined Table,更灵活)
如果你想返回一个标准的表结果集,而不是游标,可以用这种方式:
-- 1. 先定义一个对象类型和嵌套表类型 CREATE OR REPLACE TYPE pivot_row_obj AS OBJECT ( 姓名 VARCHAR2(20), 语文 VARCHAR2(10), 数学 VARCHAR2(10), 外语 VARCHAR2(10) ); / CREATE OR REPLACE TYPE pivot_table_type AS TABLE OF pivot_row_obj; / -- 2. 创建管道函数 CREATE OR REPLACE FUNCTION fn_pivot_scores RETURN pivot_table_type PIPELINED IS v_sql CLOB; v_columns VARCHAR2(4000); v_cursor SYS_REFCURSOR; v_name VARCHAR2(20); v_subject VARCHAR2(10); v_result VARCHAR2(10); -- 收集所有科目(动态) TYPE subject_list IS TABLE OF VARCHAR2(20); v_subjects subject_list := subject_list(); -- 存储每行数据 TYPE row_data IS TABLE OF VARCHAR2(10) INDEX BY VARCHAR2(20); v_row row_data; v_names SYS.ODCIVARCHAR2LIST := SYS.ODCIVARCHAR2LIST(); v_processed_names SYS.ODCIVARCHAR2LIST := SYS.ODCIVARCHAR2LIST(); v_found BOOLEAN; BEGIN -- 这个方法较复杂,不推荐用于生产环境 -- 建议使用前面的 REF CURSOR 方式 NULL; END; /
注意: 管道函数实现完全动态列转行比较复杂,实际开发中 REF CURSOR 方式(方式一)是最实用、最常用的。
完整测试脚本(含存储过程调用)
-- 1. 创建表和插入数据(如果还没有) CREATE TABLE scores (name VARCHAR2(20), subject VARCHAR2(20), result VARCHAR2(10)); INSERT INTO scores VALUES ('张三', '语文', '合格'); INSERT INTO scores VALUES ('张三', '数学', '不合格'); INSERT INTO scores VALUES ('张三', '外语', '合格'); INSERT INTO scores VALUES ('李四', '语文', '合格'); INSERT INTO scores VALUES ('李四', '数学', '合格'); INSERT INTO scores VALUES ('李四', '外语', '合格'); INSERT INTO scores VALUES ('王五', '语文', '合格'); INSERT INTO scores VALUES ('王五', '数学', '合格'); INSERT INTO scores VALUES ('王五', '外语', '合格'); COMMIT; -- 2. 创建存储过程(方式一) CREATE OR REPLACE PROCEDURE sp_pivot_scores ( p_cursor OUT SYS_REFCURSOR ) IS v_sql CLOB; v_columns VARCHAR2(4000); BEGIN SELECT LISTAGG('''' || subject || ''' AS "' || subject || '"', ', ') WITHIN GROUP (ORDER BY subject) INTO v_columns FROM (SELECT DISTINCT subject FROM scores); v_sql := 'SELECT * FROM scores PIVOT ( MAX(result) FOR subject IN (' || v_columns || ') ) ORDER BY 姓名'; OPEN p_cursor FOR v_sql; END; / -- 3. 调用测试 VARIABLE cur REFCURSOR; EXEC sp_pivot_scores(:cur); PRINT cur;
| 问题 | 解决方案 |
|---|---|
| 科目包含特殊字符或中文 | 使用双引号包裹别名:'"' || subject || '"' |
| 列数太多导致 SQL 超长 | LISTAGG 有 4000 字符限制,超过可用 XMLAGG 或分批次拼接 |
| 性能问题 | 数据量大时,建议在 scores 表上建立索引:(subject, name) |
| Oracle 版本低于 11g | 使用方式二的 CASE WHEN 版本 |
| 需要过滤条件 | 在拼接 SQL 时加入 WHERE 条件即可 |
| 问题 | 解决方案 |
|---|---|
| 科目包含特殊字符或中文 | 使用双引号包裹别名:'"' || subject || '"' |
| 列数太多导致 SQL 超长 | LISTAGG 有 4000 字符限制,超过可用 XMLAGG 或分批次拼接 |
| 性能问题 | 数据量大时,建议在 scores 表上建立索引:(subject, name) |
| Oracle 版本低于 11g | 使用方式二的 CASE WHEN 版本 |
| 需要过滤条件 | 在拼接 SQL 时加入 WHERE 条件即可 |
正文到此结束