Is there a SUBSTR alternative in Oracle SQL - sql

I'm trying to extract the the different sub-strings within one string. The I want different strings for every string divided by the dash (-) symbol.
I have tried using the SUBSTR position function. It does not work since sometimes there are 4 chars in the second sub string, therefore, the 3rd sub string is not correct.
SELECT SUBSTR(STR, INSTR (STR, '-', -1)+ 1)
STR = F-123-A123-B12 or F-1234-A123-B12
I am trying to get a query that will give me F.
I need another query that will give me 123 or 1234 if there are 4 chars
I need another query to get me A123
I need another query to get B12
I was thinking there would be a regex function that I could use. I could not find one.

For example:
SQL> with test (col) as
2 (select 'F-123-A123-B12' from dual)
3 select regexp_substr(col, '\w+', 1, level) result
4 from test
5 connect by level <= regexp_count(col, '-') + 1;
RESULT
--------------
F
123
A123
B12
SQL>

You don't need regular expressions. INSTR and SUBSTR will work (and are faster):
Oracle Setup:
CREATE TABLE test_data ( str ) AS
SELECT 'F-123-A123-B12' FROM DUAL UNION ALL
SELECT 'F-1234-A123-B12' FROM DUAL
Query 1:
SELECT SUBSTR( str, 1, delimiter1 - 1 ) AS substr1,
SUBSTR( str, delimiter1 + 1, delimiter2 - delimiter1 - 1 ) AS substr2,
SUBSTR( str, delimiter2 + 1, delimiter3 - delimiter2 - 1 ) AS substr3,
SUBSTR( str, delimiter3 + 1 ) AS substr4
FROM (
SELECT str,
INSTR( str, '-', 1, 1 ) AS delimiter1,
INSTR( str, '-', 1, 2 ) AS delimiter2,
INSTR( str, '-', 1, 3 ) AS delimiter3
FROM test_data
) s
Output:
SUBSTR1 | SUBSTR2 | SUBSTR3 | SUBSTR4
:------ | :------ | :------ | :------
F | 123 | A123 | B12
F | 1234 | A123 | B12
If you do want to use regular expressions, then there is no need for a hierarchical query:
Query 2:
SELECT REGEXP_SUBSTR( str, '[^-]+', 1, 1 ) AS substr1,
REGEXP_SUBSTR( str, '[^-]+', 1, 2 ) AS substr2,
REGEXP_SUBSTR( str, '[^-]+', 1, 3 ) AS substr3,
REGEXP_SUBSTR( str, '[^-]+', 1, 4 ) AS substr4
FROM test_data
(Output as Query 1 above.)
Query 3
If you don't know how many delimited values there will be and want to parse them all to rows then you still don't need to use (slow) regular expressions or hierarchical queries and can just use a recursive sub-query factoring clause with simple string functions (and it works with zero-width/NULL sub-strings between delimiters):
WITH substr_bounds ( str, idx, startidx, endidx ) AS (
SELECT str,
1,
1,
INSTR( str, '-', 1 )
FROM test_data
UNION ALL
SELECT str,
idx + 1,
endidx + 1,
INSTR( str, '-', endidx + 1 )
FROM substr_bounds
WHERE endidx > 0
)
SELECT str,
idx,
CASE
WHEN endidx = 0
THEN SUBSTR( str, startidx )
ELSE SUBSTR( str, startidx, endidx - startidx )
END AS substr
FROM substr_bounds
ORDER BY str, idx
Output:
STR | IDX | SUBSTR
:-------------- | --: | :-----
F-123-A123-B12 | 1 | F
F-123-A123-B12 | 2 | 123
F-123-A123-B12 | 3 | A123
F-123-A123-B12 | 4 | B12
F-1234-A123-B12 | 1 | F
F-1234-A123-B12 | 2 | 1234
F-1234-A123-B12 | 3 | A123
F-1234-A123-B12 | 4 | B12
db<>fiddle here

