oracle sql split string to rows - sql

I have string 'ABC' I need to split into rows as below
A
B
C
.I know how do do when delimiter is present. How about when delimiter is not present
with test as
(select 'A,B,C' col1 from dual)
select regexp_substr(col1, '[^,]+', 1, rownum) result1
from test
connect by level <= length(regexp_replace(col1, '[^,]+')) + 1;

Without a delimiter it should be even easier - use the same approach, but just use substr with level as the index of the string:
with test as
(select 'ABC' col1 from dual)
select substr(col1, level, 1) result1
from test
connect by level <= length(col1);

You can use a function like this
-- define type
CREATE OR REPLACE TYPE TABLE_OF_STRING AS TABLE OF VARCHAR2(32767);
-- function
function SPLIT_STRING_TO_STRINGS
(
p_list varchar2,
p_delimiter varchar2 := ','
) return TABLE_OF_STRING pipelined
is
l_idx pls_integer;
l_list varchar2(32767) := p_list;
begin
loop
l_idx := instr(l_list, p_delimiter);
if l_idx > 0 then
pipe row(substr(l_list, 1, l_idx-1));
l_list := substr(l_list, l_idx + length(p_delimiter));
else
pipe row(l_list);
exit;
end if;
end loop;
return;
end SPLIT_STRING_TO_STRINGS;
-- usage example
select * from table(SPLIT_STRING_TO_STRINGS('A,B,C',','))

Related

select in a loop in oracle sql?

My stored procedure is used for getting string values of ids separate by ',' from a string contains names in table AUTH_GROUPS.
for example:
id name
1 role_1
2 role_2
3 role_3
the input is: 'role_1,role_2,role_3'
the output is: '1,2,3'
CREATE OR REPLACE PROCEDURE PROCEDURE1(P_ERROR_MSG OUT VARCHAR2, P_ROLE_STRING IN VARCHAR2 ) AS
BEGIN
DECLARE
lvOutPut VARCHAR2(2000);
vId varchar2(1000);
BEGIN
lvOutPut := '';
FOR i IN
(SELECT trim(regexp_substr(P_ROLE_STRING, '[^,]+', 1, LEVEL)) l
FROM dual
CONNECT BY LEVEL <= regexp_count(P_ROLE_STRING, ',')+1
)
LOOP
select id into vId from AUTH_GROUPS where NAME = i; --this line got error 'expression is of wrong type'
lvOutPut := lvOutPut || vId || ',';
END LOOP;
P_ERROR_MSG := lvOutPut;
P_ERROR_MSG := substr(P_ERROR_MSG, 1, LENGTH(P_ERROR_MSG) - 1);
END;
END PROCEDURE1;
But it has an error in the line that I commented. I tried i.1 or i.value but still got errors.
You need to use the actual column name. i is loop handle name.
....
BEGIN
lvOutPut := '';
FOR i IN
(SELECT trim(regexp_substr(P_ROLE_STRING, '[^,]+', 1, LEVEL)) l -- this is column name to be used inside the loop
FROM dual
CONNECT BY LEVEL <= regexp_count(P_ROLE_STRING, ',')+1
)
LOOP
select id into vId from AUTH_GROUPS where NAME = i.l; -- change here
...
/* Formatted on 6/23/2020 2:08:34 PM (QP5 v5.354) */
CREATE OR REPLACE PROCEDURE PROCEDURE1 (P_ERROR_MSG OUT VARCHAR2,
P_ROLE_STRING IN VARCHAR2)
AS
BEGIN
DECLARE
lvOutPut VARCHAR2 (2000);
vId VARCHAR2 (1000);
BEGIN
lvOutPut := '';
FOR i IN ( SELECT TRIM (REGEXP_SUBSTR (P_ROLE_STRING,
'[^,]+',
1,
LEVEL)) l
FROM DUAL
CONNECT BY LEVEL <= REGEXP_COUNT (P_ROLE_STRING, ',') + 1)
LOOP
SELECT id
INTO vId
FROM AUTH_GROUPS
WHERE NAME = i.l; --this line got error 'expression is of wrong type'
lvOutPut := lvOutPut || vId || ',';
END LOOP;
P_ERROR_MSG := lvOutPut;
P_ERROR_MSG := SUBSTR (P_ERROR_MSG, 1, LENGTH (P_ERROR_MSG) - 1);
END;
END PROCEDURE1;

Converting String concatenated with ',' to be used in subquery at oracle SQL

