Split IPv4 address into 4 numbers in Oracle sql - sql

I'm trying to split a given IPv4 address into four numbers.
In SQL Server this query works well for me:
select CAST (PARSENAME('10.20.30.40',4) AS INT)
result: 10
select CAST (PARSENAME('10.20.30.40',3) AS INT)
result: 20
and so on.
I need the equivalent syntax in Oracle SQL, but can't find it. Any idea?

You could use regexp_substr:
select ip,
regexp_substr(ip, '\d+',1,1) as first_octet,
regexp_substr(ip, '\d+',1,2) as second_octet,
regexp_substr(ip, '\d+',1,3) as third_octet,
regexp_substr(ip, '\d+',1,4) as fourth_octet
from (select '10.20.30.40' AS ip from dual )ips;
Rextester Demo

You can use simple string functions (INSTR and SUBSTR) that are much faster than regular expressions:
SQL Fiddle
Oracle 11g R2 Schema Setup:
CREATE TABLE sample_data ( ip_address ) AS
SELECT '10.20.30.40' FROM DUAL
Query 1:
SELECT TO_NUMBER(
SUBSTR( ip_address, 1, first_sep - 1 )
) AS ClassA,
TO_NUMBER(
SUBSTR( ip_address, first_sep + 1, second_sep - first_sep )
) AS ClassB,
TO_NUMBER(
SUBSTR( ip_address, second_sep + 1, third_sep - second_sep )
) AS ClassC,
TO_NUMBER(
SUBSTR( ip_address, third_sep + 1 )
) AS ClassD
FROM (
SELECT ip_address,
INSTR( ip_address, '.', 1, 1 ) AS first_sep,
INSTR( ip_address, '.', 1, 2 ) AS second_sep,
INSTR( ip_address, '.', 1, 3 ) AS third_sep
FROM sample_data
)
Results:
| CLASSA | CLASSB | CLASSC | CLASSD |
|--------|--------|--------|--------|
| 10 | 20 | 30 | 40 |

In case you need all in one function, this is another solution:
SELECT REGEXP_SUBSTR('10.20.30.40', '\d+', 1, LEVEL) as octet, level
FROM dual
CONNECT BY LEVEL <= 4;
OCTET LEVEL
10 1
20 2
30 3
40 4

Related

Duplicate values when splitting a string

