Split one column data into multiple columns in oracle - sql

In my oracle query I am using like below for retrieving the records and the result looks like this -
SELECT columnC
, LISTAGG(r.columnA,',') WITHIN GROUP (ORDER BY r.columnB) AS Test_sensor
FROM tableA
GROUP BY columnC
Currently the output looks like below -
ColumnC | Test_Sensor
=============================
Z12345 | 20,30,40,50,60,70
But I want this data to be displayed as below -
ColumnC | Test_Sensor1 | Test_Sensor2 | Test_Sensor3 | Test_Sensor4
==========================================================================
Z12345 | 20 | 30 | 40 | 50
Please help me on this
Thanks
Kranthi RTR

You can use a PIVOT (and do not need to use LISTAGG):
SQL Fiddle
Oracle 11g R2 Schema Setup:
CREATE TABLE TableA ( ColumnA, ColumnB, ColumnC ) AS
SELECT 20, 'A', 'Z12345' FROM DUAL UNION ALL
SELECT 30, 'B', 'Z12345' FROM DUAL UNION ALL
SELECT 40, 'C', 'Z12345' FROM DUAL UNION ALL
SELECT 50, 'D', 'Z12345' FROM DUAL UNION ALL
SELECT 60, 'E', 'Z12345' FROM DUAL UNION ALL
SELECT 70, 'F', 'Z12345' FROM DUAL;
Query 1:
SELECT *
from (
SELECT columnA,
columnC,
ROW_NUMBER() OVER ( PARTITION BY columnC ORDER BY columnB ) AS rn
FROM tableA
) a
PIVOT ( MAX( columnA ) FOR rn IN (
1 AS test_sensor1,
2 AS test_sensor2,
3 AS test_sensor3,
4 AS test_sensor4
) )
Results:
| COLUMNC | TEST_SENSOR1 | TEST_SENSOR2 | TEST_SENSOR3 | TEST_SENSOR4 |
|---------|--------------|--------------|--------------|--------------|
| Z12345 | 20 | 30 | 40 | 50 |
Query 2:
You can do it using LISTAGG but it is much, much less efficient than using PIVOT:
SELECT ColumnC,
REGEXP_SUBSTR( test_sensor, '[^,]+', 1, 1 ) AS test_sensor1,
REGEXP_SUBSTR( test_sensor, '[^,]+', 1, 2 ) AS test_sensor2,
REGEXP_SUBSTR( test_sensor, '[^,]+', 1, 3 ) AS test_sensor3,
REGEXP_SUBSTR( test_sensor, '[^,]+', 1, 4 ) AS test_sensor4
FROM (
SELECT ColumnC,
LISTAGG( ColumnA, ',' ) WITHIN GROUP ( ORDER BY ColumnB )
AS test_sensor
FROM TableA
GROUP BY ColumnC
)
Results:
| COLUMNC | TEST_SENSOR1 | TEST_SENSOR2 | TEST_SENSOR3 | TEST_SENSOR4 |
|---------|--------------|--------------|--------------|--------------|
| Z12345 | 20 | 30 | 40 | 50 |