I want to use my result of function e.g. 'S500,S600,S700,S800' in a subquery in another script like:
where dept_no in (my result of function)
So I want to convert my string result to be like this ('S500','S600','S700','S800').
I tried to do this with dynamic SQL but I can't get it to work.
Hope below snipet suffice your requirement.
Approach 1 -> More effective
--Create a table type of VARCHAR
CREATE OR REPLACE type string_table
IS
TABLE OF VARCHAR2(100);
--Function to return tabl type
CREATE OR REPLACE
FUNCTION string_manipulate
RETURN string_table
AS
str_tab string_table;
BEGIN
SELECT 's00'||level bulk collect INTO str_tab FROM dual CONNECT BY level < 10;
RETURN str_tab;
end;
--Use function in the query
SELECT distinct 1
FROM
(SELECT 's001' dn FROM dual
UNION ALL
SELECT 's002' dn FROM dual
UNION ALL
SELECT 's003' dn FROM dual
UNION ALL
SELECT 's004' dn FROM dual
UNION ALL
SELECT 's005' dn FROM dual
UNION ALL
SELECT 's006' dn FROM dual
UNION ALL
SELECT 's007' dn FROM dual
UNION ALL
SELECT 's008' dn FROM dual
UNION ALL
SELECT 's009' dn FROM dual
)a
WHERE a.dn IN
(SELECT * FROM TABLE(string_manipulate)
);
--Approach 2
--Function to get output as mentioned.
CREATE OR REPLACE
FUNCTION string_manipulate
RETURN VARCHAR2
AS
BEGIN
RETURN 'S2009,S2020,S2021';
END;
-- Use function value in a query
SELECT 1
FROM dual
WHERE '''S2009'',''S2020'',''S2021''' = (''''
||REPLACE(string_manipulate,',',''',''')
||'''');
You need an iterator and text splitting by comma sign:
select empno,ename,sal,deptno
from emp
where empno in (
select to_number(
rtrim(
substr(emps,
instr(emps,',',1,iter.pos)+1,
instr(emps,',',1,iter.pos+1) -
instr(emps,',',1,iter.pos)),',')) emps
from (select ','||'7654,7698,7782,7788'||',' emps from t1) csv,
(select rownum pos from emp) iter
where iter.pos <= ((length(csv.emps) -
length(replace(csv.emps,',')))/length(','))-1
)
But better rewrite your function to return cursor.
you can use collection:
SELECT *
FROM YOUR_TABLE
WHERE DEPT_NO IN (SELECT *
FROM TABLE (SPLIT ('S500,S600,S700,S800')))--splits text with comma, for other chars use split(text, split_char)
With usage of MEMBER OF
SELECT *
FROM YOUR_TABLE
WHERE DEPT_NO MEMBER OF SPLIT ('S500,S600,S700,S800')--splits text with comma, for other chars use split(text, split_char)
the split fuction is:
CREATE OR REPLACE TYPE SPLIT_TBL AS TABLE OF VARCHAR2 (32767);
CREATE OR REPLACE FUNCTION SPLIT (P_LIST VARCHAR2, P_DEL VARCHAR2 := ',')
RETURN SPLIT_TBL
PIPELINED
IS
L_IDX PLS_INTEGER;
L_LIST VARCHAR2 (32767) := P_LIST;
BEGIN
LOOP
L_IDX := INSTR (L_LIST, P_DEL);
IF L_IDX > 0
THEN
PIPE ROW (SUBSTR (L_LIST, 1, L_IDX - 1));
L_LIST := SUBSTR (L_LIST, L_IDX + LENGTH (P_DEL));
ELSE
PIPE ROW (L_LIST);
EXIT;
END IF;
END LOOP;
RETURN;
END SPLIT;
FUNCTION GET_TS_EACH_DAY_DEPARTMENT (P_SER_NO VARCHAR2,
P_TS_DATE DATE
)
RETURN STRING_TABLE
IS
V_DEPT_NO VARCHAR2 (4000);
V_DEPT VARCHAR2(4000);
V_TABLE STRING_TABLE:=STRING_TABLE();
J NUMBER:=1;
BEGIN
for i in (select distinct ts_day dayy from WEB_TS_USER_LOCATIONS_V ) loop
V_TABLE.EXTEND;
V_TABLE(J):= WEB_TS_PKG.GET_TS_DAY_DEPARTMENT (P_SER_NO ,P_TS_DATE , i.dayy );
J:=J+1;
end loop;
RETURN V_TABLE;
END GET_TS_EACH_DAY_DEPARTMENT;

Extracting values from a pipe separated string

I have a string which looks like --
12361_BBMS_GTECHL|12362_BBMS_PRIM|12363_BBMS_SEC|....and so on
So i need to fetch
12361 and BBMS_GTECHL
12362 and BBMS_PRIM
12363 and BBMS_SEC
i used --
select *
FROM
TABLE(XMLSEQUENCE(
EXTRACT(
XMLTYPE('<rowset><row><Code>'||
replace(replace('12361=BBMS_GTECHL|12362=BBMS_PRIM','|','</Value></row><row><Code>'),'=','</Code><Value>')||'</Value>'||'</row></rowset>'),'/rowset/row')));
declare
l_val varchar2(1000);
begin
select substr('12361_BBMS_GTECHL|12362_BBMS_PRIM', instr('|')+1) into l_val from dual;
dbms_output.put_line(l_val);
end;
But getting problem in getting desired result !
I have need to write this logic in a package that i will do if i got some hint here.
My DB version is --
Oracle Database 12c Enterprise Edition Release 12.1.0.2.0 - 64bit Production
Here is a solution using a recursive factored subquery ("recursive CTE"). Note the use of pointers to the location of pipe symbols and the first underscore after each pipe (disregarding the other underscores). Also, the solution uses only standard INSTR and SUBSTR, avoiding the use of regular expressions (which perform somewhat slower - important if you process lots of data).
with input_data (input_str) as (
select '12361_BBMS_GTECHL|12362_BBMS_PRIM|12363_BBMS_SEC' from dual
),
t (str) as (
select '|' || input_str || '|' from input_data
),
r (lvl, code, descr, str, p1_from, p2_from, p1_to, p2_to) as (
select 0, null, null, str, 1, 1, instr(str, '_', 1, 1), instr(str, '|', 1, 2)
from t
union all
select lvl+1, substr(str, p2_from + 1, p1_to - p2_from - 1),
substr(str, p1_to + 1, p2_to - p1_to - 1),
str, p1_to, p2_to, instr(str, '_', p2_to + 1, 1),
instr(str, '|', p2_to + 1, 1)
from r
where p1_to != 0
)
select code, descr
from r
where lvl != 0;
Output:
CODE DESCR
------- --------------------
12361 BBMS_GTECHL
12362 BBMS_PRIM
12363 BBMS_SEC
If I were you and my primary consideration was performance i would use Table Functions. mathguys solution works perfectly but it will be more performant if we use a pipelined function.
First we create types which are necessary for our function.
drop type type_test_table;
drop type type_test_row;
CREATE TYPE type_test_row AS OBJECT (
code varchar2(2000),
descr VARCHAR2(50)
)
/
CREATE TYPE type_test_table IS TABLE OF type_test_row
/
Then we create our function :
create or replace function test_pipe_func return type_test_table pipelined as
cursor c_data_in is
select '12361'||level||'_BBMS_GTECHL'||level||'|12362'||level||'_BBMS_PRIM'||level||'|12363'||level||'_BBMS_SEC'||level||'|12364'||level||'_BBU_SEC'||level as str from dual
connect by level <= 1000000;
v_element varchar2(300);
v_code varchar2(100);
v_descr varchar2(200);
p_deb number;
p_fin number;
begin
for l_data_in in c_data_in loop
p_deb := 0;
p_fin := 1;
while p_fin > 0 loop
p_fin := case when p_deb = 0 then instr(l_data_in.str,'|',1, 1) else instr(l_data_in.str,'|',p_deb-1, 2) end;
p_deb := case when p_deb = 0 then 1 else instr(l_data_in.str,'|',p_deb-1, 1)+1 end;
v_element := case when p_fin = 0 then substr(l_data_in.str, p_deb) else substr(l_data_in.str, p_deb, p_fin - p_deb) end;
p_deb := p_fin +1;
v_code := substr(v_element, 1 , instr(v_element, '_' , 1,1)-1);
v_descr := substr(v_element, instr(v_element, '_' , 1,1)+1);
pipe row(type_test_row(v_code, v_descr));
end loop;
end loop;
end test_pipe_func;
/
I changed the test case a little bit to be able to generate as many lines as necessary for my tests. And i used a pipelined function to limit usage of Process memory in case of big datasets and to be able to use it with a select. If your use case is different(i don't know maybe to insert into a table using the input) another option can be to use bulk collect into and forall.
create or replace procedure test_bulk_collect_proc as
cursor c_data_in is
select '12361'||level||'_BBMS_GTECHL'||level||'|12362'||level||'_BBMS_PRIM'||level||'|12363'||level||'_BBMS_SEC'||level as str from dual
connect by level <= 1000000;
type type_table_data_in is table of c_data_in%rowtype;
table_data_in type_table_data_in;
v_element varchar2(300);
v_code varchar2(100);
v_descr varchar2(200);
p_deb number;
p_fin number;
v_str varchar2(4000);
v_t_insr type_test_table;
limit_in number := 100000;
i number;
begin
OPEN c_data_in;
LOOP
FETCH c_data_in BULK COLLECT INTO table_data_in LIMIT limit_in;
v_t_insr := type_test_table();
i := 1;
for indx IN 1 .. table_data_in.COUNT LOOP
v_str := table_data_in(indx).str;
p_deb := 0;
p_fin := 1;
while p_fin > 0 loop
p_fin := case when p_deb = 0 then instr(v_str,'|',1, 1) else instr(v_str,'|',p_deb-1, 2) end;
p_deb := case when p_deb = 0 then 1 else instr(v_str,'|',p_deb-1, 1)+1 end;
v_element := case when p_fin = 0 then substr(v_str, p_deb) else substr(v_str, p_deb, p_fin - p_deb) end;
p_deb := p_fin +1;
v_code := substr(v_element, 1 , instr(v_element, '_' , 1,1)-1);
v_descr := substr(v_element, instr(v_element, '_' , 1,1)+1);
v_t_insr.extend;
v_t_insr(i) := type_test_row(v_code, v_descr);
i:= i+1;
end loop;
END LOOP;
forall t in v_t_insr.first..v_t_insr.last
insert into test_bbu(CODE, DESCR) values (v_t_insr(t).code, v_t_insr(t).descr);
EXIT WHEN table_data_in.COUNT < limit_in;
END LOOP;
End;
/
I tested all three methods on my database. To test the sql of mathguy and the pipelined function i used CTAS and for the bulk collect into i simply executed the procedure.
create table test_bbu as
with input_data (input_str) as (
select '12361'||level||'_BBMS_GTECHL'||level||'|12362'||level||'_BBMS_PRIM'||level||'|12363'||level||'_BBMS_SEC'||level from dual
connect by level <= 1000000
),
t (str) as (
select '|' || input_str || '|' from input_data
),
r (lvl, code, descr, str, p1_from, p2_from, p1_to, p2_to) as (
select 0, null, null, str, 1, 1, instr(str, '_', 1, 1), instr(str, '|', 1, 2)
from t
union all
select lvl+1, substr(str, p2_from + 1, p1_to - p2_from - 1),
substr(str, p1_to + 1, p2_to - p1_to - 1),
str, p1_to, p2_to, instr(str, '_', p2_to + 1, 1),
instr(str, '|', p2_to + 1, 1)
from r
where p1_to != 0
)
select code, descr
from r
where lvl != 0;
create table test_bbu2 as
select * from table(test_pipe_func);
execute test_bulk_collect_proc;
I tested three methods with 500K and 1M lines. Here are my results but i urge you to test on your environnement before you make your decision.
500K 1M
----------------------------------------
SQL 36s 1m:15s
Pipelined 11s 23s
Bulk Collect 8s 17s

Comparing two comma delimited strings

I have 2 strings str1: 'abc,def,ghi' and str2: 'tyu,abc,fgh'.
I want to compare these two strings using the delimiter ,. Now since the 2 strings have abc it should return true. I want a function in Oracle SQL which can perform this operation.
Looks complicated but it is just a couple of helper functions to split a list into separate values (to be contained into a table type) and then a very simple function to test the intersection of two collections.
Oracle Setup:
CREATE TYPE VARCHAR2s_Table AS TABLE OF VARCHAR2(4000);
CREATE FUNCTION regexp_escape(
expression VARCHAR2
) RETURN VARCHAR2 DETERMINISTIC
AS
BEGIN
RETURN REGEXP_REPLACE( expression, '([$^[()+*?{\|])', '\\\1', 1, 0, 'c' );
END;
/
CREATE FUNCTION splitList(
list VARCHAR2,
delim VARCHAR2 := ','
) RETURN VARCHAR2s_Table DETERMINISTIC
AS
pattern VARCHAR2(256);
len BINARY_INTEGER;
t_items VARCHAR2s_Table := VARCHAR2s_Table();
BEGIN
IF list IS NULL THEN
NULL;
ELSIF delim IS NULL THEN
t_items.EXTEND( LENGTH( list ) );
FOR i IN 1 .. LENGTH( list ) LOOP
t_items(i) := SUBSTR( list, i, 1 );
END LOOP;
ELSE
pattern := '(.*?)($|' || REGEXP_ESCAPE( delim ) || ')';
len := REGEXP_COUNT( list, pattern ) - 1;
t_items.EXTEND( len );
IF len = 1 THEN
t_items(1) := list;
ELSE
FOR i IN 1 .. len LOOP
t_items(i) := REGEXP_SUBSTR( list, pattern, 1, i, NULL, 1 );
END LOOP;
END IF;
END IF;
RETURN t_items;
END;
/
CREATE FUNCTION check_list_intersect(
list1 VARCHAR2,
list2 VARCHAR2
) RETURN NUMBER DETERMINISTIC
AS
BEGIN
IF splitList( list1 ) MULTISET INTERSECT splitList( list2 ) IS EMPTY THEN
RETURN 0;
ELSE
RETURN 1;
END IF;
END;
/
Query 1:
SELECT check_list_intersect( 'abc,def,ghi', 'abc' ) AS matches
FROM DUAL;
Results:
MATCHES
---------
1
Query 2:
SELECT check_list_intersect( 'abc,def,ghi', 'abcd' ) AS matches
FROM DUAL;
Results:
MATCHES
---------
0
The below will make the trick.
with temp as (
select 1 strid, 'abc,def,ghi' Error from dual
union all
select 2, 'tyu,abc,fgh' from dual
)
select str
from (
SELECT strid, trim(regexp_substr(str, '[^,]+', 1, level)) str
FROM (SELECT strid, Error str FROM temp) t
CONNECT BY instr(str, ',', 1, level - 1) > 0
)
group by str
having count(distinct strid) > 1;
Warning: This answer is not fully correct. (See the comments.)
Starting from dcieslak answer(csv split with regexp), a variation of the subject would be:
create or replace function check_string_intersec(str1 varchar2, str2 varchar2) return number
is
begin
for k in (SELECT trim(regexp_substr(str1, '[^,]+', 1, level)) item
FROM dual
CONNECT BY instr(str1, ',', 1, level - 1) > 0
)
loop
if instr(str2, k.item,1) > 0 then return 1; end if;
end loop;
return 0;
end;
This splits first string and search for every item in the second string.

Wants numeric data from varchar2 datatype column

I have a table Product with a varchar2 datatype of column name Value, in this column values are stored as
All,10:23,0.84522,1.245,10:54:68,
All,1:22:00,0.245,45:12:00
etc.
We have to extract all the floating values like (0.84522,1.245,0.245) and ones that ends with ":00" like (1:22:00,45:12:00).
I have following query, but it doesn't seems to work; it gives me all the values except characters.
select * from Product where Values BETWEEN to_char (0) and to_char (2);
I think this would work
select *
FROM Product
WHERE
(Value LIKE '%:00' AND Value<> 'ALL') AND (Value BETWEEN to_NUMBER (0) and to_NUMBER (2))
Try this query:
select *
from (select distinct regexp_substr(t.value, '[^,]+', 1, level) phrase
from Product t
connect by regexp_substr(t.value, '[^,]+', 1, level) is not null) ph
where regexp_like(ph.phrase, '(\d+\.\d+)|(.+:00)')
The regular expression in the where clause may need some tunning
What it does is-
seperates all phrases (the inner query)
selects only those that matches your criteria
UPDATE
If you suffer from performance you can try a different approach:
create or replace type phrase_typ is object
(
phrase varchar2(100)
)
;
/
create or replace type phrase_tab as table of phrase_typ;
/
create or replace function split_string(del in varchar2) return phrase_tab
pipelined is
phrase varchar2(1000);
str_t varchar2(1000);
v_del_i number;
cursor c is with t as
select value from product;
begin
for r in c loop
str_t := r.value;
while str_t is not null loop
v_del_i := instr(str_t, del, 1, 1);
if v_del_i = 0 then
phrase := str_t;
str_t := '';
else
phrase := substr(str_t, 1, v_del_i - 1);
str_t := substr(str_t, v_del_i + 1);
end if;
if regexp_like(phrase, '(\d+\.\d+)|(.+:00)') then
pipe row(phrase_typ(phrase));
end if;
end loop;
end loop;
return;
end split_string;
/
Now your query should look like this:
select * from table(split_string(','))