PLSQL find ascii symbol in blob - sql

I need to find specific ascii symbols in blob, eg. non breaking space (160).
I'm stuck with this code
select *
from tableName
where dbms_lob.instr (blobColumn,
NOT SURE WHAT I SHOULD LOOK FOR,
1,
1
) > 0
I've tried to pass char(160), hextoraw, but none of them worked.

The documented syntax is:
DBMS_LOB.INSTR (
lob_loc IN BLOB,
pattern IN RAW,
offset IN INTEGER := 1,
nth IN INTEGER := 1)
RETURN INTEGER;
So you can use:
select *
from tableName
where dbms_lob.instr(blobColumn, UTL_RAW.CAST_TO_RAW(CHR(160)), 1, 1) > 0
or:
select *
from tableName
where dbms_lob.instr(blobColumn, HEXTORAW('A0'), 1, 1) > 0
fiddle

Related

Character position number in string

Try to get from the value - "some kind of tex #123fpe2"
all characters ​​after the # sign
Prepared the code below
but the problem is that not always the length of the value after the sign # 8 characters
select REVERSE(Left(Reverse(vValue), 8)),
(
select vValue from Runtime.dbo.Live where TagName = 'CurrentBaseRecipeName32000000io') as ValueForCheck,
from Runtime.dbo.Live where TagName = 'CurrentBaseRecipeName32000000io'
Getting error:
select REVERSE(Left(Reverse(vValue), POSITION('#' IN vValue))),
(
select vValue ...
select REVERSE(Left(Reverse(vValue), 8)),
(
select vValue from Runtime.dbo.Live where TagName = 'CurrentBaseRecipeName32000000io') as ValueForCheck,
from Runtime.dbo.Live where TagName = 'CurrentBaseRecipeName32000000io'
If you are sure that there is only 1 # in the string, or if there are more than 1 and you want the part of the string after the last occurrence of #, then you can use SUBSTRING_INDEX():
SELECT SUBSTRING_INDEX('some kind of tex #123fpe2', '#', -1);
will return:
123fpe2
I assume this is MySQL because of the use of POSITION in the question. Use INSTR to get the position of the '#' and then SUBSTRING to return everything after the index returned by INSTR + 1
SELECT SUBSTRING(vValue, INSTR(vValue, '#') + 1) FROM Runtime.dbo.Live
WHERE ...
Just in case, here is a version for SQL Server using RIGHT
SELECT RIGHT(vValue, LEN(vValue) - CHARINDEX('#', vValue)) FROM Runtime.dbo.Live
WHERE ...

Oracle - need to extract text between given strings

Example - need to extract everything between "Begin begin" and "End end". I tried this way:
with phrases as (
select 'stackoverflow is awesome. Begin beginHello, World!End end It has everything!' as phrase
from dual
)
select regexp_replace(phrase
, '([[:print:]]+Begin begin)([[:print:]]+)(End end[[:print:]]+)', '\2')
from phrases
;
Result: Hello, World!
However it fails if my text contains new line characters. Any tip how to fix this to allow extracting text containing also new lines?
[edit]How does it fail:
with phrases as (
select 'stackoverflow is awesome. Begin beginHello,
World!End end It has everything!' as phrase
from dual
)
select regexp_replace(phrase
, '([[:print:]]+Begin begin)([[:print:]]+)(End end[[:print:]]+)', '\2')
from phrases
;
Result:
stackoverflow is awesome. Begin beginHello, World!End end It has
everything!
Should be:
Hello,
World!
[edit]
Another issue. Let's see to this sample:
WITH phrases AS (
SELECT 'stackoverflow is awesome. Begin beginHello,
World!End end It has everything!End endTESTESTESTES' AS phrase
FROM dual
)
SELECT REGEXP_REPLACE(phrase, '.+Begin begin(.+)End end.+', '\1', 1, 1, 'n')
FROM phrases;
Result:
Hello,
World!End end It has everything!
So it matches last occurence of end string and this is not what I want. Subsgtring should be extreacted to first occurence of my label, so result should be:
Hello,
World!
Everything after first occurence of label string should be ignored. Any ideas?
I'm not that familiar with the POSIX [[:print:]] character class but I got your query functioning using the wildcard .. You need to specify the n match parameter in REGEXP_REPLACE() so that . can match the newline character:
WITH phrases AS (
SELECT 'stackoverflow is awesome. Begin beginHello,
World!End end It has everything!' AS phrase
FROM dual
)
SELECT REGEXP_REPLACE(phrase, '.+Begin begin(.+)End end.+', '\1', 1, 1, 'n')
FROM phrases;
I used the \1 backreference as I didn't see the need to capture the other groups from the regular expression. It might also be a good idea to use the * quantifier (instead of +) in case there is nothing preceding or following the delimiters. If you want to capture all of the groups then you can use the following:
WITH phrases AS (
SELECT 'stackoverflow is awesome. Begin beginHello,
World!End end It has everything!' AS phrase
FROM dual
)
SELECT REGEXP_REPLACE(phrase, '(.+Begin begin)(.+)(End end.+)', '\2', 1, 1, 'n')
FROM phrases;
UPDATE - FYI, I tested with [[:print:]] and it doesn't work. This is not surprising since [[:print:]] is supposed to match printable characters. It doesn't match anything with an ASCII value below 32 (a space). You need to use ..
UPDATE #2 - per update to question - I don't think a regex will work the way you want it to. Adding the lazy quantifier to (.+) has no effect and Oracle regular expressions don't have lookahead. There are a couple of things you might do, one is to use INSTR() and SUBSTR():
WITH phrases AS (
SELECT 'stackoverflow is awesome. Begin beginHello,
World!End end It has everything!End endTESTTESTTEST' AS phrase
FROM dual
)
SELECT SUBSTR(phrase, str_start, str_end - str_start) FROM (
SELECT INSTR(phrase, 'Begin begin') + LENGTH('Begin begin') AS str_start
, INSTR(phrase, 'End end') AS str_end, phrase
FROM phrases
);
Another is to combine INSTR() and SUBSTR() with a regular expression:
WITH phrases AS (
SELECT 'stackoverflow is awesome. Begin beginHello,
World!End end It has everything!End endTESTTESTTEST' AS phrase
FROM dual
)
SELECT REGEXP_REPLACE(SUBSTR(phrase, 1, INSTR(phrase, 'End end') + LENGTH('End end')), '.+Begin begin(.+)End end.+', '\1', 1, 1, 'n')
FROM phrases;
Try this regex:
([[:print:]]+Begin begin)(.+?)(End end[[:print:]]+)
Sample usage:
SELECT regexp_replace(
phrase ,
'([[:print:]]+Begin begin)(.+?)(End end[[:print:]]+)',
'\2',
1, -- Start at the beginning of the phrase
0, -- Replace ALL occurences
'n' -- Let dot meta character matches new line character
)
FROM
(SELECT 'stackoverflow is awesome. Begin beginHello, '
|| chr(10)
|| ' World!End end It has everything!' AS phrase
FROM DUAL
)
The dot meta character (.) matches any character in the database character set and the new line character. However, when regexp_replace is called, the match_parameter must contain n switch for dot matches new lines.
In order to get your second option to work you need to add [[:space:][:print:]]* as follows:
with phrases as (
select 'stackoverflow is awesome. Begin beginHello,
World!End end It has everything!' as phrase
from dual
)
select regexp_replace(phrase
, '([[:print:]]+Begin begin)([[:print:]]+[[:space:][:print:]]*)(End end[[:print:]]+)', '\2')
from phrases
;
But still it will break if you have more \n, for instance it won't work for
with phrases as (
select 'stackoverflow is awesome. Begin beginHello,
World!End end
It has everything!' as phrase
from dual
)
select regexp_replace(phrase
, '([[:print:]]+Begin begin)([[:print:]]+[[:space:][:print:]]*)(End end[[:print:]]+)', '\2')
from phrases
;
Then you need to add
with phrases as (
select 'stackoverflow is awesome. Begin beginHello,
World!End end
It has everything!' as phrase
from dual
)
select regexp_replace(phrase
, '([[:print:]]+Begin begin)([[:print:]]+[[:space:][:print:]]*)(End end[[:print:]]+[[:space:][:print:]]*)', '\2')
from phrases
;
The problem of regex is that you might have to scope the variations and create a rule that match all of them. If something falls out of your scope, you'll have to visit the regex and add the new exception.
You can find extra info here.
Description.........: This is a function similar to the one that was available from PRIME Computers
back in the late 80/90's. This function will parse out a segment of a string
based on a supplied delimiter. The delimiters can be anything.
Usage:
Field(i_string =>'This.is.a.cool.function'
,i_deliiter => '.'
,i_start_pos => 2
,i_occurrence => 2)
Return value = is.a
FUNCTION field(i_string VARCHAR2
,i_delimiter VARCHAR2
,i_occurance NUMBER DEFAULT 1
,i_return_instances NUMBER DEFAULT 1) RETURN VARCHAR2 IS
--
v_delimiter VARCHAR2(1);
n_end_pos NUMBER;
n_start_pos NUMBER := 1;
n_delimiter_pos NUMBER;
n_seek_pos NUMBER := 1;
n_tbl_index PLS_INTEGER := 0;
n_return_counter NUMBER := 0;
v_return_string VARCHAR2(32767);
TYPE tbl_type IS TABLE OF VARCHAR2(4000) INDEX BY PLS_INTEGER;
tbl tbl_type;
e_no_delimiters EXCEPTION;
v_string VARCHAR2(32767) := i_string || i_delimiter;
BEGIN
BEGIN
LOOP
----------------------------------------
-- Search for the delimiter in the
-- string
----------------------------------------
n_delimiter_pos := instr(v_string, i_delimiter, n_seek_pos);
--
IF n_delimiter_pos = length(v_string) AND n_tbl_index = 0 THEN
------------------------------------------
-- The delimiter you are looking for is
-- not in this string.
------------------------------------------
RAISE e_no_delimiters;
END IF;
--
EXIT WHEN n_delimiter_pos = 0;
n_start_pos := n_seek_pos;
n_end_pos := n_delimiter_pos - n_seek_pos;
n_seek_pos := n_delimiter_pos + 1;
--
n_tbl_index := n_tbl_index + 1;
-----------------------------------------------
-- Store the segments of the string in a tbl
-----------------------------------------------
tbl(n_tbl_index) := substr(i_string, n_start_pos, n_end_pos);
END LOOP;
----------------------------------------------
-- Prepare the results for return voyage
----------------------------------------------
v_delimiter := NULL;
FOR a IN tbl.first .. tbl.last LOOP
IF a >= i_occurance AND n_return_counter < i_return_instances THEN
v_return_string := v_return_string || v_delimiter || tbl(a);
v_delimiter := i_delimiter;
n_return_counter := n_return_counter + 1;
END IF;
END LOOP;
--
EXCEPTION
WHEN e_no_delimiters THEN
v_return_string := i_string;
END;
RETURN TRIM(v_return_string);
END;

How do I expand a string with wildcards in PL/SQL using string functions

I have a column, which stores a 4 character long string with 4 or less wild characters (for eg. ????, ??01', 0??1 etc). For each such string like 0??1 I have to insert into another table values 0001 to 0991; for the string ??01, values will be be 0001 to 9901; for string ???? values will be 0000 to 9999 and so on.
How could I accomplish this using PL/SQL and string functions?
EDIT
The current code is:
declare
v_rule varchar2(50) := '????52132';
v_cc varchar2(50);
v_nat varchar2(50);
v_wild number;
n number;
begin
v_cc := substr(v_rule,1,4);
v_nat := substr(v_rule,5);
dbms_output.put_line (v_cc || ' '|| v_nat);
if instr(v_cc, '????') <> 0 then
v_wild := 4;
end if;
n := power(10,v_wild);
for i in 0 .. n - 1 loop
dbms_output.put_line(substr(lpad(to_char(i),v_wild,'0' ),0,4));
end loop;
end;
/
Would something like the following help?
BEGIN
FOR source_row IN (SELECT rule FROM some_table)
LOOP
INSERT INTO some_other_table (rule_match)
WITH numbers AS (SELECT LPAD(LEVEL - 1, 4, '0') AS num FROM DUAL CONNECT BY LEVEL <= 10000)
SELECT num FROM numbers WHERE num LIKE REPLACE(source_row.rule, '?', '_');
END LOOP;
END;
/
This assumes you have a table called some_table with a column rule, which contains text such as ??01, 0??1 and ????. It inserts into some_other_table all numbers from 0000 to 9999 that match these wild-carded patterns.
The subquery
SELECT LPAD(LEVEL - 1, 4, '0') AS num FROM DUAL CONNECT BY LEVEL <= 10000)
generates all numbers in the range 0000 to 9999. We then filter out from this list of numbers any that match this pattern, using LIKE. Note that _ is the single-character wildcard when using LIKE, not ?.
I set this up with the following data:
CREATE TABLE some_table (rule VARCHAR2(4));
INSERT INTO some_table (rule) VALUES ('??01');
INSERT INTO some_table (rule) VALUES ('0??1');
INSERT INTO some_table (rule) VALUES ('????');
COMMIT;
CREATE TABLE some_other_table (rule_match VARCHAR2(4));
After running the above PL/SQL block, the table some_other_table had 10200 rows in it, all the numbers that matched all three of the patterns given.
Replace * to %, ? to _ and use LIKE clause with resulting values.
To expand on #Oleg Dok's answer, which uses the little known fact that an underscore means the same as % but only for a single character and using PL\SQL I think the following is the simplest way to do it. A good description of how to use connect by is here.
declare
cursor c_min_max( Crule varchar2 ) is
select to_number(min(numb)) as min_n, to_number(max(numb)) as max_n
from ( select '0000' as numb
from dual
union
select lpad(level, 4, '0') as numb
from dual
connect by level <= 9999 )
where to_char(numb) like replace(Crule, '?', '_');
t_mm c_min_max%rowtype;
l_rule varchar2(4) := '?091';
begin
open c_min_max(l_rule);
fetch c_min_max
into t_mm;
close c_min_max;
for i in t_mm.min_n .. t_mm.max_n loop
dbms_output.put_line(lpad(i, 4, '0'));
end loop;
end;
/

Parameters in query with in clause?

I want to use parameter for query like this :
SELECT * FROM MATABLE
WHERE MT_ID IN (368134, 181956)
so I think about this
SELECT * FROM MATABLE
WHERE MT_ID IN (:MYPARAM)
but it doesn't work...
Is there a way to do this ?
I actually use IBX and Firebird 2.1
I don't know how many parameters in IN clause.
For whom ever is still interested. I did it in Firebird 2.5 using another stored procedure inspired by this post.
How to split comma separated string inside stored procedure?
CREATE OR ALTER PROCEDURE SPLIT_STRING (
ainput varchar(8192))
RETURNS (
result varchar(255))
AS
DECLARE variable lastpos integer;
DECLARE variable nextpos integer;
DECLARE variable tempstr varchar(8192);
BEGIN
AINPUT = :AINPUT || ',';
LASTPOS = 1;
NEXTPOS = position(',', :AINPUT, LASTPOS);
WHILE (:NEXTPOS > 1) do
BEGIN
TEMPSTR = substring(:AINPUT from :LASTPOS for :NEXTPOS - :LASTPOS);
RESULT = :TEMPSTR;
LASTPOS = :NEXTPOS + 1;
NEXTPOS = position(',', :AINPUT, LASTPOS);
suspend;
END
END
When you pass the SP the following list
CommaSeperatedList = 1,2,3,4
and call
SELECT * FROM SPLIT_STRING(:CommaSeperatedList)
the result will be :
RESULT
1
2
3
4
And can be used as follows:
SELECT * FROM MyTable where MyKeyField in ( SELECT * FROM SPLIT_STRING(:CommaSeperatedList) )
I ended up using a global temporary table in Firebird, inserting parameter values first and to retrieve results I use a regular JOIN instead of a WHERE ... IN clause. The temporary table is transaction-specific and cleared on commit (ON COMMIT DELETE ROWS).
Maybe you should wite it like this:
SELECT * FROM MATABLE
WHERE MT_ID IN (:MYPARAM1 , :MYPARAM2)
I don't think it's something that can be done. Are there any particular reason why you don't want to build the query yourself?
I've used this method a couple of times, it doesn't use parameters though. It uses a stringlist and it's property DelimitedText. You create a IDList and populate it with your IDs.
Query.SQL.Add(Format('MT_ID IN (%s)', [IDList.DelimitedText]));
You might also be interested in reading the following:
http://www.sommarskog.se/dynamic_sql.html
and
http://www.sommarskog.se/arrays-in-sql-2005.html
Covers dynamic sql with 'in' clauses and all sorts. Very interesting.
Parameters are placeholders for single values, that means that an IN clause, that accepts a comma delimited list of values, cannot be used with parameters.
Think of it this way: wherever I place a value, I can use a parameter.
So, in a clause like: IN (:param)
I can bind the variable to a value, but only 1 value, eg: IN (4)
Now, if you consider an "IN clause value expression", you get a string of values: IN (1, 4, 6) -> that's 3 values with commas between them. That's part of the SQL string, not part of a value, which is why it cannot be bound by a parameter.
Obviously, this is not what you want, but it's the only thing possible with parameters.
The answer from Yurish is a solution in two out of three cases:
if you have a limited number of items to be added to your in clause
or, if you are willing to create parameters on the fly for each needed element (you don't know the number of elements in design time)
But if you want to have arbitrary number of elements, and sometimes no elements at all, then you can generate SLQ statement on the fly. Using format helps.
SELECT * FROM MATABLE
WHERE MT_ID IN (:MYPARAM) instead of using MYPARAM with :, use parameter name.
like SELECT * FROM MATABLE
WHERE MT_ID IN (SELECT REGEXP_SUBSTR(**MYPARAM,'[^,]+', 1, LEVEL)
FROM DUAL
CONNECT BY REGEXP_SUBSTR(MYPARAM, '[^,]+', 1, LEVEL) IS NOT NULL))**
MYPARAM- '368134,181956'
If you are using Oracle, then you should definitely check out Tom Kyte's blog post on exactly this subject (link).
Following Mr Kyte's lead, here is an example:
SELECT *
FROM MATABLE
WHERE MT_ID IN
(SELECT TRIM(substr(text, instr(text, sep, 1, LEVEL) + 1,
instr(text, sep, 1, LEVEL + 1) -
instr(text, sep, 1, LEVEL) - 1)) AS token
FROM (SELECT sep, sep || :myparam || sep AS text
FROM (SELECT ',' AS sep
FROM dual))
CONNECT BY LEVEL <= length(text) - length(REPLACE(text, sep, '')) - 1)
Where you would bind :MYPARAM to '368134,181956' in your case.
Here is a technique I have used in the past to get around that 'IN' statement problem. It builds an 'OR' list based on the amount of values specified with parameters (unique). Then all I had to do was add the parameters in the order they appeared in the supplied value list.
var
FilterValues: TStringList;
i: Integer;
FilterList: String;
Values: String;
FieldName: String;
begin
Query.SQL.Text := 'SELECT * FROM table WHERE '; // set base sql
FieldName := 'some_id'; // field to filter on
Values := '1,4,97'; // list of supplied values in delimited format
FilterList := '';
FilterValues := TStringList.Create; // will get the supplied values so we can loop
try
FilterValues.CommaText := Values;
for i := 0 to FilterValues.Count - 1 do
begin
if FilterList = '' then
FilterList := Format('%s=:param%u', [FieldName, i]) // build the filter list
else
FilterList := Format('%s OR %s=:param%u', [FilterList, FieldName, i]); // and an OR
end;
Query.SQL.Text := Query.SQL.Text + FilterList; // append the OR list to the base sql
// ShowMessage(FilterList); // see what the list looks like.
if Query.ParamCount <> FilterValues.Count then
raise Exception.Create('Param count and Value count differs.'); // check to make sure the supplied values have parameters built for them
for i := 0 to FilterValues.Count - 1 do
begin
Query.Params[i].Value := FilterValues[i]; // now add the values
end;
Query.Open;
finally
FilterValues.Free;
end;
Hope this helps.
There is one trick to use reversed SQL LIKE condition.
You pass the list as string (VARCHAR) parameter like '~12~23~46~567~'
Then u have query like
where ... :List_Param LIKE ('%~' || CAST( NumField AS VARCHAR(20)) || '~%')
CREATE PROCEDURE TRY_LIST (PARAM_LIST VARCHAR(255)) RETURNS (FIELD1....)
AS
BEGIN
/* Check if :PARAM_LIST begins with colon "," and ands with colon ","
the list should look like this --> eg. **",1,3,4,66,778,33,"**
if the format of list is right then GO if not just add then colons
*/
IF (NOT SUBSTRING(:PARAM_LIST FROM 1 FOR 1)=',') THEN PARAM_LIST=','||PARAM_LIST;
IF (NOT SUBSTRING(:PARAM_LIST FROM CHAR_LENGTH(:PARAM_LIST) FOR 1)=',') THEN PARAM_LIST=PARAM_LIST||',';
/* Now you are shure thet :PARAM_LIST format is correct */
/ * NOW ! */
FOR SELECT * FROM MY_TABLE WHERE POSITION(','||MY_FIELD||',' in :PARAM_LIST)>0
INTO :FIELD1, :FIELD2 etc... DO
BEGIN
SUSPEND;
END
END
How to use it.
SELECT * FROM TRY_LIST('3,4,544,87,66,23')
or SELECT * FROM TRY_LIST(',3,4,544,87,66,23,')
if the list have to be longer then 255 characters then just change the part of header f.eg. like PARAM_LIST VARCHAR(4000)

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

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 )