If you go with LISTAGG, there is a caveat with LISTAGG in that it ignores NULL values. Are you sure your columnA will ALWAYS have data? If, say, test_sensor2 is NULL, the output from the listagg operation will be 20,40,50,60,70 so after the NULL value all sensor's data will be reported wrong! Correct that by using this:
replace(LISTAGG( nvl(to_char(ColumnA), ','), ',' )
WITHIN GROUP ( ORDER BY ColumnB ), ',,,',',,')
Now your output is 20,,40,50,60,70, keeping test_sensor2's NULL value and the rest in proper position.
HOWEVER now there is another problem in that the regex of the form '[^,]+' causes the same issue! So even though the listagg output is fixed, the parsed output is back to being off after the column with the NULL value! There is a different form shown below that fixes this problem. Here is an example set up so you can comment/uncomment to see the differences as the data is processed. Always expect the unexpected!
with TableA ( ColumnA, ColumnB, ColumnC ) AS (
SELECT 20, 'A', 'Z12345' FROM DUAL UNION ALL
SELECT NULL, 'B', 'Z12345' FROM DUAL UNION ALL -- make NULL
SELECT 40, 'C', 'Z12345' FROM DUAL UNION ALL
SELECT 50, 'D', 'Z12345' FROM DUAL UNION ALL
SELECT 60, 'E', 'Z12345' FROM DUAL UNION ALL
SELECT 70, 'F', 'Z12345' FROM DUAL
),
tbl_tmp as (
SELECT ColumnC,
-- Preserve the NULL in position 2
replace(LISTAGG( nvl(to_char(ColumnA), ','), ',' )
WITHIN GROUP ( ORDER BY ColumnB ), ',,,',',,')
AS test_sensor
FROM TableA
GROUP BY ColumnC
)
--select * from tbl_tmp;
-- regex of format [^,]+ does not handle NULLs
SELECT ColumnC,
-- REGEXP_SUBSTR( test_sensor, '[^,]+', 1, 1) AS test_sensor1,
-- REGEXP_SUBSTR( test_sensor, '[^,]+', 1, 2 ) AS test_sensor2,
-- REGEXP_SUBSTR( test_sensor, '[^,]+', 1, 3 ) AS test_sensor3,
-- REGEXP_SUBSTR( test_sensor, '[^,]+', 1, 4 ) AS test_sensor4
REGEXP_SUBSTR( test_sensor, '(.*?)(,|$)', 1, 1, NULL, 1) AS test_sensor1,
REGEXP_SUBSTR( test_sensor, '(.*?)(,|$)', 1, 2, NULL, 1 ) AS test_sensor2,
REGEXP_SUBSTR( test_sensor, '(.*?)(,|$)', 1, 3, NULL, 1 ) AS test_sensor3,
REGEXP_SUBSTR( test_sensor, '(.*?)(,|$)', 1, 4, NULL, 1 ) AS test_sensor4
FROM tbl_tmp;

Related

SUBSTR to ADD value in oracle