If your string could have a NULL element, use this format to handle it (Note list element 2 is NULL), else you risk the following elements being returned in the wrong positions:
with test (col) as
(select 'F--A123-B12' from dual)
select regexp_substr(col, '(.*?)(-|$)', 1, level, null, 1) result
from test
connect by level <= regexp_count(col, '-') + 1;
RESULT
-----------
F
A123
B12
4 rows selected.

I am working with newest Node 17.3.1 where "substr" function is obsolete and I replace it with "substring" instead and it is the nearest and simplest alternative.

Related

Oracle REGEXP_SUBSTR to SUBSTR + INSTR

I am trying to convert the following code from oracle:
SELECT DISTINCT REPLACE(SUBSTR (REGEXP_SUBSTR (code,'[^,]+',1,LEVEL),2,8),']','') code
FROM DUAL
CONNECT BY REGEXP_SUBSTR (code, '[^,]+', 1, LEVEL) IS NOT NULL;
Please assist to convert it into some SUBSTR / INSTR combination or CASE function.
Thanks.
Regards,
Your code does not work, as the DUAL table does not have a code column; however, if we assume that you are using a different table which does have that column:
CREATE TABLE table_name ( code ) AS
SELECT '[1],[22],[333],[4444],[55555],[666666],[7777777],[88888888],[999999999],[0000000000]' FROM DUAL
Then you can use a recursive sub-query factoring clause:
WITH rsqfc ( code, start_pos, end_pos ) AS (
SELECT code,
1,
INSTR( code, ',', 1 )
FROM table_name
UNION ALL
SELECT code,
end_pos + 1,
INSTR( code, ',', end_pos + 1 )
FROM rsqfc
WHERE end_pos > 0
)
SELECT DISTINCT
REPLACE(
CASE end_pos
WHEN 0
THEN SUBSTR( code, start_pos + 1, 8 )
ELSE SUBSTR( code, start_pos + 1, LEAST( end_pos - start_pos - 1, 8 ) )
END,
']'
) AS code
FROM rsqfc;
Which outputs:
| CODE |
| :------- |
| 99999999 |
| 4444 |
| 55555 |
| 1 |
| 333 |
| 22 |
| 666666 |
| 88888888 |
| 00000000 |
| 7777777 |
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

SQL check if fields contains same letters

I want to check if there is a row in my table that contains the same letters but in different order, but it must have the exact same letters, no more and no less.
For example, I have the letters "abc":
bca -> true
acb -> true
abcd -> **false**
ab -> **false**
Thanks!
You can use recursive CTEs to split the parameter 'abc' and each column value to letters and compare them:
with
recursive paramletters as (
select 'abc' col, 1 pos, substr('abc', 1, 1) letter
union all
select col, pos + 1, substr(col, pos + 1, 1)
from paramletters
where pos < length(col)
),
param as (
select group_concat(letter, '') over (order by letter) paramvalue
from paramletters
order by paramvalue desc limit 1
),
cteletters as (
select col, 1 pos, substr(col, 1, 1) letter
from tablename
union all
select col, pos + 1, substr(col, pos + 1, 1)
from cteletters
where pos < length(col)
),
cte as (
select * from (
select col, group_concat(letter, '') over (partition by col order by letter) colvalue
from cteletters
)
where length(colvalue) = length(col)
)
select c.col, c.colvalue = p.paramvalue result
from cte c cross join param p
See the demo.
Results:
| col | result |
| ---- | ------ |
| ab | 0 |
| abcd | 0 |
| acb | 1 |
| bca | 1 |
If the letters of the parameter are already sorted (like 'abc') then this code can be simplified to use only the last 2 CTEs.

ask for a split function in oracle

