Oracle Regex Connect By - sql

I am trying to produce multiple rows after performing a regex on a column splitting all values in square brackets. I'm only able to return a single value though, currently.
The field I am performing the regex has this value:
[1265]*[1263]
I am trying to get 1265 and 1263 in my result set as separate rows.
SELECT REGEXP_SUBSTR(column,'\[(.*?)\]',1,LEVEL) AS "col1"
FROM table
CONNECT BY REGEXP_SUBSTR(column,'\[(.*?)\]',1,LEVEL) IS NOT NULL;
Instead I just get this in the result set.
[1263]

with test (rn, col) as
(
select 'a', '[123]*[abc] []' from dual union all
select 'b', '[45][def] ' from dual union all
select 'c', '[678],.*' from dual
),
coll (rn, col) as
(
select rn,regexp_replace(col, '(\[.*?\])|.', '\1') from test
),
cte (rn, cnt, col, i) as
(
select rn, 1, col, regexp_substr(col, '(\[(.*?)\])', 1, 1, null, 2)
from coll
union all
select rn, cnt+1, col, regexp_substr(col, '(\[(.*?)\])', 1, cnt+1, null, 2)
from cte
where cnt+1 <= regexp_count(col, '\[.*?\]')
)
select * from cte
order by 1,2;

This regex counts elements by looking for closing brackets and returns the digits inside the brackets, allowing for NULLs. Separators are ignored since the data elements you want are surrounded by square brackets we can focus on those.
SQL> with test(rownbr, col) as (
select 1, '[1265]**[1263]' from dual union
select 2, '[123]' from dual union
select 3, '[111][222]*[333]' from dual union
select 4, '[411]*[][433]' from dual
)
select distinct rownbr, level as element,
regexp_substr(col, '\[([0-9]*)\]', 1, level, null, 1) value
from test
connect by level <= regexp_count(col, ']')
order by rownbr, element;
ROWNBR ELEMENT VALUE
---------- ---------- -----
1 1 1265
1 2 1263
2 1 123
3 1 111
3 2 222
3 3 333
4 1 411
4 2
4 3 433
9 rows selected.
SQL>

Related

SUBSTR to ADD value in oracle

I have table with column having data in below format in Oracle DB.
COL 1
abc,mno:EMP
xyz:EMP;tyu,opr:PROF
abc,mno:EMP;tyu,opr:PROF
I am trying to convert the data in below format
COL 1
abc:EMP;mno:EMP
xyz:EMP;tyu:PROF;opr:PROF
abc:EMP;mno:EMP;tyu:PROF;opr:PROF
Basically trying to get everything after : and before ; to move it substitute comma with it.
I tried some SUBSTR and LISTAGG but couldn't get anything worth sharing.
Regards.
Here's one option; read comments within code.
SQL> with test (id, col) as
2 -- sample data
3 (select 1, 'abc,mno:EMP' from dual union all
4 select 2, 'xyz:EMP;tyu,opr:PROF' from dual union all
5 select 3, 'abc,mno:EMP;tyu,opr:PROF' from dual
6 ),
7 temp as
8 -- split sample data to rows
9 (select id,
10 column_value cv,
11 regexp_substr(col, '[^;]+', 1, column_value) val
12 from test cross join
13 table(cast(multiset(select level from dual
14 connect by level <= regexp_count(col, ';') + 1
15 ) as sys.odcinumberlist))
16 )
17 -- finally, replace comma with a string that follows a colon sign
18 select id,
19 listagg(replace(val, ',', substr(val, instr(val, ':')) ||';'), ';') within group (order by cv) new_val
20 from temp
21 group by id
22 order by id;
ID NEW_VAL
---------- ----------------------------------------
1 abc:EMP;mno:EMP
2 xyz:EMP;tyu:PROF;opr:PROF
3 abc:EMP;mno:EMP;tyu:PROF;opr:PROF
SQL>
Using the answer of littlefoot, if i were to use cross apply i wouldnt need to cast as multiset...
with test (id, col) as
-- sample data
(select 1, 'abc,mno:EMP' from dual union all
select 2, 'xyz:EMP;tyu,opr:PROF' from dual union all
select 3, 'abc,mno:EMP;tyu,opr:PROF' from dual
),
temp as
-- split sample data to rows
(select id,
column_value cv,
regexp_substr(col, '[^;]+', 1, column_value) val
from test
cross apply (select level as column_value
from dual
connect by level<= regexp_count(col, ';') + 1)
)
-- finally, replace comma with a string that follows a colon sign
select id,
listagg(replace(val, ',', substr(val, instr(val, ':')) ||';'), ';') within group (order by cv) new_val
from temp
group by id
order by id;
You do not need recursive anything, just basic regex: if the pattern is always something,something2:someCode (e.g. you have no colon before the comma), then it would be sufficient.
with test (id, col) as (
select 1, 'abc,mno:EMP' from dual union all
select 2, 'xyz:EMP;tyu,opr:PROF' from dual union all
select 3, 'abc,mno:EMP;tyu,opr:PROF' from dual union all
select 3, 'abc,mno:EMP;tyu,opr:PROF;something:QWE;something2:QWE' from dual
)
select
/*
Grab this groups:
1) Everything before the comma
2) Then everything before the colon
3) And then everything between the colon and a semicolon
Then place group 3 between 1 and 2
*/
trim(trailing ';' from regexp_replace(col || ';', '([^,]+),([^:]+):([^;]+)', '\1:\3;\2:\3')) as res
from test
| RES |
| :------------------------------------------------------------- |
| abc:EMP;mno:EMP |
| xyz:EMP;tyu:PROF;opr:PROF |
| abc:EMP;mno:EMP;tyu:PROF;opr:PROF |
| abc:EMP;mno:EMP;tyu:PROF;opr:PROF;something:QWE;something2:QWE |
db<>fiddle here

