Order versions as numbers - sql

I have a table with file-names and version with subversion of files separated by ..
FNAME, VERSION
A 0.0.10
B 10.12.412
-- For example
create table file_versions as
select chr(mod(level,13)+65) as fname
, decode(mod(level,99),0, '0',
mod(level,10)||'.'||mod(level,500)||'.'||mod(level,14)
)
as version
from dual connect by level < 1001;
I'd like to order files by version, but use versions as numbers
select fname, version from file_versions
order by fname, version
FNAME, VERSION
A 0.0.10
A 0.0.6
...
I'd like don't think about subversion level(there may be one number (0) or more(1.23.14123)). How should I write order by statement ?
I may write something like:
select fname, version from file_versions
order by fname
, to_number(substr(version, 1, instr(version, '.',1,1)-1))
, to_number(substr(version, instr(version, '.',1,1)+1, instr(version, '.',1,2)-instr(version, '.',1,1)-1))
, to_number(substr(version, instr(version, '.',1,2)+1))
But its not so good and will not work if one digit was added to the version string (e.g. 0.0.0.123). Is there a better solution?

You can use regexp_substr():
order by fname,
cast(regexp_substr(version, '[^.]+', 1, 1) as number),
cast(regexp_substr(version, '[^.]+', 1, 2) as number),
cast(regexp_substr(version, '[^.]+', 1, 3) as number)

You may use two regexp first for enhance you group to add 5 zeros to any group. And another one to take last 5 digits from each group. And you get constant length rows and be able to sort it as chars.
with s(txt) as (select '1' from dual
union all
select '1.12' from dual
union all
select '1.12.410' from dual
union all
select rpad('1.12.410',401,'.03') from dual
union all
select rpad('1.12.410',401,'.03')||'.01' from dual
union all
select rpad('1.12.410',401,'.03')||'.02' from dual
)
select txt,regexp_replace(regexp_replace(txt, '(\d+)','00000\1'),'\d+ (\d{5})','\1') from s
order by regexp_replace(regexp_replace(txt, '(\d+)','00000\1'),'\d+(\d{5})','\1')
It will work up to 99999 version or subversion.

More for fun than as a serious suggestion, here's an alternative to parsing the string -- treating the version numbers as inet addresses.
Trickier when you have three levels in your version, but trivial for four levels:
Starting with the idea of:
select a.i::varchar
from (select '192.168.100.128'::inet i union
select '22.168.100.128'::inet) a
order by 1;
i
--------------------
192.168.100.128/32
22.168.100.128/32
(2 rows)
So for three-level versions you can:
with
versions as (
select '1.12.1' v union
select '1.3.100'),
inets as (
select (v||'.0')::inet i
from versions)
select substr(i::varchar,1,length(i::varchar)-5)
from inets
order by i;
substr
---------
1.3.100
1.12.1
(2 rows)
Maybe everyone should have four level versions ...

Related

Sort string containing combination of characters, period and number in descending order in oracle

