Find the greatest version - oracle

I am using Oracle Database 11g Enterprise Edition Release 11.2.0.2.0
I have a table as follows:
Table1:
Name Null Type
----------------- -------- -------------
NAME NOT NULL VARCHAR2(64)
VERSION NOT NULL VARCHAR2(64)
Table1
Name Version
---------------
A 1
B 12.1.0.2
B 8.2.1.2
B 12.0.0
C 11.1.2
C 11.01.05
I want the output as:
Name Version
---------------
A 1
B 12.1.0.2
C 11.01.05
Basically, I want to get the row for each name which have highest version. For this I am using the following query:
SELECT t1.NAME,
t1.VERSION
FROM TABLE1 t1
LEFT OUTER JOIN TABLE1 t2
on (t1.NAME = t2.NAME and t1.VERSION < t2.VERSION)
where t2.NAME is null
Now 't1.VERSION < t2.VERSION' only works in normal version cases but in cases such as:
B 12.1.0.2
B 8.2.1.2
It fails, I need a PL/SQL script to normalize the version strings and compare them for higher value.

You can do this with judicious use of REGEXP_SUBSTR(); there's no need to use PL/SQL.
select *
from ( select a.*
, row_number() over (
partition by name
order by to_number(regexp_substr(version, '[^.]+', 1, 1)) desc
, to_number(regexp_substr(version, '[^.]+', 1, 2)) desc
, to_number(regexp_substr(version, '[^.]+', 1, 3)) desc
, to_number(regexp_substr(version, '[^.]+', 1, 4)) desc
) as rnum
from table1 a )
where rnum = 1
Here's a SQL Fiddle to demonstrate. Please note how I've had to convert each portion to a number to avoid a binary sort on numbers not between 0 and 9.
However, I cannot emphasise enough how much easier your life would be if you separated these up into different columns, major version, minor version etc. You could then have a virtual column that concatenates them all together to ensure that your export is always standardised, should you wish.
If, for instance, you created a table as follows:
create table table1 (
name varchar2(64)
, major number
, minor number
, build number
, revision number
, version varchar2(200) generated always as (
to_char(major) || '.' ||
to_char(minor) || '.' ||
to_char(build) || '.' ||
to_char(revision)
)
Your query becomes simpler to understand; also in SQL Fiddle
select name, version
from ( select a.*
, row_number() over (
partition by name
order by major desc
, minor desc
, build desc
, revision desc ) as rnum
from table1 a )
where rnum = 1

This solution is independent on how many numerical parts are inside version code.
It only assumes that every numerical part consists of not more than 6 digits.
select
name,
max(version) keep (dense_rank first order by version_norm desc)
as max_version
from (
select
t.*,
regexp_replace(
regexp_replace('000000'||version, '\.', '.000000')||'.',
'\d*(\d{6}\.)', '\1')
as version_norm
from table1 t
)
group by name
SQL Fiddle

You somehow need to convert the string values into numeric values, and then scale them by some appropriate multiplier. Assume that each version value must be a number between 0..99 as an example. So, if your string was "8.2.1.2", you would scale the numeric values of the string, "a.b.c.d" = d + c*100 + b*10000 + a*1000000, = 2 + 100 + 20000 + 8000000 =
8020102, then you can use that value to order.
I found a function you can use to parse a token from a delimited string:
CREATE OR REPLACE FUNCTION get_token (the_list VARCHAR2,
the_index NUMBER,
delim VARCHAR2 := ',')
RETURN VARCHAR2
IS
start_pos NUMBER;
end_pos NUMBER;
BEGIN
IF the_index = 1
THEN
start_pos := 1;
ELSE
start_pos :=
INSTR (the_list,
delim,
1,
the_index - 1);
IF start_pos = 0
THEN
RETURN NULL;
ELSE
start_pos := start_pos + LENGTH (delim);
END IF;
END IF;
end_pos :=
INSTR (the_list,
delim,
start_pos,
1);
IF end_pos = 0
THEN
RETURN SUBSTR (the_list, start_pos);
ELSE
RETURN SUBSTR (the_list, start_pos, end_pos - start_pos);
END IF;
END get_token;
so call something like
select to_number(get_token(version,1,'.'))*1000000 + to_number(get_token(version,2,'.'))*10000 + .. etc.

Just wrote a MySQL user defined function to accomplish the task, you can easily port it to ORACLE PL/SQL.
DELIMITER $$
DROP FUNCTION IF EXISTS `VerCmp`$$
CREATE FUNCTION VerCmp (VerX VARCHAR(64), VerY VARCHAR(64), Delim CHAR(1))
RETURNS INT DETERMINISTIC
BEGIN
DECLARE idx INT UNSIGNED DEFAULT 1;
DECLARE xVer INT DEFAULT 0;
DECLARE yVer INT DEFAULT 0;
DECLARE xCount INT UNSIGNED DEFAULT 0;
DECLARE yCount INT UNSIGNED DEFAULT 0;
DECLARE counter INT UNSIGNED DEFAULT 0;
SET xCount = LENGTH(VerX) - LENGTH(REPLACE(VerX, Delim,'')) +1;
SET yCount = LENGTH(VerY) - LENGTH(REPLACE(VerY, Delim,'')) +1;
IF xCount > yCount THEN
SET counter = xCount;
ELSE
SET counter = yCount;
END IF;
WHILE (idx <= counter) DO
IF (xCount >= idx) THEN
SET xVer = SUBSTRING_INDEX(SUBSTRING_INDEX(VerX, Delim, idx), Delim, -1) +0;
ELSE
SET xVer =0;
END IF;
IF (yCount >= idx) THEN
SET yVer = SUBSTRING_INDEX(SUBSTRING_INDEX(VerY, Delim, idx), Delim, -1) +0;
ELSE
SET yVer = 0;
END IF;
IF (xVer > yVer) THEN
RETURN 1;
ELSEIF (xVer < yVer) THEN
RETURN -1;
END IF;
SET idx = idx +1;
END WHILE;
RETURN 0;
END$$;
DELIMITER ;
Few test that I ran:
select vercmp('5.2.4','5.2.5','.');
+------------------------------+
| vercmp('5.2.4','5.2.5','.') |
+------------------------------+
| -1 |
+------------------------------+
select vercmp('5.2.4','5.2.4','.');
+------------------------------+
| vercmp('5.2.4','5.2.4','.') |
+------------------------------+
| 0 |
+------------------------------+
select vercmp('5.2.4','5.2','.');
+----------------------------+
| vercmp('5.2.4','5.2','.') |
+----------------------------+
| 1 |
+----------------------------+
select vercmp('1,2,4','5,2',',');
+----------------------------+
| vercmp('1,2,4','5,2',',') |
+----------------------------+
| -1 |
+----------------------------+

Related

Issues in inserting comma separated strings to table

I have following 3 parameters from stored procedure
P_Param1 = 12
P_Paramj2= 'val:15,val:16'
P_param3 = 'Name:check values,Name:bv,Name:cv'
I have a table and need to insert above details and final table looks like below
proID CatID CatName
12 15 check values
12 15 bv
12 15 cv
12 16 check values
12 16 bv
12 16 cv
I have written a query to split P_param3 as below and getting splitted values but stuck in generating loops to make a table like above.
SELECT
regexp_substr('Name:check values,Name:bv,Name:cv', '(Name:)?(.*?)(,Name:|$)', 1, level, NULL,
2) AS "CatName"
FROM
dual
CONNECT BY
level <= regexp_count('Name:check values,Name:bv,Name:cv', 'Name:');
Split the catIds into rows and split the catNames into rows and then CROSS JOIN them and insert.
You can do it with simple (fast) string functions using:
CREATE PROCEDURE insertCats(
p_proid IN table_name.proid%TYPE,
p_catids IN VARCHAR2,
p_catnames IN VARCHAR2
)
IS
c_catid_prefix CONSTANT VARCHAR2(10) := 'val:';
c_catid_length CONSTANT PLS_INTEGER := LENGTH(c_catid_prefix);
c_catname_prefix CONSTANT VARCHAR2(10) := 'Name:';
c_catname_length CONSTANT PLS_INTEGER := LENGTH(c_catname_prefix);
BEGIN
INSERT INTO table_name (proid, catid, catname)
WITH catid_bounds (catids, spos, epos) AS (
SELECT p_catids,
1 + c_catid_length,
INSTR(p_catids, ',', 1 + c_catid_length)
FROM DUAL
UNION ALL
SELECT catids,
epos + 1 + c_catid_length,
INSTR(catids, ',', epos + 1 + c_catid_length)
FROM catid_bounds
WHERE epos > 0
),
catids (catid) AS (
SELECT CASE epos
WHEN 0
THEN SUBSTR(catids, spos)
ELSE SUBSTR(catids, spos, epos - spos)
END
FROM catid_bounds
),
catname_bounds (catnames, spos, epos) AS (
SELECT p_catnames,
1 + c_catname_length,
INSTR(p_catnames, ',', 1 + c_catname_length)
FROM DUAL
UNION ALL
SELECT catnames,
epos + 1 + c_catname_length,
INSTR(catnames, ',', epos + 1 + c_catname_length)
FROM catname_bounds
WHERE epos > 0
),
catnames (catname) AS (
SELECT CASE epos
WHEN 0
THEN SUBSTR(catnames, spos)
ELSE SUBSTR(catnames, spos, epos - spos)
END
FROM catname_bounds
)
SELECT p_proid,
i.catid,
n.catname
FROM catids i CROSS JOIN catnames n;
END;
/
db<>fiddle here

Function in Oracle that returns 2 different columns of a table

I'm trying to create a function that returns 2 different columns in a single table, can anyone help me with that?
I've already tried this way:
CREATE FUNCTION return_id_grade(subjectId IN NUMBER, semesterYear IN DATE , n IN INT, option IN INT)
RETURN NUMBER
IS studentId NUMBER(5),
IS studentGrade NUMBER(2,1);
BEGIN
SELECT DISTINCT student_id INTO studentId,
grade INTO studentGrade
FROM (SELECT studentId, grade, dense_rank() over (ORDER BY grade desc) rank FROM old_students)
WHERE subject_id = subjectId
AND semester_year = semesterYear
AND rank = n
AND rownum <= 1
CASE
WHEN option = 1 then RETURN(student_id)
WHEN option = 2 then RETURN(grade)
END;
END;
I expected to output the n'NTH grade of an university class and the student Id, but the actual can just output the option received on parameter field.
a.You cant use Select colum1 INTO variable1 , colum2 INTO variable2 . It has to be like :
Select column1 , column2 INTO variable1 , variable 2
b.Create an object type and use it as out parameter in a procedure
c.Have the option condition after the procedure is called.
Sample Code:
CREATE OR REPLACE TYPE ty_obj_idgrade AS OBJECT
(studentId NUMBER(5)
,studentGrade NUMBER(2,1)
);
CREATE OR REPLACE PROCEDURE return_id_grade(
subjectId IN NUMBER,
semesterYear IN DATE ,
n IN INT,
-- options IN INT
,p_idgrade OUT ty_obj_idgrade) IS
BEGIN
SELECT DISTINCT student_id --INTO studentId,
,grade --INTO studentGrade
INTO p_idgrade.studentId
,p_idgrade.grade
FROM (SELECT studentId
,grade
,dense_rank() over (ORDER BY grade desc) rank
,subject_id
,semester_year
FROM old_students )
WHERE subject_id = subjectId
AND semester_year = semesterYear
AND rank = n
AND rownum <= 1;
EXCEPTION
WHEN OTHERS THEN
dbms_output.put_line('we are inside when others -->'||sqlerrm);
END;
Call your procedure.
Since options was used as IN parameter , it should be availabe outside the prc/fnc
So this can be done after the prc/fnc call
If options = 1
THEN
value := p_idgrade.conatct
ELSE
value := p_idgrade.grade
END IF;
Hope it helps.
Did not try compiling it, but something like this should be close.
CREATE FUNCTION return_id_grade(subjectId IN NUMBER, semesterYear IN DATE , n IN INT, option IN INT)
RETURN NUMBER IS
studentId NUMBER(5),
studentGrade NUMBER(2,1);
BEGIN
SELECT DISTINCT student_id, grade
INTO studentId, studentGrade
FROM (SELECT studentId, grade, dense_rank() over (ORDER BY grade desc) rank FROM old_students)
WHERE subject_id = subjectId
AND semester_year = semesterYear
AND rank = n
AND rownum <= 1;
IF option = 1 then
RETURN studentId ;
ELSE
RETURN studentGrade ;
END IF;
END;
END;
However, this function is really not a good design. A function should perform a single task. If you want to return both items, create a PL/SQL record type, and use a stored procedure with an OUT parameter and return that in the procedure.
You can directly try to use OPTION in query as following:
CREATE FUNCTION RETURN_ID_GRADE (
SUBJECTID IN NUMBER,
SEMESTERYEAR IN DATE,
N IN INT,
OPTION IN INT
) RETURN NUMBER IS
LV_RETURN_NUMBER NUMBER(6, 1);
BEGIN
-- QUERY TO FETCH REQUIRED DATA ONLY
SELECT -- DISTINCT -- DISTINCT IS NOT NEEDED AS ROWNUM <= 1 IS USED
CASE
WHEN OPTION = 1 THEN STUDENT_ID
ELSE GRADE
END AS LV_RETURN_NUMBER
INTO LV_RETURN_NUMBER -- STORE VALUE BASED ON OPTION
FROM
(
SELECT
STUDENTID,
GRADE,
DENSE_RANK() OVER(
ORDER BY
GRADE DESC
) RANK
FROM
OLD_STUDENTS
)
WHERE
SUBJECT_ID = SUBJECTID
AND SEMESTER_YEAR = SEMESTERYEAR
AND RANK = N
AND ROWNUM <= 1;
RETURN LV_RETURN_NUMBER; -- RETURN THE VARIABLE
END;
Cheers!!

Oracle filter results by limit

I am new to Oracle and was hoping someone could help me.
I have this stored procedure:
procedure ListCatalogues(P_CUR out sys_refcursor,
P_CATALOGUENAME varchar2 default '%',
P_LIMIT number,
P_MEMBERS number default -1) is
begin
open P_CUR for
select *
from ( select h.catalogueid id,
h.cataloguename name,
case
when h.uniquecatalogue = 'N'
then 1
else 0
end includeproducts,
case
when h.active = 'Y'
then 1
else 0
end active,
case
when h.ownbrandedlabels = 'Y'
then 1
else 0
end ownlabels,
( select count(*)
from cc_custprofiles t
where t.catalogueid = h.catalogueid
) members
from cc_ob_catalogueheader h
where upper(h.cataloguename) like upper('%'||P_CATALOGUENAME||'%')
and (select count(*) from cc_custprofiles t where t.catalogueid = h.catalogueid) >= P_MEMBERS
order by h.catalogueid
)
where rownum <= P_LIMIT;
end ListCatalogues;
As you can see, it accepts a P_LIMIT parameter which allows for limiting the results returned. This is fine, but I want to expand on it a little.
If the limit is 10, then return 10 rows, but if the limit is 0, return everything. Can someone help me change the query to match my criteria?
I managed this after a bit of looking around:
where rownum <= case when P_LIMIT = 0 then rownum else P_LIMIT end;

Query with iteration in Oracle pl/sql

I have the following table with a single column
Z_NUM
--------
34545
345
656
32
42
...
I want to build a following dependence
i | SUM(Z_NUM)
----------------
2 | 40934
3 | 51244
4 | 54793
...
based on query
SELECT SUM(z_num) FROM table WHERE z_num < i;
The variable i is a parameter and should be incremented by 1.
How to implement this query in ORACLE?
If I were doing this in MYSQL, I would write something like
SELECT
​#n := #n + 1 n,
SUM(z_num)
FROM table, (SELECT #n := 1) m
WHERE z_num < n;
But unfortunately it does not work in Oracle PL/SQL.
Use two subqueries:
one with connect by to generate numbers i from 2 to N
another dependend subquery to calculate a sum for each i
SELECT i,
( SELECT coalesce( sum(z_num), 0 )
FROM table1 WHERE z_num < i
) as myresult
FROM (
SELECT level+1 As i FROM dual
CONNECT BY LEVEL <= ( SELECT max(Z_NUM) FROM table1 )-1
)
ORDER BY i
Demo: http://sqlfiddle.com/#!4/c460c/5
use ROWNUM,
SELECT SUM(z_num)
FROM (SELECT z_num, rownum rown
FROM table)
WHERE z_num < rown;
OR
SELECT SUM(z_num)
FROM (SELECT z_num, rownum rown
FROM table)
WHERE rown < 10;
Try this:
SELECT i,
( SELECT coalesce( sum(z_num), 0 )
FROM Table_NUM WHERE ROWNUM <= i
) as SUM(Z_NUM)
FROM (
SELECT level+1 As i FROM dual
CONNECT BY LEVEL <= ( SELECT Max(ROWNUM) FROM Table_NUM )-1
)
ORDER BY i;
Result:
I SUM_RESULT
-----------------
2 34890
3 35546
4 35578
5 35620
It may be help you.

Oracle procedure or function to return multiple values

I need to write a procedure or a function which returns the count of status, age and type which should satisfy the below criteria
select * from ABC
where ABC_id = 2001
and ABC_LEVEL_ID = 1 --status
and ABC_REQUEST_DATE < sysdate --age
and ABC_TYPE_ID = 5; --type
If ABC_ID = 2001 and ABC_LEVEL_ID = 1
THEN return COUNT(STATUS)
If ABC_ID = 2001 and ABC_REQUEST_DATE < SYSDATE
THEN return COUNT(AGE)
If ABC_ID = 2001 and ABC_TYPE_ID = 5
THEN return COUNT(TYPE)
All three values should be OUT parameters which are passed to front end application.
You can use a CASE expression to your query to include those constraint like
select *,
case when ABC_ID = 2001 and ABC_LEVEL_ID = 1 then COUNT(STATUS) else null end as testcol1,
case when ABC_ID = 2001 and ABC_REQUEST_DATE < SYSDATE then COUNT(AGE) else null end as testcol2,
case when ABC_ID = 2001 and ABC_TYPE_ID = 5 then COUNT(TYPE) else null end as testcol3
from ABC
where ABC_id = 2001
and ABC_LEVEL_ID = 1 --status
and ABC_REQUEST_DATE < sysdate --age
and ABC_TYPE_ID = 5; --type
Per Comment: modified query (to include #jeffrykemps earlier edit)
select *,
case when ABC_LEVEL_ID = 1 then COUNT(STATUS) end as testcol1,
case when ABC_REQUEST_DATE < SYSDATE then COUNT(AGE) end as testcol2,
case when ABC_TYPE_ID = 5 then COUNT(TYPE) end as testcol3
from ABC
where ABC_id = 2001
and ABC_LEVEL_ID = 1 --status
and ABC_REQUEST_DATE < sysdate --age
and ABC_TYPE_ID = 5; --type
I think it would make more sense if your WHERE clause uses OR operations rather than AND. It's easy to do the counts in the projection of the query, using CASE statements.
As the code belongs to a stored procedure you need to select into something. Here I've assumed direct assignment to the OUT parameters. However, if you code contains additional requirements you should populate local variables instead, and assign them to the OUT parameters at the end of the procedure.
create or replace procedure get_counts
( p_out_status_count out pls_integer
, p_out_age_count out pls_integer
, p_out_type_count out pls_integer
as
begin
select
count (case ABC_LEVEL_ID = 1 then 1 else null end),
count (case ABC_REQUEST_DATE < SYSDATE then 1 else null end),
count (case ABC_TYPE_ID = 5 then 1 else null end)
into p_out_status_count
, p_out_age_count
, p_out_type_count
from ABC
where ABC_id = 2001
and (ABC_LEVEL_ID = 1 --status
or ABC_REQUEST_DATE < sysdate --age
or ABC_TYPE_ID = 5); -- type
end get_counts;
Also you might want to paramterize the ABC_ID. In which case the procedure's signature might be:
create or replace procedure get_counts
( p_abc_id in abc.abc_id%type
, p_out_status_count out pls_integer
, p_out_age_count out pls_integer
, p_out_type_count out pls_integer
)
and the WHERE clause would be
....
from ABC
where ABC_id = p_abc_id
....

Resources