How to write user defined average function in oracle - oracle

I am trying to create a user defined avg function, e.g. to calculate an average, sum or count - are there any examples I can follow?

As #David Aldridge mentioned, the "official" way to build a user-defined aggregate function is to use Oracle Data Cartridge. But in my experience, it is simpler, faster, and less buggy to use the CAST(COLLECT method. The only downside is your SQL statement requires some extra syntax.
For example, to create a custom average function that ignores the number 1:
--Created nested table of numbers
create or replace type number_nt is table of number;
--Create a custom average function.
--For example, average everything except the number "1".
create or replace function my_avg(numbers number_nt) return number is
v_sum number := 0;
v_count number := 0;
begin
--Sum and count all the values, excluding nulls and "1".
for i in 1 .. numbers.count loop
if numbers(i) is not null and numbers(i) <> 1 then
v_sum := v_sum + numbers(i);
v_count := v_count + 1;
end if;
end loop;
if v_count = 0 then
return null;
else
return v_sum/v_count;
end if;
end;
/
Here's how to call the function:
--Regular average is 2, our custom average that excludes 1 should be 2.5.
select
avg(test_value) avg
,my_avg(cast(collect(test_value) as number_nt)) my_avg
from
(
select 1 test_value from dual union all
select 2 test_value from dual union all
select 3 test_value from dual
);
AVG MY_AVG
-- ------
2 2.5

What you're looking for is the User Defined Aggregates Function Interface, documented in the Data Cartridge Developers' Guide.
Some examples are documented here.

Related

Database users loop with password generator

I have created some kind of code block till now, and I'm stuck. I'm selecting from table called FND_ORACLE_USERID for EBS database user schemas. Idea is to have those accounts printed in format like this (password is placeholder):
FNDCPASS user/password 0 Y system/test11 ALLORACLE password
Till now I have done this:
set serveroutput on;
DECLARE
c1 SYS_REFCURSOR;
l_pass varchar(16);
l_count number(3);
BEGIN
select count (*) into l_count from FND_ORACLE_USERID
where READ_ONLY_FLAG='A';
OPEN c1 for
select xmlagg(xmlelement("r", ch)).extract('//text()').getstringval() ch
from
(
select distinct first_value(ch) over (partition by lower(ch)) as ch
from (
select substr('abcd$efghijklmn#pqrstuvw#xyzABC$DEFGHIJK$LMNPQR!STUVWXYZ1!23456789',
level, 1) as ch
from dual
connect by level <= 59
order by dbms_random.value
)
where rownum <= dbms_random.value(17,17)
);
LOOP
FETCH c1
INTO l_pass;
EXIT WHEN c1%NOTFOUND;
DBMS_OUTPUT.PUT_LINE('FNDCPASS user/'||l_pass||' 0 Y system/test11 ALLORACLE '||l_pass||'');
END LOOP;
CLOSE c1;
END;
/
So I have started to count schemas, and I would like to stop printing new lines from above, when it reach end of count variable.
I'm not so good with plsql so please could you give some advice?
Thanks
Ok, i have created password function which returns string value for my password generator code now, like this :
create or replace function generic_passwd
return varchar2 is passwd VARCHAR2(17);
begin
select xmlagg(xmlelement("r", ch)).extract('//text()').getstringval() ch into passwd
from
(
select distinct first_value(ch) over (partition by lower(ch)) as ch
from (
select substr('abcd$efghijklmn#pqrstuvw#xyzABC$DEFGHIJK$LMNPQR!STUVWXYZ1!23456789',
level, 1) as ch
from dual
connect by level <= 59
order by dbms_random.value
)
where rownum <= dbms_random.value(17,17)
);
return passwd;
end;
/
Then i called this function from my code block and it is fine to me, like this:
set serveroutput on size 1000000
DECLARE
c1 SYS_REFCURSOR;
l_user varchar(10);
l_passd varchar(17);
BEGIN
OPEN c1 for
select oracle_username, generic_passwd() from FND_ORACLE_USERID where READ_ONLY_FLAG='A';
LOOP
FETCH c1
INTO l_user,l_passd;
EXIT WHEN c1%NOTFOUND ;
DBMS_OUTPUT.PUT_LINE('FNDCPASS '||l_user||'/'||l_passd||' 0 Y system/oracle11 ALLORACLE '||l_passd||'');
END LOOP;
CLOSE c1;
END;
/
Thank you for your time Jeff.
Found your own solution. Good on you. There are perhaps a couple inconsistencies, which may actually be intentional. But I think at least one or two are not:
The digits 3-9 can never appear in the output. You sub string
individual characters up to the 59th in the base string, however
that string is 66 characters in length. Perhaps the original base
string changed but the hard coded value did not. Intentional?
The output cannot have a repeated character, seems intentional, but
neither can it have a letter and the corresponding alternate case of
that letter. I.E cannot have both "a" and "A" in the output. Intentional?
The Expression: dbms_random.value(17,17) will always return 17, at least in a piratical since as rownum is always as integer.
So there is no reason to pay the function call overhead. Just use
the equivalent rownum <= 17;
Just a side note: This seems to be a limited use function. If that
is not the case then creating a standalone function is the way to
go. If, however this limited and it is version 12.1 or higher then
you may want to build the function directly into query using WITH
FUNCTION .... Doing so eliminate context switch between the
PL/SQL and SQL thus gaining performance.
The following addresses all these: See Demo But i reiterate. Except for #1 and #2 above what you have will work well.
with function generic_passwd --<<< define function directly as part of the query
return varchar2
is
l_generic_password varchar2(17);
begin
with base (stg) as
( select 'abcd$efghijklmn#pqrstuvw#xyzABC$DEFGHIJK$LMNPQR!STUVWXYZ1!23456789' from dual)
select xmlagg(xmlelement("r", ch)).extract('//text()').getstringval()
into l_generic_password
from
(
select distinct ch as ch --<< do not allow duplicate letters, but allow lower and upper of same letter
from (
select substr(stg,level, 1) as ch
from base
connect by level <= length(stg) --<< determine stop point from base string itself.
order by dbms_random.value
)
where rownum <= 17 --<< avoid subroutine overhead that always returns same value.
);
return l_generic_password;
end;
select username, generic_passwd() password
from fnd_oracle_userid;

