Oracle: How can I implement a "natural" order-by in a SQL query? - sql

e.g,
foo1
foo2
foo10
foo100
rather than
foo1
foo10
foo100
foo2
Update: not interested in coding the sort myself (although that's interesting in its own right), but having the database to do the sort for me.

You can use functions in your order-by clause. In this case,
you can split the non-numeric and numeric portions of the
field and use them as two of the ordering criteria.
select * from t
order by to_number(regexp_substr(a,'^[0-9]+')),
to_number(regexp_substr(a,'[0-9]+$')),
a;
You can also create a function-based index to support this:
create index t_ix1
on t (to_number(regexp_substr(a, '^[0-9]+')),
to_number(regexp_substr(a, '[0-9]+$')),
a);

For short strings, small number of numerics
If number of "numerics" and the maximum length are limited, there is a regexp-based solution.
The idea is:
Pad all numerics with 20 zeroes
Remove excessive zeroes using another regexp. This might be slow due to regexp backtracking.
Assumptions:
Maximum length of numerics is known beforehand (e.g. 20)
All the numerics can be padded (in other words, lpad('1 ', 3000, '1 ') will fail due do unable to fit padded numerics into varchar2(4000))
The following query is optimized for "short numerics" case (see *?) and it takes 0.4 seconds. However, when using such approach, you need to predefine padding length.
select * from (
select dbms_random.string('X', 30) val from xmltable('1 to 1000')
)
order by regexp_replace(regexp_replace(val, '(\d+)', lpad('0', 20, '0')||'\1')
, '0*?(\d{21}(\D|$))', '\1');
"Clever" approach
Even though separate natural_sort function can be handy, there is a little-known trick to do that in pure SQL.
Key ideas:
Strip leading zeroes from all the numerics so 02 is ordered between 1 and 3: regexp_replace(val, '(^|\D)0+(\d+)', '\1\2'). Note: this might result in "unexpected" sorting of 10.02 > 10.1 (since 02 is converted to 2), however there is no single answer how things like 10.02.03 should be sorted
Convert " to "" so text with quotes works properly
Convert input string to comma delimited format: '"'||regexp_replace(..., '([^0-9]+)', '","\1","')||'"'
Convert csv to the list of items via xmltable
Augment numeric-like items so string sort works properly
Use length(length(num))||length(num)||num instead of lpad(num, 10, '0') as the latter is less compact and does not support 11+ digit numbers.
Note:
Response time is something like 3-4 seconds for sorting list of 1000 random strings of length 30 (the generation of the random strings takes 0.2 sec itself).
The main time consumer is xmltable that splits text into rows.
If using PL/SQL instead of xmltable to split string into rows the response time reduces to 0.4sec for the same 1000 rows.
The following query performs natural sort of 100 random alpha-numeric strings (note: it produces wrong results in Oracle 11.2.0.4 and it works in 12.1.0.2):
select *
from (
select (select listagg(case when regexp_like(w, '^[0-9]')
then length(length(w))||length(w)||w else w
end
) within group (order by ord)
from xmltable(t.csv columns w varchar2(4000) path '.'
, ord for ordinality) q
) order_by
, t.*
from (
select '"'||regexp_replace(replace(
regexp_replace(val, '(^|\D)0+(\d+)', '\1\2')
, '"', '""')
, '([^0-9]+)', '","\1","')||'"' csv
, t.*
from (
select dbms_random.string('X', 30) val from xmltable('1 to 100')
) t
) t
) t
order by order_by;
The fun part is this order by can be expressed without subqueries, so it is a handy tool to make your reviewer crazy (it works in both 11.2.0.4 and 12.1.0.2):
select *
from (select dbms_random.string('X', 30) val from xmltable('1 to 100')) t
order by (
select listagg(case when regexp_like(w, '^[0-9]')
then length(length(w))||length(w)||w else w
end
) within group (order by ord)
from xmltable('$X'
passing xmlquery(('"'||regexp_replace(replace(
regexp_replace(t.val, '(^|\D)0+(\d+)', '\1\2')
, '"', '""')
, '([^0-9]+)', '","\1","')||'"')
returning sequence
) as X
columns w varchar2(4000) path '.', ord for ordinality) q
);

I use the following function to 0-pad all sequences of digits shorter than 10 that could be found in the value, so that the total length of each to become 10 digits. It is compatible even with mixed sets of values that have one, many or none sequences of digits in them.
CREATE OR replace function NATURAL_ORDER(
P_STR varchar2
) return varchar2
IS
/** --------------------------------------------------------------------
Replaces all sequences of numbers shorter than 10 digits by 0-padded
numbers that exactly 10 digits in length. Usefull for ordering-by
using NATURAL ORDER algorithm.
*/
l_result varchar2( 32700 );
l_len integer;
l_ix integer;
l_end integer;
begin
l_result := P_STR;
l_len := LENGTH( l_result );
l_ix := 1;
while l_len > 0 loop
l_ix := REGEXP_INSTR( l_result, '[0-9]{1,9}', l_ix, 1, 0 );
EXIT when l_ix = 0;
l_end := REGEXP_INSTR( l_result, '[^0-9]|$', l_ix, 1, 0 );
if ( l_end - l_ix >= 10 ) then
l_ix := l_end;
else
l_result := substr( l_result, 1, l_ix - 1 )
|| LPAD( SUBSTR( l_result, l_ix, l_end-l_ix ), 10, '0' )
|| substr( l_result, l_end )
;
l_ix := l_ix + 10;
end if;
end loop;
return l_result;
end;
/
For example:
select 'ABC' || LVL || 'DEF' as STR
from (
select LEVEL as LVL
from DUAL
start with 1=1
connect by LEVEL <= 35
)
order by NATURAL_ORDER( STR )

Related

Can we sort the characters of a string in SQL oracle? [duplicate]

I'm looking for a function that would sort chars in varchar2 alphabetically.
Is there something built-in into oracle that I can use or I need to create custom in PL/SQL ?
From an answer at http://forums.oracle.com/forums/thread.jspa?messageID=1791550 this might work, but don't have 10g to test on...
SELECT MIN(permutations)
FROM (SELECT REPLACE (SYS_CONNECT_BY_PATH (n, ','), ',') permutations
FROM (SELECT LEVEL l, SUBSTR ('&col', LEVEL, 1) n
FROM DUAL
CONNECT BY LEVEL <= LENGTH ('&col')) yourtable
CONNECT BY NOCYCLE l != PRIOR l)
WHERE LENGTH (permutations) = LENGTH ('&col')
In the example col is defined in SQL*Plus, but if you make this a function you can pass it in, or could rework it to take a table column directly I suppose.
I'd take that as a start point rather than a solution; the original question was about anagrams so it's designed to find all permutations, so something similar but simplified might be possible. I suspect this doesn't scale very well for large values.
So eventually I went PL/SQL route, because after searching for some time I realized that there is no build-in function that I can use.
Here is what I came up with. Its based on the future of associative array which is that Oracle keeps the keys in sorted order.
create or replace function sort_chars(p_string in varchar2) return varchar deterministic
as
rv varchar2(4000);
ch varchar2(1);
type vcArray is table of varchar(4000) index by varchar2(1);
sorted vcArray;
key varchar2(1);
begin
for i in 1 .. length(p_string)
loop
ch := substr(p_string, i, 1);
if (sorted.exists(ch))
then
sorted(ch) := sorted(ch) || ch;
else
sorted(ch) := ch;
end if;
end loop;
rv := '';
key := sorted.FIRST;
WHILE key IS NOT NULL LOOP
rv := rv || sorted(key);
key := sorted.NEXT(key);
END LOOP;
return rv;
end;
Simple performance test:
set timing on;
create table test_sort_fn as
select t1.object_name || rownum as test from user_objects t1, user_objects t2;
select count(distinct test) from test_sort_fn;
select count (*) from (select sort_chars(test) from test_sort_fn);
Table created.
Elapsed: 00:00:01.32
COUNT(DISTINCTTEST)
-------------------
384400
1 row selected.
Elapsed: 00:00:00.57
COUNT(*)
----------
384400
1 row selected.
Elapsed: 00:00:00.06
You could use the following query:
select listagg(letter)
within group (order by UPPER(letter), ASCII(letter) DESC)
from
(
select regexp_substr('gfedcbaGFEDCBA', '.', level) as letter from dual
connect by regexp_substr('gfedcbaGFEDCBA', '.', level) is not null
);
The subquery splits the string into records (single character each) using regexp_substr, and the outer query merges the records into one string using listagg, after sorting them.
Here you should be careful, because alphabetical sorting depends on your database configuration, as Cine pointed.
In the above example the letters are sorted ascending "alphabetically" and descending by ascii code, which - in my case - results in "aAbBcCdDeEfFgG".
The result in your case may be different.
You may also sort the letters using nlssort - it would give you better control of the sorting order, as you would get independent of your database configuration.
select listagg(letter)
within group (order by nlssort(letter, 'nls_sort=german')
from
(
select regexp_substr('gfedcbaGFEDCBA', '.', level) as letter from dual
connect by regexp_substr('gfedcbaGFEDCBA', '.', level) is not null
);
The query above would give you also "aAbBcCdDeEfFgG", but if you changed "german" to "spanish", you would get "AaBbCcDdEeFfGg" instead.
You should remember that there is no common agreement what "alphabetically" means. It all depends on which country it is, and who is looking at your data and what context it is in.
For instance in DK, there are a large number of different sortings of a,aa,b,c,æ,ø,å
per the alphabet: a,aa,b,c,æ,ø,å
for some dictionary: a,aa,å,b,c,æ,ø
for other dictionaries: a,b,c,æ,ø,aa,å
per Microsoft standard: a,b,c,æ,ø,aa,å
check out http://www.siao2.com/2006/04/27/584439.aspx for more info. Which also happens to be a great blog for issues as these.
Assuming you don't mind having the characters returned 1 per row:
select substr(str, r, 1) X from (
select 'CAB' str,
rownum r
from dual connect by level <= 4000
) where r <= length(str) order by X;
X
=
A
B
C
For people using Oracle 10g, select listagg within group won't work. The accepted answer does work, but it generates every possible permutation of the input string, which results in terrible performance - my Oracle database struggles with an input string only 10 characters long.
Here is another alternative working for Oracle 10g. It's similar to Jeffrey Kemp's answer, only the result isn't splitted into rows:
select replace(wm_concat(ch), ',', '') from (
select substr('CAB', level, 1) ch from dual
connect by level <= length('CAB')
order by ch
);
-- output: 'ABC'
wm_concat simply concatenates records from different rows into a single string, using commas as separator (that's why we are also doing a replace later).
Please note that, if your input string had commas, they will be lost. Also, wm_concat is an undocumented feature, and according to this answer it has been removed in Oracle 12c. Use it only if you're stuck with 10g and don't have a better option (such as listagg, if you can use 11g instead).
From Oracle 12, you can use:
SELECT *
FROM table_name t
CROSS JOIN LATERAL (
SELECT LISTAGG(SUBSTR(t.value, LEVEL, 1), NULL) WITHIN GROUP (
ORDER BY SUBSTR(t.value, LEVEL, 1)
) AS ordered_value
FROM DUAL
CONNECT BY LEVEL <= LENGTH(t.value)
)
Which, for the sample data:
CREATE TABLE table_name (value) AS
SELECT 'ZYX' FROM DUAL UNION ALL
SELECT 'HELLO world' FROM DUAL UNION ALL
SELECT 'aæøåbcæøåaæøå' FROM DUAL;
Outputs:
VALUE
ORDERED_VALUE
ZYX
XYZ
HELLO world
EHLLOdlorw
aæøåbcæøåaæøå
aabcåååæææøøø
If you want to change how the letters are sorted then use NLSSORT:
SELECT *
FROM table_name t
CROSS JOIN LATERAL (
SELECT LISTAGG(SUBSTR(t.value, LEVEL, 1), NULL) WITHIN GROUP (
ORDER BY NLSSORT(SUBSTR(t.value, LEVEL, 1), 'NLS_SORT = BINARY_AI')
) AS ordered_value
FROM DUAL
CONNECT BY LEVEL <= LENGTH(t.value)
)
Outputs:
VALUE
ORDERED_VALUE
ZYX
XYZ
HELLO world
dEHLLlOorw
aæøåbcæøåaæøå
aaåååbcøøøæææ
db<>fiddle here

Oracle SQL Developer Query on recommended password setup

Setting up a random password for user using
select
dbms_random.string('L',2) || dbms_random.string('X',6) || '1!' as deflvrpwd,
'${access_request_cri_acc_cas9}' as ACNTDN
from dual
New requirement
New Hire Details:
Name :John Doe
Region: America
WDID : 876214
WDID Reverse and split
Region in the middle with the letter A replaced with # symbol
Should read if we follow your formula.
= 412#meric#s678
Please suggest attribute are same as mentioned.
Thank You
Here's one option; read comments within code.
SQL> WITH
2 -- sample data
3 test (name, region, wdid)
4 AS
5 (SELECT 'John Doe', 'America', '876214' FROM DUAL),
6 temp
7 AS
8 -- reverse WDID; don't use undocumented REVERSE function
9 -- replace "A" (or "a") with "#" in REGION
10 ( SELECT name,
11 REPLACE (REPLACE (region, 'A', '#'), 'a', '#') new_region,
12 LISTAGG (letter, '') WITHIN GROUP (ORDER BY lvl DESC) new_wdid
13 FROM ( SELECT SUBSTR (wdid, LEVEL, 1) letter,
14 LEVEL lvl,
15 name,
16 region
17 FROM test
18 CONNECT BY LEVEL <= LENGTH (wdid))
19 GROUP BY name, region)
20 -- finally
21 SELECT SUBSTR (new_wdid, 1, 3) || new_region || SUBSTR (new_wdid, 4) AS result
22 FROM temp;
RESULT
--------------------------------------------------------------------------------
412#meric#678
SQL>
I don't know where s in your result comes from (this: 412#meric#s678).
There's a small cost in context switching between SQL and PL/SQL, but this doesn't sound like a high-volume or performance-critical thing, so you might find it cleaner to put the logic in a function:
create or replace function get_password (p_wdid varchar2, p_region varchar2)
return varchar2 as
l_split pls_integer;
l_password varchar2(30);
begin
-- split WDID halfway, but allow for odd lengths
l_split := floor(length(p_wdid)/2);
-- iterate over the WDID in reverse
for i in reverse 1..length(p_wdid) LOOP
-- when we reach the split point, append the modified region
if i = l_split then
l_password := l_password || translate(p_region, 'Aax', '##x');
end if;
-- append each WDID character, in reverse order
l_password := l_password || substr(p_wdid, i, 1);
end loop;
return l_password;
end get_password;
/
The WDID is reversed in a loop, and the modified region is included at the midway point, based on the length of the WDID value.
You can then do:
select get_password('876214', 'America') from dual;
GET_PASSWORD('876214','AMERICA')
--------------------------------
412#meric#678
This also doesn't have the unexplained 's' from the example in your question.
If you can't create a function but are on a recent version of Oracle then you can define an ad hoc function in a CTE:
with
function invert (p_input varchar2) return varchar2 as
l_output varchar2(30);
begin
for i in reverse 1..length(p_input) LOOP
l_output := l_output || substr(p_input, i, 1);
end loop;
return l_output;
end invert;
t (wdid, region) as (
select invert('876214'), translate('America', 'Aax', '##x')
from dual
)
select substr(wdid, 1, floor(length(wdid)/2))
|| region
|| substr(wdid, floor(length(wdid)/2) + 1)
from t;
which gets the same result. (I've called the function invert to avoid confusion with the undocumented reverse function.)
db<>fiddle showing both.

Get the words after specific character PLSQL

i want to get words after specific character in pl/sql
for example :
text = '2 - 99 - 7051B'
I want to see 7051B which means after second '-' to the last character.
function try ( text in varchar2 )
is begin
v_textout varchar2(100) := '';
--some process
return v_textout;
end;
No need for PL/SQL to extract the desired part. Two alternative methods use REGEXP_REPLACE() and REGEXP_SUBSTR() regular expression functions respectively
WITH t(text) AS
(
SELECT '2 - 99 - 7051B' FROM dual
)
SELECT REGEXP_REPLACE(text,'(.*- )(\S+)','\2') AS first_method,
REGEXP_SUBSTR(text,'[^- ]+$') AS second_method
FROM t;
FIRST_METHOD SECOND_METHOD
------------ -------------
7051B 7051B
Demo
where the spaces after dash characters are left deliberately according to the sample, and plus(+) stands for one or more occurences for the match .
I found the solution :
select substr(text , instr(text , '-', 1, 2) + 1, length(text))
from dual;

separate values from a string using SQL

I have this requirement where we need to separate values from a string the format of the is like
{Feature1=Value1} | {Feature2=Value2} | .. | {FeatureN=ValueN}
{12345=Gold}|{12346=Silver}
so need to separate features and values from the given srting..
To separate PIPE separated values i am using..
select *
from xmltable('r/c' passing xmltype('<r><c>' || replace('{12345=Gold}|{12346=Silver}','|','</c><c>') || '</c></r>')
columns new_str varchar2(30) path '.');
NEW_STR
------------------------------
{12345=Gold}
{12346=Silver}
I am writing a PLSQL block which iterate through each pipe separate values using above query.
I can store these values in PLSQL variable.
Now another task here is to get features and values from two above strings for this i write below SQL
select substr ('{12345=Gold}',2, instr('{12345=Gold}', '=')-2) features from dual;
FEATURES
----------------------
12345
SELECT SUBSTR('{12345=Gold}', instr('{12345=Gold}', '=')+1, LENGTH(substr ('{12345=Gold}', instr('{12345=Gold}', '=')+1, INSTR('{12345=Gold}', '}', 2)))-1) value FROM DUAL;
VALUE
--------------
Gold
So here i am able to get the features and values from a string......
I am looking for another or alternate SQL for my SQL's specially for the last one i find it complex function use so if you have any better idea for the above scenario then please Post !
Please ask if the scenario is not clear
MY DB is --
Oracle Database 12c Enterprise Edition Release 12.1.0.2.0 - 64bit Production
with line as (
select '{12345=Gold}|{12346=Silver}|{12399=Copper}' str from dual)
select substr (parse, 2, instr(parse,'=')-2) as feature
,substr (parse, instr(parse,'=')+1, length(parse)-instr(parse,'=')-1 ) as value
from
(select distinct regexp_substr(str, '[^|]+', 1, level) as parse
from line
connect by regexp_substr(str, '[^|]+', 1, level) is not null)
Here is a way using types and a handy built-in package called apex_util:
create type keyval_t is object (key varchar2(10), value varchar2(100));
/
create type keyval_tab_t is table of keyval_t;
/
create or replace package test_pkg is
function keyval_tab (p_keyval_string varchar2) return keyval_tab_t;
end;
/
create or replace package body test_pkg is
function keyval_tab (p_keyval_string varchar2) return keyval_tab_t
is
l_tab apex_application_global.vc_arr2;
l_tab2 apex_application_global.vc_arr2;
l_keyval_tab keyval_tab_t := keyval_tab_t();
l_str long;
begin
-- Split string at pipe delimiters
l_tab := apex_util.string_to_table (p_keyval_string, '|');
-- For each {key=value} pair
for i in 1..l_tab.count loop
l_str := l_tab(i);
-- Remove the {}
l_str := ltrim (l_str, '{ ');
l_str := rtrim (l_str, '} ');
-- Split string into key and value
l_tab2 := apex_util.string_to_table (l_str, '=');
if l_tab2.count = 2 then
l_keyval_tab.extend;
l_keyval_tab(i) := keyval_t (l_tab2(1), l_tab2(2));
else
-- ?? invalid string
null;
end if;
end loop;
return l_keyval_tab;
end;
end;
/
Now you can query:
SQL> select value from table(test_pkg.keyval_tab ('{12345=Gold}|{12346=Silver}'))
2* where key='12346';
VALUE
--------------------------------------------------------------------------------
Silver
SQL> select key from table(test_pkg.keyval_tab ('{12345=Gold}|{12346=Silver}'))
2 where value='Gold';
KEY
----------
12345
Use this query to get your expected output. Sorry if there are too many replace functions. But this is quite easier.
select EXTRACTVALUE (COLUMN_VALUE, '/row/Code') code,
EXTRACTVALUE (COLUMN_VALUE, '/row/Value') Value
from TABLE(XMLSEQUENCE(EXTRACT(XMLTYPE('<rowset><row>'||replace(replace(replace(replace('{12345=Gold}|{12346=Silver}','}|{','</Value></row><row><Code>'),'{','<Code>'),'=','</Code><Value>'),'}','</Value>')||'</row></rowset>'),'/rowset/row')));
If you already have separated columns into rows the work to do is fairly simple with REGEXP_REPLACE function.
Considering that your cols now is:
NEW_STR
--------------
{12345=Gold}
{12346=Silver}
You can do this sql to transform it into two different columns:
select regexp_replace( col, '\{(\d+)=\w+\}', '\1' ) as feature,
regexp_replace( col, '\{\d+=(\w+)\}', '\1' ) as value
from testTable
Transform that into a view and then just select with columns as you like:
create or replace view testView as
select regexp_replace( col, '\{(\d+)=\w+\}', '\1' ) as feature,
regexp_replace( col, '\{\d+=(\w+)\}', '\1' ) as value
from testTable
Then just do:
select * from testView where feature = '12345'
Or
Select * from testView where value = 'Gold'
If you like to transform the feature value as a number just use the to_number function on that column as:
to_number(regexp_replace( col, '\{(\d+)=\w+\}', '\1' ))
Remember that in order to do this you must be absolute sure that it is only numbers on that, otherwise you will have conversion errors
You can also use a pivot table and REGEXP_SUBSTR
with MyStrings as
(select '{Feature1=Value1}|{Feature2=Value2}|{FeatureN=ValueN}' Str from dual
union all
select '{12345=Gold}|{12346=Silver}' from dual
)
,pivot as (
Select Rownum Pnum
From dual
Connect By Rownum <= 100
)
SELECT rownum rn
,REGEXP_SUBSTR (ms.Str,'[^|]+',1,pv.pnum) TXT
FROM MyStrings ms
,pivot pv
where REGEXP_SUBSTR (ms.Str,'[^|]+',1,pv.pnum) is not null

How to reverse a string after tokenizing it in SQL

I need to tokenize a string and reverse it in SQL. For example if the string is, 'L3:L2:L1:L0', i need to reverse it as 'L0:L1:L2:L3'. The tokenizing could be done using a delimiter ':' and then reverse it. Please suggest a Function in SQL for the same.
Thanks in advance,
Geetha
If possible, the best solution would be to change your data so that each value is stored in a different row.
If that doesn't work, you can create a PL/SQL function.
If you want a purely SQL solution, typically you'll have to split each value into multiple rows (cross join with an object table, or connect by level <= max number of items), and then re-aggregate the data using one of a dozen different methods (listagg, collect, stragg, xml, sys_connect_by_path, etc.)
Another SQL-only way is to use regular expressions. This is probably the fastest, but it only works with up to 9 items because Oracle only supports 9 back references:
--Get everything except the extra ':' at the end.
select substr(string, 1, length(string) - 1) string from
(
select regexp_replace(
--Add a delimter to the end so all items are the same
'L3:L2:L1:L0'||':'
--Non-greedy search for anything up to a : (I bet there's a better way to do this)
,'(.*?:)?(.*?:)?(.*?:)?(.*?:)?(.*?:)?(.*?:)?(.*?:)?(.*?:)?(.*?:)?(.*?:)?'
--Reverse the back-references
,'\9\8\7\6\5\4\3\2\1') string
from dual
);
Something like :
SELECT
REGEXP_REPLACE('L1:L2:L3',
'([[:alnum:]]{1,}):([[:alnum:]]{1,}):([[:alnum:]]{1,})',
'\3 \2 \1') "REGEXP_REPLACE"
from dual
But you might need to detail what constitutes a token.
Here is a solution using a PL/SQL pipelined function to split the elements:
create type t_str_array as table of varchar2(4000);
create or replace function split_str (p_str in varchar2,
p_separator in varchar2 := ':') return t_str_array pipelined
as
l_str varchar2(32000) := p_str || p_separator;
l_pos pls_integer;
begin
loop
l_pos := instr(l_str, p_separator);
exit when (nvl(l_pos,0) = 0);
pipe row (ltrim(rtrim(substr(l_str,1,l_pos-1))));
l_str := substr(l_str, l_pos+1);
end loop;
return;
end split_str;
Then you would use normal SQL to order the elements:
select * from table(split_str('L3:L2:L1:L0')) order by column_value
declare
s varchar2(1000) := 'L 1 0:L9:L8:L7:L6:L5:L4:L3:L2:L1:L0';
j number := length(s);
begin
for i in reverse 1..length(s) loop
if substr(s, i, 1) = ':' then
dbms_output.put(substr(s, i + 1, j - i) || ':');
j := i - 1;
end if;
end loop;
dbms_output.put_line(substr(s, 1, j));
end;
Convert elements in a CSV string into records, suppressing all NULLs:
SELECT REGEXP_SUBSTR( :csv,'[^,]+', 1, LEVEL ) AS element
FROM dual
CONNECT BY REGEXP_SUBSTR( :csv, '[^,]+', 1, LEVEL ) IS NOT NULL ;
Convert elements in a CSV string into records, preserving NULLs (but not order):
SELECT REGEXP_SUBSTR( :csv,'[^,]+', 1, LEVEL ) AS element
FROM dual
CONNECT BY LEVEL <= LENGTH( :csv ) - LENGTH( REPLACE( :CSV, ',' ) ) + 1 ;
Improving upon Kevan's answer, here is what I tried:
select listagg(TOKEN, ':') WITHIN GROUP (ORDER BY TOKEN_LEVEL DESC)
from
(SELECT REGEXP_SUBSTR( myStr,'[^:]+', 1, LEVEL ) AS TOKEN, LEVEL TOKEN_LEVEL
FROM dual
CONNECT BY REGEXP_SUBSTR( myStr, '[^:]+', 1, LEVEL ) IS NOT NULL);
Since you use Oracle it would be easy to generate a java stored procedure passing the string and then
split sting into array
loop array backwards and concate the resulting string
return the resulting string
this will be a small java code and not slower then pl/sql. but if you want to use pl/sql you can possibly also use DBMS_UTILITY.table_to_comma/.comma_to_table. But as the function name let assume -> you have to use "," as token.