I have table with column having data in below format in Oracle DB.
COL 1
abc,mno:EMP
xyz:EMP;tyu,opr:PROF
abc,mno:EMP;tyu,opr:PROF
I am trying to convert the data in below format
COL 1
abc:EMP;mno:EMP
xyz:EMP;tyu:PROF;opr:PROF
abc:EMP;mno:EMP;tyu:PROF;opr:PROF
Basically trying to get everything after : and before ; to move it substitute comma with it.
I tried some SUBSTR and LISTAGG but couldn't get anything worth sharing.
Regards.
Here's one option; read comments within code.
SQL> with test (id, col) as
2 -- sample data
3 (select 1, 'abc,mno:EMP' from dual union all
4 select 2, 'xyz:EMP;tyu,opr:PROF' from dual union all
5 select 3, 'abc,mno:EMP;tyu,opr:PROF' from dual
6 ),
7 temp as
8 -- split sample data to rows
9 (select id,
10 column_value cv,
11 regexp_substr(col, '[^;]+', 1, column_value) val
12 from test cross join
13 table(cast(multiset(select level from dual
14 connect by level <= regexp_count(col, ';') + 1
15 ) as sys.odcinumberlist))
16 )
17 -- finally, replace comma with a string that follows a colon sign
18 select id,
19 listagg(replace(val, ',', substr(val, instr(val, ':')) ||';'), ';') within group (order by cv) new_val
20 from temp
21 group by id
22 order by id;
ID NEW_VAL
---------- ----------------------------------------
1 abc:EMP;mno:EMP
2 xyz:EMP;tyu:PROF;opr:PROF
3 abc:EMP;mno:EMP;tyu:PROF;opr:PROF
SQL>
Using the answer of littlefoot, if i were to use cross apply i wouldnt need to cast as multiset...
with test (id, col) as
-- sample data
(select 1, 'abc,mno:EMP' from dual union all
select 2, 'xyz:EMP;tyu,opr:PROF' from dual union all
select 3, 'abc,mno:EMP;tyu,opr:PROF' from dual
),
temp as
-- split sample data to rows
(select id,
column_value cv,
regexp_substr(col, '[^;]+', 1, column_value) val
from test
cross apply (select level as column_value
from dual
connect by level<= regexp_count(col, ';') + 1)
)
-- finally, replace comma with a string that follows a colon sign
select id,
listagg(replace(val, ',', substr(val, instr(val, ':')) ||';'), ';') within group (order by cv) new_val
from temp
group by id
order by id;
You do not need recursive anything, just basic regex: if the pattern is always something,something2:someCode (e.g. you have no colon before the comma), then it would be sufficient.
with test (id, col) as (
select 1, 'abc,mno:EMP' from dual union all
select 2, 'xyz:EMP;tyu,opr:PROF' from dual union all
select 3, 'abc,mno:EMP;tyu,opr:PROF' from dual union all
select 3, 'abc,mno:EMP;tyu,opr:PROF;something:QWE;something2:QWE' from dual
)
select
/*
Grab this groups:
1) Everything before the comma
2) Then everything before the colon
3) And then everything between the colon and a semicolon
Then place group 3 between 1 and 2
*/
trim(trailing ';' from regexp_replace(col || ';', '([^,]+),([^:]+):([^;]+)', '\1:\3;\2:\3')) as res
from test
| RES |
| :------------------------------------------------------------- |
| abc:EMP;mno:EMP |
| xyz:EMP;tyu:PROF;opr:PROF |
| abc:EMP;mno:EMP;tyu:PROF;opr:PROF |
| abc:EMP;mno:EMP;tyu:PROF;opr:PROF;something:QWE;something2:QWE |
db<>fiddle here

Bigquery aggregating into array based on id and id_type