right now there is a string like this:
789+456-239
I want to get a list like this:
sign | num
+ 789
+ 456
- 239
This could be a way, by using a commonly used split string method, just adapted to your need to handle the sign.
-- test case
with yourString(str) as
(
select '+789+456-239 ' str
from dual
)
-- query
SELECT regexp_substr(str, '[+-]', 1, level) sign,
regexp_substr(str, '[^+-]+', 1, level) num
FROM ( select case
when substr(str, 1, 1) in ('+','-') then str
-- I add a plus sign if the first char of the string is not a sign
else '+' || str
end as str
from yourString
)
CONNECT BY regexp_instr(str, '[+-]', 1, level ) > 0
This gives:
SIGN NUM
-------------- --------------
+ 789
+ 456
- 239
Here I assume that if no sign is given in the beginning of this string, the first sign will be +.
If you need to handle more than one string, you need to identify an ID in your table and the CONNECT BY becomes a bit more complex:
-- test case
with yourString(id, str) as
(
select 1, '+789+456-239' from dual union all
select 2, '789+456-239' from dual union all
select 3, '-789+456-239' from dual
)
-- query
SELECT id,
regexp_substr(str, '[+-]', 1, level) sign,
regexp_substr(str, '[^+-]+', 1, level) num
FROM ( select id,
case
when substr(str, 1, 1) in ('+','-') then str
-- I add a plus sign if the first char of the string is not a sign
else '+' || str
end as str
from yourString
)
CONNECT BY regexp_instr(str, '[+-]', 1, level ) > 0
and prior id = id
and prior sys_guid() is not null
The result:
ID SIGN NUM
---------- ----- --------------------------------------------
1 + 789
1 + 456
1 - 239
2 + 789
2 + 456
2 - 239
3 - 789
3 + 456
3 - 239
Here is a method that does not use regular expressions and can handle multiple input rows:
SQL Fiddle
Oracle 11g R2 Schema Setup:
CREATE TABLE table_name ( id, list ) AS
SELECT 1, '789+456-239' FROM DUAL UNION ALL
SELECT 2, '-123' FROM DUAL UNION ALL
SELECT 3, '+456' FROM DUAL UNION ALL
SELECT 4, '789' FROM DUAL;
Query 1:
WITH bounds ( id, list, start_pos, end_pos, lvl ) AS (
SELECT id,
list,
1,
CASE
WHEN INSTR( list, '+', 2 ) = 0 THEN INSTR( list, '-', 2 )
WHEN INSTR( list, '-', 2 ) = 0 THEN INSTR( list, '+', 2 )
ELSE LEAST( INSTR( list, '+', 2 ), INSTR( list, '-', 2 ) )
END,
1
FROM table_name
UNION ALL
SELECT id,
list,
end_pos,
CASE
WHEN INSTR( list, '+', end_pos + 1 ) = 0 THEN INSTR( list, '-', end_pos + 1 )
WHEN INSTR( list, '-', end_pos + 1 ) = 0 THEN INSTR( list, '+', end_pos + 1 )
ELSE LEAST( INSTR( list, '+', end_pos + 1 ), INSTR( list, '-', end_pos + 1 ) )
END,
lvl + 1
FROM bounds
WHERE end_pos > 0
)
SELECT id,
DECODE( SUBSTR( list, start_pos, 1 ), '-', '-', '+' ) AS sign,
SUBSTR(
list,
start_pos + DECODE( SUBSTR( list, start_pos, 1 ), '-', 1, '+', 1, 0 ),
DECODE( end_pos, 0, LENGTH( list ) + 1, end_pos ) - start_pos - DECODE( SUBSTR( list, start_pos, 1 ), '-', 1, '+', 1, 0 )
) AS item,
lvl
FROM bounds
ORDER BY id, lvl
Results:
| ID | SIGN | ITEM | LVL |
|----|------|------|-----|
| 1 | + | 789 | 1 |
| 1 | + | 456 | 2 |
| 1 | - | 239 | 3 |
| 2 | - | 123 | 1 |
| 3 | + | 456 | 1 |
| 4 | + | 789 | 1 |

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/ |