Generate random String in PostgreSQL - sql

I'm using this SQL query to generate random value in PostgreSQL
chr(ascii('B') + (random() * 25)::integer)
How I can generate 15 characters random String using the same query?

Another solution that's pretty easy to read (perf should be reasonable, but no benchmarks were performed):
select substr(md5(random()::text), 0, 25);
Could be uppercased if you prefer:
select upper(substr(md5(random()::text), 0, 25));

Here is my contrib
postgres=# SELECT array_to_string(array(select substr('ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789',((random()*(36-1)+1)::integer),1) from generate_series(1,50)),'');
array_to_string
----------------------------------------------------
4XOS6TQG5JORLF3D1RPXUWR2FQKON9HIXV0UGH0CQFT1LN5D4L
(1 row)
It lets you specify the set of allowed characters and the length of the string.

This will give you a random word of length 15 consisting of the letters configured in the source values constant
select
string_agg(substr(characters, (random() * length(characters) + 1)::integer, 1), '') as random_word
from (values('ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789')) as symbols(characters)
-- length of word
join generate_series(1, 15) on 1 = 1
EDIT: to obtain multiple random words you can use the following:
with symbols(characters) as (VALUES ('ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'))
select string_agg(substr(characters, (random() * length(characters) + 1) :: INTEGER, 1), '')
from symbols
join generate_series(1,8) as word(chr_idx) on 1 = 1 -- word length
join generate_series(1,10000) as words(idx) on 1 = 1 -- # of words
group by idx

Yes can do this by single query also but if you want every char should be separate according to range then above is solution
SELECT array_to_string(ARRAY(
SELECT chr((ascii('B') + round(random() * 25)) :: integer)
FROM generate_series(1,15)),
'');

