Parse varchar2 to table (Oracle) - tokenize

Is there built-in function in Oracle DB 11g r2 that could parse varchar2 variable to table? Opposite of listagg or wm_concat. I found only Tom Kyte's method dated 2006:
with data as
(
select trim(substr (txt, instr(txt, ',', 1, level) + 1
, instr(txt, ',', 1, level + 1) - instr(txt, ',', 1, level) - 1)) as token
from (select ',' || :txt || ',' txt from dual)
connect by level <= length(:txt) - length(replace(:txt, ',', '')) + 1
)
select * from data;
I think Oracle must have simpler way.

No.
I would simplify Tom's method slightly, but not by much; you can now use regular expressions as well:
select regexp_substr(:txt, '[^,]+', 1, level)
from dual
connect by regexp_substr(:txt, '[^,]+', 1, level) is not null
SQL Fiddle

Ben's regexp_substr solution is generally the preferred solution. If your string happens to consist of strings that are valid Oracle identifiers-- they are less than or equal to 30 characters and begin with an alphabetic character, you can also use the dbms_utility.comma_to_table function. Given those limitations, however, it's generally better to use the general-purpose solution.
SQL> ed
Wrote file afiedt.buf
1 declare
2 l_string varchar2(1000) := 'foo,bar,b123,FuzzyBunny,abcdefghij12345678901234567890';
3 l_num pls_integer;
4 l_arr dbms_utility.uncl_array;
5 begin
6 dbms_utility.comma_to_table( l_string, l_num, l_arr );
7 for i in 1..l_arr.count
8 loop
9 dbms_output.put_line( l_arr(i) );
10 end loop;
11* end;
SQL> /
foo
bar
b123
FuzzyBunny
abcdefghij12345678901234567890
PL/SQL procedure successfully completed.

Related

optimizing insert query

I am trying to insert comma separated values in a global table. When data is large it's taking long time to process the data. I need to optimize my insert query, is there any other ways to achieve below insert statement for better optimization? Please check code below for more info. Appreciated for any help.
//my proc
emp_id in CLOB;
//insert statement
insert into Global_Emp_Tbl
with inputs(str) as(
select to_clob(emp_id)
from dual
),
temp_table(s, n, empid, st_pos, end_pos) as (
select ',' || str || ',', -1, null, null, 1
from inputs
union all
selct s, n+1, substr(s, st_pos, end_pos - st_pos),
end_pos + 1, instr(s, ',', 1, n+3)
from temp_table
where end_pos != 0
)
select empid from temp_table where empid is not null;
commit;
//using insert table in where clause
exists( select 1 from Global_Emp_Tbl gt where e.id =gt.emp_id ) //joining with main table
You can use simple REGEXP_SUBSTR to achieve the same
insert into Global_Emp_Tbl
SELECT Regexp_substr(empid, '[^,]+', 1, LEVEL) AS empid
FROM (SELECT To_clob(emp_id) empid
FROM dual)
CONNECT BY LEVEL <= Regexp_count(emp_id, '[^,]+');
commit;
Also, one more suggestion try changing the below in your function
select empid from temp_table where n > 0;
Well I can't reporduce your problem in Oracle Database 11g Express Edition Release 11.2.0.2.0 - 64bit Production
This procedure provides the sample data, a CLOB csv list of the first N interger
create or replace function get_list(N NUMBER) return CLOB
as
v_lst CLOB;
i PLS_INTEGER;
BEGIN
v_lst :='1';
for i in 2 .. n loop
v_lst :=v_lst||','||to_char(i);
end loop;
return(v_lst);
END;
/
Note that the 5K parameter give approximately 20K long list.
select length(get_list(5000)) from dual;
23892
The parsing of this list in a global temporary table is done in seconds not in minutes. Here an example using your SELECT
SQL> set timi on;
SQL> create global temporary table csv_tbl
2 ON COMMIT PRESERVE ROWS
3 as
4 with inputs(str) as(
5 select get_list(5000)
6 from dual
7 ),
8 temp_table(s, n, empid, st_pos, end_pos) as (
9 select ',' || str || ',', -1, null, null, 1
10 from inputs
11 union all
12 select s, n+1, substr(s, st_pos, end_pos - st_pos),
13 end_pos + 1, instr(s, ',', 1, n+3)
14 from temp_table
15 where end_pos != 0
16 )
17 select empid from temp_table where empid is not null
18 ;
Table created.
Elapsed: 00:00:01.35
So the most probably explanation is, that the most elapsed time is spend in the query with the EXISTS clause
exists( select 1 from Global_Emp_Tbl gt where e.id =gt.emp_id )
Two problems come to my mind
1) The datatypes of the EMP_ID in the temporary table and in the EMP table differs, e.g. in temporary table it is VARCHAR2, in the EMP table NUMBER - this will prohibit the use of the index on the EMP table.
2) The missing object statistics on the global temporary table leads the CBO to an wrong execution plan and you use the index (in a large nested loops) where you should use a full table scan.

I can't able to print the following pyramid in oracle. (PL/SQL Block)