I am trying to sort a string field in descending order.
Rules for sorting:
'P' and 'S' will remain constant all the time
Nulls should be last
The entire character needs to be sorted in descending order starting with first preference order to the chars before 'P' , then the chars after 'P' and finally followed by chars after S.
The chars before P should be sorted in descending like a decimal number , highest first ex - 4.5,4.2,3.9 and they decide the main order.
If the chars before 'P' are same then it should use the chars between 'P' and 'S' as the second preference order and sorted as a number , highest first.
If the chars before and after 'P' are same then the chars after 'S' should be considered for sorting as number ,highest first.
Sample source data:
3.9P2S1
4.0P5S1
3.10P4S1
3.11P2S3
3.7P2S1
3.2P10S1
4.0P4S1
3.5P2S1
4.0P16S1
3.12P6S1
3.12P10S2
3.14P3S2
Expected output:
4.0P16S1
4.0P5S1
4.0P4S1
3.14P3S2
3.12P10S2
3.12P6S1
3.11P2S3
3.10P4S1
3.9P2S1
3.7P2S1
3.5P2S1
3.2P10S1
Here is what I have tried so far, but I am not able to get the desired output.
with firmware_name as (
select '3.9P2S1' as firmware from dual union all
select '4.0P5S1' as firmware from dual union all
select '3.10P4S1' as firmware from dual union all
select '3.11P2S3' as firmware from dual union all
select '3.7P2S1' as firmware from dual union all
select '3.2P10S1' as firmware from dual union all
select '4.0P4S1' as firmware from dual union all
select '3.5P2S1' as firmware from dual union all
select '4.0P16S1' as firmware from dual union all
select '3.12P6S1' as firmware from dual union all
select '3.12P10S2' as firmware from dual union all
select '3.14P3S2' as firmware from dual)
select * from firmware_name
order by to_number(regexp_substr(firmware, '^\d+')) desc nulls last,
to_number(regexp_substr(firmware, '^\d+\.(\d+)', 1, 1, null, 1)) desc,
regexp_replace(firmware, '\d+\.\d+') desc;
According to the above '4.0P5S1' is the highest which is clearly wrong. Am not able to sort the last part. Any help would be awesome.
Thanks
I would use regexp_substr():
order by
to_number(regexp_substr(firmware, '^[^P]+')) desc nulls last,
to_number(regexp_substr(firmware, 'P([^S]+)', 1, 1, '', 1)) desc,
to_number(regexp_substr(firmware, '[^S]+$')) desc
The first expression captures characters at the beginning of the string until a 'P' is met (excluded). The second captures everything after 'P' until a 'S' is met. The final expression captures everything after the last 'S'.
What about this:
order by
to_number(regexp_replace(firmware, 'P.+$')) desc nulls last,
to_number(regexp_replace(firmware, '^.+P(\d+)S.+$', '\1')) desc,
to_number(regexp_replace(firmware, '^.+S')) desc
You could use the following, where you use the same regex, but different occurrence number:
order by
to_number(regexp_substr(firmware, '[^PS]+', 1, 1)) desc,
to_number(regexp_substr(firmware, '[^PS]+', 1, 2)) desc,
to_number(regexp_substr(firmware, '[^PS]+', 1, 3)) desc

Apply order by in comma separated string in oracle

I have one of the column in oracle table which has below value :
select csv_val from my_table where date='09-OCT-18';
output
==================
50,100,25,5000,1000
I want this values to be in ascending order with select query, output would looks like :
output
==================
25,50,100,1000,5000
I tried this link, but looks like it has some restriction on number of digits.
Here, I made you a modified version of the answer you linked to that can handle an arbitrary (hardcoded) number of commas. It's pretty heavy on CTEs. As with most LISTAGG answers, it'll have a 4000-char limit. I also changed your regexp to be able to handle null list entries, based on this answer.
WITH
T (N) AS --TEST DATA
(SELECT '50,100,25,5000,1000' FROM DUAL
UNION
SELECT '25464,89453,15686' FROM DUAL
UNION
SELECT '21561,68547,51612' FROM DUAL
),
nums (x) as -- arbitrary limit of 20, can be changed
(select level from dual connect by level <= 20),
splitstr (N, x, substring) as
(select N, x, regexp_substr(N, '(.*?)(,|$)', 1, x, NULL, 1)
from T
inner join nums on x <= 1 + regexp_count(N, ',')
order by N, x)
select N, listagg(substring, ',') within group (order by to_number(substring)) as sorted_N
from splitstr
group by N
;
Probably it can be improved, but eh...
Based on sample data you posted, relatively simple query would work (you need lines 3 - 7). If data doesn't really look like that, query might need adjustment.
SQL> with my_table (csv_val) as
2 (select '50,100,25,5000,1000' from dual)
3 select listagg(token, ',') within group (order by to_number(token)) result
4 from (select regexp_substr(csv_val, '[^,]+', 1, level) token
5 from my_table
6 connect by level <= regexp_count(csv_val, ',') + 1
7 );
RESULT
-------------------------
25,50,100,1000,5000
SQL>