Oracle using the like comparison taken from function in value

I am trying to create a simple function that takes in 3 parameters, 2 numbers and a string. I have written the function but am not getting the expected results from a simple select statement when using the LIKE comparison for the string.
The select from the function below returns no rows when executed with the string input value set to ebts, but if I run this as a standalone select state it returns 2 rows which what I would expect. Have used dbms output to determine if whitespace were being passed but all looks OK.
CREATE OR REPLACE FUNCTION OPC_OP.sitezone_exists
(in_site_id IN NUMBER, in_zone_id IN NUMBER, in_mod VARCHAR2)
RETURN NUMBER
IS
v_count_rec NUMBER;
v_return NUMBER;
v_mod VARCHAR2(4) := in_mod;
BEGIN
SELECT COUNT(*)
INTO v_count_rec
FROM AW_ACTIVE_ALARMS
WHERE AW_ACTIVE_ALARMS.site_id = in_site_id
AND AW_ACTIVE_ALARMS.zone_id = in_zone_id
AND AW_ACTIVE_ALARMS.module LIKE 'v_mod%';
IF v_count_rec > 0
THEN
DBMS_OUTPUT.PUT_LINE('count'||v_count_rec||'=========='||v_mod||'=============');
v_return:= 1;
RETURN (v_return);
ELSE
DBMS_OUTPUT.PUT_LINE('count'||v_count_rec||'=========='||v_mod||'=============');
v_return:= 0;
RETURN (v_return);
END IF;
END sitezone_exists;
When passing in values 12, 12, ebts the output displayed is:
count 0 ==========ebts=============
RetVal = 0
If I run the same select subtituting only passing in the above values the query returns 2 rows - I have removed the like clause of the function and it then returns 2 rows, any idea why the like part of clause is failing to match with rows.
You are trying to match a string literal with this:
AND AW_ACTIVE_ALARMS.module LIKE 'v_mod%';
Change it to:
AND AW_ACTIVE_ALARMS.module LIKE v_mod||'%';
MarioAna has the right answer, IMO, but as an aside (which is too long for a comment), your function can be better written.
You're duplicating the dbms_output code, plus it's considered best practice to have a single RETURN in a function (although I would argue that more might be ok - one in the body and one per exception in the exception block...), so you could rewrite it as:
CREATE OR REPLACE FUNCTION OPC_OP.sitezone_exists
(in_site_id IN NUMBER, in_zone_id IN NUMBER, in_mod VARCHAR2)
RETURN NUMBER
IS
v_count_rec NUMBER;
v_return NUMBER;
v_mod VARCHAR2(4) := in_mod;
BEGIN
SELECT COUNT(*)
INTO v_count_rec
FROM AW_ACTIVE_ALARMS aaa
WHERE aaa.site_id = in_site_id
AND aaa.zone_id = in_zone_id
AND aaa.module LIKE v_mod||'%';
DBMS_OUTPUT.PUT_LINE('count '||v_count_rec||'=========='||v_mod||'=============');
IF v_count_rec > 0 THEN
v_return := 1;
ELSE
v_return:= 0;
END IF;
RETURN (v_return);
END sitezone_exists;
/