Print number and character in two different column from a single column in oracle 11g

My table
create table tdata (val varchar(5));
val
a
1
b
c
dd
ee
f
2
3
4
5
--Output i want is
Number Character
1 a
2 b
3 c
4 d
5 e
I have done this but the only problem is that i'm gettting null values in both columns in place
of character in number column & vice versa
--Query
select REGEXP_SUBSTR(val, '[0-9]+') as num1,
REGEXP_SUBSTR(substr(val,1,1),'[a-z]')as char1 from tdata
The way you put it, see if something like this helps:
create two additional tables (using CTE) - one for numbers, another for letters
fetch ROWNUM which will then be used to join those tables
join them!
Sample data in lines #1 - 9, the rest might be what you need.
SQL> with tdata (val) as
2 (select 'a' from dual union all
3 select '1' from dual union all
4 select 'b' from dual union all
5 select 'c' from dual union all
6 select '2' from dual union all
7 select '3' from dual
8 ),
9 --
10 numbers as
11 (select val,
12 rownum rn
13 from tdata
14 where regexp_like(val, '[[:digit:]]')
15 ),
16 letters as
17 (select substr(val, 1, 1) val,
18 rownum rn
19 from tdata
20 where regexp_like(val, '[[:alpha:]]')
21 )
22 select n.val, l.val
23 from numbers n join letters l on n.rn = l.rn;
VAL VAL
----- -----
1 a
2 b
3 c
SQL>
WITH cte AS (
SELECT SUBSTR(val, 1, 1) Character FROM tdata
UNION
SELECT SUBSTR(val, 2, 1) Character FROM tdata WHERE LENGTH(val) > 1
UNION
SELECT SUBSTR(val, 3, 1) Character FROM tdata WHERE LENGTH(val) > 2
UNION
SELECT SUBSTR(val, 4, 1) Character FROM tdata WHERE LENGTH(val) > 3
UNION
SELECT SUBSTR(val, 5, 1) Character FROM tdata WHERE LENGTH(val) > 4
)
SELECT ROW_NUMBER() OVER (ORDER BY Character) "Number", Character
FROM cte
WHERE Character BETWEEN 'a' AND 'z';
fiddle

Oracle/SQL - Need query that will select max value from string in each row