Retrieve certain number from data set in Oracle 10g

1. <0,0><120.96,2000><241.92,4000><362.88,INF>
2. <0,0><143.64,2000><241.92,4000><362.88,INF>
3. <0,0><125.5,2000><241.92,4000><362.88,INF>
4. <0,0><127.5,2000><241.92,4000><362.88,INF>
Above is the data set I have in Oracle 10g. I need output as below
1. 120.96
2. 143.64
3. 125.5
4. 125.5
the output I want is only before "comma" (120.96). I tried using REGEXP_SUBSTR but I could not get any output. It will be really helpful if someone could provide effective way to solve this
Here is one method that first parses out the second element and then gets the first number in it:
select regexp_substr(regexp_substr(x, '<[^>]*>', 1, 2), '[0-9.]+', 1, 1)
Another method just gets the third number in the string:
select regexp_substr(x, '[0-9.]+', 1, 3)
Here is an approach without using Regexp.
Find the index of second occurrence of '<'. Then find the second occurrence of ',' use those values in substring.
with
data as
(
select '<0,0><120.96,2000><241.92,4000><362.88,INF>' x from dual
UNION ALL
select '<0,0><143.64,2000><241.92,4000><362.88,INF>' x from dual
UNION ALL
select '<0,0><125.5,2000><241.92,4000><362.88,INF>' from dual
)
select substr(x, instr(x,'<',1,2)+1, instr(x,',',1,2)- instr(x,'<',1,2)-1)
from data
Approach Using Regexp:
Identify the 2nd occurence of numerical value followed by a comma
Then remove the trailing comma.
with
data as
(
select '<0,0><120.96,2000><241.92,4000><362.88,INF>' x from dual
UNION ALL
select '<0,0><143.64,2000><241.92,4000><362.88,INF>' x from dual
UNION ALL
select '<0,0><125.5,2000><241.92,4000><362.88,INF>' from dual
)
select
trim(TRAILING ',' FROM regexp_substr(x,'[0-9.]+,',1,2))
from data
This example uses regexp_substr to get the string contained within the 2nd occurance of a less than sign and a comma:
SQL> with tbl(id, str) as (
select 1, '<0,0><120.96,2000><241.92,4000><362.88,INF>' from dual union
select 2, '<0,0><143.64,2000><241.92,4000><362.88,INF>' from dual union
select 3, '<0,0><125.5,2000><241.92,4000><362.88,INF>' from dual union
select 4, '<0,0><127.5,2000><241.92,4000><362.88,INF>' from dual
)
select id,
regexp_substr(str, '<(.*?),', 1, 2, null, 1) value
from tbl;
ID VALUE
---------- -------------------------------------------
1 120.96
2 143.64
3 125.5
4 127.5
EDIT: I realized the OP specified 10g and the regexp_substr example I gave used the 6th argument (subgroup) which was added in 11g. Here is an example using regexp_replace instead which should work with 10g:
SQL> with tbl(id, str) as (
select 1, '<0,0><120.96,2000><241.92,4000><362.88,INF>' from dual union
select 2, '<0,0><143.64,2000><241.92,4000><362.88,INF>' from dual union
select 3, '<0,0><125.5,2000><241.92,4000><362.88,INF>' from dual union
select 4, '<0,0><127.5,2000><241.92,4000><362.88,INF>' from dual
)
select id,
regexp_replace(str, '^(.*?)><(.*?),.*$', '\2') value
from tbl;
ID VALUE
---------- ----------
1 120.96
2 143.64
3 125.5
4 127.5
SQL>

Index like sql order