ORA-02014- How do I update a randomly selected row from a table?

I'm trying to randomly select a card from a table of cards with columns c_value and c_suit using a procedure. After selecting it, the procedure should update that entry's taken field to be 'Y'.
create or replace procedure j_prc_sel_card(p_value OUT number,
p_suit OUT number)
AS
CURSOR CUR_GET_RAND_CARD IS SELECT c_value,
c_suit
FROM (SELECT c_value,
c_suit,
taken
FROM jackson_card
ORDER BY dbms_random.value)
WHERE rownum = 1
FOR UPDATE OF taken;
BEGIN
OPEN CUR_GET_RAND_CARD;
FETCH CUR_GET_RAND_CARD into p_value, p_suit;
UPDATE jackson_card
SET taken = 'Y'
WHERE c_value = p_value
AND c_suit = p_suit;
CLOSE CUR_GET_RAND_CARD;
END;
Then I am trying to get the selected card and output what it is as a start. With this:
SET serveroutput on;
DECLARE v_value number;
v_suit number;
BEGIN
j_prc_sel_card(p_value => v_value,p_suit => v_suit);
DBMS_OUTPUT.PUT_LINE(v_value);
DBMS_OUTPUT.PUT_LINE(v_suit);
END;
/
However i got the error stated in the title and it seems my way of selecting a random card is stopping me from doing an update. Thanks in advance!
Here is a different take on the scenario (I did also address your immediate problem in a different answer).
Given that we really are building a card-dealing program (as opposed to working with a test case for a business scenario) I didn't like the TAKEN column. Updating a table column to mark a transitory state seems wrong. What happens when we want to play another game?
The following solution resolves this by populating an array with all the cards in a random order upfront (the shuffle). The cards are dealt by simply taking the next entry off the stack. The package offers a choice of approach for running out of cards: either throw a user-defined exception or just cycle through the deck again.
create or replace package card_deck is
no_more_cards exception;
pragma exception_init(no_more_cards, -20000);
procedure shuffle;
function deal_one
( p_yn_continuous in varchar2 := 'N')
return cards%rowtype;
end card_deck;
/
create or replace package body card_deck is
type deck_t is table of cards%rowtype;
the_deck deck_t;
card_counter pls_integer;
procedure shuffle is
begin
dbms_random.seed (to_number(to_char(sysdate, 'sssss')));
select *
bulk collect into the_deck
from cards
order by dbms_random.value;
card_counter := 0;
end shuffle;
function deal_one
( p_yn_continuous in varchar2 := 'N')
return cards%rowtype
is
begin
card_counter := card_counter + 1;
if card_counter > the_deck.count()
then
if p_yn_continuous = 'N'
then
raise no_more_cards;
else
card_counter := 1;
end if;
end if;
return the_deck(card_counter);
end deal_one;
end card_deck;
/
Here it is in action. Don't use an open LOOP if you set the continuous dealing mode to Y.
SQL> set serveroutput on
SQL>
SQL> declare
2 my_card cards%rowtype;
3 begin
4 card_deck.shuffle;
5 loop
6 my_card := card_deck.deal_one;
7 dbms_output.put_line ('my card is '||my_card.c_suit||my_card.c_value);
8 end loop;
9 exception
10 when card_deck.no_more_cards then
11 dbms_output.put_line('no more cards!');
12 end;
13 /
my card is HA
my card is H7
my card is DJ
my card is CQ
my card is D9
my card is SK
no more cards!
PL/SQL procedure successfully completed.
SQL>
You may think I'm not dealing with a full deck. You wouldn't be the first to think that ;)
You are using an explicit cursor already, so you don't need the ROWNUM = 1 filter. Try this:
create or replace procedure j_prc_sel_card(p_value OUT number,
p_suit OUT number)
AS
CURSOR CUR_GET_RAND_CARD IS
SELECT c_value,
c_suit,
taken
FROM jackson_card
WHERE taken != 'Y'
ORDER BY dbms_random.value
FOR UPDATE OF taken;
BEGIN
OPEN CUR_GET_RAND_CARD;
FETCH CUR_GET_RAND_CARD into p_value, p_suit;
UPDATE jackson_card
SET taken = 'Y'
WHERE CURRENT OF cur_get_rand_card;
CLOSE CUR_GET_RAND_CARD;
END;
Note the use of WHERE CURRENT OF. This is the most efficient way of locating a row when we are using the FOR UPDATE CLAUSE. Without the use of the NOWAIT clause the cursor will hang if the chosen card is locked by another session. An unlikely scenario perhaps but one worth considering when you move beyond card games and into real scenarios.
Also, remember that for a truly random shuffle you need to call DBMS_RANDOM.SEED() at the start of proceedings.