I want to print the following pyramid:-
ORACLE
ORACL
ORAC
ORA
OR
O
Somewhat shorter code than previous answers:
SQL> select substr('ORACLE', 1, level) val
2 from dual
3 connect by level <= length('ORACLE')
4 order by level desc;
VAL
------------------------
ORACLE
ORACL
ORAC
ORA
OR
O
6 rows selected.
SQL>
you can achieve this using a With expression and a LEVEL connector
with str as (select 'ORACLE' as str
from dual
)
select substr(s.str, 1, length(s.str)+1 - level) as Pyramid
from str s
connect by level-(length(s.str)-1) <= (length(s.str)+1) - level
;
also if you needed as PL/SQL block, then solution is:
set serveroutput on
declare v_pyramid varchar2(100);
begin
with str as (select 'ORACLE' as str from dual)
select listagg(t.Pyramid, chr(13)) within group (order by t.lvl)
into v_pyramid
from ( select substr(s.str, 1, length(s.str)+1 - level) as Pyramid
, level as lvl
from str s
connect by level-(length(s.str)-1) <= (length(s.str)+1) - level
) t ;
dbms_output.put_line(v_pyramid);
end;
good luck :)
Here's an option if you just want plain PL/SQL.
declare
v_str varchar2(6) := 'ORACLE';
begin
for i in 0 .. length(v_str) loop
dbms_output.put_line(substr(v_str, 1, length(v_str)-i));
end loop;
end;
/
Here is Simple code:-
DECLARE
VAL VARCHAR2(200):='ORACLE';
OT VARCHAR2(200);
BEGIN
FOR I IN REVERSE 1..LENGTH(VAL) LOOP
OT:=SUBSTR(VAL,1,I);
DBMS_OUTPUT.PUT_LINE(OT);
END LOOP;
END;

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>

Oracle: using IN clause with text field? [duplicate]

This question already has answers here:
Closed 10 years ago.
Possible Duplicate:
How to best split csv strings in oracle 9i
I have some legacy data where there's a VARCHAR2(100) field SUBID that has comma-delimited data:
empno subid
1 1, 3, 2
2 18,19, 3, 6, 9
etc.
I need to write the equivalent of
select *
from table
where id in ( select SUBID from subidtable where empno = 1 )
Is there a way to accomplish this in Oracle?
Edit:
Added some clarification. I need to do the IN clause against the values stored in a string from a single row, not all rows.
You can, but it's a little ugly. Depending on the Oracle version
You can use a variant of this askTom thread to parse the data into a collection and use the collection in your SQL statement. This should work in any version of Oracle since 8.1.5 but the syntax has gotten a bit simpler over the years.
SQL> create or replace type myTableType as table
2 of varchar2 (255);
3 /
Type created.
SQL> ed
Wrote file afiedt.buf
1 create or replace
2 function in_list( p_string in varchar2 ) return myTableType
3 as
4 l_string long default p_string || ',';
5 l_data myTableType := myTableType();
6 n number;
7 begin
8 loop
9 exit when l_string is null;
10 n := instr( l_string, ',' );
11 l_data.extend;
12 l_data(l_data.count) :=
13 ltrim( rtrim( substr( l_string, 1, n-1 ) ) );
14 l_string := substr( l_string, n+1 );
15 end loop;
16 return l_data;
17* end;
SQL> /
Function created.
SQL> select ename
2 from emp
3 where empno in (select column_value
4 from table( in_list( '7934, 7698, 7521' )));
ENAME
----------
WARD
BLAKE
MILLER
You can also use regular expressions as discussed in this StackOverflow thread
SQL> ed
Wrote file afiedt.buf
1 select ename
2 from emp
3 where empno in (select regexp_substr(str, '[^,]+',1,level)
4 from (select '7934, 7698, 7521' str from dual)
5* connect by level <= regexp_count(str,'[^,]+'))
SQL> /
ENAME
----------
WARD
MILLER
BLAKE
If you can 'fix' your table structure to have a 1:many relationship, such that each row in your subidtable contains only one id, that's your best bet.
If you can't, then you could get hold of one of the many split() functions that people have coded around the web. These take a string and return the data as a set. The problem here is that they are designed to take a single string and return a table of values, not to take a table of strings...
As this data seems to be in a bit of a hacked format, you may only need a one-time hack solution with minimal code. In such cases you can try this...
SELECT
*
FROM
table
WHERE
EXISTS (SELECT * FROM subidtable WHERE (',' || subid || ',') LIKE ('%,' || table.id || ',%'))
But be warned, it scales VERY badly. So expect slow performance if you have a large amount of data in either table.
EDIT
As your edit now shows that you're only ever processing one string from the subidtable table, the split function option becomes a lot easier to implement. See Justin's answer :)
A modification to the 'simple hack' above would be...
SELECT
*
FROM
table
WHERE
(SELECT ',' || subid || ',' FROM subidtable WHERE empno=1) LIKE ('%,' || table.id || ',%')
This is in no way elegant and an abuse of "execute immediate" but in your specific case this might work:
DECLARE
i INTEGER;
subid VARCHAR2(100) := '18,19, 3, 6, 9';
BEGIN
EXECUTE IMMEDIATE 'select 1 from dual where 6 in (' || subid || ')'
INTO i;
dbms_output.put_line('returned: ' || i);
EXCEPTION
WHEN others THEN
dbms_output.put_line('Exception: ' || SQLERRM);
END;