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)
Related
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
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
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' */
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;
I wrote a query that works like a charm in SQL Server. Unfortunately it needs to be run on an Oracle db. I have been searching the web inside out for a solution on how to convert it, without any success :/
The query looks like this i SQL:
UPDATE tab1 SET budgpost_gr1=
CASE WHEN (budgpost in (1001,1012,50055)) THEN 'BP_GR_A'
WHEN (budgpost in (5,10,98,0)) THEN 'BP_GR_B'
WHEN (budgpost in (11,876,7976,67465))
ELSE 'Missing' END`
My problem is also that the columns budgetpost_gr1 and budgetpost is alphanumeric and Oracle seems to want to see the list as numbers. The list are variables/parameters that is pre-defined as comma separated lists, which is just dumped into the query.
You said that budgetpost is alphanumeric. That means it is looking for comparisons against strings. You should try enclosing your parameters in single quotes (and you are missing the final THEN in the Case expression).
UPDATE tab1
SET budgpost_gr1= CASE
WHEN (budgpost in ('1001','1012','50055')) THEN 'BP_GR_A'
WHEN (budgpost in ('5','10','98','0')) THEN 'BP_GR_B'
WHEN (budgpost in ('11','876','7976','67465')) THEN 'What?'
ELSE 'Missing'
END
Use to_number to convert budgpost to a number:
when to_number(budgpost,99999) in (1001,1012,50055) THEN 'BP_GR_A'
EDIT: Make sure there are enough 9's in to_number to match to largest budget post.
If there are non-numeric budget posts, you could filter them out with a where clause at then end of the query:
where regexp_like(budgpost, '^-?[[:digit:],.]+$')
Got a solution that runs. Don't know if it is optimal though. What I do is to split the string according to http://blogs.oracle.com/aramamoo/2010/05/how_to_split_comma_separated_string_and_pass_to_in_clause_of_select_statement.html
Using:
select regexp_substr(' 1, 2 , 3 ','[^,]+', 1, level) from dual
connect by regexp_substr('1 , 2 , 3 ', '[^,]+', 1, level) is not null;
So my final code looks like this ($bp_gr1' are strings like 1,2,3):
UPDATE TAB1
SET BUDGPOST_GR1 =
CASE
WHEN ( BUDGPOST IN (SELECT REGEXP_SUBSTR ( '$BP_GR1',
'[^,]+',
1,
LEVEL )
FROM DUAL
CONNECT BY REGEXP_SUBSTR ( '$BP_GR1',
'[^,]+',
1,
LEVEL )
IS NOT NULL) )
THEN
'BP_GR1'
WHEN ( BUDGPOST IN (SELECT REGEXP_SUBSTR ( ' $BP_GR2',
'[^,]+',
1,
LEVEL )
FROM DUAL
CONNECT BY REGEXP_SUBSTR ( '$BP_GR2',
'[^,]+',
1,
LEVEL )
IS NOT NULL) )
THEN
'BP_GR2'
WHEN ( BUDGPOST IN (SELECT REGEXP_SUBSTR ( ' $BP_GR3',
'[^,]+',
1,
LEVEL )
FROM DUAL
CONNECT BY REGEXP_SUBSTR ( '$BP_GR3',
'[^,]+',
1,
LEVEL )
IS NOT NULL) )
THEN
'BP_GR3'
WHEN ( BUDGPOST IN (SELECT REGEXP_SUBSTR ( '$BP_GR4',
'[^,]+',
1,
LEVEL )
FROM DUAL
CONNECT BY REGEXP_SUBSTR ( '$BP_GR4',
'[^,]+',
1,
LEVEL )
IS NOT NULL) )
THEN
'BP_GR4'
ELSE
'SAKNAR BUDGETGRUPP'
END;
Is there a way to make it run faster?
"The list are variables/paramaters that is pre-defined as comma separated lists".
Do you mean that your query is actually
UPDATE tab1 SET budgpost_gr1=
CASE WHEN (budgpost in ('1001,1012,50055')) THEN 'BP_GR_A'
WHEN (budgpost in ('5,10,98,0')) THEN 'BP_GR_B'
WHEN (budgpost in ('11,876,7976,67465'))
ELSE 'Missing' END`
If so, you need a function to take a string and parse it into a list of numbers.
create type tab_num is table of number;
create or replace function f_str_to_nums (i_str in varchar2) return tab_num is
v_tab_num tab_num := tab_num();
v_start number := 1;
v_end number;
v_delim VARCHAR2(1) := ',';
v_cnt number(1) := 1;
begin
v_end := instr(i_str||v_delim,v_delim,1, v_start);
WHILE v_end > 0 LOOP
v_cnt := v_cnt + 1;
v_tab_num.extend;
v_tab_num(v_tab_num.count) :=
substr(i_str,v_start,v_end-v_start);
v_start := v_end + 1;
v_end := instr(i_str||v_delim,v_delim,v_start);
END LOOP;
RETURN v_tab_num;
end;
/
Then you can use the function like so:
select column_id,
case when column_id in
(select column_value from table(f_str_to_nums('1,2,3,4'))) then 'red'
else 'blue' end
from user_tab_columns
where table_name = 'EMP'
There is another workaround you can use to update using a join. This example below assumes you want to de-normalize a table by including a lookup value (in this case storing a users name in the table). The update includes a join to find the name and the output is evaluated in a CASE statement that supports the name being found or not found. The key to making this work is ensuring all the columns coming out of the join have unique names. In the sample code, notice how b.user_name conflicts with the a.user_name column and must be aliased with the unique name "user_user_name".
UPDATE
(
SELECT a.user_id, a.user_name, b.user_name as user_user_name
FROM some_table a
LEFT OUTER JOIN user_table b ON a.user_id = b.user_id
WHERE a.user_id IS NOT NULL
)
SET user_name = CASE
WHEN user_user_name IS NOT NULL THEN user_user_name
ELSE 'UNKNOWN'
END;