Oracle filter results by limit - oracle

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;

Related

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!!

How to compare two sets of rows in Oracle?

So, the problem is that i have two results (eg. number):
RES1:
10
11
RES2:
10
13
I need to compare those like if RES1 in RES2 and RES2 in RES1.
I would like to have result like:
RES3:
11
13
How do i do that?
I tried
RES1 MINUS RES2
UNION
RES2 MINUS RES1
but this approach is very slow, becouse my table contains milions of rows...
Why not to use one of supplied packages. DBMS_COMPARISON
The Package allows to compare and sync tables. It's only required that tables have an index.
1) create diff datasets
create table to_compare2 as (select OBJECT_NAME, SUBOBJECT_NAME, OBJECT_ID, DATA_OBJECT_ID, OBJECT_TYPE, case when mod(object_id,18) = 0 then CREATED +1 else CREATED end CREATED from all_objects where mod(object_id,6) = 0 );
CREATE table to_compare1 as (SELECT OBJECT_NAME, SUBOBJECT_NAME, OBJECT_ID, DATA_OBJECT_ID, OBJECT_TYPE, case when mod(object_id,12) = 0 then CREATED +1 else CREATED end CREATED FROM ALL_OBJECTS where mod(object_id,3) = 0 );
2) create indexes.
CREATE UNIQUE INDEX to_compare1_idx on to_compare1(object_id);
CREATE UNIQUE INDEX to_compare2_idx on to_compare2(object_id);
3) Prepare comparision context
BEGIN
DBMS_COMPARISON.create_comparison (
comparison_name => 'MY_COMPARISION',
schema_name => user,
object_name => 'to_compare1',
dblink_name => NULL,
remote_schema_name => null,
remote_object_name => 'to_compare2');
END;
/
4) Execute comparison and check results.
DECLARE
v_scan_info DBMS_COMPARISON.comparison_type;
v_result BOOLEAN;
BEGIN
v_result := DBMS_COMPARISON.compare (
comparison_name => 'MY_COMPARISION',
scan_info => v_scan_info,
perform_row_dif => TRUE
);
IF NOT v_result THEN
DBMS_OUTPUT.put_line('Differences. scan_id=' || v_scan_info.scan_id);
ELSE
DBMS_OUTPUT.put_line('No differences.');
END IF;
END;
/
4) Results
SELECT *
FROM user_comparison_row_dif
WHERE comparison_name = 'MY_COMPARISION';
if local_rowid is not null and remote_rowid is null -> record exit in table_1
if local_rowid is null and remote_rowid is not null -> record exit in table_2
if local_rowid is not null and remote_rowid is not null -> record exist in both tables but it has different values
solutin 1:
try UNION ALL instead of UNION.
why UNION ALL is better then UNION you can read here: What is the difference between UNION and UNION ALL?
solutin 2:
you can try to use full outer join
select coalesce(a.id,b.id)
from a
full outer join b on a.id = b.id
where a.id is null
or b.id is null
Example: http://www.sqlfiddle.com/#!4/88f81/3
Are those Values unique in RES1 or RES2? Then you could try counting:
SELECT col
FROM (
SELECT col FROM RES1
UNION ALL
SELECT col FROM RES2
)
GROUP BY col
HAVING COUNT(1) = 1
If it is not unique, you'd have to add a distinct on both sides of the union, which makes it a lot slower

How to use apex_string.split?

I'm trying to split a string : OK#15#78 by #
I would like to get the first part of the string : Ok
I tried the following queries but it's not working :
select apex_string.split('OK#15#78','#')[0] from dual;
Can anyone help please ?
Cheers
You could use TABLE and rownum:
SELECT val
FROM (Select rownum AS rn, column_value AS val
FROM TABLE(apex_string.split('OK#15#78','#')))
WHERE rn = 1;
ORIGNAL POST:
Even simpler:
SELECT COLUMN_VALUE val, ROWNUM
FROM apex_string.split('OK#15#78','#')
WHERE ROWNUM = 1;
Or maybe even faster:
SELECT COLUMN_VALUE val
FROM apex_string.split('OK#15#78','#')
FETCH FIRST 1 ROWS ONLY
REVISED ANSWER
There is a caveat here: APEX_STRING.SPLIT results in a nested table, and there are no guarantees about the order of the returned items, so this actually doesn't work.
You should use the regxp_substr method
SELECT val c
FROM
(
(SELECT regexp_substr('OK#15#78','[^#]+', 1, level)
FROM DUAL
CONNECT BY regexp_substr(, '[^#]+', 1, level) IS NOT NULL)
)
FETCH FIRST 1 ROWS ONLY

