Oracle 将列分隔为多行

Posted

技术标签:

【中文标题】Oracle 将列分隔为多行【英文标题】:Oracle delimited column(s) into multiple rows 【发布时间】:2017-02-22 23:29:05 【问题描述】:

我正在寻找一个 oracle 查询,它将分隔列(2 列转换为多行并将其与另一个表连接起来

表 1

id  - col1     - col2                                            
1   - PC1,PC2  - F1,F2 

表2

 co1  - co2
 F1   - V1
 F2   - V2

我正在寻找结果

1,PC1,F1,V1   
1,PC1,F2,V2
1,PC2,F1,V1
1,PC2,F2,V2

我试过了

with EXTED as (SELECT id,col1,trim(COLUMN_VALUE) col2
FROM Table1,
  xmltable(('"'
  || REPLACE(col2, ',', '","')
 || '"'))) select  FG.id,FG.col2,
 trim(COLUMN_VALUE) col2,VF.co2 from EXTED FG ,Table2 VF,
 xmltable(('"'
  || REPLACE(col2, ',', '","')
 || '"'))
 where FG.col2 = VF.col1

但这需要很多时间才能给出结果。有没有更好的方法来获得结果..?

【问题讨论】:

也许,通过避免 XML 操作和使用标准分层查询拆分字符串;但无论如何性能都会很差。问题不在于查询,而在于数据模型,这违反了健全表设计的最基本要求——著名的“第一范式”。单个字段不应包含超过 一个 的值。单个字段中的逗号分隔值是学生在数据库课程中第一次测验失败的最常见原因(或者如果不是,应该是)。性能缓慢只是第一范式很重要的众多非常好的原因之一。 是的,但是它的旧数据模型(创建于 25 年前).. 我们不允许更改它。 我并没有以任何方式批评你——我只是帮助你向你的客户(或老板)解释为什么你不能在这个模型上写一个“快速”的查询。 好的,谢谢..在回去之前我想确保没有其他方法可以改进查询..(数据量非常小,一个表有 12K,第二个表有 4K 记录) - 我的查询的总结果大约是 40K 记录,大约需要一分钟才能返回结果。 【参考方案1】:

您的查询可以写成:

SELECT id,
       x1.COLUMN_VALUE.getStringVal() AS col1,
       x2.COLUMN_VALUE.getStringVal() AS col2,
       t2.col2
FROM   Table1 t1
       CROSS JOIN
       xmltable( ('"'|| REPLACE(t1.col1, ',', '","')|| '"') ) x1
       CROSS JOIN
       xmltable( ('"'|| REPLACE(t1.col2, ',', '","')|| '"') ) x2
       INNER JOIN
       table2 t2
       ON ( x2.COLUMN_VALUE.getStringVal() = t2.col1 );

交替

你可以使用一个简单的函数:

CREATE TYPE stringlist IS TABLE OF VARCHAR2(4000);
/

CREATE OR REPLACE FUNCTION split_String(
  i_str    IN  VARCHAR2,
  i_delim  IN  VARCHAR2 DEFAULT ','
) RETURN stringlist DETERMINISTIC
AS
  p_result       stringlist := stringlist();
  p_start        NUMBER(5) := 1;
  p_end          NUMBER(5);
  c_len CONSTANT NUMBER(5) := LENGTH( i_str );
  c_ld  CONSTANT NUMBER(5) := LENGTH( i_delim );
BEGIN
  IF c_len > 0 THEN
    p_end := INSTR( i_str, i_delim, p_start );
    WHILE p_end > 0 LOOP
      p_result.EXTEND;
      p_result( p_result.COUNT ) := SUBSTR( i_str, p_start, p_end - p_start );
      p_start := p_end + c_ld;
      p_end := INSTR( i_str, i_delim, p_start );
    END LOOP;
    IF p_start <= c_len + 1 THEN
      p_result.EXTEND;
      p_result( p_result.COUNT ) := SUBSTR( i_str, p_start, c_len - p_start + 1 );
    END IF;
  END IF;
  RETURN p_result;
END;
/

那么你可以这样做:

SELECT t1.id,
       c1.COLUMN_VALUE AS t1_c1,
       t2.col1 AS t2_c1,
       t2.col2 AS t2_c2
FROM   table1 t1
       CROSS JOIN
       TABLE( split_string( t1.col1 ) ) c1
       CROSS JOIN
       TABLE( split_string( t1.col2 ) ) c2
       INNER JOIN
       table2 t2
       ON ( c2.COLUMN_VALUE = t2.col1 )

输出

ID T1_C1 T2_C1 T2_C1
-- ----- ----- -----
 1 PC1   F1    V1   
 1 PC1   F2    V2
 1 PC2   F1    V1
 1 PC2   F2    V2

备选方案 2

使用递归子查询分解子句:

WITH bounds ( id, a, b, start_a, end_a, start_b, end_b ) AS (
  SELECT id, col1, col2, 1, INSTR( col1, ',', 1 ), 1, INSTR( col2, ',', 1 )
  FROM   table1
UNION ALL
  SELECT id, a, b,
         end_a + 1,
         INSTR( a, ',', end_a + 1 ),
         CASE end_a WHEN 0 THEN end_b + 1 ELSE start_b END,
         CASE end_a WHEN 0 THEN INSTR( b, ',', end_b + 1 ) ELSE end_b END
  FROM   bounds
  WHERE  end_a > 0 OR end_b > 0
),
data ( id, col1, col2 ) AS (
  SELECT id,
         SUBSTR( a, start_a, CASE end_a WHEN 0 THEN LENGTH(a) + 1 ELSE end_a END - start_a ),
         SUBSTR( b, start_b, CASE end_b WHEN 0 THEN LENGTH(b) + 1 ELSE end_b END - start_b )
  FROM   bounds
)
SELECT d.id,
       d.col1,
       d.col2,
       t.col2
FROM   data d
       INNER JOIN
       table2 t
       ON ( d.col2 = t.col1 )

【讨论】:

抱歉回复太晚了,感谢您的回复,您的第一个解决方案比我之前的查询提供了更好的时间。我将尝试使用其他选项来测量时间【参考方案2】:

这里有几个涉及递归子查询分解的替代方案:

WITH t1 AS (SELECT 1 id, 'PC1,PC2' col1, 'F1,F2' col2 FROM dual UNION ALL
            SELECT 2 id, 'PC1,PC3,PC4' col1, 'F2,F3,F4' col2 FROM dual),
     t2 AS (SELECT 'F1' col1, 'V1' col2 FROM dual UNION ALL
            SELECT 'F2' col1, 'V2' col2 FROM dual UNION ALL
            SELECT 'F3' col1, 'V3' col2 FROM dual UNION ALL
            SELECT 'F4' col1, 'V4' col2 FROM dual),
     -- end of mimicking your tables; see below for the rest of the query you'd need:
     t1_rcrsv1 (ID, col1, split_col1, col2, lvl) AS (SELECT ID, col1, regexp_substr(col1,'[^,]+',1,1), col2, 1 lvl
                                                     FROM   t1
                                                     UNION ALL
                                                     SELECT ID, col1, regexp_substr(col1,'[^,]+',1,lvl+1), col2, lvl + 1
                                                     FROM   t1_rcrsv1
                                                     WHERE  regexp_substr(col1,'[^,]+',1,lvl+1) IS NOT NULL),
     t1_rcrsv2 (ID, col1, col2, split_col2, lvl) AS (SELECT ID, split_col1, col2, regexp_substr(col2,'[^,]+',1,1), 1 lvl
                                                     FROM   t1_rcrsv1
                                                     UNION ALL
                                                     SELECT ID, col1, col2, regexp_substr(col2,'[^,]+',1,lvl+1), lvl + 1
                                                     FROM   t1_rcrsv2
                                                     WHERE  regexp_substr(col2,'[^,]+',1,lvl+1) IS NOT NULL)
SELECT t1a.id,
       t1a.col1 t1_c1,
       t1a.split_col2 t1_c2,
       t2.col2 t2_c2
FROM   t1_rcrsv2 t1a
       INNER JOIN t2 ON t1a.split_col2 = t2.col1
ORDER BY t1a.ID, t1a.col1, t1a.split_col2;

        ID T1_C1       T1_C2    T2_C2
---------- ----------- -------- -----
         1 PC1         F1       V1
         1 PC1         F2       V2
         1 PC2         F1       V1
         1 PC2         F2       V2
         2 PC1         F2       V2
         2 PC1         F3       V3
         2 PC1         F4       V4
         2 PC3         F2       V2
         2 PC3         F3       V3
         2 PC3         F4       V4
         2 PC4         F2       V2
         2 PC4         F3       V3
         2 PC4         F4       V4

上面的查询首先循环通过col1,然后在加入第二个表之前循环通过col2。但是,您可能会发现在先循环 col2 之后,更早地进行连接会更快 - 如下所示:

WITH t1 AS (SELECT 1 id, 'PC1,PC2' col1, 'F1,F2' col2 FROM dual UNION ALL
            SELECT 2 id, 'PC1,PC3,PC4' col1, 'F2,F3,F4' col2 FROM dual),
     t2 AS (SELECT 'F1' col1, 'V1' col2 FROM dual UNION ALL
            SELECT 'F2' col1, 'V2' col2 FROM dual UNION ALL
            SELECT 'F3' col1, 'V3' col2 FROM dual UNION ALL
            SELECT 'F4' col1, 'V4' col2 FROM dual),
     -- end of mimicking your tables; see below for the rest of the query you'd need:
     t1_rcrsv1 (ID, col1, col2, split_col2, lvl) AS (SELECT ID, col1, col2, regexp_substr(col2,'[^,]+',1,1), 1 lvl
                                                     FROM   t1
                                                     UNION ALL
                                                     SELECT ID, col1, col2, regexp_substr(col2,'[^,]+',1,lvl+1), lvl + 1
                                                     FROM   t1_rcrsv1
                                                     WHERE  regexp_substr(col2,'[^,]+',1,lvl+1) IS NOT NULL),
     t1_rcrsv2 (ID, col1, split_col1, col2, lvl, t2_c2) AS (SELECT t1a.ID, t1a.col1, regexp_substr(t1a.col1,'[^,]+',1,1), t1a.split_col2, 1 lvl, t2.col2
                                                            FROM   t1_rcrsv1 t1a
                                                                   INNER JOIN t2 ON t1a.split_col2 = t2.col1
                                                            UNION ALL
                                                            SELECT ID, col1, regexp_substr(col1,'[^,]+',1,lvl+1), col2, lvl + 1, t2_c2
                                                            FROM   t1_rcrsv2
                                                            WHERE  regexp_substr(col1,'[^,]+',1,lvl+1) IS NOT NULL)
SELECT id,
       split_col1 t1_c1,
       col2 t1_c2,
       t2_c2
FROM   t1_rcrsv2
ORDER BY ID, t1_c1, t1_c2;

        ID T1_C1       T1_C2    T2_C2
---------- ----------- -------- -----
         1 PC1         F1       V1
         1 PC1         F2       V2
         1 PC2         F1       V1
         1 PC2         F2       V2
         2 PC1         F2       V2
         2 PC1         F3       V3
         2 PC1         F4       V4
         2 PC3         F2       V2
         2 PC3         F3       V3
         2 PC3         F4       V4
         2 PC4         F2       V2
         2 PC4         F3       V3
         2 PC4         F4       V4

您需要测试这两个查询(以及 MT0 提供的解决方案),看看哪个最适合您。

注意如果要返回 table1 的所有拆分列,无论 table2 中是否存在匹配项,您可能需要将内连接转换为外连接。

【讨论】:

以上是关于Oracle 将列分隔为多行的主要内容,如果未能解决你的问题,请参考以下文章

如何根据一个字段是不是包含oracle sql中的逗号分隔字符串将单行拆分为多行?

将列拆分为多行

熊猫:将列中的列表拆分为多行[重复]

如何在 Oracle 中将多行组合成逗号分隔的列表? [复制]

在Oracle中将字符串拆分为多行

在Oracle中将字符串拆分为多行