原创

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 存储过程(支持任意科目)

这个存储过程会:

  1. 自动查询表中所有不同的科目

  2. 动态拼接 PIVOT 或 CASE WHEN 的 SQL 语句

  3. 执行并返回结果


方式一:使用 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 条件即可
正文到此结束
本文目录