Find the greatest version

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 |
+----------------------------+

how to make selecting random rows in oracle faster with table with millions of rows

Is there a way to make selecting random rows faster in oracle with a table that has million of rows. I tried to use sample(x) and dbms_random.value and its taking a long time to run.
Thanks!
Using appropriate values of sample(x) is the fastest way you can. It's block-random and row-random within blocks, so if you only want one random row:
select dbms_rowid.rowid_relative_fno(rowid) as fileno,
dbms_rowid.rowid_block_number(rowid) as blockno,
dbms_rowid.rowid_row_number(rowid) as offset
from (select rowid from [my_big_table] sample (.01))
where rownum = 1
I'm using a subpartitioned table, and I'm getting pretty good randomness even grabbing multiple rows:
select dbms_rowid.rowid_relative_fno(rowid) as fileno,
dbms_rowid.rowid_block_number(rowid) as blockno,
dbms_rowid.rowid_row_number(rowid) as offset
from (select rowid from [my_big_table] sample (.01))
where rownum <= 5
FILENO BLOCKNO OFFSET
---------- ---------- ----------
152 2454936 11
152 2463140 32
152 2335208 2
152 2429207 23
152 2746125 28
I suspect you should probably tune your SAMPLE clause to use an appropriate sample size for what you're fetching.
Start with Adam's answer first, but if SAMPLE just isn't fast enough, even with the ROWNUM optimization, you can use block samples:
....FROM [table] SAMPLE BLOCK (0.01)
This applies the sampling at the block level instead of for each row. This does mean that it can skip large swathes of data from the table so the sample percent will be very rough. It's not unusual for a SAMPLE BLOCK with a low percentage to return zero rows.
Here's the same question on AskTom:
http://asktom.oracle.com/pls/apex/f?p=100:11:0::::P11_QUESTION_ID:6075151195522
If you know how big your table is, use sample block as described above. If you don't, you can modify the routine below to get however many rows you want.
Copied from: http://asktom.oracle.com/pls/apex/f?p=100:11:0::::P11_QUESTION_ID:6075151195522#56174726207861
create or replace function get_random_rowid
( table_name varchar2
) return urowid
as
sql_v varchar2(100);
urowid_t dbms_sql.urowid_table;
cursor_v integer;
status_v integer;
rows_v integer;
begin
for exp_v in -6..2 loop
exit when (urowid_t.count > 0);
if (exp_v < 2) then
sql_v := 'select rowid from ' || table_name
|| ' sample block (' || power(10, exp_v) || ')';
else
sql_v := 'select rowid from ' || table_name;
end if;
cursor_v := dbms_sql.open_cursor;
dbms_sql.parse(cursor_v, sql_v, dbms_sql.native);
dbms_sql.define_array(cursor_v, 1, urowid_t, 100, 0);
status_v := dbms_sql.execute(cursor_v);
loop
rows_v := dbms_sql.fetch_rows(cursor_v);
dbms_sql.column_value(cursor_v, 1, urowid_t);
exit when rows_v != 100;
end loop;
dbms_sql.close_cursor(cursor_v);
end loop;
if (urowid_t.count > 0) then
return urowid_t(trunc(dbms_random.value(0, urowid_t.count)));
end if;
return null;
exception when others then
if (dbms_sql.is_open(cursor_v)) then
dbms_sql.close_cursor(cursor_v);
end if;
raise;
end;
/
show errors
Below Solution to this question is not the exact answer but in many scenarios you try to select a row and try to use it for some purpose and then update its status with "used" or "done" so that you do not select it again.
Solution:
Below query is useful but that way if your table is large, I just tried and see that you definitely face performance problem with this query.
SELECT * FROM
( SELECT * FROM table
ORDER BY dbms_random.value )
WHERE rownum = 1
So if you set a rownum like below then you can work around the performance problem. By incrementing rownum you can reduce the possiblities. But in this case you will always get rows from the same 1000 rows. If you get a row from 1000 and update its status with "USED", you will almost get different row everytime you query with "ACTIVE"
SELECT * FROM
( SELECT * FROM table
where rownum < 1000
and status = 'ACTIVE'
ORDER BY dbms_random.value )
WHERE rownum = 1
update the rows status after selecting it, If you can not update that means another transaction has already used it. Then You should try to get a new row and update its status. By the way, getting the same row by two different transaction possibility is 0.001 since rownum is 1000.
Someone told sample(x) is the fastest way you can.
But for me this method works slightly faster than sample(x) method.
It should take fraction of the second (0.2 in my case) no matter what is the size of the table. If it takes longer try to use hints (--+ leading(e) use_nl(e t) rowid(t)) can help
SELECT *
FROM My_User.My_Table
WHERE ROWID = (SELECT MAX(t.ROWID) KEEP(DENSE_RANK FIRST ORDER BY dbms_random.value)
FROM (SELECT o.Data_Object_Id,
e.Relative_Fno,
e.Block_Id + TRUNC(Dbms_Random.Value(0, e.Blocks)) AS Block_Id
FROM Dba_Extents e
JOIN Dba_Objects o ON o.Owner = e.Owner AND o.Object_Type = e.Segment_Type AND o.Object_Name = e.Segment_Name
WHERE e.Segment_Name = 'MY_TABLE'
AND(e.Segment_Type, e.Owner, e.Extent_Id) =
(SELECT MAX(e.Segment_Type) AS Segment_Type,
MAX(e.Owner) AS Owner,
MAX(e.Extent_Id) KEEP(DENSE_RANK FIRST ORDER BY Dbms_Random.Value) AS Extent_Id
FROM Dba_Extents e
WHERE e.Segment_Name = 'MY_TABLE'
AND e.Owner = 'MY_USER'
AND e.Segment_Type = 'TABLE')) e
JOIN My_User.My_Table t
ON t.Rowid BETWEEN Dbms_Rowid.Rowid_Create(1, Data_Object_Id, Relative_Fno, Block_Id, 0)
AND Dbms_Rowid.Rowid_Create(1, Data_Object_Id, Relative_Fno, Block_Id, 32767))
Version with retries when no rows returned:
WITH gen AS ((SELECT --+ inline leading(e) use_nl(e t) rowid(t)
MAX(t.ROWID) KEEP(DENSE_RANK FIRST ORDER BY dbms_random.value) Row_Id
FROM (SELECT o.Data_Object_Id,
e.Relative_Fno,
e.Block_Id + TRUNC(Dbms_Random.Value(0, e.Blocks)) AS Block_Id
FROM Dba_Extents e
JOIN Dba_Objects o ON o.Owner = e.Owner AND o.Object_Type = e.Segment_Type AND o.Object_Name = e.Segment_Name
WHERE e.Segment_Name = 'MY_TABLE'
AND(e.Segment_Type, e.Owner, e.Extent_Id) =
(SELECT MAX(e.Segment_Type) AS Segment_Type,
MAX(e.Owner) AS Owner,
MAX(e.Extent_Id) KEEP(DENSE_RANK FIRST ORDER BY Dbms_Random.Value) AS Extent_Id
FROM Dba_Extents e
WHERE e.Segment_Name = 'MY_TABLE'
AND e.Owner = 'MY_USER'
AND e.Segment_Type = 'TABLE')) e
JOIN MY_USER.MY_TABLE t ON t.ROWID BETWEEN Dbms_Rowid.Rowid_Create(1, Data_Object_Id, Relative_Fno, Block_Id, 0)
AND Dbms_Rowid.Rowid_Create(1, Data_Object_Id, Relative_Fno, Block_Id, 32767))),
Retries(Cnt, Row_Id) AS (SELECT 1, gen.Row_Id
FROM Dual
LEFT JOIN gen ON 1=1
UNION ALL
SELECT Cnt + 1, gen.Row_Id
FROM Retries
LEFT JOIN gen ON 1=1
WHERE Retries.Row_Id IS NULL AND Retries.Cnt < 10)
SELECT *
FROM MY_USER.MY_TABLE
WHERE ROWID = (SELECT Row_Id
FROM Retries
WHERE Row_Id IS NOT NULL)
Can you use pseudorandom rows?
select * from (
select * from ... where... order by ora_hash(rowid)
) where rownum<100

Resources