I have a column with a string value, something like 1, 1.1, 1.1.2, 1.2, 2, 2.1, 1.3, 1.1.3, one for record, of course, and i want a sentence that returns the records ordered by this field, like a book index
1
1.1
1.1.2
1.1.3
1.2
1.3
2
2.1
Thanks
Use ORDER BY:
CREATE TABLE #tab(col VARCHAR(1000));
INSERT INTO #tab(col)
SELECT '1'
UNION ALL SELECT '1.1'
UNION ALL SELECT '1.1.2'
UNION ALL SELECT '1.1.3'
UNION ALL SELECT '1.2'
UNION ALL SELECT '1.3'
UNION ALL SELECT '2'
UNION ALL SELECT '2.1';
SELECT *
FROM #tab
ORDER BY col;
LiveDemo
EDIT:
Just for fun and experiment solution for SQL Server 2012+:
WITH cte AS (
SELECT col,
CASE LEN(col) - LEN(REPLACE(col, '.', ''))
WHEN 0 THEN col + '.0.0.0'
WHEN 1 THEN col + '.0.0'
WHEN 2 THEN col + '.0'
ELSE col
END AS col_alt
FROM #tab
)
SELECT col
FROM cte
ORDER BY
LEN(PARSENAME(col_alt,4)),
PARSENAME(col_alt,4),
LEN(PARSENAME(col_alt,3)),
PARSENAME(col_alt,3),
LEN(PARSENAME(col_alt,2)),
PARSENAME(col_alt,2),
LEN(PARSENAME(col_alt,1)),
PARSENAME(col_alt,1);
LiveDemo2
If the values between the dots are all single characters (as in the question), then the easiest way is to order by the length of the string and then the string:
order by len(col), col
(In some databases, len might be spelled length.)
Note: this only works when single digits separate the dots. A more general solution requires some knowledge of the database.

Help with T-sql special sorting rules

I have a field likeļ¼š
SELECT * FROM
(
SELECT 'A9t' AS sortField UNION ALL
SELECT 'A10t' UNION ALL
SELECT 'A11t' UNION ALL
SELECT 'AB9F' UNION ALL
SELECT 'AB10t' UNION ALL
SELECT 'AB11t'
) t ORDER BY sortField
and the result is:
sortField
---------
A10t
A11t
A9t
AB10t
AB11t
AB9F
Actually I need is to combine the string and number sorting rules:
sortField
---------
A9t
A10t
A11t
AB9F
AB10t
AB11t
SELECT *
FROM (
SELECT 'A9t' AS sortField UNION ALL
SELECT 'A10t' UNION ALL
SELECT 'A11t' UNION ALL
SELECT 'AB9F' UNION ALL
SELECT 'AB10t' UNION ALL
SELECT 'AB11t'
)
t
ORDER BY LEFT(sortField,PATINDEX('%[0-9]%',sortField)-1) ,
CAST(substring(sortField,PATINDEX('%[0-9]%',sortField),1 + PATINDEX('%[0-9][A-Z]%',sortField) -PATINDEX('%[0-9]%',sortField) ) AS INT),
substring(sortField,PATINDEX('%[0-9][A-Z]%',sortField) + 1,LEN(sortField))
If the first character is always a letter, try:
SELECT * FROM
(
SELECT 'A9t' AS sortField UNION ALL
SELECT 'A10t' UNION ALL
SELECT 'A11t'
) t ORDER BY substring(sortField,2,len(sortField)-1) desc
I would say that you have combined the alpha and numeric sort. But what I think you're asking is that you want to sort letters in ascending order and numbers in descending order, and that might be hard to do in a nice looking way. The previous answers will not working for your problem, the problem is that Martin Smith's solution doesn't take strings with two letters as prefix and Parkyprg doesn't sort numbers before letters as you ask for.
What you need to do is to use a custom order, see example here: http://www.emadibrahim.com/2007/05/25/custom-sort-order-in-a-sql-statement/, but that is a tedious way to do it.
EDIT: Martins Smith's solution is updated and works just fine!