I tried to use solution from #Bennit
but noticed some flaws. The random part is calculated a bit wrongly, that leads wrong results: the resulting lenght is differs (shorter) than desired.
[took a quick look at the #lyndon-s version - most probably it also has the same drawback]
So here is updated version of #bennit version:
select
string_agg(substr(characters, (random() * length(characters) + 0.5)::integer, 1), '') as random_word
from (values('ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789')) as symbols(characters)
-- length of word
join generate_series(1, 15) on 1 = 1
And here is demonstration why the change is required:
Corrected:
select n,count(*) from (
select (random() * 10 + 0.5)::integer as n from dbfiles
join generate_series(1, 100000) on 1 = 1
) as s group by n
order by 1;
Original:
select n,count(*) from (
select (random() * 10 + 1)::integer as n from dbfiles
join generate_series(1, 100000) on 1 = 1
) as s group by n
order by 1;

I use this, for generating random strings...
If you dont mind dashes and have the uuid extension enabled...
select substr(uuid_generate_v4()::text,1,15);
e.g. to generate a random string in the name column, i will use
select concat('name-', substr(uuid_generate_v4()::text,1,10)) as name;
e.g. name-91fc72dc-d
else, use the excellent md5 example from #fncomp
nb: To enable the uuid extension
create extension if not exists "uuid-ossp";

Here is the idea:
select (chr(ascii('B') + (random() * 25)::integer) ||
chr(ascii('B') + (random() * 25)::integer) ||
chr(ascii('B') + (random() * 25)::integer) ||
chr(ascii('B') + (random() * 25)::integer) ||
chr(ascii('B') + (random() * 25)::integer) ||
chr(ascii('B') + (random() * 25)::integer) ||
chr(ascii('B') + (random() * 25)::integer) ||
chr(ascii('B') + (random() * 25)::integer) ||
chr(ascii('B') + (random() * 25)::integer) ||
chr(ascii('B') + (random() * 25)::integer) ||
chr(ascii('B') + (random() * 25)::integer) ||
chr(ascii('B') + (random() * 25)::integer) ||
chr(ascii('B') + (random() * 25)::integer) ||
chr(ascii('B') + (random() * 25)::integer) ||
chr(ascii('B') + (random() * 25)::integer)
) as Random15

For me the most convenient way is to create a function:
CREATE OR REPLACE FUNCTION random_string(int) RETURNS TEXT as $$
SELECT substr(md5(random()::text), 0, $1+1);
$$ language sql;
The function is named random_string
and It takes string length as an argument.
And then I use this function anywhere I want
Just see the result:
select random_string(6);
Make a single insert:
insert into users values(nextval('users_sequence'), random_string(6), random_string(6));
Generate multiple rows with random data:
do $$
begin
for i in 1..100 loop
insert into users values(nextval('users_sequence'), random_string(6), random_string(6));
end loop;
end;
$$;
and so on.

Throwing in my 2c here. I needed random strings to do some benchmarks, so all that really mattered to me was that the strings were unique from each other.
select rpad(generate_series::varchar, 1000, 'hi') from generate_series(1,10);
rpad - pad right till length(1000), padded with 'hi'
generate_series(1,10) - generate 10 rows
Combining with an answer above, you could also do this:
select rpad(generate_series::varchar, 1000, md5(random()::text)) from generate_series(1,10)
That makes sure you have 200 chars, or whatever the desired length is.

Well how about a recursive CTE. Combine with generate series to get however many you want.
with recursive brs(rstg, n) as
( select chr(ascii('B') + (random() * 25)::integer), 1
from generate_series(1,50) --- or however many you want
union all
select rstg || chr(ascii('B') + (random() * 25)::integer), n+1
from brs
where n<= 15
)
select rstg
from brs
where n=15;

Here's my PL/pgSQL take:
there is an option to generate texts in english or russian symbols, and it's easy to expand for more languages;
optional usage of digits, punctuation symbols, whitespaces and linebreaks;
create or replace function random_string (
str_length integer, lang varchar(2) default 'en',
w_dig boolean default true, w_punct boolean default true,
w_space boolean default false, w_newline boolean default false
)
returns text
language plpgsql
as $function$
declare
chars_eng text[] := '{A,B,C,D,E,F,G,H,I,J,K,L,M,N,O,P,Q,R,S,T,U,V,W,X,Y,Z,a,b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u,v,w,x,y,z}';
chars_rus text[] := '{А,Б,В,Г,Д,Е,Ё,Ж,З,И,Й,К,Л,М,Н,О,П,Р,С,Т,У,Ф,Х,Ц,Ч,Ш,Щ,Ъ,Ы,Ь,Э,Ю,Я,а,б,в,г,д,е,ё,ж,з,и,й,к,л,м,н,о,п,р,с,т,у,ф,х,ц,ч,ш,щ,ъ,ы,ь,э,ю,я}';
chars_dig text[] := '{}';
chars_punct text[] := '{}';
chars_space text[] := '{}';
chars_newline text[] := '{}';
chars_final text[] := '{}';
result text := '';
i integer := 0;
begin
-- checking string length arg
if str_length < 0 then
raise exception 'Length of string cannot be a negative value';
end if;
-- checking chars selection
if w_dig = true then
chars_dig := '{0,1,2,3,4,5,6,7,8,9}';
end if;
if w_punct = true then
chars_punct := string_to_array(E'!d"d#d$d%d&d\'d(d)d*d+d,d-d.d/d:d;d<d=d>d?d#d[d\\d]d^d_d`d{d|d}d~','d');
end if;
if w_space = true then
chars_space := string_to_array(' ',',');
end if;
if w_newline = true then
chars_newline := string_to_array(E'\r\n',',');
end if;
-- checking language selection
if lang = 'en' then
chars_final := chars_eng||chars_dig||chars_punct||chars_space||chars_newline;
elsif lang = 'ru' then
chars_final := chars_rus||chars_dig||chars_punct||chars_space||chars_newline;
else
raise exception 'Characters set for that language is not defined';
end if;
-- filling the string
for i in 1..str_length loop
result := result || chars_final[1 + round(random() * (array_length(chars_final, 1) - 1))];
end loop;
-- trimming extra symbols that may appear from /r/n usage
if length(result) > str_length then
result := left(result, str_length);
end if;
-- getting the result
return result;
end;
$function$ ;

Related

Postgresql - Function that generates random phone number

I'm very new to postgresql and I'm wondering how to go about creating a function that will generate random phone numbers in (888) 888-8888 format. I can't even wrap my head around how to do this so if anyone has any feedback that would be great.
To generate a single, completely random number in the requested format:
SELECT format('(%s%s%s) %s%s%s-%s%s%s%s'
, a[1], a[2], a[3], a[4], a[5], a[6], a[7], a[8], a[9], a[10])
FROM (
SELECT ARRAY (
SELECT trunc(random() * 10)::int
FROM generate_series(1, 10)
) AS a
) sub;
Returns:
(213) 633-4337
Or similar.
Not the most elegant of code but it's very simple so should give you a basis to work from:
SELECT '('
|| (RANDOM() * 9)::INT
|| (RANDOM() * 9)::INT
|| (RANDOM() * 9)::INT
|| ') '
|| (RANDOM() * 9)::INT
|| (RANDOM() * 9)::INT
|| (RANDOM() * 9)::INT
|| '-'
|| (RANDOM() * 9)::INT
|| (RANDOM() * 9)::INT
|| (RANDOM() * 9)::INT
|| (RANDOM() * 9)::INT;
I was thinking of this:
select replace(replace(replace('(#n1) #n2-#n3),
'#n1', lpad((random()*1000)::int::text, 3, '0')
), '#n2', lpad((random()*1000)::int::text, 3, '0')
), lpad((random()*10000)::int::text, 4, '0')
)
That is, use three different random numbers, one for each grouping.
The arithmetic would be slightly different if you don't allow one or more elements to start with a zero.
select regexp_replace(CAST (random() AS text),'^0\.(\d{3})(\d{3})(\d{4}).*$','(\1)\2-\3') AS random;
The random() function is used to generate a random number between 0 and 1. It is then CAST as text and a regexp_replace is used to add the formatting characters.
Updated to pad the string with some arbitrary numbers:
select regexp_replace(rpad(CAST (random() AS text),12,CAST(random() AS text)),'^0\.(\d{3})(\d{3})(\d{4}).*$','(\1)\2-\3') AS random;
There is no guarantee this will produce valid phone numbers - for example area codes can't start with zero or one, but if you just need to fill in some numbers quickly, this should do the trick.

Function to convert IP address in Integer using sql which will run on any database

I wrote function
CREATE FUNCTION ip2int(text) RETURNS bigint AS $$
SELECT split_part($1,'.',1)::bigint*16777216 + split_part($1,'.',2)::bigint*65536 +
split_part($1,'.',3)::bigint*256 + split_part($1,'.',4)::bigint;
$$ LANGUAGE SQL IMMUTABLE RETURNS NULL ON NULL INPUT;
SELECT ip2int('200.233.1.2');
which works on postgres database.
But when I run it on Oracle It gave me following error
Error(1,21): PLS-00103: Encountered the symbol ")" when expecting one
of the following: in out long double ref char time timestamp interval
date binarynational character nchar
What is the solution for this?
Is there any way to make it database independent?
I realize that in PostgreSQL it's common to refer to one of the more commonly used programming languages as "plsql", but the language supported by PostgreSQL is properly known as "plpgsql", and PL/SQL and PL/pgSQL are two similar-but-different languages. The syntax of this function is not valid PL/SQL as defined by Oracle. PL/SQL does not use $$ to quote the text of the routine; the text parameter is untyped, which is not allowed in PL/SQL; PL/SQL does not support the $1, $2, etc parameter markers; bigint isn't supported in Oracle (although you could define a subtype of NUMBER to handle this); Oracle doesn't support the :: cast operator; doesn't provide split_part (although I suppose you could write your own); and doesn't use the LANGUAGE SQL IMMUTABLE... stuff at the end. Oracle requires that a function consist of a statement block (DECLARE...BEGIN...END), while this function is purely a SELECT statement; and the SELECT statement in the function isn't valid for Oracle because Oracle requires a FROM clause. So, basically, this function is not valid PL/SQL. You could transform it into something like
CREATE OR REPLACE FUNCTION ip2int(text IN VARCHAR2)
RETURN NUMBER
AS
nRetval NUMBER;
BEGIN
SELECT TO_NUMBER(REGEXP_SUBSTR(text, '[0-9]', 1, 1)) * 16777216 +
TO_NUMBER(REGEXP_SUBSTR(text, '[0-9]', 1, 2)) * 65536 +
TO_NUMBER(REGEXP_SUBSTR(text, '[0-9]', 1, 3)) * 256 +
TO_NUMBER(REGEXP_SUBSTR(text, '[0-9]', 1, 4))
INTO nRetval
FROM DUAL;
RETURN nRetval;
END IP2INT;
but making such radical alterations doesn't seem to be what you want to do.
The PostgreSQL documentation on converting from PL/SQL to PL/pgSQL might give you some ideas of the challenges involved in this.
Best of luck.
CREATE OR REPLACE FUNCTION split_part(string VARCHAR2, delimiter VARCHAR2, n NUMBER)
RETURN VARCHAR2
IS
v_start NUMBER(5) := 1;
v_end NUMBER(5);
BEGIN
-- Find the position of n-th -1 delimiter
IF n > 1 THEN
v_start := INSTR(string, delimiter, 1, n - 1);
-- Delimiter not found
IF v_start = 0 THEN
RETURN NULL;
END IF;
v_start := v_start + LENGTH(delimiter);
END IF;
-- Find the position of n-th delimiter
v_end := INSTR(string, delimiter, v_start, 1);
-- If not found return until the end of string
IF v_end = 0 THEN
RETURN SUBSTR(string, v_start);
END IF;
RETURN SUBSTR(string, v_start, v_end - v_start);
END;
CREATE OR REPLACE FUNCTION ip2int(text IN VARCHAR2)
RETURN NUMBER
AS
nRetval NUMBER;
BEGIN
SELECT TO_NUMBER(split_part(text,'.',1)) * 16777216 +
TO_NUMBER(split_part(text,'.',2)) * 65536 +
TO_NUMBER(split_part(text,'.',3)) * 256 +
TO_NUMBER(split_part(text,'.',4))
INTO nRetval
FROM DUAL;
RETURN nRetval;
END IP2INT;
create or replace FUNCTION ip_to_number(ip IN VARCHAR2)
RETURN NUMBER
AS
a varchar(20 char);
b varchar(20 char);
c varchar(20 char);
d varchar(20 char);
i integer;
j integer;
k integer;
ip_value integer;
begin
i := instr(ip, '.');
j := instr(ip, '.', 1, 2);
k := instr(ip, '.', 2, 3);
if (k = 0) then
raise_application_error(-20101, 'Incorrect IP format (missing at least one dot)');
end if;
a := substr(ip, 1, i - 1);
b := substr(ip, i + 1, j - i - 1);
c := substr(ip, j + 1, k - j - 1);
d := substr(ip, k + 1);
if (a not between 0 and 255) or (b not between 0 and 255) or (c not between 0 and 255) or (d not between 0 and 255) then
raise_application_error(-20102, 'Incorrect IP format (octet out of bounds)');
end if;
ip_value := to_number(a) * 16777216 + to_number(b) * 65536 + to_number(c) * 256 + to_number(d);
RETURN ip_value;
end;
You can convert it onflight:
select TO_NUMBER(REGEXP_SUBSTR(YourIpAddress, '[0-9]+', 1, 1)) * power(2, 24) +
TO_NUMBER(REGEXP_SUBSTR(YourIpAddress, '[0-9]+', 1, 2)) * power(2, 16) +
TO_NUMBER(REGEXP_SUBSTR(YourIpAddress, '[0-9]+', 1, 3)) * power(2, 8) +
TO_NUMBER(REGEXP_SUBSTR(YourIpAddress, '[0-9]+', 1, 4))
from dual;
in this block you can easily check this translation
declare
YourIpAddress varchar2(32) := '192.168.1.1';
res number;
begin
select TO_NUMBER(REGEXP_SUBSTR(YourIpAddress, '[0-9]+', 1, 1)) * power(2, 24) +
TO_NUMBER(REGEXP_SUBSTR(YourIpAddress, '[0-9]+', 1, 2)) * power(2, 16) +
TO_NUMBER(REGEXP_SUBSTR(YourIpAddress, '[0-9]+', 1, 3)) * power(2, 8) +
TO_NUMBER(REGEXP_SUBSTR(YourIpAddress, '[0-9]+', 1, 4))
into res
from dual;
DBMS_OUTPUT.put_line(res);
end;
on this site you can check correctness.

how to return a mixed case alphanumeric string with DBMS_RANDOM oracle?

based on the specification here (section STRING function, parameters)
http://docs.oracle.com/cd/B19306_01/appdev.102/b14258/d_random.htm
there is no way to set a mixed case alphanumeric string. Is this feature not supported or
are there further settings to consider?
If a function is ok.....
CREATE OR REPLACE FUNCTION GET_RANDOM_STRING(v_length NUMBER) RETURN VARCHAR2
IS
lKey VARCHAR2(4000);
BEGIN
FOR I IN 1..v_length LOOP
lKey := lKey || substr( 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789', mod(abs(dbms_random.random), 62)+1, 1);
END LOOP;
RETURN lKey;
END;
/
Also, you can just make a simple query based on function from cagcowboy. Something like:
SELECT LISTAGG (
(SELECT SUBSTR ('abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789', rnd, 1)
FROM DUAL),
'')
WITHIN GROUP (ORDER BY lvl)
FROM (SELECT LEVEL AS lvl, MOD (ABS (DBMS_RANDOM.random), 62) + 1 AS rnd
FROM DUAL
CONNECT BY LEVEL < 10)
Where 10 is the length of your random string

Reverse String Word by Word using SQL

I would need to reverse the word positions in a sentence or String.
For example : "Hello World! I Love StackOverflow", to be displayed as "StackOverflow Love I World! Hello".
Can it be done with a SQL ? The word length is no greater than VARCHAR2(4000) which is the maximum length support in a Oracle VARCHAR2 table column.
I got solutions for reversing a string (Characters in reverse order) only
XML-based version to avoid defining your own function; requires 11g for listagg():
select listagg(word, ' ') within group (order by rn desc) as reversed
from (
select word, rownum as rn
from xmltable('for $i in ora:tokenize($STR, " ") return $i'
passing 'Hello World! I Love StackOverflow' as str
columns word varchar2(4000) path '.'
)
);
REVERSED
----------------------------------------
StackOverflow Love I World! Hello
The XMLTable() does the tokenising, and assigns a row number:
select rownum as rn, word
from xmltable('for $i in ora:tokenize($STR, " ") return $i'
passing 'Hello World! I Love StackOverflow' as str
columns word varchar2(4000) path '.'
);
RN WORD
---------- --------------------
1 Hello
2 World!
3 I
4 Love
5 StackOverflow
The listagg() then pieces it back together in reverse order.
Create a Function:
REGEXP_SUBSTR('Your text here','[^ ]+', 1, ?) will extract a word from the text using Space as a delimiter. Tt returns the original String itself on Exception!
CREATE OR REPLACE FUNCTION reverse_words (v_STRING IN VARCHAR2)
RETURN VARCHAR2
IS
L_TEMP_TEXT VARCHAR2(4000);
L_FINAL_TEXT VARCHAR2(4000);
V_LOOPCOUNT NUMBER :=0;
T_WORD VARCHAR2(4000);
BEGIN
L_TEMP_TEXT := regexp_replace(V_STRING,'[[:space:]]+',' '); -- Replace multiple spaces as single
LOOP
v_LOOPCOUNT := v_LOOPCOUNT+1;
T_WORD := REGEXP_SUBSTR(L_TEMP_TEXT,'[^ ]+', 1, V_LOOPCOUNT);
L_final_TEXT := T_WORD||' '||L_final_TEXT;
EXIT WHEN T_WORD IS NULL;
END LOOP;
RETURN(TRIM(L_final_TEXT));
EXCEPTION
WHEN OTHERS THEN
DBMS_OUTPUT.PUT_LINE(sqlerrm||chr(10)||dbms_utility.format_error_backtrace);
RETURN V_STRING;
END reverse_words;
/
Sample Result:
You can call reverse_words(yourcolumn) from your_table
SQL> select reverse_words('Hello World! I Love StackOverflow') "Reversed" from dual;
Reversed
--------------------------------------------------------------------------------
StackOverflow Love I World! Hello
Here you go:
WITH sel_string AS
(SELECT 'Hello World! I Love StackOverflow' AS fullstring FROM DUAL)
SELECT SUBSTR(fullstring, beg + 1, end_p - beg - 1) AS token
FROM (SELECT beg, LEAD(beg) OVER (ORDER BY beg) AS end_p, fullstring
FROM (SELECT beg, fullstring
FROM (SELECT LEVEL beg, fullstring
FROM sel_string
CONNECT BY LEVEL <= LENGTH(fullstring))
WHERE INSTR(' ', SUBSTR(fullstring, beg, 1)) > 0
UNION ALL
SELECT 0, fullstring FROM sel_string
UNION ALL
SELECT LENGTH(fullstring) + 1, fullstring FROM sel_string))
WHERE end_p IS NOT NULL AND
end_p > beg + 1
ORDER BY ROWNUM DESC;
All in one SQL query. I wish I could claim credit for this query but I can't - found it years ago on the net and have used it ever since.
Share and enjoy.
One more solution
WITH str_tab(str1, rn) AS
(SELECT regexp_substr(str, '[^\[:space:]]+', 1, LEVEL),
LEVEL
FROM (SELECT 'Hello World! I Love StackOverflow' str
FROM dual) tab
CONNECT BY LEVEL <= LENGTH(str) - LENGTH(REPLACE(str, ' ')) + 1)
SELECT listagg(str1, ' ') WITHIN GROUP (ORDER BY rn DESC) AS new_text
FROM str_tab;
DECLARE
in_string VARCHAR2(500);
pros_string VARCHAR2(500);
out_string VARCHAR2(800);
spce_cnt NUMBER;
BEGIN
in_string := 'Hello World! I Love StackOverflow';
pros_string := ' '||in_string||' ' ;
spce_cnt := REGEXP_COUNT(pros_string,' ',1);
FOR i IN reverse 1.. spce_cnt-1
LOOP
out_string := out_string||' '|| SubStr (pros_string,InStr(pros_string, ' ',1,i)+1 ,InStr(SubStr (pros_string,InStr(pros_string, ' ',1,i)+1 ),' ' ));
Dbms_Output.Put_Line(out_string);
END LOOP;
END;

SQL sort by version "number", a string of varying length

I'm trying to create an SQL query that will order the results by a version number (e.g. 1.1, 4.5.10, etc.)
Here's what I tried:
SELECT * FROM Requirements
WHERE Requirements.Release NOT LIKE '%Obsolete%'
ORDER BY Requirements.ReqNum
Now, the ReqNum field is a string field and unfortunately I can't change it to a float or something like that because I have requirement numbers like 162.1.11.
When I get the results back, I'll get ordering like this:
1.1
1.10
1.11
1.3
How can I write a query that will sort by lexicographic order?
... or,
How can I correctly sort the data?
Thanks for the input in advance!
In PostgreSQL you can do:
SELECT * FROM Requirements
ORDER BY string_to_array(version, '.')::int[];
This last ::int[] makes it convert string values into integers and then compare as such.
For best results, refactor version number storage so that each section has it's own column: MajorVersion, MinorVersion, Revision, Build. Then the ordering problem suddenly becomes trivial. You can also build a computed column for easy retrieval of the full string.
SELECT * FROM Requirements
WHERE Requirements.Release NOT LIKE '%Obsolete%'
ORDER BY cast('/' + replace(Requirements.ReqNum , '.', '/') + '/' as hierarchyid);
A slight variation on #vuttipong-l answer (T-SQL)
SELECT VersionNumber
FROM (
SELECT '6.1.3' VersionNumber UNION
SELECT '6.11.3' UNION
SELECT '6.2.3' UNION
SELECT '6.1.12'
) AS q
ORDER BY cast('/' + VersionNumber + '/' as hierarchyid)
Works in SQL Server starting with 2008, dots are OK in a string representation of a hierarchyid column, so we don't need to replace them with slashes.
A quote from the doc:
Comparison is performed by comparing the integer sequences separated
by dots in dictionary order.
There's one caveat though: the version segments must not be prefixed with zeroes.
If you are in SQL Server land...
DECLARE #string varchar(40)
SET #string = '1.2.3.4'
SELECT PARSENAME(#string, 1), PARSENAME(#string, 2), PARSENAME(#string, 3), PARSENAME(#string, 4)
Results:
4, 3, 2, 1
Useful for parsing IP Addresses and other dotted items, such as a version number. (You can use REPLACE() to convert items into dotted notation too... e.g. 1-2-3-4 -> 1.2.3.4)
If you don't re-design the table as Joel Coehoorn sensibly suggests, then you need to re-format the version numbers to a string that sorts as you require, e.g.
1.1 -> 0001.0001.0000
162.1.11 -> 0162.0001.0011
This could be done by a function, or using a computed/virtual column if your DBMS has these. Then you can use that function or column in the ORDER BY clause.
The following function will take a version number and format each level out to 3 digits:
Usage:
select * from TableX order by dbo.fn_VersionPad(VersionCol1)
Function:
CREATE FUNCTION [dbo].[fn_VersionPad]
(
#version varchar(20)
)
RETURNS varchar(20)
AS
BEGIN
/*
Purpose: Pads multi-level Version Number sections to 3 digits
Example: 1.2.3.4
Returns: 001.002.003.004
*/
declare #verPad varchar(20)
declare #i int
declare #digits int
set #verPad = ''
set #i = len(#version)
set #digits = 0
while #i > 0
begin
if (substring(#version, #i, 1) = '.')
begin
while (#digits < 3)
begin
-- Pad version level to 3 digits
set #verPad = '0' + #verPad
set #digits = #digits + 1
end
set #digits = -1
end
set #verPad = substring(#version, #i, 1) + #verPad
set #i = #i - 1
set #digits = #digits + 1
end
while (#digits < 3)
begin
-- Pad version level to 3 digits
set #verPad = '0' + #verPad
set #digits = #digits + 1
end
return #verPad
END
You could split up the string (you already know the delimiters: ".") with CHARINDEX / SUBSTR and ORDER BY the different parts. Do it in a function or do it part by part.
It won't be pretty and it won't be fast: so if you need fast queries, follow Tony or Joel.
NOT USİNG CODE
Insert into #table
Select 'A1' union all
Select 'A3' union all
Select 'A5' union all
Select 'A15' union all
Select 'A11' union all
Select 'A10' union all
Select 'A2' union all
Select 'B2' union all
Select 'C2' union all
Select 'C22' union all
Select 'C221' union all
Select 'A7'
Select cod from #table
Order by LEN(cod),cod
Result :
A1
A2
A3
A5
A7
B2
C2
A10
A11
A15
C22
C221
It's simple as:
Declare #table table(id_ int identity(1,1), cod varchar(10))
Insert into #table
Select 'A1' union all
Select 'A3' union all
Select 'A5' union all
Select 'A15' union all
Select 'A11' union all
Select 'A10' union all
Select 'A2' union all
Select 'A7'
Select cod from #table
Order by LEN(cod),cod
On PostgreSQL, it couldn't be easier:
SELECT ver_no FROM version ORDER BY string_to_array(ver_no, '.', '')::int[]
This would work if you're using Microsoft SQL Server:
create function fnGetVersion (#v AS varchar(50)) returns bigint as
begin
declare #n as bigint;
declare #i as int;
select #n = 0;
select #i = charindex('.',#v);
while(#i > 0)
begin
select #n = #n * 1000;
select #n = #n + cast(substring(#v,1,#i-1) as bigint);
select #v = substring(#v,#i+1,len(#v)-#i);
select #i = charindex('.',#v);
end
return #n * 1000 + cast(#v as bigint);
end
Test by running this command:
select dbo.fnGetVersion('1.2.3.4')
That would return the number 1002003004 wich is perfectly sortable. Is you need 9.0.1 to be bigger than 2.1.2.3 then you would need to change the logic slightly. In my example 9.0.1 would be sorted before 2.1.2.3.
Function for PostgreSQL
Simply use
select *
from sample_table
order by _sort_version(column_version);
CREATE FUNCTION _sort_version (
p_version text
)
RETURNS text AS
$body$
declare
v_tab text[];
begin
v_tab := string_to_array(p_version, '.');
for i in 1 .. array_length(v_tab, 1) loop
v_tab[i] := lpad(v_tab[i], 4, '0');
end loop;
return array_to_string(v_tab, '.');
end;
$body$
LANGUAGE 'plpgsql'
VOLATILE
CALLED ON NULL INPUT
SECURITY DEFINER
COST 1;
I've had the same problem, though mine was with apartment numbers like A1, A2, A3, A10, A11, etc, that they wanted to sort "right". If splitting up the version number into separate columns doesn't work, try this PL/SQL. It takes a string like A1 or A10and expands it into A0000001, A0000010, etc, so it sorts nicely. Just call this in ORDER BY clause, like
select apt_num
from apartment
order by PAD(apt_num)
function pad(inString IN VARCHAR2)
return VARCHAR2
--This function pads the numbers in a alphanumeric string.
--It is particularly useful in sorting, things like "A1, A2, A10"
--which would sort like "A1, A10, A2" in a standard "ORDER BY name" clause
--but by calling "ORDER BY pkg_sort.pad(name)" it will sort as "A1, A2, A10" because this
--function will convert it to "A00000000000000000001, A00000000000000000002, A00000000000000000010"
--(but since this is in the order by clause, it will
--not be displayed.
--currently, the charTemplate variable pads the number to 20 digits, so anything up to 99999999999999999999
--will work correctly.
--to increase the size, just change the charTemplate variable. If the number is larger than 20 digits, it will just
--appear without padding.
is
outString VARCHAR2(255);
numBeginIndex NUMBER;
numLength NUMBER;
stringLength NUMBER;
i NUMBER;
thisChar VARCHAR2(6);
charTemplate VARCHAR2(20) := '00000000000000000000';
charTemplateLength NUMBER := 20;
BEGIN
outString := null;
numBeginIndex := -1;
numLength := 0;
stringLength := length(inString);
--loop through each character, get that character
FOR i IN 1..(stringLength) LOOP
thisChar := substr(inString, i, 1);
--if this character is a number
IF (FcnIsNumber(thisChar)) THEN
--if we haven't started a number yet
IF (numBeginIndex = -1) THEN
numBeginIndex := i;
numLength := 1;
--else if we're in a number, increase the length
ELSE
numLength := numLength + 1;
END IF;
--if this is the last character, we have to append the number
IF (i = stringLength) THEN
outString:= FcnConcatNumber(inString, outString, numBeginIndex, numLength, charTemplate, charTemplateLength);
END IF;
--else this is a character
ELSE
--if we were previously in a number, concat that and reset the numBeginIndex
IF (numBeginIndex != -1) THEN
outString:= FcnConcatNumber(inString, outString, numBeginIndex, numLength, charTemplate, charTemplateLength);
numBeginIndex := -1;
numLength := 0;
END IF;
--concat the character
outString := outString || thisChar;
END IF;
END LOOP;
RETURN outString;
--any exception, just return the original string
EXCEPTION WHEN OTHERS THEN
RETURN inString;
END;
Here is an example query that extracts the string. You should be able to use this in either the UPDATE refactoring of the database, or simply in your query as-is. However, I'm not sure how it is on time; just something to watch out and test for.
SELECT SUBSTRING_INDEX("1.5.32",'.',1) AS MajorVersion,
SUBSTRING_INDEX(SUBSTRING_INDEX("1.5.32",'.',-2),'.',1) AS MinorVersion,
SUBSTRING_INDEX("1.5.32",'.',-1) AS Revision;
this will return:
MajorVersion | MinorVersion | Revision
1 | 5 | 32
Ok, if high performance is an issue then your only option is to change your values into something numeric.
However, if this is a low usage query then you can just split your numbers and order by those.
This query assumes just major and minor version numbers and that they contain just numbers.
SELECT
*
FROM
Requirements
WHERE
Requirements.Release NOT LIKE '%Obsolete%'
ORDER BY
CONVERT(int, RIGHT(REPLICATE('0', 10) + LEFT(Requirements.ReqNum, CHARINDEX('.', Requirements.ReqNum)-1), 10)),
CONVERT(int, SUBSTRING(Requirements.ReqNum, CHARINDEX('.', Requirements.ReqNum )+1, LEN(Requirements.ReqNum) - CHARINDEX('.', Requirements.ReqNum )))
For the all-in-one-query purists, assuming Oracle, some instr/substr/decode/to_number voodoo can solve it:
SELECT *
FROM Requirements
WHERE Release NOT LIKE '%Obsolete%'
ORDER BY
to_number(
substr( reqnum, 1, instr( reqnum, '.' ) - 1 )
)
, to_number(
substr(
reqnum
, instr( reqnum, '.' ) + 1 -- start: after first occurance
, decode(
instr( reqnum, '.', 1, 2 )
, 0, length( reqnum )
, instr( reqnum, '.', 1, 2 ) - 1
) -- second occurance (or end)
- instr( reqnum, '.', 1, 1) -- length: second occurance (or end) less first
)
)
, to_number(
decode(
instr( reqnum, '.', 1, 2 )
, 0, null
, substr(
reqnum
, instr( reqnum, '.', 1, 2 ) + 1 -- start: after second occurance
, decode(
instr( reqnum, '.', 1, 3 )
, 0, length( reqnum )
, instr( reqnum, '.', 1, 3 ) - 1
) -- third occurance (or end)
- instr( reqnum, '.', 1, 2) -- length: third occurance (or end) less second
)
)
)
, to_number(
decode(
instr( reqnum, '.', 1, 3 )
, 0, null
, substr(
reqnum
, instr( reqnum, '.', 1, 3 ) + 1 -- start: after second occurance
, decode(
instr( reqnum, '.', 1, 4 )
, 0, length( reqnum )
, instr( reqnum, '.', 1, 4 ) - 1
) -- fourth occurance (or end)
- instr( reqnum, '.', 1, 3) -- length: fourth occurance (or end) less third
)
)
)
;
I suspect there are plenty of caveats including:
assumption of the presence of minor version (second)
limited to four versions as specified in question's comments
Here's a comparison function for PostgreSQL that will compare arbitrary strings such that sequences of digits are compared numerically. In other words, "ABC123" > "ABC2", but "AB123" < "ABC2". It returns -1, 0, or +1 as such comparison functions usually do.
CREATE FUNCTION vercmp(a text, b text) RETURNS integer AS $$
DECLARE
ar text[];
br text[];
n integer := 1;
BEGIN
SELECT array_agg(y) INTO ar FROM (SELECT array_to_string(regexp_matches(a, E'\\d+|\\D+|^$', 'g'),'') y) x;
SELECT array_agg(y) INTO br FROM (SELECT array_to_string(regexp_matches(b, E'\\d+|\\D+|^$', 'g'),'') y) x;
WHILE n <= array_length(ar, 1) AND n <= array_length(br, 1) LOOP
IF ar[n] ~ E'^\\d+$' AND br[n] ~ E'^\\d+$' THEN
IF ar[n]::integer < br[n]::integer THEN
RETURN -1;
ELSIF ar[n]::integer > br[n]::integer THEN
RETURN 1;
END IF;
ELSE
IF ar[n] < br[n] THEN
RETURN -1;
ELSIF ar[n] > br[n] THEN
RETURN 1;
END IF;
END IF;
n := n + 1;
END LOOP;
IF n > array_length(ar, 1) AND n > array_length(br, 1) THEN
RETURN 0;
ELSIF n > array_length(ar, 1) THEN
RETURN 1;
ELSE
RETURN -1;
END IF;
END;
$$ IMMUTABLE LANGUAGE plpgsql;
You can then create an operator class so that sorting can be done by using the comparison function with ORDER BY field USING <#:
CREATE OR REPLACE FUNCTION vernum_lt(a text, b text) RETURNS boolean AS $$
BEGIN
RETURN vercmp(a, b) < 0;
END;
$$ IMMUTABLE LANGUAGE plpgsql;
CREATE OR REPLACE FUNCTION vernum_lte(a text, b text) RETURNS boolean AS $$
BEGIN
RETURN vercmp(a, b) <= 0;
END;
$$ IMMUTABLE LANGUAGE plpgsql;
CREATE OR REPLACE FUNCTION vernum_eq(a text, b text) RETURNS boolean AS $$
BEGIN
RETURN vercmp(a, b) = 0;
END;
$$ IMMUTABLE LANGUAGE plpgsql;
CREATE OR REPLACE FUNCTION vernum_gt(a text, b text) RETURNS boolean AS $$
BEGIN
RETURN vercmp(a, b) > 0;
END;
$$ IMMUTABLE LANGUAGE plpgsql;
CREATE OR REPLACE FUNCTION vernum_gte(a text, b text) RETURNS boolean AS $$
BEGIN
RETURN vercmp(a, b) >= 0;
END;
$$ IMMUTABLE LANGUAGE plpgsql;
CREATE OPERATOR <# ( PROCEDURE = vernum_lt, LEFTARG = text, RIGHTARG = text);
CREATE OPERATOR ># ( PROCEDURE = vernum_gt, LEFTARG = text, RIGHTARG = text);
CREATE OPERATOR =# ( PROCEDURE = vernum_lte, LEFTARG = text, RIGHTARG = text);
CREATE OPERATOR <=# ( PROCEDURE = vernum_lte, LEFTARG = text, RIGHTARG = text);
CREATE OPERATOR >=# ( PROCEDURE = vernum_gte, LEFTARG = text, RIGHTARG = text);
CREATE OPERATOR CLASS vernum_ops FOR TYPE varchar USING btree AS
OPERATOR 1 <# (text, text),
OPERATOR 2 <=# (text, text),
OPERATOR 3 =#(text, text),
OPERATOR 4 >=# (text, text),
OPERATOR 5 ># (text, text),
FUNCTION 1 vercmp(text, text)
;
FİXED THİS WAY.
<pre>
00000001 1
00000001.00000001 1.1
00000001.00000001.00000001 1.1.1
00000001.00000002 1.2
00000001.00000009 1.9
00000001.00000010 1.10
00000001.00000011 1.11
00000001.00000012 1.12
00000002 2
00000002.00000001 2.1
00000002.00000001.00000001 2.1.1
00000002.00000002 2.2
00000002.00000009 2.9
00000002.00000010 2.10
00000002.00000011 2.11
00000002.00000012 2.12
select * from (select '000000001' as tCode,'1' as Code union
select '000000001.000000001' as tCode,'1.1'as Code union
select '000000001.000000001.000000001' as tCode,'1.1.1'as Code union
select '000000001.000000002' as tCode,'1.2' union
select '000000001.000000010' as tCode,'1.10'as Code union
select '000000001.000000011' as tCode,'1.11'as Code union
select '000000001.000000012' as tCode,'1.12'as Code union
select '000000001.000000009' as tCode,'1.9' as Code
union
select '00000002' as tCode,'2'as Code union
select '00000002.00000001' as tCode,'2.1'as Code union
select '00000002.00000001.00000001' as tCode,'2.1.1'as Code union
select '00000002.00000002' as tCode,'2.2'as Code union
select '00000002.00000010' as tCode,'2.10'as Code union
select '00000002.00000011' as tCode,'2.11'as Code union
select '00000002.00000012' as tCode,'2.12'as Code union
select '00000002.00000009' as tCode,'2.9'as Code ) as t
order by t.tCode
</pre>
<pre>
public static string GenerateToCodeOrder(this string code)
{
var splits = code.Split('.');
var codes = new List<string>();
foreach (var str in splits)
{
var newStr = "";
var zeroLength = 10 - str.Length;
for (int i = 1; i < zeroLength; i++)
{
newStr += "0";
}
newStr += str;
codes.Add(newStr);
}
return string.Join(".", codes);
}
</pre>
In M$ SQL I had issues with hierachyid with some data...
select Convert(hierarchyid, '/' + '8.3.0000.1088' + '/')
To get around this I used pasename (relies on '.' being the separator)...
Order by
convert(int, reverse (Parsename( reverse(tblSoftware.softwareVersion) , 1))),
convert(int, reverse (Parsename( reverse(tblSoftware.softwareVersion) , 2))),
convert(int, reverse (Parsename( reverse(tblSoftware.softwareVersion) , 3))),
convert(int, reverse (Parsename( reverse(tblSoftware.softwareVersion) , 4))),
convert(int, reverse (Parsename( reverse(tblSoftware.softwareVersion) , 5)))
If the column type for version is varchar the sorting is done as expected.
This is beacuse varchar is not padded by spaces.
Here is an ORACLE expression you can use in an ORDER BY:
select listagg(substr('0000000000' || column_value,-9), '.') within group(order by rownum) from xmltable(replace(version, '.',','))
assuming your version column has only dot as separator (any number of levels).
(if not, up to you to change the replace by e.g. translate(version, '.-', ',,'))
I would do as Joel Coehoorn said. Then to re-arrange your data structure you don't have to manually do it. You can write a simple script that will do the job for all 600 records.
Just remove the dots (Inline, replace with empty string) cast the result as int and order by the result. Works great:
a.Version = 1.4.18.14
select...
Order by cast( replace (a.Version,'.','') as int)