I need a graceful way to select the max value from a field holding a comma delimited list.
Expected Values:
List_1 | Last
------ | ------
A,B,C | C
B,D,C | D
I'm using the following query and I'm not getting what's expected.
select
list_1,
(
select max(values) WITHIN GROUP (order by 1)
from (
select
regexp_substr(list_1,'[^,]+', 1, level) as values
from dual
connect by regexp_substr(list_1, '[^,]+', 1, level) is not null)
) as last
from my_table
Anyone have any ideas to fix my query?
with
test_data ( id, list_1 ) as (
select 101, 'A,B,C' from dual union all
select 102, 'B,D,C' from dual union all
select 105, null from dual union all
select 122, 'A' from dual union all
select 140, 'A,B,B' from dual
)
-- end of simulated table (for testing purposes only, not part of the solution)
select id, list_1, max(token) as max_value
from ( select id, list_1,
regexp_substr(list_1, '([^,])(,|$)', 1, level, null, 1) as token
from test_data
connect by level <= 1 + regexp_count(list_1, ',')
and prior id = id
and prior sys_guid() is not null
)
group by id, list_1
order by id
;
ID LIST_1_ MAX_VAL
---- ------- -------
101 A,B,C C
102 B,D,C D
105
122 A A
140 A,B,B B
In Oracle 12.1 or higher, this can be re-written using the LATERAL clause:
select d.id, d.list_1, x.max_value
from test_data d,
lateral ( select max(regexp_substr(list_1, '([^,]*)(,|$)',
1, level, null, 1)) as max_value
from test_data x
where x.id = d.id
connect by level <= 1 + regexp_count(list_1, ',')
) x
order by d.id
;

Splitting a comma separated string in Oracle not working

I would like to retrieve a set of values from a comma separated string into an IN clause in a query. Does some one knows why the following code is not working properly;
WITH xtable AS (
SELECT 1 ID, '116,117,169,170,173,175,9015,44008,44367,44446,45081,45083,46779,47161,47222' AGT FROM DUAL
UNION ALL
SELECT 2 ID, '456,789' AGT FROM DUAL
UNION ALL
SELECT 3 ID, '116,117,169,170,173,175,9015,44008,44367,44446,45081,45083,46779,47161' AGT FROM DUAL
)
select regexp_substr(x.AGT,'[^,]+', 1, level)
from xtable x
where x.ID = 3
connect by regexp_substr(x.AGT, '[^,]+', 1, level) is not null;
In this scenario the result should be
AGT
1 116
2 117
3 169
4 170
5 173
6 175
7 9015
8 44008
9 44367
10 44446
11 45081
12 45083
13 46779
14 47161
Instead I get an almost infinite loop of the same values
Issue in your query is that the where clause will be applied to only level 1 not any further.
Try this using nested table:
WITH xtable AS (
SELECT 1 ID, '116,117,169,170,173,175,9015,44008,44367,44446,45081,45083,46779,47161,47222' AGT FROM DUAL
UNION ALL
SELECT 2 ID, '456,789' AGT FROM DUAL
UNION ALL
SELECT 3 ID, '116,117,169,170,173,175,9015,44008,44367,44446,45081,45083,46779,47161' AGT FROM DUAL
)
select regexp_substr(x.AGT,'[^,]+', 1, t.column_value) agt
from xtable x cross join table(
cast(
multiset(
select level
from dual
connect by level <= regexp_count(x.AGT,',') + 1
)as sys.odcinumberlist
)
) t
where x.id = 3;
It's a general purpose query which you can use even without where clause if you wanted to convert all of the them at once.
In Oracle 12c+, you can use OUTER APPLY to achieve the same effect and simpler syntax:
WITH xtable AS (
SELECT 1 ID, '116,117,169,170,173,175,9015,44008,44367,44446,45081,45083,46779,47161,47222' AGT FROM DUAL
UNION ALL
SELECT 2 ID, '456,789' AGT FROM DUAL
UNION ALL
SELECT 3 ID, '116,117,169,170,173,175,9015,44008,44367,44446,45081,45083,46779,47161' AGT FROM DUAL
)
select regexp_substr(x.AGT,'[^,]+', 1, t.n) agt
from xtable x
outer apply (
select level n
from dual
connect by level <= regexp_count(x.AGT,',') + 1
) t
where x.id = 3;
You have the same values in multiple rows, so your connect-by is bouncing between them. Hierarchical queries are a little tricky with multiple rows involved. If you really only want the values for a single ID you can filter in a subquery:
WITH xtable AS (
SELECT 1 ID, '116,117,169,170,173,175,9015,44008,44367,44446,45081,45083,46779,47161,47222' AGT FROM DUAL
UNION ALL
SELECT 2 ID, '456,789' AGT FROM DUAL
UNION ALL
SELECT 3 ID, '116,117,169,170,173,175,9015,44008,44367,44446,45081,45083,46779,47161' AGT FROM DUAL
)
select regexp_substr(x.AGT,'[^,]+', 1, level)
from (select AGT from xtable where ID = 3) x
connect by regexp_substr(x.AGT, '[^,]+', 1, level) is not null;
REGEXP_SUBSTR(X.AGT,'[^,]+',1,LEVEL)
----------------------------------------------------------------------------
116
117
169
170
173
175
9015
44008
44367
44446
45081
45083
46779
47161
14 rows selected.
Or you can include id = prior id, but then also need to use a non-deterministic function call to prevent cycling:
WITH ... (...)
select regexp_substr(x.AGT,'[^,]+', 1, level)
from xtable x
where x.ID = 3
connect by id = prior id and prior dbms_random.value is not null
and regexp_substr(x.AGT, '[^,]+', 1, level) is not null;
which gets the same 14 rows back, and would work if you included multiple IDs.
There are multiple solutions for this in the Splitting Delimited Strings - SO Oracle Documentation page.
A solution using a correlated hierarchical query is:
WITH xtable (id, agt ) AS (
SELECT 1, '116,117,169,170,173,175,9015,44008,44367,44446,45081,45083,46779,47161,47222' FROM DUAL
UNION ALL
SELECT 2, '456,789' FROM DUAL
UNION ALL
SELECT 3, '116,117,169,170,173,175,9015,44008,44367,44446,45081,45083,46779,47161' FROM DUAL
)
SELECT id,
REGEXP_SUBSTR( x.agt, '[^,]+', 1, t.COLUMN_VALUE ) AS item
FROM xtable x
CROSS JOIN
TABLE(
CAST(
MULTISET(
SELECT LEVEL
FROM DUAL
CONNECT BY LEVEL <= REGEXP_COUNT( x.agt, '[^,]+' )
)
) AS SYS.ODCINUMBERLIST
) t
WHERE x.id = 3;
Output:
ID ITEM
-- -----
3 116
3 117
3 169
3 170
3 173
3 175
3 9015
3 44008
3 44367
3 44446
3 45081
3 45083
3 46779
3 47161