I'm trying to create a row for each person, str but I am getting extra output.
Can someone please explain what I did wrong and show me how to fix it.
Below is my test CASE and expected results. Thanks to all who answer and your expertise.
with rws as (
select 'Bob' person, 'AB,CR,DE' str from dual UNION ALL
select 'Jane' person, 'AB' str from dual
)
select person,
regexp_substr (
str,
'[^,]+',
1,
level
) value
from rws
connect by level <=
length ( str ) - length ( replace ( str, ',' ) ) + 1
ORDER BY person, str;
PERSON VALUE
Bob AB
Bob CR
Bob DE
Bob DE
Bob CR
Jane AB
Expected results
PERSON VALUE
Bob AB
Bob CR
Bob DE
Jane AB
The problem with your original query is that connect-by is looking at previous rows more than once - essentially, the second level of rows for Bob is also picking up the first row for Jane. This is a fairly well-known issue. You can avoid that by including a unique ID (with this example you'd have to rely on the name, and hope it's unique); but that then will loop, which you can avoid by adding a non-deterministic function call:
...
connect by level <=
length ( str ) - length ( replace ( str, ',' ) ) + 1
and prior person = person
and prior dbms_random.value is not null
ORDER BY person, str;
You could also use recursive subquery factoring instead of a hierarchical query:
with rws as (
select 'Bob' person, 'AB,CR,DE' str from dual UNION ALL
select 'Jane' person, 'AB' str from dual
),
rcte (person, str, cnt, lvl, value) as (
select person, str, length ( str ) - length ( replace ( str, ',' ) ), 1,
regexp_substr (
str,
'[^,]+',
1,
1
)
from rws
union all
select person, str, cnt, lvl + 1,
regexp_substr (
str,
'[^,]+',
1,
lvl + 1
)
from rcte
where lvl <= cnt
)
select person, value
from rcte
order by person, value;
fiddle
but you might find one of the other answers performs better, or at least is easy to understand and maintain.
Incidentally, your regular expression pattern might cause issues if you ever have a null element (i.e. two adjacent commas); this this answer for an explanation.
Here's one option:
SQL> WITH
2 rws
3 AS
4 (SELECT 'Bob' person, 'AB,CR,DE' str FROM DUAL
5 UNION ALL
6 SELECT 'Jane' person, 'AB' str FROM DUAL)
7 SELECT person,
8 REGEXP_SUBSTR (str,
9 '[^,]+',
10 1,
11 COLUMN_VALUE) VALUE
12 FROM rws
13 CROSS JOIN
14 TABLE (
15 CAST (
16 MULTISET ( SELECT LEVEL
17 FROM DUAL
18 CONNECT BY LEVEL <= REGEXP_COUNT (str, ',') + 1)
19 AS SYS.odcinumberlist))
20 ORDER BY person, str;
PERS VALUE
---- --------
Bob AB
Bob CR
Bob DE
Jane AB
SQL>
Your solution would return desired result if you applied SELECT DISTINCT (and fixed order by clause, but that's irrelevant), but that would also behave badly as number of rows you're working with grows.
SQL> with rws as (
2 select 'Bob' person, 'AB,CR,DE' str from dual UNION ALL
3 select 'Jane' person, 'AB' str from dual
4 )
5 select distinct person,
6 regexp_substr (
7 str,
8 '[^,]+',
9 1,
10 level
11 ) value
12 from rws
13 connect by level <=
14 length ( str ) - length ( replace ( str, ',' ) ) + 1;
PERS VALUE
---- --------
Jane AB
Bob CR
Bob AB
Bob DE
SQL>
You can use a recursive query and simple string functions (which is slightly more to type but is faster than regular expressions):
with rws (person, str) as (
select 'Bob', 'AB,CR,DE' from dual UNION ALL
select 'Jane', 'AB' from dual
),
bounds (person, str, spos, epos) AS (
SELECT person,
str,
1,
INSTR(str, ',', 1)
FROM rws
UNION ALL
SELECT person,
str,
epos + 1,
INSTR(str, ',', epos + 1)
FROM bounds
WHERE epos > 0
)
SELECT person,
CASE epos
WHEN 0
THEN SUBSTR(str, spos)
ELSE SUBSTR(str, spos, epos - spos)
END AS value
FROM bounds
ORDER BY person, value;
Which outputs:
PERSON
VALUE
Bob
AB
Bob
CR
Bob
DE
Jane
AB
fiddle
If you don't have quotes in the data, for 12c+ you may use JSON_TABLE and lateral join instead of recursion.
with rws as (
select 'Bob' person, 'AB,CR,DE' str from dual UNION ALL
select 'Jane' person, 'AB' str from dual union all
select 'Mark', null from dual
)
select
rws.person,
l.val_splitted,
l.rn
from rws
left join lateral (
select *
from json_table(
'["' || replace(rws.str, ',', '","') || '"]',
'$[*]'
columns (
val_splitted varchar2(10) path '$',
rn for ordinality
)
)
) l
on 1 = 1
order by 1
PERSON
VAL_SPLITTED
RN
Bob
AB
1
Bob
CR
2
Bob
DE
3
Jane
AB
1
Mark
1

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

Splitting data field String value in Oracle SQl

I have a String Field value in Oracle SQL table. Is there any query so that I can split the string into new lines with certain number of equal characters in each line and the excess characters at the bottom?
eg- ABCDEFGHIJ
I want to have lines with equal number of characters 4 in each line as follows
ABCD
EFGH
IJ
The remainder of 2 letters should be at the bottom. Is it possible to achieve this using an Oracle sql query?
You can use a query like the one below using CONNECT BY and LEVEL based on the length of the string.
WITH d AS (SELECT 'ABCDEFGHIJ' AS str FROM DUAL)
SELECT SUBSTR (str, ((LEVEL - 1) * 4) + 1, 4) AS four_letters
FROM d
CONNECT BY LEVEL < (LENGTH (str) / 4) + 1;
If you have multiple rows, you can use OUTER APPLY with a hierarchical query:
SELECT s.split_value,
s.position
FROM table_name t
OUTER APPLY (
SELECT LEVEL AS position,
SUBSTR( t.value, 4 * LEVEL - 3, 4 ) AS split_value
FROM DUAL
CONNECT BY LEVEL <= CEIL( LENGTH( t.value ) / 4 )
) s
Which, for the sample data:
CREATE TABLE table_name ( value ) AS
SELECT 'ABCDEFGHIJ' FROM DUAL UNION ALL
SELECT '123456789012' FROM DUAL;
Outputs:
SPLIT_VALUE | POSITION
:---------- | -------:
ABCD | 1
EFGH | 2
IJ | 3
1234 | 1
5678 | 2
9012 | 3
db<>fiddle here
Update
if I want to have like 36 words characters in a line how can I modify your 1st answer?
SELECT s.split_value,
s.position
FROM table_name t
OUTER APPLY (
SELECT LEVEL AS position,
SUBSTR( t.value, 36 * ( LEVEL - 1 ) + 1, 36 ) AS split_value
FROM DUAL
CONNECT BY LEVEL <= CEIL( LENGTH( t.value ) / 36 )
) s
Which, for the sample data:
CREATE TABLE table_name ( value ) AS
SELECT 'ABCDEFGHIJ' FROM DUAL UNION ALL
SELECT '________10________20________30________40________50________60________70________80' FROM DUAL;
Outputs:
SPLIT_VALUE | POSITION
:----------------------------------- | -------:
ABCDEFGHIJ | 1
________10________20________30______ | 1
__40________50________60________70__ | 2
______80 | 3
db<>fiddle here

regexp_substr to bring back data before a foward slash

I have the following pattern of characters in a dataset. I need to manipulate the data & cross refer it to another table. I'm trying to write a regexp_substr to bring back data before a foward slash starting from the left. for example:-
abc/ab/123/zzz
so I need to get the following results back to then compare to another table
abc
abc/ab
abc/ab/123
I have worked out the other logic but an struggling with the various regexp.
Here is the recursive query with SUBSTR and INSTR:
with cte(col) as
(
select substr(col, 1, instr(col, '/', -1) - 1) from mytable
union all
select substr(col, 1, instr(col, '/', -1) - 1) from cte where instr(col, '/') > 0
)
select col from cte;
And here is the query with REGEXP_REPLACE:
with cte(col) as
(
select regexp_replace(col, '/[^/]*$', '') from mytable
union all
select regexp_replace(col, '/[^/]*$', '') from cte where instr(col, '/') > 0
)
select col from cte;
You don't need a regular expression. You can do it with (faster) string functions:
SQL Fiddle
Oracle 11g R2 Schema Setup:
CREATE TABLE test_data ( id, value ) AS
SELECT 1, 'abc/ab/123/zzz' FROM DUAL;
Query 1:
WITH bounds ( id, value, end_pos ) AS (
SELECT id,
value,
INSTR( value, '/', 1 )
FROM test_data
WHERE INSTR( value, '/', 1 ) > 0
UNION ALL
SELECT id,
value,
INSTR( value, '/', end_pos + 1 )
FROM bounds
WHERE INSTR( value, '/', end_pos + 1 ) > 0
)
SELECT id,
SUBSTR( value, 1, end_pos ) AS item
FROM bounds
ORDER BY id, end_pos
Results:
| ID | ITEM |
|----|-------------|
| 1 | abc/ |
| 1 | abc/ab/ |
| 1 | abc/ab/123/ |
However, if you did want to use regular expressions then you could do:
Query 2:
WITH bounds ( id, value, lvl, item ) AS (
SELECT id,
value,
1,
REGEXP_SUBSTR( value, '.*?/', 1, 1 )
FROM test_data
WHERE REGEXP_SUBSTR( value, '.*?/', 1, 1 ) IS NOT NULL
UNION ALL
SELECT id,
value,
lvl + 1,
item || REGEXP_SUBSTR( value, '.*?/', 1, lvl + 1 )
FROM bounds
WHERE REGEXP_SUBSTR( value, '.*?/', 1, lvl + 1 ) IS NOT NULL
)
SELECT id,
item
FROM bounds
Results:
| ID | ITEM |
|----|-------------|
| 1 | abc/ |
| 1 | abc/ab/ |
| 1 | abc/ab/123/ |

How to replace comma separated text values in a column in Oracle?

I have a table t1 with a varchar col V_RELNIST_SKEY which contains comma separated numbers between 1 and 12 as shown. I want to write a select statement to replace numbers by string. For e.g., value 5,6 should be replaced by five,six and so on.
|V_RELNIST_SKEY|
|6 |
|5,6 |
|1,12 |
|1,2,3,12 |
Oracle Setup:
CREATE TABLE test_data ( value ) as
SELECT '9' FROM DUAL UNION ALL
SELECT '6' FROM DUAL UNION ALL
SELECT '1' FROM DUAL UNION ALL
SELECT '2,3' FROM DUAL UNION ALL
SELECT '5,6,7' FROM DUAL UNION ALL
SELECT '8,4' FROM DUAL UNION ALL
SELECT '1,2,3,4,5,6,7,8,9,10,11,12' FROM DUAL;
Query:
SELECT value,
column_value AS words
FROM test_data t
CROSS JOIN
TABLE(
CAST(
MULTISET(
SELECT LISTAGG(
TO_CHAR(
TO_DATE(
REGEXP_SUBSTR( t.value, '\d+', 1, LEVEL ),
'J'
),
'JSP'
),
','
) WITHIN GROUP ( ORDER BY LEVEL )
FROM DUAL
CONNECT BY LEVEL <= REGEXP_COUNT( t.value, '\d+' )
) AS SYS.ODCIVARCHAR2LIST
)
) w;
Output:
VALUE WORDS
-------------------------- ----------------------------------------
9 NINE
6 SIX
1 ONE
2,3 TWO,THREE
5,6,7 FIVE,SIX,SEVEN
8,4 EIGHT,FOUR
1,2,3,4,5,6,7,8,9,10,11,12 ONE,TWO,THREE,FOUR,FIVE,SIX,SEVEN,EIGHT,
NINE,TEN,ELEVEN,TWELVE
Update
What if I have to replace 1 with A, 2 with B, 3 with C and so on?
SELECT value,
COLUMN_VALUE AS words
FROM test_data t
CROSS JOIN
TABLE(
CAST(
MULTISET(
SELECT LISTAGG(
CHR( 64 + REGEXP_SUBSTR( t.value, '\d+', 1, LEVEL ) ),
','
) WITHIN GROUP ( ORDER BY LEVEL )
FROM DUAL
CONNECT BY LEVEL <= REGEXP_COUNT( t.value, '\d+' )
) AS SYS.ODCIVARCHAR2LIST
)
) w;
Output:
VALUE WORDS
-------------------------- ----------------------------------------
9 I
6 F
1 A
2,3 B,C
5,6,7 E,F,G
8,4 H,D
1,2,3,4,5,6,7,8,9,10,11,12 A,B,C,D,E,F,G,H,I,J,K,L