Split comma separated values to columns in Oracle - sql

I have values being returned with 255 comma separated values. Is there an easy way to split those into columns without having 255 substr?
ROW | VAL
-----------
1 | 1.25, 3.87, 2, ...
2 | 5, 4, 3.3, ....
to
ROW | VAL | VAL | VAL ...
---------------------
1 |1.25 |3.87 | 2 ...
2 | 5 | 4 | 3.3 ...

Beware! The regexp_substr expression of the format '[^,]+' will not return the expected value if there is a null element in the list and you want that item or one after it. Consider this example where the 4th element is NULL and I want the 5th element and thus expect the '5' to be returned:
SQL> select regexp_substr('1,2,3,,5,6', '[^,]+', 1, 5) from dual;
R
-
6
Surprise! It returns the 5th NON-NULL element, not the actual 5th element! Incorrect data returned and you may not even catch it. Try this instead:
SQL> select regexp_substr('1,2,3,,5,6', '(.*?)(,|$)', 1, 5, NULL, 1) from dual;
R
-
5
So, the above corrected REGEXP_SUBSTR says to look for the 5th occurrence of 0 or more comma-delimited characters followed by a comma or the end of the line (allows for the next separator, be it a comma or the end of the line) and when found return the 1st subgroup (the data NOT including the comma or end of the line).
The search match pattern '(.*?)(,|$)' explained:
( = Start a group
. = match any character
* = 0 or more matches of the preceding character
? = Match 0 or 1 occurrences of the preceding pattern
) = End the 1st group
( = Start a new group (also used for logical OR)
, = comma
| = OR
$ = End of the line
) = End the 2nd group
EDIT: More info added and simplified the regex.
See this post for more info and a suggestion to encapsulate this in a function for easy reuse: REGEX to select nth value from a list, allowing for nulls
It's the post where I discovered the format '[^,]+' has the problem. Unfortunately it's the regex format you will most commonly see as the answer for questions regarding how to parse a list. I shudder to think of all the incorrect data being returned by '[^,]+'!

You can use regexp_substr():
select regexp_substr(val, '[^,]+', 1, 1) as val1,
regexp_substr(val, '[^,]+', 1, 2) as val2,
regexp_substr(val, '[^,]+', 1, 3) as val3,
. . .
I would suggest that you generate a column of 255 numbers in Excel (or another spreadsheet), and use the spreadsheet to generate the SQL code.