I have a table that looks similar to this:
WITH
table AS (
SELECT 1 object_id, 234 type_id, 2 type_level UNION ALL
SELECT 1, 23, 1 UNION ALL
SELECT 1, 24, 1 UNION ALL
SELECT 1, 2, 0 UNION ALL
SELECT 1, 2, 0 UNION ALL
SELECT 2, 34, 1 UNION ALL
SELECT 2, 46, 1 UNION ALL
SELECT 2, 465, 2 UNION ALL
SELECT 2, 349, 2 UNION ALL
SELECT 2, 4, 0 UNION ALL
SELECT 2, 3, 0 )
SELECT
object_id,
type_id,
type_level
FROM
table
Now I am trying to create three new columns type_level_0_array,type_level_1_array,type_level_2_array for each object and aggregate the type_id of corresponding level of types into those array (I am not looking for string separated by commas).
So my resultant table should look like the following:
+----+--------------------+--------------------+--------------------+
| id | type_level_0_array | type_level_1_array | type_level_2_array |
+----+--------------------+--------------------+--------------------+
| 1 | 2 | 24,23 | 234 |
+----+--------------------+--------------------+--------------------+
| 2 | 3,4 | 34,46 | 465,349 |
+----+--------------------+--------------------+--------------------+
Is there any way to accomplish that?
Update:
Although it seems that my type_id has certain pattern e.g. level 0 types are of 1 length, level 1 types are of 2 length and so on, in my real dataset there is no such pattern. The identification of level is solely possible by looking at type_level of any row.
Below is for BigQuery Standard SQL
#standardSQL
SELECT object_id,
ARRAY_AGG(DISTINCT IF(type_level = 0, type_id, NULL) IGNORE NULLS) AS type_level_0_array,
ARRAY_AGG(DISTINCT IF(type_level = 1, type_id, NULL) IGNORE NULLS) AS type_level_1_array,
ARRAY_AGG(DISTINCT IF(type_level = 2, type_id, NULL) IGNORE NULLS) AS type_level_2_array
FROM `project.dataset.table`
GROUP BY object_id
You can test, play with above using sample data from your question as in below
#standardSQL
WITH `project.dataset.table` AS (
SELECT 1 object_id, 234 type_id, 2 type_level UNION ALL
SELECT 1, 23, 1 UNION ALL
SELECT 1, 24, 1 UNION ALL
SELECT 1, 2, 0 UNION ALL
SELECT 1, 2, 0 UNION ALL
SELECT 2, 34, 1 UNION ALL
SELECT 2, 46, 1 UNION ALL
SELECT 2, 465, 2 UNION ALL
SELECT 2, 349, 2 UNION ALL
SELECT 2, 4, 0 UNION ALL
SELECT 2, 3, 0 )
SELECT object_id,
ARRAY_AGG(DISTINCT IF(type_level = 0, type_id, NULL) IGNORE NULLS) AS type_level_0_array,
ARRAY_AGG(DISTINCT IF(type_level = 1, type_id, NULL) IGNORE NULLS) AS type_level_1_array,
ARRAY_AGG(DISTINCT IF(type_level = 2, type_id, NULL) IGNORE NULLS) AS type_level_2_array
FROM `project.dataset.table`
GROUP BY object_id
with result
Row object_id type_level_0_array type_level_1_array type_level_2_array
1 1 2 24 234
23
2 2 4 34 349
3 46 465
Try this. Works for me.
Bigquery won't let you create an array with Nulls in them, which is why the IGNORE NULLS is required.
EDIT: I've updated the code to be based off the type_level column
WITH table
AS (
SELECT 1 object_id, 234 type_id, 2 type_level UNION ALL
SELECT 1, 23, 1 UNION ALL
SELECT 1, 24, 1 UNION ALL
SELECT 1, 2, 0 UNION ALL
SELECT 1, 2, 0 UNION ALL
SELECT 2, 34, 1 UNION ALL
SELECT 2, 46, 1 UNION ALL
SELECT 2, 465, 2 UNION ALL
SELECT 2, 349, 2 UNION ALL
SELECT 2, 4, 0 UNION ALL
SELECT 2, 3, 0 )
SELECT
ARRAY_AGG(CASE WHEN type_level = 0 THEN type_id ELSE NULL END IGNORE NULLS) AS type_level_0_array
, ARRAY_AGG(CASE WHEN type_level = 1 THEN type_id ELSE NULL END IGNORE NULLS) AS type_level_1_array
, ARRAY_AGG(CASE WHEN type_level = 2 THEN type_id ELSE NULL END IGNORE NULLS) AS type_level_2_array
FROM
table

Oracle/SQL - Need query that will select max value from string in each row

I need a graceful way to select the max value from a field holding a comma delimited list.
Expected Values:
List_1 | Last
------ | ------
A,B,C | C
B,D,C | D
I'm using the following query and I'm not getting what's expected.
select
list_1,
(
select max(values) WITHIN GROUP (order by 1)
from (
select
regexp_substr(list_1,'[^,]+', 1, level) as values
from dual
connect by regexp_substr(list_1, '[^,]+', 1, level) is not null)
) as last
from my_table
Anyone have any ideas to fix my query?
with
test_data ( id, list_1 ) as (
select 101, 'A,B,C' from dual union all
select 102, 'B,D,C' from dual union all
select 105, null from dual union all
select 122, 'A' from dual union all
select 140, 'A,B,B' from dual
)
-- end of simulated table (for testing purposes only, not part of the solution)
select id, list_1, max(token) as max_value
from ( select id, list_1,
regexp_substr(list_1, '([^,])(,|$)', 1, level, null, 1) as token
from test_data
connect by level <= 1 + regexp_count(list_1, ',')
and prior id = id
and prior sys_guid() is not null
)
group by id, list_1
order by id
;
ID LIST_1_ MAX_VAL
---- ------- -------
101 A,B,C C
102 B,D,C D
105
122 A A
140 A,B,B B
In Oracle 12.1 or higher, this can be re-written using the LATERAL clause:
select d.id, d.list_1, x.max_value
from test_data d,
lateral ( select max(regexp_substr(list_1, '([^,]*)(,|$)',
1, level, null, 1)) as max_value
from test_data x
where x.id = d.id
connect by level <= 1 + regexp_count(list_1, ',')
) x
order by d.id
;