Selecting Values from Oracle Table Variable / Array?

Following on from my last question (Table Variables in Oracle PL/SQL?)...
Once you have values in an array/table, how do you get them back out again? Preferably using a select statement or something of the like?
Here's what I've got so far:
declare
type array is table of number index by binary_integer;
pidms array;
begin
for i in (
select distinct sgbstdn_pidm
from sgbstdn
where sgbstdn_majr_code_1 = 'HS04'
and sgbstdn_program_1 = 'HSCOMPH'
)
loop
pidms(pidms.count+1) := i.sgbstdn_pidm;
end loop;
select *
from pidms; --ORACLE DOESN'T LIKE THIS BIT!!!
end;
I know I can output them using dbms_output.putline(), but I'm hoping to get a result set like I would from selecting from any other table.
Thanks in advance,
Matt
You might need a GLOBAL TEMPORARY TABLE.
In Oracle these are created once and then when invoked the data is private to your session.
Oracle Documentation Link
Try something like this...
CREATE GLOBAL TEMPORARY TABLE temp_number
( number_column NUMBER( 10, 0 )
)
ON COMMIT DELETE ROWS;
BEGIN
INSERT INTO temp_number
( number_column )
( select distinct sgbstdn_pidm
from sgbstdn
where sgbstdn_majr_code_1 = 'HS04'
and sgbstdn_program_1 = 'HSCOMPH'
);
FOR pidms_rec IN ( SELECT number_column FROM temp_number )
LOOP
-- Do something here
NULL;
END LOOP;
END;
/
In Oracle, the PL/SQL and SQL engines maintain some separation. When you execute a SQL statement within PL/SQL, it is handed off to the SQL engine, which has no knowledge of PL/SQL-specific structures like INDEX BY tables.
So, instead of declaring the type in the PL/SQL block, you need to create an equivalent collection type within the database schema:
CREATE OR REPLACE TYPE array is table of number;
/
Then you can use it as in these two examples within PL/SQL:
SQL> l
1 declare
2 p array := array();
3 begin
4 for i in (select level from dual connect by level < 10) loop
5 p.extend;
6 p(p.count) := i.level;
7 end loop;
8 for x in (select column_value from table(cast(p as array))) loop
9 dbms_output.put_line(x.column_value);
10 end loop;
11* end;
SQL> /
1
2
3
4
5
6
7
8
9
PL/SQL procedure successfully completed.
SQL> l
1 declare
2 p array := array();
3 begin
4 select level bulk collect into p from dual connect by level < 10;
5 for x in (select column_value from table(cast(p as array))) loop
6 dbms_output.put_line(x.column_value);
7 end loop;
8* end;
SQL> /
1
2
3
4
5
6
7
8
9
PL/SQL procedure successfully completed.
Additional example based on comments
Based on your comment on my answer and on the question itself, I think this is how I would implement it. Use a package so the records can be fetched from the actual table once and stored in a private package global; and have a function that returns an open ref cursor.
CREATE OR REPLACE PACKAGE p_cache AS
FUNCTION get_p_cursor RETURN sys_refcursor;
END p_cache;
/
CREATE OR REPLACE PACKAGE BODY p_cache AS
cache_array array;
FUNCTION get_p_cursor RETURN sys_refcursor IS
pCursor sys_refcursor;
BEGIN
OPEN pCursor FOR SELECT * from TABLE(CAST(cache_array AS array));
RETURN pCursor;
END get_p_cursor;
-- Package initialization runs once in each session that references the package
BEGIN
SELECT level BULK COLLECT INTO cache_array FROM dual CONNECT BY LEVEL < 10;
END p_cache;
/
The sql array type is not neccessary. Not if the element type is a primitive one. (Varchar, number, date,...)
Very basic sample:
declare
type TPidmList is table of sgbstdn.sgbstdn_pidm%type;
pidms TPidmList;
begin
select distinct sgbstdn_pidm
bulk collect into pidms
from sgbstdn
where sgbstdn_majr_code_1 = 'HS04'
and sgbstdn_program_1 = 'HSCOMPH';
-- do something with pidms
open :someCursor for
select value(t) pidm
from table(pidms) t;
end;
When you want to reuse it, then it might be interesting to know how that would look like.
If you issue several commands than those could be grouped in a package.
The private package variable trick from above has its downsides.
When you add variables to a package, you give it state and now it doesn't act as a stateless bunch of functions but as some weird sort of singleton object instance instead.
e.g. When you recompile the body, it will raise exceptions in sessions that already used it before. (because the variable values got invalided)
However, you could declare the type in a package (or globally in sql), and use it as a paramter in methods that should use it.
create package Abc as
type TPidmList is table of sgbstdn.sgbstdn_pidm%type;
function CreateList(majorCode in Varchar,
program in Varchar) return TPidmList;
function Test1(list in TPidmList) return PLS_Integer;
-- "in" to make it immutable so that PL/SQL can pass a pointer instead of a copy
procedure Test2(list in TPidmList);
end;
create package body Abc as
function CreateList(majorCode in Varchar,
program in Varchar) return TPidmList is
result TPidmList;
begin
select distinct sgbstdn_pidm
bulk collect into result
from sgbstdn
where sgbstdn_majr_code_1 = majorCode
and sgbstdn_program_1 = program;
return result;
end;
function Test1(list in TPidmList) return PLS_Integer is
result PLS_Integer := 0;
begin
if list is null or list.Count = 0 then
return result;
end if;
for i in list.First .. list.Last loop
if ... then
result := result + list(i);
end if;
end loop;
end;
procedure Test2(list in TPidmList) as
begin
...
end;
return result;
end;
How to call it:
declare
pidms constant Abc.TPidmList := Abc.CreateList('HS04', 'HSCOMPH');
xyz PLS_Integer;
begin
Abc.Test2(pidms);
xyz := Abc.Test1(pidms);
...
open :someCursor for
select value(t) as Pidm,
xyz as SomeValue
from table(pidms) t;
end;