Oracle SQL : Regexp_substr

I have below sample values in a column
Abc-123-xyz
Def-456-uvw
Ghi-879-rst-123
Jkl-abc
Expected output is the third element split by '-', in case there is no third element, the last element will be retrieve.
See expected output below:
Xyz
Uvw
Rst
Abc
Thanks ahead for the help.
SELECT initcap(nvl(regexp_substr(word, '[^-]+', 1,3),regexp_substr(word, '[^-]+', 1,2))) FROM your_table;
Another approach:
SQL> with t1(col) as(
2 select 'Abc-123-xyz' from dual union all
3 select 'Def-456-uvw' from dual union all
4 select 'Ghi-879-rst-123' from dual union all
5 select 'Jkl-Abc' from dual
6 )
7 select regexp_substr( col
8 , '[^-]+'
9 , 1
10 , case
11 when regexp_count(col, '[^-]+') >= 3
12 then 3
13 else regexp_count(col, '[^-]+')
14 end
15 ) as res
16 from t1
17 ;
Result:
RES
---------------
xyz
uvw
rst
Abc
regexp_substr(column, '(.*?-){0,2}([^-]+)', 1, 1, '', 2)
You can also do it without RegEx:
with t1 as(
select 'Abc-123-xyz' as MyText from dual union all
select 'Def-456-uvw' from dual union all
select 'Ghi-879-rst-123' from dual union all
select 'Jkl-Abc' from dual
)
SELECT
SUBSTR(t1.mytext, LENGTH(t1.mytext) - INSTR(REVERSE(t1.mytext), '-') + 2)
FROM t1
;