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中的逗号分隔字符串将单行拆分为多行?