How to split a string in rows? - sql

I have the following query. I need to pull from a table, a reference for every order number (unique identifier). I will have in the end something like:
Order ID Ref
A Xz|Yz
But I want to have:
Order ID Ref
A Xz
A Yz
The catch is for each order ID, I could have more or less references concatenated but they are always separated by '|'.
I need to use a Select statement somehow (to read the data in Tableau in the proposed format above).
Does anybody have any ideas on how to achieve this?

This version handles NULL list elements and multiple rows:
WITH tbl(order_id, ref) AS (
SELECT 'A', 'Xz|Yz' FROM DUAL union all
SELECT 'B', 'B1||B3' FROM DUAL union all
SELECT 'C', '|C2|C3B3' FROM DUAL
)
SELECT order_id, regexp_substr(ref, '(.*?)(\||$)', 1, level, NULL, 1) ref
FROM tbl
CONNECT BY level <= regexp_count(ref, '\|') + 1
and prior order_id = order_id
and prior sys_guid() is not null
order by order_id;
DB Fiddle example

In Oracle, you can use a recursive query with CONNECT BY and REGEXP_SUBSTR:
SELECT order_id, TRIM(REGEXP_SUBSTR(ref, '[^|]+', 1, level)) ref
FROM t
CONNECT BY instr(ref, '|', 1, level - 1) > 0
ORDER BY order_id, ref
Demo on DB Fiddle:
WITH t AS (
SELECT 'A' order_id, 'Xz|Yz' ref FROM DUAL
)
SELECT order_id, trim(regexp_substr(ref, '[^|]+', 1, level)) ref
FROM t
CONNECT BY instr(ref, '|', 1, level - 1) > 0
order by order_id, ref
ORDER_ID | REF
:------- | :--
A | Xz
A | Yz

You will always have difficulty with that data, because its data is not normalised.
Specifically, the values in each column are currently not atomic; they fail “first normal form”.
The |-separated values should instead be broken out to separate values; for example, rows in some other table.

Related

Oracle SQL - Combining columns with 'OR' bit function