Oracle Regex Connect By

I am trying to produce multiple rows after performing a regex on a column splitting all values in square brackets. I'm only able to return a single value though, currently.
The field I am performing the regex has this value:
[1265]*[1263]
I am trying to get 1265 and 1263 in my result set as separate rows.
SELECT REGEXP_SUBSTR(column,'\[(.*?)\]',1,LEVEL) AS "col1"
FROM table
CONNECT BY REGEXP_SUBSTR(column,'\[(.*?)\]',1,LEVEL) IS NOT NULL;
Instead I just get this in the result set.
[1263]
with test (rn, col) as
(
select 'a', '[123]*[abc] []' from dual union all
select 'b', '[45][def] ' from dual union all
select 'c', '[678],.*' from dual
),
coll (rn, col) as
(
select rn,regexp_replace(col, '(\[.*?\])|.', '\1') from test
),
cte (rn, cnt, col, i) as
(
select rn, 1, col, regexp_substr(col, '(\[(.*?)\])', 1, 1, null, 2)
from coll
union all
select rn, cnt+1, col, regexp_substr(col, '(\[(.*?)\])', 1, cnt+1, null, 2)
from cte
where cnt+1 <= regexp_count(col, '\[.*?\]')
)
select * from cte
order by 1,2;
This regex counts elements by looking for closing brackets and returns the digits inside the brackets, allowing for NULLs. Separators are ignored since the data elements you want are surrounded by square brackets we can focus on those.
SQL> with test(rownbr, col) as (
select 1, '[1265]**[1263]' from dual union
select 2, '[123]' from dual union
select 3, '[111][222]*[333]' from dual union
select 4, '[411]*[][433]' from dual
)
select distinct rownbr, level as element,
regexp_substr(col, '\[([0-9]*)\]', 1, level, null, 1) value
from test
connect by level <= regexp_count(col, ']')
order by rownbr, element;
ROWNBR ELEMENT VALUE
---------- ---------- -----
1 1 1265
1 2 1263
2 1 123
3 1 111
3 2 222
3 3 333
4 1 411
4 2
4 3 433
9 rows selected.
SQL>

Self referencing table SQL Query to rows