SELECT COUNT(*) vs. fetching twice with an explicit cursor

I have read a book whose title is "Oracle PL SQL Programming" (2nd ed.) by Steven Feuerstein & Bill Pribyl. On page 99, there is a point suggested that
Do not "SELECT COUNT(*)" from a table unless you really need to know the total number of "hits." If you only need to know whether there is more than one match, simply fetch twice with an explicit cursor.
Could you anyone explain this point more to me by providing example? Thank you.
Update:
As Steven Feuerstein & Bill Pribyl recommends us not to use SELECT COUNT() to check whether records in a table exist or not, could anyone help me edit the code below in order to avoid using SELECT COUNT(*) by using explicit cursor instead? This code is written in the Oracle stored procedure.
I have a table emp(emp_id, emp_name, ...), so to check the provided employee ID corret or not:
CREATE OR REPLACE PROCEDURE do_sth ( emp_id_in IN emp.emp_id%TYPE )
IS
v_rows INTEGER;
BEGIN
...
SELECT COUNT(*) INTO v_rows
FROM emp
WHERE emp_id = emp_id_in;
IF v_rows > 0 THEN
/* do sth */
END;
/* more statements */
...
END do_sth;
There are a number of reasons why developers might perform select COUNT(*) from a table in a PL/SQL program:
1) They genuinely need to know how many rows there are in the table.
In this case there is no choice: select COUNT(*) and wait for the result. This will be pretty fast on many tables, but could take a while on a big table.
2) They just need to know whether a row exists or not.
This doesn't warrant counting all the rows in the table. A number of techniques are possible:
a) Explicit cursor method:
DECLARE
CURSOR c IS SELECT '1' dummy FROM mytable WHERE ...;
v VARCHAR2(1);
BEGIN
OPEN c;
FETCH c INTO v;
IF c%FOUND THEN
-- A row exists
...
ELSE
-- No row exists
...
END IF;
END;
b) SELECT INTO method
DECLARE
v VARCHAR2(1);
BEGIN
SELECT '1' INTO v FROM mytable
WHERE ...
AND ROWNUM=1; -- Stop fetching if 1 found
-- At least one row exists
EXCEPTION
WHEN NO_DATA_FOUND THEN
-- No row exists
END;
c) SELECT COUNT(*) with ROWNUM method
DECLARE
cnt INTEGER;
BEGIN
SELECT COUNT(*) INTO cnt FROM mytable
WHERE ...
AND ROWNUM=1; -- Stop counting if 1 found
IF cnt = 0 THEN
-- No row found
ELSE
-- Row found
END IF;
END;
3) They need to know whether more than 1 row exists.
Variations on the techniques for (2) work:
a) Explicit cursor method:
DECLARE
CURSOR c IS SELECT '1' dummy FROM mytable WHERE ...;
v VARCHAR2(1);
BEGIN
OPEN c;
FETCH c INTO v;
FETCH c INTO v;
IF c%FOUND THEN
-- 2 or more rows exists
...
ELSE
-- 1 or 0 rows exist
...
END IF;
END;
b) SELECT INTO method
DECLARE
v VARCHAR2(1);
BEGIN
SELECT '1' INTO v FROM mytable
WHERE ... ;
-- Exactly 1 row exists
EXCEPTION
WHEN NO_DATA_FOUND THEN
-- No row exists
WHEN TOO_MANY_ROWS THEN
-- More than 1 row exists
END;
c) SELECT COUNT(*) with ROWNUM method
DECLARE
cnt INTEGER;
BEGIN
SELECT COUNT(*) INTO cnt FROM mytable
WHERE ...
AND ROWNUM <= 2; -- Stop counting if 2 found
IF cnt = 0 THEN
-- No row found
IF cnt = 1 THEN
-- 1 row found
ELSE
-- More than 1 row found
END IF;
END;
Which method you use is largely a matter of preference (and some religious zealotry!) Steven Feuerstein has always favoured explicit cursors over implicit (SELECT INTO and cursor FOR loops); Tom Kyte favours implicit cursors (and I agree with him).
The important point is that to select COUNT(*) without restricting the ROWCOUNT is expensive and should therefore only be done when a count is trully needed.
As for your supplementary question about how to re-write this with an explicit cursor:
CREATE OR REPLACE PROCEDURE do_sth ( emp_id_in IN emp.emp_id%TYPE )
IS
v_rows INTEGER;
BEGIN
...
SELECT COUNT(*) INTO v_rows
FROM emp
WHERE emp_id = emp_id_in;
IF v_rows > 0 THEN
/* do sth */
END;
/* more statements */
...
END do_sth;
That would be:
CREATE OR REPLACE PROCEDURE do_sth ( emp_id_in IN emp.emp_id%TYPE )
IS
CURSOR c IS SELECT 1
FROM emp
WHERE emp_id = emp_id_in;
v_dummy INTEGER;
BEGIN
...
OPEN c;
FETCH c INTO v_dummy;
IF c%FOUND > 0 THEN
/* do sth */
END;
CLOSE c;
/* more statements */
...
END do_sth;
But really, in your example it is no better or worse, since you are selecting the primary key and Oracle is clever enough to know that it only needs to fetch once.
If two is all you are interested in, try
SELECT 'THERE ARE AT LEAST TWO ROWS IN THE TABLE'
FROM DUAL
WHERE 2 =
(
SELECT COUNT(*)
FROM TABLE
WHERE ROWNUM < 3
)
It will take less code than doing the manual cursor method,
and it is likely to be faster.
The rownum trick means to stop fetching rows once it has two of them.
If you don't put some sort of limit on the count(*), it could take a long while to finish, depending on the number of rows you have. In that case, using a cursor loop, to read 2 rows from the table manually, would be faster.
This comes from programmers writing code similar to the following (this is psuedo code!).
You want to check to see if the customer has more than one order:
if ((select count(*) from orders where customerid = :customerid) > 1)
{
....
}
That is a terribly inefficient way to do things. As Mark Brady would say, if you want to know if a jar contains pennies, would you count all the pennies in the jar, or just make sure there is 1 (or 2 in your example)?
This could be better written as:
if ((select 1 from (select 1 from orders where customerid = :customerid) where rownum = 2) == 1)
{
....
}
This prevents the "counting all of the coins" dilemma since Oracle will fetch 2 rows, then finish. The previous example would cause oracle to scan (an index or table) for ALL rows, then finish.
He means open a cursor and fetch not only the first record but the second, and then you will know there is more than one.
Since I never seem to need to know that SELECT COUNT(*) is >= 2, I have no idea why this is a useful idiom in any SQL variant. Either no records or at least one, sure, but not two or more. And anyway, there's always EXISTS.
That, and the fact that Oracle's optimizer seems to be pretty poor... - I would question the relevance of the technique.
To address TheSoftwareJedi's comments:
WITH CustomersWith2OrMoreOrders AS (
SELECT CustomerID
FROM Orders
GROUP BY CustomerID
HAVING COUNT(*) >= 2
)
SELECT Customer.*
FROM Customer
INNER JOIN CustomersWith2OrMoreOrders
ON Customer.CustomerID = CustomersWith2OrMoreOrders.CustomerID
Appropriately indexed, I've never had performance problems even with whole universe queries like this in SQL Server. However, I have consistently run into comments about Oracle optimizer problems here and on other sites.
My own experience with Oracle has not been good.
The comment from the OP appears to be saying that full COUNT(*) from tables are not well handled by the optimizer. i.e.:
IF EXISTS (SELECT COUNT(*) FROM table_name HAVING COUNT(*) >= 2)
BEGIN
END
(which, when a primary key exists, can be reduced to a simple index scan - in a case of extreme optimization, one can simply query the index metadata in sysindexes.rowcnt - to find the number of entries - all without a cursor) is to be generally avoided in favor of:
DECLARE CURSOR c IS SELECT something FROM table_name;
BEGIN
OPEN c
FETCH c INTO etc. x 2 and count rows and handle exceptions
END;
IF rc >= 2 THEN BEGIN
END
That, to me would result in less readable, less portable, and less maintainable code.
Before you take Steven Feuerstein's suggestions too serious, just do a little benchmark. Is count(*) noticeably slower than the explicit cursor in your case? No? Then better use the construct that allows for simple, readable code. Which, in most cases, would be "select count(*) into v_cnt ... if v_cnt>0 then ..."
PL/SQL allows for very readable programs. Don't waste that just to nano-optimize.
Depending on the DB, there may be a sys table which stores an approximate count and can be queried in constant time. Useful if you want to know whether the table has 20 rows or 20,000 or 20,000,000.
SQL Server:
if 2 = (
select count(*) from (
select top 2 * from (
select T = 1 union
select T = 2 union
select T = 3 ) t) t)
print 'At least two'
Also, don't ever use cursors. If you think you really really need them, beat yourself with a shovel until you change your mind. Let relics from an ancient past remain relics from an ancient past.
If you want to get number of rows in a table, please don't used count(*), I would suggest count(0) that 0 is the column index of your primary key column.

Resources