Oracle 12.2 - I have a table with 3 columns... ID, ParentID and ProductList. ID is unique, with multiple IDs rolling up to a ParentID. (this is a account model... basically multiple accounts have the same parent...) ProductList is a string...also exactly 20 bytes... right now it is 20 letters of 'Y' and 'N', such as YYNYNYNYNNNY... but I can change the 'Y' and 'N' to 1 and 0 if it will help... what I need to do is within a group of ParentID, calculate a bitwise OR of the ProductList. The end result I need is a 20 byte string (or some type of 20 bits of data) that says - for each respective letter/bit - if any 'Y' then return 'Y'. Again, I can use 1/0 if easier than Y/N.
Here is pseudoCode of what I am trying to do... Any help appreciated.
with T1 as
(
select 10 as ID, 20 as ParentID, 'YYNNYNYNYNYYNNYNYNYN' as ProductList from dual
union
select 11 as ID, 20 as ParentID, 'NNNNNNNNNNYYYYYYYYYY' as ProductList from dual
union
select 22 as ID, 20 as ParentID, 'YYNNNNNNNNNNNNNNNNNN' as ProductList from dual
)
SELECT ParentID, BitWiseOr(ProductList) FROM t1
group by ParentID;
You can use the brute force method of taking the maximum of each character and then using ||:
SELECT ParentID,
(max(substr(productlist, 1, 1)) ||
max(substr(productlist, 2, 1)) ||
max(substr(productlist, 3, 1)) ||
. . .
max(substr(productlist, 20, 1)) ||
)
FROM t1
GROUP BY ParentID;
This works because 'Y' > 'N'.
Note: This is a lousy data model. You should have a separate table with one row per id and product.
You can destruct string to atomic values, compute result of or operation and assemble it back into string. (Credit to #GordonLinoff for Y>N trick.) dbfiddle here.
Unfortunately, Oracle does not allow something like unpivot (val FOR substring(ProductList,i,1 in ... and also Oracle does not have equivalent to Postgres bool_or, which would both made solution simpler. At least this solution scales with ProductList length.
Anyway you should avoid violating 1st normal form. If you cannot, it IMHO does not matter how boolean is modelled.
with T1 as
(
select 10 as ID, 20 as ParentID, 'YYNNYNYNYNYYNNYNYNYN' as ProductList from dual
union
select 11 as ID, 20 as ParentID, 'NNNNNNNNNNYYYYYYYYYY' as ProductList from dual
union
select 22 as ID, 20 as ParentID, 'YYNNNNNNNNNNNNNNNNNN' as ProductList from dual
), series (i) as (
select level as i
from dual
connect by level <= 20
), applied_or as (
select t1.parentid
, max(substr(t1.productlist, series.i, 1)) as or_result
, series.i
from t1
cross join series
group by t1.parentid, series.i
)
select parentid
, listagg(or_result) within group (order by i)
from applied_or
group by parentid

Apply order by in comma separated string in oracle

I have one of the column in oracle table which has below value :
select csv_val from my_table where date='09-OCT-18';
output
==================
50,100,25,5000,1000
I want this values to be in ascending order with select query, output would looks like :
output
==================
25,50,100,1000,5000
I tried this link, but looks like it has some restriction on number of digits.
Here, I made you a modified version of the answer you linked to that can handle an arbitrary (hardcoded) number of commas. It's pretty heavy on CTEs. As with most LISTAGG answers, it'll have a 4000-char limit. I also changed your regexp to be able to handle null list entries, based on this answer.
WITH
T (N) AS --TEST DATA
(SELECT '50,100,25,5000,1000' FROM DUAL
UNION
SELECT '25464,89453,15686' FROM DUAL
UNION
SELECT '21561,68547,51612' FROM DUAL
),
nums (x) as -- arbitrary limit of 20, can be changed
(select level from dual connect by level <= 20),
splitstr (N, x, substring) as
(select N, x, regexp_substr(N, '(.*?)(,|$)', 1, x, NULL, 1)
from T
inner join nums on x <= 1 + regexp_count(N, ',')
order by N, x)
select N, listagg(substring, ',') within group (order by to_number(substring)) as sorted_N
from splitstr
group by N
;
Probably it can be improved, but eh...
Based on sample data you posted, relatively simple query would work (you need lines 3 - 7). If data doesn't really look like that, query might need adjustment.
SQL> with my_table (csv_val) as
2 (select '50,100,25,5000,1000' from dual)
3 select listagg(token, ',') within group (order by to_number(token)) result
4 from (select regexp_substr(csv_val, '[^,]+', 1, level) token
5 from my_table
6 connect by level <= regexp_count(csv_val, ',') + 1
7 );
RESULT
-------------------------
25,50,100,1000,5000
SQL>

Search comma separated value in oracle 12

I have a Table - Product In Oracle, wherein p_spc_cat_id is stored as comma separated values.
p_id p_name p_desc p_spc_cat_id
1 AA AAAA 26,119,27,15,18
2 BB BBBB 0,0,27,56,57,4
3 BB CCCC 26,0,0,15,3,8
4 CC DDDD 26,0,27,7,14,10
5 CC EEEE 26,119,0,48,75
Now I want to search p_name which have p_spc_cat_id in '26,119,7' And this search value are not fixed it will some time '7,27,8'. The search text combination change every time
my query is:
select p_id,p_name from product where p_spc_cat_id in('26,119,7');
when i execute this query that time i can't find any result
I am little late in answering however i hope that i understood the question correctly.
Read further if: you have a table storing records like
1. 10,20,30,40
2. 50,40,20,70
3. 80,60,30,40
And a search string like '10,60', in which cases it should return rows 1 & 3.
Please try below, it worked for my small table & data.
create table Temp_Table_Name (some_id number(6), Ab varchar2(100))
insert into Temp_Table_Name values (1,'112,120')
insert into Temp_Table_Name values (2,'7,8,100,26')
Firstly lets breakdown the logic:
The table contains comma separated data in one of the columns[Column AB].
We have a comma separated string which we need to search individually in that string column. ['26,119,7,18'-X_STRING]
ID column is primary key in the table.
1.) Lets multiple each record in the table x times where x is the count of comma separated values in the search string [X_STRING]. We can use below query to create the cartesian join sub-query table.
Select Rownum Sequencer,'26,119,7,18' X_STRING
from dual
CONNECT BY ROWNUM <= (LENGTH( '26,119,7,18') - LENGTH(REPLACE( '26,119,7,18',',',''))) + 1
Small note: Calculating count of comma separated values =
Length of string - length of string without ',' + 1 [add one for last value]
2.) Create a function PARSING_STRING such that PARSING_STRING(string,position). So If i pass:
PARSING_STRING('26,119,7,18',3) it should return 7.
CREATE OR REPLACE Function PARSING_STRING
(String_Inside IN Varchar2, Position_No IN Number)
Return Varchar2 Is
OurEnd Number; Beginn Number;
Begin
If Position_No < 1 Then
Return Null;
End If;
OurEnd := Instr(String_Inside, ',', 1, Position_No);
If OurEnd = 0 Then
OurEnd := Length(String_Inside) + 1;
End If;
If Position_No = 1 Then
Beginn := 1;
Else
Beginn := Instr(String_Inside, ',', 1, Position_No-1) + 1;
End If;
Return Substr(String_Inside, Beginn, OurEnd-Beginn);
End;
/
3.) Main query, with the join to multiply records.:
select t1.*,PARSING_STRING(X_STRING,Sequencer)
from Temp_Table_Name t1,
(Select Rownum Sequencer,'26,119,7,18' X_STRING from dual
CONNECT BY ROWNUM <= (Select (LENGTH( '26,119,7,18') - LENGTH(REPLACE(
'26,119,7,18',',',''))) + 1 from dual)) t2
Please note that with each multiplied record we are getting 1 particular position value from the comma separated string.
4.) Finalizing the where condition:
Where
/* For when the value is in the middle of the strint [,value,] */
AB like '%,'||PARSING_STRING(X_STRING,Sequencer)||',%'
OR
/* For when the value is in the start of the string [value,]
parsing the first position comma separated value to match*/
PARSING_STRING(AB,1) = PARSING_STRING(X_STRING,Sequencer)
OR
/* For when the value is in the end of the string [,value]
parsing the last position comma separated value to match*/
PARSING_STRING(AB,(LENGTH(AB) - LENGTH(REPLACE(AB,',',''))) + 1) =
PARSING_STRING(X_STRING,Sequencer)
5.) Using distinct in the query to get unique ID's
[Final Query:Combination of all logic stated above: 1 Query to find them all]
select distinct Some_ID
from Temp_Table_Name t1,
(Select Rownum Sequencer,'26,119,7,18' X_STRING from dual
CONNECT BY ROWNUM <= (Select (LENGTH( '26,119,7,18') - LENGTH(REPLACE( '26,119,7,18',',',''))) + 1 from dual)) t2
Where
AB like '%,'||PARSING_STRING(X_STRING,Sequencer)||',%'
OR
PARSING_STRING(AB,1) = PARSING_STRING(X_STRING,Sequencer)
OR
PARSING_STRING(AB,(LENGTH(AB) - LENGTH(REPLACE(AB,',',''))) + 1) = PARSING_STRING(X_STRING,Sequencer)
You can use like to find it:
select p_id,p_name from product where p_spc_cat_id like '%26,119%'
or p_spc_cat_id like '%119,26%' or p_spc_cat_id like '%119,%,26%' or p_spc_cat_id like '%26,%,119%';
Use the Oracle function instr() to achieve what you want. In your case that would be:
SELECT p_name
FROM product
WHERE instr(p_spc_cat_id, '26,119') <> 0;
Oracle Doc for INSTR
If the string which you are searching will always have 3 values (i.e. 2 commas present) then you can use below approach.
where p_spc_cat_id like regexp_substr('your_search_string, '[^,]+', 1, 1)
or p_spc_cat_id like regexp_substr('your_search_string', '[^,]+', 1, 2)
or p_spc_cat_id like regexp_substr('your_search_string', '[^,]+', 1, 3)
If you cant predict how many values will be there in your search string
(rather how many commas) in that case you may need to generate dynamic query.
Unfortunately sql fiddle is not working currently so could not test this code.
SELECT p_id,p_name
FROM product
WHERE p_spc_cat_id
LIKE '%'||'&i_str'||'%'`
where i_str is 26,119,7 or 7,27,8
This solution uses CTE's. "product" builds the main table. "product_split" turns products into rows so each element in p_spc_cat_id is in it's own row. Lastly, product_split is searched for each value in the string '26,119,7' which is turned into rows by the connect by.
with product(p_id, p_name, p_desc, p_spc_cat_id) as (
select 1, 'AA', 'AAAA', '26,119,27,15,18' from dual union all
select 2, 'BB', 'BBBB', '0,0,27,56,57,4' from dual union all
select 3, 'BB', 'CCCC', '26,0,0,15,3,8' from dual union all
select 4, 'CC', 'DDDD', '26,0,27,7,14,10' from dual union all
select 5, 'CC', 'EEEE', '26,119,0,48,75' from dual
),
product_split(p_id, p_name, p_spc_cat_id) as (
select p_id, p_name,
regexp_substr(p_spc_cat_id, '(.*?)(,|$)', 1, level, NULL, 1)
from product
connect by level <= regexp_count(p_spc_cat_id, ',')+1
and prior p_id = p_id
and prior sys_guid() is not null
)
-- select * from product_split;
select distinct p_id, p_name
from product_split
where p_spc_cat_id in(
select regexp_substr('26,119,7', '(.*?)(,|$)', 1, level, NULL, 1) from dual
connect by level <= regexp_count('26,119,7', ',') + 1
)
order by p_id;
P_ID P_
---------- --
1 AA
3 BB
4 CC
5 CC
SQL>

Finding out the highest number in a comma separated string using Oracle SQL

I have a table with two columns:
OLD_REVISIONS |NEW_REVISIONS
-----------------------------------
1,25,26,24 |1,26,24,25
1,56,55,54 |1,55,54
1 |1
1,2 |1
1,96,95,94 |1,96,94,95
1 |1
1 |1
1 |1
1 |1
1,2 |1,2
1 |1
1 |1
1 |1
1 |1
For each row there will be a list of revisions for a document (comma separated)
The comma separated list might be the same in both columns but the order/sort might be different - e.g.
2,1 |1,2
I would like to find all the instances where the highest revision in the OLD_REVISIONS column is lower than than the highest revision in NEW_REVISIONS
The following would fit that criteria
OLD_REVISIONS |NEW_REVISIONS
-----------------------------------
1,2 |1
1,56,55,54 |1,55,54
I tried a solution using the MINUS option (joining the table to itself) but it returns differences even for when the list is the same but in the wrong order
I tried the function GREATEST (i.e where greatest(new_Revisions) < greatest(old_revisions)) but i am not sure why greatest(OLD_REVISIONS) always just returns the comma separated value. It does not return the max value. I suspect it is comparing strings because the columns are VARCHAR.
Also, MAX function expects a single number.
Is there another way i can achieve the above? I am looking for a pure SQL option so i can print out the results (or a PL/SQL option that can print out the results)
Edit
Apologies for not mentioning this but for the NEW_REVISIONS i do actually have the data in a table where each revision is in a separate row:
"DOCNUMBER" "REVISIONNUMBER"
67 1
67 24
67 25
67 26
75 1
75 54
75 55
75 56
78 1
79 1
79 2
83 1
83 96
83 94
Just to give some content, a few weeks ago i suspected that there are revisions disappearing.
To investigate this, i decided to take a count of all revisions for all documents and take a snapshot to compare later to see if revisions are indeed missing.
The snapshot that i took contained the following columns:
docnumber, count, revisions
The revisions were stored in a comma separated list using the listagg function.
The trouble i have now is the on live table, new revisions have been added so when i compare the main table and the snapshot using a MINUS i get a difference because
of the new revisions in the main table.
Even though in the actual table the revisions are individual rows, in the snapshot table i dont have the individual rows.
I am thinking the only way to recreate the snapshot in the same format and compare them find out if maximum revision in the main table is lower than the max revision in the snapshot table (hence why im trying to find out how to find out the max in a comma separated string)
Enjoy.
select xmlcast(xmlquery(('max((' || OLD_REVISIONS || '))') RETURNING CONTENT) as int) as OLD_REVISIONS_max
,xmlcast(xmlquery(('max((' || NEW_REVISIONS || '))') RETURNING CONTENT) as int) as NEW_REVISIONS_max
from t
;
Assuming your base table has an id column (versions of what?) - here is a solution based on splitting the rows.
Edit: If you like this solution, check out vkp's solution, which is better than mine. I explain why his solution is better in a Comment to his Answer.
with
t ( id, old_revisions, new_revisions ) as (
select 101, '1,25,26,24', '1,26,24,25' from dual union all
select 102, '1,56,55,54', '1,55,54' from dual union all
select 103, '1' , '1' from dual union all
select 104, '1,2' , '1' from dual union all
select 105, '1,96,95,94', '1,96,94,95' from dual union all
select 106, '1' , '1' from dual union all
select 107, '1' , '1' from dual union all
select 108, '1' , '1' from dual union all
select 109, '1' , '1' from dual union all
select 110, '1,2' , '1,2' from dual union all
select 111, '1' , '1' from dual union all
select 112, '1' , '1' from dual union all
select 113, '1' , '1' from dual union all
select 114, '1' , '1' from dual
)
-- END of TEST DATA; the actual solution (SQL query) begins below.
select id, old_revisions, new_revisions
from (
select id, old_revisions, new_revisions, 'old' as flag,
to_number(regexp_substr(old_revisions, '\d+', 1, level)) as rev_no
from t
connect by level <= regexp_count(old_revisions, ',') + 1
and prior id = id
and prior sys_guid() is not null
union all
select id, old_revisions, new_revisions, 'new' as flag,
to_number(regexp_substr(new_revisions, '\d+', 1, level)) as rev_no
from t
connect by level <= regexp_count(new_revisions, ',') + 1
and prior id = id
and prior sys_guid() is not null
)
group by id, old_revisions, new_revisions
having max(case when flag = 'old' then rev_no end) !=
max(case when flag = 'new' then rev_no end)
order by id -- ORDER BY is optional
;
ID OLD_REVISION NEW_REVISION
--- ------------ ------------
102 1,56,55,54 1,55,54
104 1,2 1
You can compare every value by putting together the revisions in the same order using listagg function.
SELECT listagg(o,',') WITHIN GROUP (ORDER BY o) old_revisions,
listagg(n,',') WITHIN GROUP (ORDER BY n) new_revisions
FROM (
SELECT DISTINCT rowid r,
regexp_substr(old_revisions, '[^,]+', 1, LEVEL) o,
regexp_substr(new_revisions, '[^,]+', 1, LEVEL) n
FROM table
WHERE regexp_substr(old_revisions, '[^,]+', 1, LEVEL) IS NOT NULL
CONNECT BY LEVEL<=(SELECT greatest(MAX(regexp_count(old_revisions,',')),MAX(regexp_count(new_revisions,',')))+1 c FROM table)
)
GROUP BY r
HAVING listagg(o,',') WITHIN GROUP (ORDER BY o)<>listagg(n,',') WITHIN GROUP (ORDER BY n);
This could be a way:
select
OLD_REVISIONS,
NEW_REVISIONS
from
REVISIONS t,
table(cast(multiset(
select level
from dual
connect by level <= length (regexp_replace(t.OLD_REVISIONS, '[^,]+')) + 1
) as sys.OdciNumberList
)
) levels_old,
table(cast(multiset(
select level
from dual
connect by level <= length (regexp_replace(t.NEW_REVISIONS, '[^,]+')) + 1
)as sys.OdciNumberList
)
) levels_new
group by t.ROWID,
OLD_REVISIONS,
NEW_REVISIONS
having max(to_number(trim(regexp_substr(t.OLD_REVISIONS, '[^,]+', 1, levels_old.column_value)))) >
max(to_number(trim(regexp_substr(t.new_REVISIONS, '[^,]+', 1, levels_new.column_value))))
This uses a double string split to pick the values from every field, and then simply finds the rows where the max values among the two collections match your requirement.
You should edit this by adding some unique key in the GROUP BYclause, or a rowid if you don't have any unique key on your table.
One way to do is to split the columns on comma separation using regexp_substr and checking if the max and min values are different.
Sample Demo
with rownums as (select t.*,row_number() over(order by old_revisions) rn from t)
select old_revisions,new_revisions
from rownums
where rn in (select rn
from rownums
group by rn
connect by regexp_substr(old_revisions, '[^,]+', 1, level) is not null
or regexp_substr(new_revisions, '[^,]+', 1, level) is not null
having max(cast(regexp_substr(old_revisions,'[^,]+', 1, level) as int))
<> max(cast(regexp_substr(new_revisions,'[^,]+', 1, level) as int))
)
Comments say normalise data. I agree but also I understand it may be not possible. I would try something like query below:
select greatest(val1, val2), t1.r from (
select max(val) val1, r from (
select regexp_substr(v1,'[^,]+', 1, level) val, rowid r from tab1
connect by regexp_substr(v1, '[^,]+', 1, level) is not null
) group by r) t1
inner join (
select max(val) val2, r from (
select regexp_substr(v2,'[^,]+', 1, level) val, rowid r from tab1
connect by regexp_substr(v2, '[^,]+', 1, level) is not null
) group by r) t2
on (t1.r = t2.r);
Tested on:
create table tab1 (v1 varchar2(100), v2 varchar2(100));
insert into tab1 values ('1,3,5','1,4,7');
insert into tab1 values ('1,3,5','1,2,9');
insert into tab1 values ('1,3,5','1,3,5');
insert into tab1 values ('1,3,5','1,4');
and seems to work fine. I left rowid for reference. I guess you have some id in table.
After your edit I would change query to:
select greatest(val1, val2), t1.r from (
select max(val) val1, r from (
select regexp_substr(v1,'[^,]+', 1, level) val, DOCNUMBER r from tab1
connect by regexp_substr(v1, '[^,]+', 1, level) is not null
) group by DOCNUMBER) t1
inner join (
select max(DOCNUMBER) val2, DOCNUMBER r from NEW_REVISIONS) t2
on (t1.r = t2.r);
You may write a PL/SQL function parsing the string and returning the maximal number
select max_num( '1,26,24,25') max_num from dual;
MAX_NUM
----------
26
The query ist than very simple:
select OLD_REVISIONS NEW_REVISIONS
from revs
where max_num(OLD_REVISIONS) < max_num(NEW_REVISIONS);
A prototyp function without validation and error handling
create or replace function max_num(str_in VARCHAR2) return NUMBER as
i number;
x varchar2(1);
n number := 0;
max_n number := 0;
pow number := 0;
begin
for i in 0.. length(str_in)-1 loop
x := substr(str_in,length(str_in)-i,1);
if x = ',' then
-- check max number
if n > max_n then
max_n := n;
end if;
-- reset
n := 0;
pow := 0;
else
n := n + to_number(x)*power(10,pow);
pow := pow +1;
end if;
end loop;
return(max_n);
end;
/

Turn to multiple records

I have records coming as below:
Item | Color Code
Bag | 1,2,3
How can I turn these record into:
Item | Color Code
Bag | 1
Bag | 2
Bag | 3
at SQL level without any intervention of new program.
I have problem to build a package and cube without this format of data
This will work
with t1(Item,ColorCode) as
(select 'Bag', '1,2,3' from dual)
select Item,regexp_substr(ColorCode, '[^,]+', 1, level) result
from t1
connect by level <= length(regexp_replace(ColorCode, '[^,]+')) + 1;
Try this:
SELECT t.Item,
trim(regexp_substr(t.ColorCode, '[^,]+', 1, lines.column_value)) ColorCode
FROM t,
TABLE (CAST (MULTISET
(SELECT LEVEL FROM dual CONNECT BY LEVEL <= regexp_count(t.ColorCode, ',')+1)
AS sys.odciNumberList
)
) lines
SQL FIDDLE DEMO