I have this oracle 11g query:
SELECT RPAD(' ', 2 * (T.ID_LEVEL - 1)) || T.IDE IDE, T.ID_LEVEL, T.CODE, T.FK_IDE
FROM TEST_DYNAMIC T
START WITH T.FK_IDE = 0
CONNECT BY NOCYCLE PRIOR T.IDE = T.FK_IDE
ORDER BY T.IDE,T.FK_IDE;
That returns this data:
IDE |IDE_LEVEL |CODE |FK_IDE |
-----|-----------|------|--------|
1 | 1|A01 | 0|
2 | 2|A01 | 1|
3 | 3|A01 | 2|
4| 4|A01 | 3|
5 | 2|A02 | 1|
6 | 2|A03 | 1|
7 | 3|A01 | 6|
8 | 1|A02 | 0|
As you can see, the data is obtained from a self refrencing table, where the IDE_LEVEL column is a foreign key of a master table that contains this values (1, 2, 3, 4) as PK and the IDE column is an autoincrement PK from TEST_DYNAMIC table.
Is there a way to convert that result to this one?:
IDE |CODE_LEVEL1 |CODE_LEVEL2 |CODE_LEVEL3 |CODE_LEVEL4 |
-----|-------------|-------------|-------------|-------------|
1 |A01 |A01 |A01 |A01 |
1 |A01 |A02 |NULL |NULL |
1 |A01 |A03 |A01 |NULL |
8 |A02 |NULL |NULL |NULL |
In the expected result above, the IDE column is shown three times that correspond to the three ocurrences in the FK_COLUMN for key 1, and one time for key 8 (this one have no children so must be shown in the resultset) of the first resultset.
Any help will be appreciated.
I didn't get the question at first. You don't need PIVOT, you need to show the path of each branch in separate columns. Anyway, depending on the number of levels you might need some dynamic sql
with test_dynamic as (
select 1 ide, 1 id_level, 'A01' code, 0 fk_ide from dual
union all select 2, 2, 'A01', 1 from dual
union all select 3, 3, 'A01', 2 from dual
union all select 4, 4, 'A01', 3 from dual
union all select 5, 2, 'A02', 1 from dual
union all select 6, 2, 'A03', 1 from dual
union all select 7, 3, 'A01', 6 from dual
union all select 8, 1, 'A02', 0 from dual
),
tree as (
select code, level lvl, sys_connect_by_path(code, '/') path, connect_by_isleaf lf, connect_by_root(ide) root_ide
from test_dynamic t
start with t.fk_ide = 0
connect by nocycle prior t.ide = t.fk_ide
)
select root_ide, path,
regexp_substr(path, '[^/]+', 1, 1),
regexp_substr(path, '[^/]+', 1, 2),
regexp_substr(path, '[^/]+', 1, 3),
regexp_substr(path, '[^/]+', 1, 4)
from tree where lf = 1;
if the number of levels is dynamic you need to use dynamic SQL anyway to create a query with dynamic columns.
There are at least 2 approaches how to create such a query:
1) Oracle with PIVOT (since 11g I suppose)
with test_dynamic as (
select 1 ide, 1 id_level, 'A01' code, 0 fk_ide from dual
union all select 2, 2, 'A01', 1 from dual
union all select 3, 3, 'A01', 2 from dual
union all select 4, 4, 'A01', 3 from dual
union all select 5, 2, 'A02', 1 from dual
union all select 6, 2, 'A03', 1 from dual
union all select 7, 3, 'A01', 6 from dual
union all select 8, 1, 'A02', 0 from dual
),
tree as (
select level lvl, t.ide, t.code
from test_dynamic t
start with t.fk_ide = 0
connect by nocycle prior t.ide = t.fk_ide
),
pivot_data as (
select * from tree pivot xml (max(code) lvl_code for lvl in (select distinct lvl from tree))
)
select ide,
extractvalue(lvl_xml,'/PivotSet/item[1]/column[2]') code_level1,
extractvalue(lvl_xml,'/PivotSet/item[2]/column[2]') code_level2,
extractvalue(lvl_xml,'/PivotSet/item[3]/column[2]') code_level3,
extractvalue(lvl_xml,'/PivotSet/item[4]/column[2]') code_level4
from pivot_data t order by 1;
(People say that EXTRACTVALUE might become deprecated and recommend using XMLTABLE, but I've never worked with this)
2) Oracle DECODE
with test_dynamic as (
select 1 ide, 1 id_level, 'A01' code, 0 fk_ide from dual
union all select 2, 2, 'A01', 1 from dual
union all select 3, 3, 'A01', 2 from dual
union all select 4, 4, 'A01', 3 from dual
union all select 5, 2, 'A02', 1 from dual
union all select 6, 2, 'A03', 1 from dual
union all select 7, 3, 'A01', 6 from dual
union all select 8, 1, 'A02', 0 from dual
),
tree as (
select level lvl, t.ide, t.code
from test_dynamic t
start with t.fk_ide = 0
connect by nocycle prior t.ide = t.fk_ide
)
select ide,
max(decode(lvl, 1, code, null)) code_level1,
max(decode(lvl, 2, code, null)) code_level2,
max(decode(lvl, 3, code, null)) code_level3,
max(decode(lvl, 4, code, null)) code_level4
from tree group by ide order by 1;