If you only have one row, and time to create your
create your own built-in cto_table function to split a string on any separator, then you can use PIVOT + LISTAGG to do it like follows:
select * from (
select rownum r , collection.*
from TABLE(cto_table(',','1.25, 3.87, 2, 19,, 1, 9, ')) collection
)
PIVOT (
LISTAGG(column_value) within group (order by 1) as val
for r in (1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
)
FYI: here is how to create the cto_table function:
CREATE OR REPLACE TYPE t_my_list AS TABLE OF VARCHAR2(100);
CREATE OR REPLACE
FUNCTION cto_table(p_sep in Varchar2, p_list IN VARCHAR2)
RETURN t_my_list
AS
l_string VARCHAR2(32767) := p_list || p_sep;
l_sep_index PLS_INTEGER;
l_index PLS_INTEGER := 1;
l_tab t_my_list := t_my_list();
BEGIN
LOOP
l_sep_index := INSTR(l_string, p_sep, l_index);
EXIT
WHEN l_sep_index = 0;
l_tab.EXTEND;
l_tab(l_tab.COUNT) := TRIM(SUBSTR(l_string,l_index,l_sep_index - l_index));
l_index := l_sep_index + 1;
END LOOP;
RETURN l_tab;
END cto_table;
/

hierarchical query could be used. pivoting can be done with case and group by.
with value_t as
(
select row_t,row_number() OVER (partition by row_t order by rownum )rn,
regexp_substr(val, '[^,]+', 1, LEVEL) val from Table1
CONNECT BY LEVEL <= regexp_count(val, '[^,]+')
AND prior row_t = row_t
AND prior sys_guid() is not null
) select row_t, max( case when rn = 1 THEN val end ) val_1,
max( case when rn = 2 THEN val end ) val_2,
max( case when rn = 3 THEN val end ) val_3
from value_t
group by row_t;

Related

How to rearrange the letter in string in alphabetical order in SQL

How to rearrange the letter in string in alphabetical order in SQL
For example
cbaz to abcz
You can split the string up into characters and then aggregate:
WITH characters ( rid, value, ch, i, l ) AS (
SELECT ROWID,
value,
SUBSTR(value, 1, 1),
1,
LENGTH(value)
FROM table_name
UNION ALL
SELECT rid,
value,
SUBSTR(value, i + 1, 1),
i + 1,
l
FROM characters
WHERE i < l
)
SELECT MAX( value ) AS original,
LISTAGG(ch) WITHIN GROUP ( ORDER BY ch ) AS ordered
FROM characters
GROUP BY rid
or:
SELECT value As original,
ordered
FROM table_name t
CROSS APPLY (
SELECT LISTAGG(SUBSTR(t.value, LEVEL, 1))
WITHIN GROUP (ORDER BY SUBSTR(t.value, LEVEL, 1)) AS ordered
FROM DUAL
CONNECT BY LEVEL <= LENGTH(t.value)
)
Which, for the sample data:
CREATE TABLE table_name ( value ) AS
SELECT 'cbaz' FROM DUAL UNION ALL
SELECT 'zyx' FROM DUAL UNION ALL
SELECT 'zyx' FROM DUAL;
Outputs:
ORIGINAL
ORDERED
cbaz
abcz
zyx
xyz
zyx
xyz
db<>fiddle here
Just for fun, you could do this programmatically:
with function sort_letters
( p_str varchar2 )
return varchar2
as
type charList is table of simple_integer index by varchar2(1);
letters charList;
letter varchar2(1);
sorted_letters long;
begin
if p_str is not null then
for i in 1..length(p_str) loop
letter := substr(p_str,i,1);
letters(letter) :=
case
when letters.exists(letter) then letters(letter) +1
else 1
end;
end loop;
letter := letters.first;
loop
sorted_letters := sorted_letters || rpad(letter, letters(letter), letter);
letter := letters.next(letter);
exit when letter is null;
end loop;
end if;
return sorted_letters;
end;
select sort_letters('abracadabra')
from dual
/
SORT_LETTERS('ABRACADABRA')
---------------------------
aaaaabbcdrr

Oracle function to compare strings in a not ordered way

I need a function to make a comparison between two strings withouth considering the order in oracle.
i.e. "asd" and "sad" should be considered as equal.
Are there similar functions? Or I need to write my own function?
This can be done with a simple java function to sort the characters of a string alphabetically:
CREATE AND COMPILE JAVA SOURCE NAMED SORTSTRING AS
public class SortString {
public static String sort( final String value )
{
final char[] chars = value.toCharArray();
java.util.Arrays.sort( chars );
return new String( chars );
}
};
/
Which you can then create a PL/SQL function to invoke:
CREATE FUNCTION SORTSTRING( in_value IN VARCHAR2 ) RETURN VARCHAR2
AS LANGUAGE JAVA NAME 'SortString.sort( java.lang.String ) return java.lang.String';
/
Then you can do a simple comparison on the sorted strings:
SELECT CASE
WHEN SORTSTRING( 'ads' ) = SORTSTRING( 'das' )
THEN 'Equal'
ELSE 'Not Equal'
END
FROM DUAL;
Not exactly a rocket science, but works (kind of, at least on simple cases).
What does it do? Alphabetically sorts letters in every string and compares them.
SQL> with test (col1, col2) as
2 (select 'asd', 'sad' from dual),
3 inter as
4 (select
5 col1, regexp_substr(col1, '[^.]', 1, level) c1,
6 col2, regexp_substr(col2, '[^.]', 1, level) c2
7 from test
8 connect by level <= greatest(length(col1), length(col2))
9 ),
10 agg as
11 (select listagg(c1, '') within group (order by c1) col1_new,
12 listagg(c2, '') within group (order by c2) col2_new
13 from inter
14 )
15 select case when col1_new = col2_new then 'Equal'
16 else 'Different'
17 end result
18 From agg;
RESULT
---------
Equal
SQL> with test (col1, col2) as
2 (select 'asd', 'sadx' from dual),
<snip>
RESULT
---------
Different
SQL>
Yet another solution, using the SUBSTR function and CONNECT BY loop.
SQL Fiddle
Query 1:
WITH a
AS (SELECT ROWNUM rn, a1.*
FROM ( SELECT SUBSTR ('2asd', LEVEL, 1) s1
FROM DUAL
CONNECT BY LEVEL <= LENGTH ('2asd')
ORDER BY s1) a1),
b
AS (SELECT ROWNUM rn, a2.*
FROM ( SELECT SUBSTR ('asd2', LEVEL, 1) s2
FROM DUAL
CONNECT BY LEVEL <= LENGTH ('asd2')
ORDER BY s2) a2)
SELECT CASE COUNT (NULLIF (s1, s2)) WHEN 0 THEN 'EQUAL' ELSE 'NOT EQUAL' END
res
FROM a INNER JOIN b ON a.rn = b.rn
Results:
| RES |
|-------|
| EQUAL |
EDIT : A PL/SQL Sort function for alphanumeric strings.
CREATE OR replace FUNCTION fn_sort(str VARCHAR2)
RETURN VARCHAR2 DETERMINISTIC AS
v_s VARCHAR2(4000);
BEGIN
SELECT LISTAGG(substr(str, LEVEL, 1), '')
within GROUP ( ORDER BY substr(str, LEVEL, 1) )
INTO v_s
FROM dual
CONNECT BY LEVEL < = length(str);
RETURN v_s;
END;
/
select fn_sort('shSdf3213Js') as s
from dual;
| S |
|-------------|
| 1233JSdfhss |
In case you want to create your own sort function, you can use below code,
CREATE OR REPLACE FUNCTION sort_text (p_text_to_sort VARCHAR2) RETURN VARCHAR2
IS
v_sorted_text VARCHAR2(1000);
BEGIN
v_sorted_text := p_text_to_sort;
FOR i IN 1..LENGTH(p_text_to_sort)
LOOP
FOR j IN 1..LENGTH(p_text_to_sort)
LOOP
IF SUBSTR(v_sorted_text, j, 1)||'' > SUBSTR(v_sorted_text, j+1, 1)||'' THEN
v_sorted_text := SUBSTR(v_sorted_text, 1, j-1)||
SUBSTR(v_sorted_text, j+1, 1)||
SUBSTR(v_sorted_text, j, 1)||
SUBSTR(v_sorted_text, j+2);
END IF;
END LOOP;
END LOOP;
RETURN v_sorted_text;
END;
/
SELECT SORT_TEXT('zlkdsadfsdfasdf') SORTED_TEXT
FROM dual;
SORTED_TEXT
---------------
aaddddfffklsssz

Oracle Regexp_ help to do a replace on string

I have a field that has several name values seperated by $;, like below:
$;James$;Paul$;
I have a function available on the names that allows me to identify data from a different table i.e. email address.
However the function only works on a single name value.
So I need to do a substring to get each name value and then run a replace command that runs the function to pull out their email address.
I want to get the following responses James#emailserver.com, Paul#emailserver.com.
So my question is: how can I run a regexp_replace command to identify the names and then split them out to run the replace?
Something like this:
with
name_list ( id, str ) as (
select 1, '$;James$;Paul$;' from dual union all
select 2, '$;Jane$;Emily$;Ann$;' from dual
)
select id, substr(str, instr(str, '$;', 1, level) + 2,
instr(str, '$;', 1, level+1) - instr(str, '$;', 1, level) - 2) as name
from name_list
connect by level <= regexp_count(str, '\$;') - 1
and prior id = id
and prior sys_guid() is not null
;
ID NAME
-- -----
1 James
1 Paul
2 Jane
2 Emily
2 Ann
And then you can use this for your comparisons.
Alternatively, if you need to see if the name James is in your input string (and assuming James is the value in a column name_col):
... where name_list.str like '$;' || name_col || '$;'
and you don't need to split the string anymore. The concatenations are necessary, though, because you don't want Anne in the name list to match the name Ann in the column.
This function helps me to split a varchar:
function f_split_string( p_string varchar2, p_separator varchar2, p_pos number) return varchar2
is
v_string varchar2(100);
begin
select partition into v_string from
(select rownum as n , partition from
(select regexp_substr(p_string, '[^'||p_separator||']+', 1, level) partition from dual
connect by regexp_substr(p_string, '[^'||p_separator||']+', 1, level) is not null)
) t1
where t1.n = p_pos;
return trim(v_string);
exception
when others then return 'nfound';
end;
Example call:
/*using the params*/
p_string := '$;James$;Paul$;'; --string to parse
p_separator := '$;'; --separator
p_pos := 1; --position to get
f_split_string(p_string,p_separator,p_pos) /*return 'James'*/
/*if p_pos :=2*/
f_split_string(p_string,p_separator,p_pos) /* return 'Paul' */
/*if p_pos :=3*/
f_split_string(p_string,p_separator,p_pos) /*return 'nfound' */
/*if p_pos :=0*/
f_split_string(p_string,p_separator,p_pos) /* return 'nfound' */

Oracle- Split string comma delimited (string contains spaces and consecutive commas)

I can't find a solution about how to split a comma-delimited string in ORACLE. Searched a lot, nothing works for my case
Code
DECLARE
TYPE T_ARRAY_OF_VARCHAR IS TABLE OF VARCHAR2(2000) INDEX BY BINARY_INTEGER;
MY_ARRAY T_ARRAY_OF_VARCHAR;
MY_STRING VARCHAR2(2000) := '12 3,456,,abc,def';
BEGIN
FOR CURRENT_ROW IN (
with test as
(select MY_STRING from dual)
select regexp_substr(MY_STRING, '[^,]+', 1, rownum) SPLIT
from test
connect by level <= length (regexp_replace(MY_STRING, '[^,]+')) + 1)
LOOP
DBMS_OUTPUT.PUT_LINE('>' || CURRENT_ROW.SPLIT || '<');
--DBMS_OUTPUT.PUT_LINE(CURRENT_ROW.SPLIT);
MY_ARRAY(MY_ARRAY.COUNT) := CURRENT_ROW.SPLIT;
END LOOP;
DBMS_OUTPUT.PUT_LINE('Array Size:' || MY_ARRAY.COUNT);
END;
/
The output is:
>12 3<
>456<
>abc<
>def<
><
Array Size:5
The empty value is out of order!!!!
Try this for the parsing the list part. It handles NULLS:
SQL> select regexp_substr('12 3,456,,abc,def', '(.*?)(,|$)', 1, level, null, 1) SPLIT, level
from dual
connect by level <= regexp_count('12 3,456,,abc,def',',') + 1
ORDER BY level;
SPLIT LEVEL
----------------- ----------
12 3 1
456 2
3
abc 4
def 5
SQL>
Unfortunately when you search for regex's for parsing lists, you will always find this form which does NOT handle nulls and should be avoided: '[^,]+'. See here for more info: Split comma separated values to columns in Oracle.
Try xmltable and flwor expresion.
The following example is not secure and throw error if you put string without comma. But is simpler to understand.
select xmlcast(column_value as varchar2(2000)) value_list
from xmltable('for $val in ora:tokenize($strList,",")
return $val'
passing '12 3,456,,abc,def' as "strList"
);
And secured version.
select xmlcast(column_value as varchar2(2000)) value_list
from xmltable('for $val at $index in ora:tokenize(concat(",",$strList),",")
where $index > 1
return $val' passing '12 3,456,,abc,def' as "strList"
);
Little modification to your query, assuming you can pick one char which will not be present in MY_STRING, e.g. pipe |
with test as
(select '12 3,456,,,,abc,def' MY_STRING from dual)
select trim('|' from regexp_substr(regexp_replace(MY_STRING,',,',',|,|'),'[^,]+',1,level)) SPLIT
from test
connect by level <= length (regexp_replace(MY_STRING, '[^,]+')) + 1;
Output:
SPLIT
-----------------------
12 3
456
(null)
(null)
(null)
abc
def
No need of PL/SQL, you could do it in plain SQL. See Split comma delimited strings in a table in Oracle.
Using MODEL clause:
WITH sample_data AS (
SELECT '12 3,456,,,,,abc,def' str FROM dual
)
-- end of sample_data mimicking real table
,
model_param AS (
SELECT str AS orig_str ,
','
|| str
|| ',' AS mod_str ,
1 AS start_pos ,
Length(str) AS end_pos ,
(LENGTH(str) -
LENGTH(REPLACE(str, ','))) + 1 AS element_count ,
0 AS element_no ,
ROWNUM AS rn
FROM sample_data )
SELECT trim(Substr(mod_str, start_pos, end_pos-start_pos)) str
FROM (
SELECT *
FROM model_param
MODEL PARTITION BY ( rn, orig_str, mod_str)
DIMENSION BY (element_no)
MEASURES (start_pos, end_pos, element_count)
RULES ITERATE (2000)
UNTIL (ITERATION_NUMBER+1 = element_count[0])
( start_pos[ITERATION_NUMBER+1] =
instr(cv(mod_str), ',', 1, cv(element_no)) + 1,
end_pos[ITERATION_NUMBER+1] =
instr(cv(mod_str), ',', 1, cv(element_no) + 1) )
)
WHERE element_no != 0
ORDER BY mod_str ,
element_no
/
Output
STR
----------------------
12 3
456
abc
def
8 rows selected.
If you want to do it in PL/SQL, then you could use a pipelined table function:
SQL> CREATE OR REPLACE TYPE test_type
2 AS
3 TABLE OF VARCHAR2(100)
4 /
Type created.
SQL> CREATE OR REPLACE FUNCTION comma_to_table(
2 p_list IN VARCHAR2)
3 RETURN test_type PIPELINED
4 AS
5 l_string LONG := p_list || ',';
6 l_comma_index PLS_INTEGER;
7 l_index PLS_INTEGER := 1;
8 BEGIN
9 LOOP
10 l_comma_index := INSTR(l_string, ',', l_index);
11 EXIT
12 WHEN l_comma_index = 0;
13 PIPE ROW ( TRIM(SUBSTR(l_string, l_index, l_comma_index - l_index)));
14 l_index := l_comma_index + 1;
15 END LOOP;
16 RETURN;
17 END comma_to_table;
18 /
Function created.
Let's see the output:
SQL> SELECT *
2 FROM TABLE(comma_to_table('12 3,456,,,,,abc,def'))
3 /
COLUMN_VALUE
------------------------------------------------------------------------------
12 3
456
abc
def
8 rows selected.
SQL>

using Oracle SQL - regexp_substr to split a record

I need to split the record for column CMD.NUM_MAI which may contain ',' or ';'.
I did this but it gave me an error:
SELECT REGEXP_SUBSTR (expression.num_mai,
'[^;|,]+',
1,
LEVEL)
FROM (SELECT CMD.num_cmd,
(SELECT COMM.com
FROM COMM
WHERE COMM.cod_soc = CMD.cod_soc AND COMM.cod_com = 'URL_DSD')
AS cod_url,
NVL (CONTACT.nom_cta, TIERS.nom_ct1) AS nom_cta,
NVL (CONTACT.num_mai, TIERS.num_mai) AS num_mai,
NVL (CONTACT.num_tel, TIERS.num_tel) AS num_tel,
TO_CHAR (SYSDATE, 'hh24:MI') AS heur_today
FROM CMD, TIERS, CONTACT
WHERE ( (CMD.cod_soc = :CMD_cod_soc)
AND (CMD.cod_eta = :CMD.cod_eta)
AND (CMD.typ_cmd = :CMD.typ_cmd)
AND (CMD.num_cmd = :CMD.num_cmd))
AND (TIERS.cod_soc(+) = CMD.cod_soc)
AND (TIERS.cod_trs(+) = CMD.cod_trs_tra)
AND (TIERS.cod_soc = CONTACT.cod_soc(+))
AND (TIERS.cod_trs = CONTACT.cod_trs(+))
AND (CONTACT.lib_cta(+) = 'EDITION')) experssion
CONNECT BY REGEXP_SUBSTR (expression.num_mai,'[^;|,]+',1,LEVEL)
Error 1:
The expression in CONNECT BY clause is unary. You have to specify both left and right hand side operands.
Try something like,
CONNECT BY REGEXP_SUBSTR (expression.num_mai,'[^;|,]+',1,LEVEL) IS NOT NULL
Error 2:
Your bind variable name is wrong. Ex: :CMD_cod_eta
Perhaps you wanted this way!
( (CMD.cod_soc = :CMD_cod_soc)
AND (CMD.cod_eta = :CMD_cod_eta)
AND (CMD.typ_cmd = :CMD_typ_cmd)
AND (CMD.num_cmd = :CMD_num_cmd))
This is a common question, I'd put into a function, then call it as needed:
CREATE OR REPLACE function fn_split(i_string in varchar2, i_delimiter in varchar2 default ',', b_dedup_tokens in number default 0)
return sys.dbms_debug_vc2coll
as
l_tab sys.dbms_debug_vc2coll;
begin
select regexp_substr(i_string,'[^' || i_delimiter || ']+', 1, level)
bulk collect into l_tab
from dual
connect by regexp_substr(i_string, '[^' || i_delimiter || ']+', 1, level) is not null
order by level;
if (b_dedup_tokens > 0) then
return l_tab multiset union distinct l_tab;
end if;
return l_tab;
end;
/
This will return a table of varchar2(1000), dbms_debug_vc2coll, which is a preloaded type owned by SYS (or you could create your own type using 4000 perhaps). Anyway, an example using it (with space, comma, or semi-colon used as delimiters):
with test_data as (
select 1 as id, 'A;test;test;string' as test_string from dual
union
select 2 as id, 'Another string' as test_string from dual
union
select 3 as id,'A,CSV,string' as test_string from dual
)
select d.*, column_value as token
from test_data d, table(fn_split(test_string, ' ,;', 0));
Output:
ID TEST_STRING TOKEN
1 A;test;test;string A
1 A;test;test;string test
1 A;test;test;string test
1 A;test;test;string string
2 Another string Another
2 Another string string
3 A,CSV,string A
3 A,CSV,string CSV
3 A,CSV,string string
You can pass 1 instead of 0 to fn_split to dedup the tokens (like the repeated "test" token above)